diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4d664da..7c443ad0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,6 @@ env: MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} FOUNDRY_PROFILE: ci - jobs: lint: runs-on: ubuntu-latest @@ -54,7 +53,6 @@ jobs: echo "✅ Passed" >> $GITHUB_STEP_SUMMARY test: - needs: ["lint", "build"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -67,14 +65,8 @@ jobs: - name: Show the Foundry config run: "forge config" - - name: Generate a fuzz seed that changes weekly to avoid burning through RPC allowance - run: > - echo "FOUNDRY_FUZZ_SEED=$( - echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800)) - )" >> $GITHUB_ENV - - name: Run tests - run: forge test -vvv --gas-report + run: forge test -vvv --gas-report --color always - name: Add test summary run: | diff --git a/.gitmodules b/.gitmodules index abee69e7..1a8e987f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts -[submodule "lib/v4-core"] - path = lib/v4-core - url = https://github.com/Uniswap/v4-core [submodule "lib/chimera"] path = lib/chimera url = https://github.com/Recon-Fuzz/chimera diff --git a/README.md b/README.md index c7ed93b6..a032efca 100644 --- a/README.md +++ b/README.md @@ -1,109 +1,372 @@ -# Liquity v2: Modular Initiative based Governance +# Liquity v2 Governance -Liquity v2 is a decentralized protocol that allows Ether and Liquid Staking Token (LST) holders to obtain -maximum liquidity against their collateral, at an interest that they set. After locking up ETH or LSTs as -collateral in a smart contract and creating an individual position called a "trove", the user can get -instant liquidity by minting BOLD, a USD-pegged stablecoin. Each trove is required to be collateralized -at a minimum level per collateral type. Any owner of BOLD can redeem their stablecoins for the underlying -collateral at any time. The redemption mechanism along with algorithmically adjusted fees guarantee a minimum -stablecoin value of USD 1. +## Table of Contents -An unprecedented liquidation mechanism based on incentivized stability deposits and a redistribution -cycle from lower interest rate paying troves to higher interest rate paying troves provides for greater -price stability for the BOLD token around the peg, balancing demand and supply for BOLD, without the need for -active governance or monetary interventions. +- [Overview](#overview) +- [Core Smart Contracts](#core-smart-contracts) + - [Governance](#governance) + - [UserProxyFactory](#userproxyfactory) + - [UserProxy](#userproxy) + - [BribeInitiative](#bribeinitiative) +- [Epochs](#epochs) + - [Epoch Structure](#epoch-structure) + - [Epoch Transitions](#epoch-transitions) +- [LQTY Deposits, Withdrawals, and v1 Staking](#lqty-deposits-withdrawals-and-v1-staking) +- [Voting Power Accrual](#voting-power-accrual) + - [Multiple Deposits Over Time](#multiple-deposits-over-time) + - [Voting Power Calculation and Internal Accounting](#voting-power-calculation-and-internal-accounting) +- [Withdrawals and Voting Power](#withdrawals-and-voting-power) +- [Allocating Voting Power to Initiatives](#allocating-voting-power-to-initiatives) + - [Allocation in Practice](#allocation-in-practice) +- [Vetoing Initiatives](#vetoing-initiatives) +- [Allocations Across Epochs](#allocations-across-epochs) +- [Path Dependence of Voting Power Actions](#path-dependence-of-voting-power-actions) +- [Registering Initiatives](#registering-initiatives) +- [Unregistering Initiatives](#unregistering-initiatives) +- [Snapshots](#snapshots) + - [Initiative Vote Snapshots](#initiative-vote-snapshots) + - [Total Vote Snapshots](#total-vote-snapshots) + - [Total BOLD Snapshots](#total-bold-snapshots) + - [Snapshot Mechanics](#snapshot-mechanics) +- [Initiative States](#initiative-states) +- [Voting Threshold Calculation](#voting-threshold-calculation) +- [Claiming for Initiatives](#claiming-for-initiatives) + - [Claim Frequency](#claim-frequency) +- [Bribes](#bribes) + - [How Bribing Works](#how-bribing-works) + - [Claiming Bribes](#claiming-bribes) + - [Tracking Allocations and Votes](#tracking-allocations-and-votes) +- [Known Issues](#known-issues) + - [Path Dependency of Depositing/Withdrawing LQTY](#path-dependency-of-depositingwithdrawing-lqty) + - [Trust Assumption: Bribe Token](#trust-assumption-bribe-token) + - [Trust Assumption: Initiative](#trust-assumption-initiative) + - [Impact of Vetoed or Low-Vote Initiatives](#impact-of-vetoed-or-low-vote-initiatives) +- [Testing](#testing) + - [Running Foundry Tests](#running-foundry-tests) + - [Invariant Testing](#invariant-testing) -The protocol has built-in incentives that encourage both price stability as well as liquidity for the BOLD stablecoin. -With 75% of revenues from borrowing activities used to incentivize the Stability Pool (SP), and the remaining 25% of -revenues from borrowing activities (incentive portion) allocated to the Modular Initiative based Governance. -Modular Initiative based Governance allows LQTY holders to allocate votes, earned through staking, -to direct the incentive portion to arbitrary addresses, which are specified as Initiatives. Voting activity is snapshotted -in a decentralized manner and paid out at the end of weekly epochs. -## Staking -Staking allows LQTY token holders to deposit LQTY to accrue voting power which can be used to direct Incentives, while -maintaining eligibility to earn rewards from Liquity v1 (https://docs.liquity.org/faq/staking). +## Overview -This is managed through the use of a UserProxy, which uses a factory pattern, to create an address specific wrapper for each -LQTY user who stakes LQTY via Governance.sol, and manages accounting and claiming from Liquity v1 staking. While Proxies can -be deployed either via the Governance.sol contract or directly, each instance of UserProxy is accessible by Governance.sol to -allow for Liquity v2 staking and voting accounting to be managed accurately. +The core Liquity v2 protocol has built-in incentives on each collateral branch that encourage both price stability as well as liquidity for the BOLD stablecoin. 75% of revenues from borrowing activities are used to incentivize the core system Stability Pools, and the remaining 25% of revenues from all borrowing activities (incentive portion) are allocated to the Modular Initiative based Governance. -A user's LQTY stake increases in voting power over time on a linear basis depending on the time it has been staked. -Upon deposit, a User's voting power will be equal to 0. +Modular Initiative based Governance allows LQTY holders to allocate votes, earned over time through staking, to direct the incentive portion to arbitrary addresses, which are specified as Initiatives. Initiatives may be registered permissionlessly. -Users' LQTY stake can be increased and decreased over time, but each increased LQTY added will require power accrual from 0, -and not assume the power of already deposited LQTY for the new staked LQTY. +Users are also able to allocate voting power as vetos, in order to attempt to block rewards to Initiatives they deem unworthy. -In order to unstake and withdraw LQTY, a User must first deallocate a sufficient number of LQTY from initiatives. +The system chunks time into weekly epochs. Voting activity is snapshotted in a decentralized manner and accrued incentives are paid out at the end of epochs to Initiatives that meet the qualifying criteria - primarily the voting threshold. Qualifying Initiatives for a given epoch receive a pro-rata share of the BOLD rewards accrued for that epoch, based on their share of the epoch’s votes. -## Initiatives -Initiative can be added permissionlessly, requiring the payment of a 100 BOLD fee, and in the following epoch become active -for voting. During each snapshot, Initiatives which received as sufficient number of Votes that their incentive payout equals -at least 500 BOLD, will be eligible to Claim ("minimum qualifying threshold"). Initiatives failing to meet the minimum qualifying threshold will not qualify to claim for that epoch. -Initiatives failing to meet the minimum qualifying threshold for a claim during four consecutive epochs may be deregistered permissionlessly, requiring reregistration to become eligible for voting again. -Claims for Initiatives which have met the minimum qualifying threshold, can be claimed permissionlessly, but must be claimed by the end of the epoch in which they are awarded. Failure to do so will result in the unclaimed portion being reused in the following epoch. +## Core smart contracts -As Initiatives are assigned to arbitrary addresses, they can be used for any purpose, including EOAs, Multisigs, or smart contracts designed for targetted purposes. Smart contracts should be designed in a way that they can support BOLD and include any additional logic about how BOLD is to be used. +- `Governance` - the central system contract which manages all governance operations including Initiative registration, staking/unstaking LQTY, voting mechanics, and reward distribution. It handles time-weighted voting power calculations, epoch transitions and BOLD token rewards, while also managing the deployment of and interactions with UserProxy contracts. -### Malicious Initiatives +- `UserProxyFactory` - A factory contract that deploys minimal proxy clones of the `UserProxy` implementation using CREATE2 for deterministic addressing. It is inherited by the `Governance` contract to provide `UserProxy` deployment and management capabilities. It also maintains the relationship between Users and their UserProxies. -It's important to note that initiatives could be malicious, and the system does it's best effort to prevent any DOS to happen, however, a malicious initiative could drain all rewards if voted on. +- `UserProxy` - Serves as the intermediary between an individual User and the Liquity v1 staking system, holding their staked LQTY position. It handles all direct v1 staking operations and reward collection. Only the `Governance` contract can call its mutating functions. The proxy architecture allows the system to hold _individual_ staked LQTY positions on behalf of its Users. -## Voting +- `BribeInitative` - A base contract that enables external parties to incentivize votes on Initiatives by depositing BOLD and other tokens as bribes. It records User vote allocations across epochs, ensuring proportional distribution of bribes to voters. The contract provides extensible hooks and functions that allow developers to create specialized Initiatives with custom logic while maintaining the core bribe distribution mechanics. -Users with LQTY staked in Governance.sol, can allocate LQTY in the same epoch in which they were deposited. But the effective voting power at that point would be insignificant. -Votes can take two forms, a vote for an Initiative or a veto vote. Initiatives which have received vetoes which are both: -three times greater than the minimum qualifying threshold, and greater than the number of votes for will not be eligible for claims by being excluded from the vote count and maybe deregistered as an Initiative. +## Epochs +The Governance system operates on a weekly epoch-based scheme that provides predictable time windows for voting and claiming rewards. Each epoch is exactly `EPOCH_DURATION` (7 days) long. The epoch scheme provides predictable windows for Users to plan their vote and veto actions. +## Epoch Structure +Each epoch has two distinct phases: +**Phase 1: votes and vetos** (First 6 days) +- Users can freely allocate and modify their LQTY votes and vetos to Initiatives +**Phase 2: vetos only** (Final day) +- Users may not increase their vote allocation to any Initiative +- Users are free to decrease their vote allocation or increase their veto allocation to any Initiative -Users may split their votes for and veto votes across any number of initiatives. But cannot vote for and veto vote the same Initiative. -Each epoch is split into two parts, a six day period where both votes for and veto votes take place, and a final 24 hour period where votes can only be made as veto votes. This is designed to give a period where any detrimental distributions can be mitigated should there be sufficient will to do so by voters, but is not envisaged to be a regular occurance. +The purpose of Phase 2 is to prevent last-minute vote allocation by a bad-faith actor to Initiatives that are misaligned with the Liquity ecosystem. + +The short veto phase at least gives other stakers a chance to veto such bad-faith Initiatives, even if they have to pull voting power away from other Initiatives. +### Epoch Transitions +Epochs transition automatically at fixed 7-day intervals. No manual intervention is required to trigger a new epoch. The first epoch-based operation in a new epoch triggers relevant snapshots - see the [snapshots section](#snapshots). + +## LQTY deposits, withdrawals and v1 staking + +LQTY token holders may deposit LQTY to the Governance system via `Governance.depositLQTY`. Deposited LQTY is staked in Liquity v1, thus earning ETH and LUSD rewards from v1 fees. See Liquity v1 (https://docs.liquity.org/faq/staking) for further details of v1 staking and rewards. + +Deposited LQTY accrues voting power linearly over time. A user’s voting power from deposited LQTY can be allocated and deallocated from Initiatives. + +Users may top up their deposited LQTY at any time, and may withdraw part or all of their deposited LQTY via `withdrawLQTY` when they have no active allocations to Initiatives. + +Both deposits and withdrawals can be made via ERC2612 permit with `depositLQTYViaPermit` and and `withdrawLQTYViaPermit` respectively. + +Deposit and withdrawal functions allow the user to optionally claim their v1 staking rewards (LUSD and ETH) by passing a `_doClaimRewards` boolean. + + +## Voting power accrual + +A user's LQTY deposit accrues voting power linearly over time. That is, the absolute voting power of a given LQTY deposit is proportional to 1) the LQTY deposited and 2) the time passed since deposit. + +Upon deposit of a chunk of LQTY, the voting power associated with that chunk will be equal to 0. + +Top-ups of a User’s existing deposit accrue voting power in the same manner: that is, a given top-up accrues votes linearly according to its size and time passed since it was made. + +The voting power of a User’s total deposited LQTY equals the sum of the voting power of all of the individual LQTY deposits/top-ups comprising their deposit. + + +## Withdrawals and voting power + +A withdrawal pulls from the User’s unallocated LQTY. Withdrawals don’t “know” anything about the deposit history. A withdrawal of x% of the User’s unallocated LQTY reduces the voting power of their unallocated LQTY by x% - even though the User may have made deposits at different times, with the older ones having accrued more voting power. + +Withdrawals are thus considered “proportional” in that they reduce the voting power of all of the user’s previous deposit chunks by the same percentage. + +As such, a User with non-zero unallocated voting power who deposits m LQTY then immediately withdraws m LQTY, will undergo a decrease in unallocated voting power. This natural penalty incentivises users to keep their LQTY deposited in the Governance system. + + + +LQTY may be assigned to: + +A User +An Initiative, as allocated “vote” LQTY +An Initiative, as allocated “veto” LQTY + +Deposited LQTY accrues voting power continuously over time, for whichever entity it is assigned to (i.e. User or Initiative). + +All LQTY accrues voting power at the same rate. + + + +### Multiple deposits over time + +For a composite LQTY amount - i.e. a deposit made up of several deposit “chunks” at different points in time - each chunk earns voting power linearly from the point at which it was deposited. + +So, the voting power for an individual User A with `n` deposits of LQTY made over time is given by: + +`V_A(t) = m_1* (t - t_1) + m_2* (t - t_2) + ... + m_n* (t - t_n)` + +i.e. + +`V_A(t) = t*sum(m_i) - sum(m_i*t_i)` + +so: + +`V_A(t) = t*M_A - S_A` + +Where: + +- `i`: Index denoting deposit i’th deposit event +- `t_i`: Time at which the i’th deposit was made +- `V_A`: total voting power for user A from `n` deposits by time `t` +- `M_A`: sum of A’s LQTY deposits +- `S_A`: The “offset”, i.e. the sum of A’s deposit chunks weighted by time deposited. + + +### Voting power calculation and internal accounting + +Voting power is calculated as above - i.e. `V_A(t) = t*M_A - S_A`. Accounting is handled by storing the LQTY amount and the “offset” sum for each user. These trackers are updated any time a user deposits, withdraws or allocates LQTY to Initiatives. + + +The general approach of using an LQTY amount and an offset tracker sum is used for both users and Initiatives. + +LQTY amounts and offsets are recorded for: + +- Per-user allocations +- Per-Initative allocations +- Per-user-per-Initiative allocations + +The full scheme is outlined in [this paper](https://docs.google.com/document/d/1nPdD-w1n_0KIzAgi3Y5c8h2t2wkFiFD5YDHKsc6KxH8/edit?usp=sharing). + + + +### Allocating voting power to Initiatives + +LQTY can be allocated and deallocated to Initiatives by Users via `Governance.allocateLQTY`. When LQTY is allocated to an Initiative, the corresponding voting power is also allocated. + +Allocation from User to Initiative is also “proportional” in the same sense as withdrawals are. + +After allocation, the voting power of the allocated LQTY continues growing linearly with time. + + +### Allocation in practice + +A user passes their chosen LQTY allocations per-Initative to `allocateLQTY`. + +Under the hood, allocation is performed in two steps internally: all their current allocations are zero’ed by a single call to the internal `_allocateLQTY` function, and then updated to the new values with a second call. + + +## Vetoing Initiatives + +Users may also allocate vetos to Initiatives via `Governance.allocateLQTY`. Just like voting power, LQTY allocated for vetoing accrues “veto power” linearly, and internal calculations and accounting are identical. + +An Initiative which has received a sufficient quantity of vetoes is not claimable, and can be permissionlessly unregistered - see the [Initiative states section](#initiative-states) for the precise threshold formulation. + + +## Allocations across epochs + +LQTY allocations to an Initiative persist across epochs, and thus the corresponding voting power allocated to that Initiative continues growing linearly across epochs. + + + +## Path dependence of voting power actions + +**Allocating** and **deallocating** LQTY/voting power is path independent - that is, when a user allocates `x` voting power to an Initiative then immediately deallocates it, their voting power remains the same. + +In contrast, **depositing** and **withdrawing** LQTY is path-dependent - for a User with non-zero voting power, a top-up and withdrawal of `x` LQTY will reduce their voting power. This is because the top-up LQTY chunk has 0 voting power, but the [proportional nature of the withdrawal](#allocating-voting-power-to-initiatives) reduces the voting power of all previous LQTY chunks comprising their deposit. + + +## Registering Initiatives + +Initiative can be registered permissionlessly via `registerInitative`. The caller pays the `REGISTRATION_FEE` in BOLD. The caller must also have accrued sufficient total voting power (i.e. the sum of their allocated and unallocated voting power) in order to register an Initiative. This threshold is dynamic - it is equal to the snapshot of the previous epoch’s total votes multiplied by the `REGISTRATION_VOTING_THRESHOLD`. Thus, the greater the total votes in the previous epoch, the more voting power needed in order to register a new Initiative. + +If the Initiative meets these requirements it becomes eligible for voting in the subsequent epoch. + +Registration records the Initiative’s address and the epoch in which it was registered in the `registeredInitiatives` mapping. + + +## Unregistering Initiatives + +Initiatives may be unregistered permissionlessly via `unregisterInitiative`. + +An Initiative can be unregistered if either: + + +1. It has spent `UNREGISTRATION_AFTER_EPOCHS` (4) epochs in SKIP and/or CLAIMABLE states, without being claimed for + + +Or: + +2. Its vetos exceed both its votes, and the voting threshold multiplied by `UNREGISTRATION_THRESHOLD_FACTOR` + ## Snapshots -Snapshots of results from the voting activity of an epoch takes place on an initiative by initiative basis in a permissionless manner. -User interactions or direct calls following the closure of an epoch trigger the snapshot logic which makes a Claim available to a qualifying Initiative. +Since BOLD rewards are distributed based on an Initiative’s pro-rata share of votes at the end of each epoch, and since votes (and vetos) accrue continuously over time, snapshots of an Initiative’s accrued votes and vetos must be taken for given epochs. + +Additionally, snapshots of total votes and vetos, and total BOLD rewards accrued, must be taken for each epoch, to perform the pro-rata reward calculations. + + +### Initiative vote snapshots + +Initiative snapshotting is handled by `Governance._snapshotVotesForInitiative`. + +It checks when the Initiative was last snapshotted, and if it is before the end of the previous epoch, a new snapshot of the Initiative’s current voting power is recorded. If a more recent snapshot has been taken, this function is a no-op. + +Initiative snapshots are taken inside user operations: allocating LQTY to Initiatives (`allocateLQTY`), registering Initiatives (unregisterInitative), and claiming an Initiative’s incentives (`claimForInitative`) all perform Initiative snapshots before updating other Initiative state. + +Initiative snapshots may also be recorded permissionlessly via the external `Governance.snapshotVotesForInitiative` and `Governance.getInitiativeState` functions. + + +### Total vote snapshots + +Total vote count is similarly snapshotted by `Governance._snapshotVotes`, which is called at all the same above user operations, and additionally upon Initiative registration (`registerInitiative`), and permissionlessly via `calculateVotingThreshold`. + + + +### Total BOLD snapshots + +The total BOLD available for claim for the previous epoch - `boldAccrued` - is snapshotted via `Governance._snapshotVotes`. This is used as the denominator in reward distribution calculations for that epoch. + + + +### Snapshot Mechanics +Since epochs transition seamlessly without need for a manual triggering action, the first relevant operation in a new epoch will trigger a snapshot calculation. + +Since voting power is a simple linear function of LQTY and time (see voting power section above [LINK]), snapshots of votes can be calculated retroactively, i.e. _after_ the end of the previous epoch has passed. All that matters is snapshots are taken before LQTY quantities are changed, which is the case. In order to take the snapshot, the previous epoch’s end timestamp is used in the voting power calculation. + +BOLD rewards are trickier - they are “lumpy” and arrive in somewhat unpredictable chunks (depending on the dynamics of the v2 core system). As such, a late BOLD snapshot may take into account some BOLD that has arrived _after_ the epoch has ended. In practice, this slightly benefits Initiatives registered in the previous epoch, and slightly takes away BOLD rewards for the current epoch. + +However, the permissionless snapshot function `Governance.calculateVotingThreshold` allows anyone to take a snapshot exactly at or very close to the epoch boundary, and ensure fair BOLD distribution. + +Snapshots are immutable once recorded for a given epoch. + +## Initiative States +The governance system uses a state machine to determine the status of each Initiative. The relevant function is `Governance.getInitiativeState`. The state determines what actions can be taken with the Initiative. + +In a given epoch, Initiatives can be in one of several states based on the previous epoch's snapshot. + +Following are the states Initiatives can be in, the conditions that lead to the states, and their consequences. +_(Note that the state machine checks conditions in the order they are presented below - e.g. an Initiative in the CLAIMABLE state is by definition not in any of the states above CLAIMABLE)_: + +image + + + +## Voting threshold calculation + +The voting threshold is used in two ways: determining whether an Initiative has sufficient net votes to be claimed for, and in part of the calculation for determining whether an Initiative can be unregistered - see Initiative states `CLAIMABLE` and `UNREGISTERABLE` in the Initiative states section [LINK]. + +It is calculated as the maximum of: + +- `VOTING_THRESHOLD_FACTOR * _snapshot.votes`, i.e. 2% of the total votes counted at the snapshot for the previous epoch + +and: + +- The `minVotes`, which is the minimum number of votes required for an Initiative to meet the `MIN_CLAIM` amount of BOLD tokens, i.e. 500 BOLD. + +Thus the voting threshold is dynamic and varies by epoch to epoch. The more total votes accrued in the previous epoch, the more are needed in the current epoch for an Initiative to be claimable. This formulation was chosen because staked LQTY earns voting power that grows linearly over time, and thus the total votes per epoch will tend to increase in the long-run. + + +## Claiming for Initiatives +Each Initiative that meets the qualifying criteria are eligible for claim, i.e. to have its share of BOLD rewards accrued during the previous epoch transferred to it. Claims are made through `claimForInitiative`and are permissionless - anyone can transfer the rewards from Governance to the qualifying Initiative. This function must be executed during the epoch following the snapshot. + +An Initiative qualifies for claim when its votes exceed both: + +- The voting threshold +- The vetos received + +The reward amount for a qualifying Initiative is calculated as the pro-rata share of the epoch's BOLD accrual (plus the previous epoch's unclaimed BOLD, if any), based on the Initiative's share of total votes among all Initiatives, including non-qualifying ones. For example, if an Initiative received 25% of all votes in an epoch, it will receive 25% of that epoch's accrued BOLD rewards. The BOLD share of non-qualifying initiatives is automatically rolled over into the next epoch's reward pool. + +If a qualifying Initiative fails to claim during the epoch following its snapshot, its potential rewards are automatically rolled over into the next epoch's reward pool. This means unclaimed rewards are not lost, but rather redistributed to the next epoch's qualifying Initiatives. + +When a successful claim is made, the BOLD tokens are immediately transferred to the Initiative address, and the `onClaimForInitiative`hook is called on the Initiative contract (if implemented). This hook allows Initiatives to execute custom logic upon receiving rewards, making the system highly flexible for different use cases. +Note that Initiatives must be claimed individually - however the [multiDelegateCall()](src/utils/MultiDelegateCall.sol) function of `Governance` may be used to claim for multiple Initiatives in a single call. + +### Claim frequency + +An Initiative can be claimed for at most once per epoch. After a claim in epoch `x`, the Initiative will be claimable again in epoch `x+1` as long as it maintains qualifying voting power. + +These constraints are enforced by the Initiative state machine [LINK]. + -## Bribing -LQTY depositors can also receive bribes in the form of ERC20s in exchange for voting for a specified initiative. -This is done externally to the Governance.sol logic and should be implemented at the initiative level. -BaseInitiative.sol is a reference implementation which allows for bribes to be set and paid in BOLD + another token, all claims for bribes are made by directly interacting with the implemented BaseInitiative contract. +## Bribes +The system includes a base `BribeInitiative`contract that enables Initiative-specific vote incentivization through token rewards ("bribes"). This provides a framework for external parties to encourage votes on specific Initiatives by offering additional rewards on top of the standard BOLD distributions. -## Example Initiatives +The `BribeInitiative` contract is offered as a reference implementation, and is designed to be inherited by custom Initiatives that may implement more specific bribing logic. +### How Bribing Works +External parties can deposit bribes denominated in two tokens: +- BOLD tokens +- One additional ERC20 token specified during Initiative deployment. -To facilitate the development of liquidity for BOLD and other relevant tokens after the launch of Liquity v2, initial example initiatives will be added. -They will be available from the first epoch in which claims are available (epoch 1), added in the construtor. Following epoch 1, these examples have no further special status and can be removed by LQTY voters -### Curve v2 +These bribes are allocated to specific future epochs via the `depositBribe`function. Users who vote for the Initiative during that epoch become eligible to claim their proportional share of that epoch's bribes. +### Claiming Bribes +Users can claim their share of bribes through the `claimBribes`function. A User's share of an Initiative’s bribes for a given epoch is calculated based on their pro-rata share of the voting power allocated to the Initiative in that epoch. The share is calculated based on the votes accrued at the epoch end. -Simple adapter to Claim from Governance.sol and deposit into a Curve v2 gauge, which must be preconfigured, and release rewards over a specified duration. -Claiming and depositing to gauges must be done manually after each epoch in which this Initiative has a Claim. +Bribe claims can be made at any time after the target epoch - bribes do not expire, and are not carried over between epochs. +### Tracking allocations and votes +The contract maintains linked lists to track vote allocations across epochs: +Per-user lists track individual vote history +A global list tracks total vote allocations +Per-user and total LQTY allocations by epoch are recorded in the above lists every time an allocation is made, via the `onAfterAllocateLQTY` hook, callable only by `Governance`. List entries store both LQTY amount and time-weighted offset, allowing accurate calculation of voting power at each epoch. -### Uniswap v4 -Simple hook for Uniswap v4 which implements a donate to a preconfigured pool. Allowing for adjustments to liquidity positions to make Claims which are smoothed over a vesting epoch. +## Known issues + +### Path dependency of depositing/withdrawing LQTY +Depositing and withdrawing LQTY when unallocated voting power is non-zero reduces the User’s unallocated voting power. See this section [LINK] -## Known Issues +### Trust assumption: Bribe token is non-malicious standard ERC20 +Since an arbitrary bribe token may be used, issues can arise if the token is non-standard - e.g. has fee-on-transfer or is rebasing, or indeed if the token is malicious and/or upgradeable. -### Vetoed Initiatives and Initiatives that receive votes that are below the treshold cause a loss of emissions to the voted initiatives +Any of the above situatons could result in Users receiving less bribe rewards than expected. -Because the system counts: valid_votes / total_votes -By definition, initiatives that increase the total_votes without receiving any rewards are stealing the rewards from other initiatives +### Trust-assumption: Initiative will not rug voters -The rewards will be re-queued in the next epoch +The owner of an upgradeable Initiative could arbitrarily change its logic, and thus change the destination of funds to one different from that which was voted for by Users. -see: `test_voteVsVeto` as well as the miro and comments +### Vetoed Initiatives and Initiatives that receive votes that are below the threshold cause a loss of emissions to the voted initiatives -### User Votes, Initiative Votes and Global State Votes can desynchronize +Because the system spits rewards in proportion to: `valid_votes / total_votes`, then by definition, Initiatives that Increase the total_votes without receiving any rewards are "stealing" the rewards from other initiatives. The rewards will be re-queued in the next epoch. -See `test_property_sum_of_lqty_global_user_matches_0` ## Testing diff --git a/foundry.toml b/foundry.toml index 5de5c284..b890f24b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,7 +12,7 @@ chain_id = 99 block_timestamp = 2592000 [profile.ci.fuzz] -runs = 5000 +runs = 500 [profile.default.fuzz] runs = 100 diff --git a/lib/forge-std b/lib/forge-std index 978ac6fa..2b59872e 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 978ac6fadb62f5f0b723c996f64be52eddba6801 +Subproject commit 2b59872eee0b8088ddcade39fe8c041e17bb79c0 diff --git a/lib/v4-core b/lib/v4-core deleted file mode 160000 index ac475328..00000000 --- a/lib/v4-core +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ac47532844db17525769b06de106a3c98f26d12d diff --git a/script/DeploySepolia.s.sol b/script/DeploySepolia.s.sol index 1a6f003a..d2ffaddb 100644 --- a/script/DeploySepolia.s.sol +++ b/script/DeploySepolia.s.sol @@ -2,9 +2,7 @@ pragma solidity ^0.8.13; import {Script} from "forge-std/Script.sol"; -import {MockERC20} from "forge-std/mocks/MockERC20.sol"; -import {PoolManager, Deployers, Hooks} from "v4-core/test/utils/Deployers.sol"; import {ICurveStableswapFactoryNG} from "../src/interfaces/ICurveStableswapFactoryNG.sol"; import {ICurveStableswapNG} from "../src/interfaces/ICurveStableswapNG.sol"; import {ILiquidityGauge} from "./../src/interfaces/ILiquidityGauge.sol"; @@ -12,21 +10,20 @@ import {ILiquidityGauge} from "./../src/interfaces/ILiquidityGauge.sol"; import {IGovernance} from "../src/interfaces/IGovernance.sol"; import {Governance} from "../src/Governance.sol"; -import {UniV4Donations} from "../src/UniV4Donations.sol"; import {CurveV2GaugeRewards} from "../src/CurveV2GaugeRewards.sol"; -import {Hooks} from "../src/utils/BaseHook.sol"; +import {MockERC20Tester} from "../test/mocks/MockERC20Tester.sol"; import {MockStakingV1} from "../test/mocks/MockStakingV1.sol"; +import {MockStakingV1Deployer} from "../test/mocks/MockStakingV1Deployer.sol"; import {HookMiner} from "./utils/HookMiner.sol"; -contract DeploySepoliaScript is Script, Deployers { +contract DeploySepoliaScript is Script, MockStakingV1Deployer { // Environment Constants - MockERC20 private lqty; - MockERC20 private bold; - address private stakingV1; - MockERC20 private usdc; + MockERC20Tester private lqty; + MockERC20Tester private bold; + MockStakingV1 private stakingV1; + MockERC20Tester private usdc; - PoolManager private constant poolManager = PoolManager(0xE8E23e97Fa135823143d6b9Cba9c699040D51F70); ICurveStableswapFactoryNG private constant curveFactory = ICurveStableswapFactoryNG(address(0xfb37b8D939FFa77114005e61CFc2e543d6F49A81)); @@ -34,7 +31,6 @@ contract DeploySepoliaScript is Script, Deployers { uint128 private constant REGISTRATION_FEE = 100e18; uint128 private constant REGISTRATION_THRESHOLD_FACTOR = 0.001e18; uint128 private constant UNREGISTRATION_THRESHOLD_FACTOR = 3e18; - uint16 private constant REGISTRATION_WARM_UP_PERIOD = 4; uint16 private constant UNREGISTRATION_AFTER_EPOCHS = 4; uint128 private constant VOTING_THRESHOLD_FACTOR = 0.03e18; uint88 private constant MIN_CLAIM = 500e18; @@ -42,19 +38,12 @@ contract DeploySepoliaScript is Script, Deployers { uint32 private constant EPOCH_DURATION = 604800; uint32 private constant EPOCH_VOTING_CUTOFF = 518400; - // UniV4Donations Constants - uint256 private immutable VESTING_EPOCH_START = block.timestamp; - uint256 private constant VESTING_EPOCH_DURATION = 7 days; - uint24 private constant FEE = 400; - int24 constant MAX_TICK_SPACING = 32767; - // CurveV2GaugeRewards Constants uint256 private constant DURATION = 7 days; // Contracts Governance private governance; address[] private initialInitiatives; - UniV4Donations private uniV4Donations; CurveV2GaugeRewards private curveV2GaugeRewards; ICurveStableswapNG private curvePool; ILiquidityGauge private gauge; @@ -71,28 +60,26 @@ contract DeploySepoliaScript is Script, Deployers { } function deployEnvironment() private { - lqty = deployMockERC20("Liquity", "LQTY", 18); - bold = deployMockERC20("Bold", "BOLD", 18); - usdc = deployMockERC20("USD Coin", "USDC", 6); - stakingV1 = address(new MockStakingV1(address(lqty))); + (stakingV1, lqty,) = deployMockStakingV1(); + bold = new MockERC20Tester("Bold", "BOLD"); + usdc = new MockERC20Tester("USD Coin", "USDC"); } function deployGovernance() private { governance = new Governance( address(lqty), address(bold), - stakingV1, + address(stakingV1), address(bold), IGovernance.Configuration({ registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: MIN_CLAIM, minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp - VESTING_EPOCH_START), + epochStart: block.timestamp - EPOCH_DURATION, /// @audit Ensures that `initialInitiatives` can be voted on epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF @@ -100,44 +87,6 @@ contract DeploySepoliaScript is Script, Deployers { deployer, initialInitiatives ); - assert(governance == uniV4Donations.governance()); - } - - function deployUniV4Donations(uint256 _nonce) private { - address gov = address(vm.computeCreateAddress(deployer, _nonce)); - uint160 flags = uint160(Hooks.AFTER_INITIALIZE_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG); - - (, bytes32 salt) = HookMiner.find( - 0x4e59b44847b379578588920cA78FbF26c0B4956C, - // address(this), - flags, - type(UniV4Donations).creationCode, - abi.encode( - gov, - address(bold), - address(lqty), - block.timestamp, - EPOCH_DURATION, - address(poolManager), - address(usdc), - FEE, - MAX_TICK_SPACING - ) - ); - - uniV4Donations = new UniV4Donations{salt: salt}( - gov, - address(bold), - address(lqty), - block.timestamp, - EPOCH_DURATION, - address(poolManager), - address(usdc), - FEE, - MAX_TICK_SPACING - ); - - initialInitiatives.push(address(uniV4Donations)); } function deployCurveV2GaugeRewards(uint256 _nonce) private { @@ -176,7 +125,6 @@ contract DeploySepoliaScript is Script, Deployers { function run() public { vm.startBroadcast(privateKey); deployEnvironment(); - deployUniV4Donations(nonce + 8); deployGovernance(); vm.stopBroadcast(); } diff --git a/src/BribeInitiative.sol b/src/BribeInitiative.sol index e71f26ff..1511a264 100644 --- a/src/BribeInitiative.sol +++ b/src/BribeInitiative.sol @@ -1,21 +1,23 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity 0.8.24; import {IERC20} from "openzeppelin/contracts/interfaces/IERC20.sol"; import {SafeERC20} from "openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IGovernance} from "./interfaces/IGovernance.sol"; +import {IGovernance, UNREGISTERED_INITIATIVE} from "./interfaces/IGovernance.sol"; import {IInitiative} from "./interfaces/IInitiative.sol"; import {IBribeInitiative} from "./interfaces/IBribeInitiative.sol"; import {DoubleLinkedList} from "./utils/DoubleLinkedList.sol"; - -import {EncodingDecodingLib} from "src/utils/EncodingDecodingLib.sol"; +import {_lqtyToVotes} from "./utils/VotingPower.sol"; contract BribeInitiative is IInitiative, IBribeInitiative { using SafeERC20 for IERC20; using DoubleLinkedList for DoubleLinkedList.List; + uint256 internal immutable EPOCH_START; + uint256 internal immutable EPOCH_DURATION; + /// @inheritdoc IBribeInitiative IGovernance public immutable governance; /// @inheritdoc IBribeInitiative @@ -24,9 +26,9 @@ contract BribeInitiative is IInitiative, IBribeInitiative { IERC20 public immutable bribeToken; /// @inheritdoc IBribeInitiative - mapping(uint16 => Bribe) public bribeByEpoch; + mapping(uint256 => Bribe) public bribeByEpoch; /// @inheritdoc IBribeInitiative - mapping(address => mapping(uint16 => bool)) public claimedBribeAtEpoch; + mapping(address => mapping(uint256 => bool)) public claimedBribeAtEpoch; /// Double linked list of the total LQTY allocated at a given epoch DoubleLinkedList.List internal totalLQTYAllocationByEpoch; @@ -34,9 +36,14 @@ contract BribeInitiative is IInitiative, IBribeInitiative { mapping(address => DoubleLinkedList.List) internal lqtyAllocationByUserAtEpoch; constructor(address _governance, address _bold, address _bribeToken) { + require(_bribeToken != _bold, "BribeInitiative: bribe-token-cannot-be-bold"); + governance = IGovernance(_governance); bold = IERC20(_bold); bribeToken = IERC20(_bribeToken); + + EPOCH_START = governance.EPOCH_START(); + EPOCH_DURATION = governance.EPOCH_DURATION(); } modifier onlyGovernance() { @@ -45,24 +52,25 @@ contract BribeInitiative is IInitiative, IBribeInitiative { } /// @inheritdoc IBribeInitiative - function totalLQTYAllocatedByEpoch(uint16 _epoch) external view returns (uint88, uint120) { - return _loadTotalLQTYAllocation(_epoch); + function totalLQTYAllocatedByEpoch(uint256 _epoch) external view returns (uint256, uint256) { + return (totalLQTYAllocationByEpoch.items[_epoch].lqty, totalLQTYAllocationByEpoch.items[_epoch].offset); } /// @inheritdoc IBribeInitiative - function lqtyAllocatedByUserAtEpoch(address _user, uint16 _epoch) external view returns (uint88, uint120) { - return _loadLQTYAllocation(_user, _epoch); + function lqtyAllocatedByUserAtEpoch(address _user, uint256 _epoch) external view returns (uint256, uint256) { + return ( + lqtyAllocationByUserAtEpoch[_user].items[_epoch].lqty, + lqtyAllocationByUserAtEpoch[_user].items[_epoch].offset + ); } /// @inheritdoc IBribeInitiative - function depositBribe(uint128 _boldAmount, uint128 _bribeTokenAmount, uint16 _epoch) external { - uint16 epoch = governance.epoch(); + function depositBribe(uint256 _boldAmount, uint256 _bribeTokenAmount, uint256 _epoch) external { + uint256 epoch = governance.epoch(); require(_epoch >= epoch, "BribeInitiative: now-or-future-epochs"); - Bribe memory bribe = bribeByEpoch[_epoch]; - bribe.boldAmount += _boldAmount; - bribe.bribeTokenAmount += _bribeTokenAmount; - bribeByEpoch[_epoch] = bribe; + bribeByEpoch[_epoch].remainingBoldAmount += _boldAmount; + bribeByEpoch[_epoch].remainingBribeTokenAmount += _bribeTokenAmount; emit DepositBribe(msg.sender, _boldAmount, _bribeTokenAmount, _epoch); @@ -70,58 +78,53 @@ contract BribeInitiative is IInitiative, IBribeInitiative { bribeToken.safeTransferFrom(msg.sender, address(this), _bribeTokenAmount); } - uint256 constant TIMESTAMP_PRECISION = 1e26; - function _claimBribe( address _user, - uint16 _epoch, - uint16 _prevLQTYAllocationEpoch, - uint16 _prevTotalLQTYAllocationEpoch + uint256 _epoch, + uint256 _prevLQTYAllocationEpoch, + uint256 _prevTotalLQTYAllocationEpoch ) internal returns (uint256 boldAmount, uint256 bribeTokenAmount) { require(_epoch < governance.epoch(), "BribeInitiative: cannot-claim-for-current-epoch"); require(!claimedBribeAtEpoch[_user][_epoch], "BribeInitiative: already-claimed"); Bribe memory bribe = bribeByEpoch[_epoch]; - require(bribe.boldAmount != 0 || bribe.bribeTokenAmount != 0, "BribeInitiative: no-bribe"); + require(bribe.remainingBoldAmount != 0 || bribe.remainingBribeTokenAmount != 0, "BribeInitiative: no-bribe"); DoubleLinkedList.Item memory lqtyAllocation = lqtyAllocationByUserAtEpoch[_user].getItem(_prevLQTYAllocationEpoch); require( - lqtyAllocation.value != 0 && _prevLQTYAllocationEpoch <= _epoch - && (lqtyAllocation.next > _epoch || lqtyAllocation.next == 0), + _prevLQTYAllocationEpoch <= _epoch && (lqtyAllocation.next > _epoch || lqtyAllocation.next == 0), "BribeInitiative: invalid-prev-lqty-allocation-epoch" ); DoubleLinkedList.Item memory totalLQTYAllocation = totalLQTYAllocationByEpoch.getItem(_prevTotalLQTYAllocationEpoch); require( - totalLQTYAllocation.value != 0 && _prevTotalLQTYAllocationEpoch <= _epoch + _prevTotalLQTYAllocationEpoch <= _epoch && (totalLQTYAllocation.next > _epoch || totalLQTYAllocation.next == 0), "BribeInitiative: invalid-prev-total-lqty-allocation-epoch" ); - (uint88 totalLQTY, uint120 totalAverageTimestamp) = _decodeLQTYAllocation(totalLQTYAllocation.value); - - // NOTE: SCALING!!! | The timestamp will work until type(uint32).max | After which the math will eventually overflow - uint120 scaledEpochEnd = ( - uint120(governance.EPOCH_START()) + uint120(_epoch) * uint120(governance.EPOCH_DURATION()) - ) * uint120(TIMESTAMP_PRECISION); - - /// @audit User Invariant - assert(totalAverageTimestamp <= scaledEpochEnd); + require(totalLQTYAllocation.lqty > 0, "BribeInitiative: total-lqty-allocation-zero"); + require(lqtyAllocation.lqty > 0, "BribeInitiative: lqty-allocation-zero"); - uint240 totalVotes = governance.lqtyToVotes(totalLQTY, scaledEpochEnd, totalAverageTimestamp); - if (totalVotes != 0) { - (uint88 lqty, uint120 averageTimestamp) = _decodeLQTYAllocation(lqtyAllocation.value); + // `Governance` guarantees that `votes` evaluates to 0 or greater for each initiative at the time of allocation. + // Since the last possible moment to allocate within this epoch is 1 second before `epochEnd`, we have that: + // - `lqtyAllocation.lqty > 0` implies `votes > 0` + // - `totalLQTYAllocation.lqty > 0` implies `totalVotes > 0` - /// @audit Governance Invariant - assert(averageTimestamp <= scaledEpochEnd); + uint256 epochEnd = EPOCH_START + _epoch * EPOCH_DURATION; + uint256 totalVotes = _lqtyToVotes(totalLQTYAllocation.lqty, epochEnd, totalLQTYAllocation.offset); + uint256 votes = _lqtyToVotes(lqtyAllocation.lqty, epochEnd, lqtyAllocation.offset); + uint256 remainingVotes = totalVotes - bribe.claimedVotes; - uint240 votes = governance.lqtyToVotes(lqty, scaledEpochEnd, averageTimestamp); - boldAmount = uint256(bribe.boldAmount) * uint256(votes) / uint256(totalVotes); - bribeTokenAmount = uint256(bribe.bribeTokenAmount) * uint256(votes) / uint256(totalVotes); - } + boldAmount = bribe.remainingBoldAmount * votes / remainingVotes; + bribeTokenAmount = bribe.remainingBribeTokenAmount * votes / remainingVotes; + bribe.remainingBoldAmount -= boldAmount; + bribe.remainingBribeTokenAmount -= bribeTokenAmount; + bribe.claimedVotes += votes; + bribeByEpoch[_epoch] = bribe; claimedBribeAtEpoch[_user][_epoch] = true; emit ClaimBribe(_user, _epoch, boldAmount, bribeTokenAmount); @@ -141,108 +144,70 @@ contract BribeInitiative is IInitiative, IBribeInitiative { bribeTokenAmount += bribeTokenAmount_; } - // NOTE: Due to rounding errors in the `averageTimestamp` bribes may slightly overpay compared to what they have allocated - // We cap to the available amount for this reason - // The error should be below 10 LQTY per annum, in the worst case - if (boldAmount != 0) { - uint256 max = bold.balanceOf(address(this)); - if (boldAmount > max) { - boldAmount = max; - } - bold.safeTransfer(msg.sender, boldAmount); - } - - if (bribeTokenAmount != 0) { - uint256 max = bribeToken.balanceOf(address(this)); - if (bribeTokenAmount > max) { - bribeTokenAmount = max; - } - bribeToken.safeTransfer(msg.sender, bribeTokenAmount); - } + if (boldAmount != 0) bold.safeTransfer(msg.sender, boldAmount); + if (bribeTokenAmount != 0) bribeToken.safeTransfer(msg.sender, bribeTokenAmount); } /// @inheritdoc IInitiative - function onRegisterInitiative(uint16) external virtual override onlyGovernance {} + function onRegisterInitiative(uint256) external virtual override onlyGovernance {} /// @inheritdoc IInitiative - function onUnregisterInitiative(uint16) external virtual override onlyGovernance {} + function onUnregisterInitiative(uint256) external virtual override onlyGovernance {} - function _setTotalLQTYAllocationByEpoch(uint16 _epoch, uint88 _lqty, uint120 _averageTimestamp, bool _insert) - private - { - uint224 value = _encodeLQTYAllocation(_lqty, _averageTimestamp); + function _setTotalLQTYAllocationByEpoch(uint256 _epoch, uint256 _lqty, uint256 _offset, bool _insert) private { if (_insert) { - totalLQTYAllocationByEpoch.insert(_epoch, value, 0); + totalLQTYAllocationByEpoch.insert(_epoch, _lqty, _offset, 0); } else { - totalLQTYAllocationByEpoch.items[_epoch].value = value; + totalLQTYAllocationByEpoch.items[_epoch].lqty = _lqty; + totalLQTYAllocationByEpoch.items[_epoch].offset = _offset; } - emit ModifyTotalLQTYAllocation(_epoch, _lqty, _averageTimestamp); + emit ModifyTotalLQTYAllocation(_epoch, _lqty, _offset); } function _setLQTYAllocationByUserAtEpoch( address _user, - uint16 _epoch, - uint88 _lqty, - uint120 _averageTimestamp, + uint256 _epoch, + uint256 _lqty, + uint256 _offset, bool _insert ) private { - uint224 value = _encodeLQTYAllocation(_lqty, _averageTimestamp); if (_insert) { - lqtyAllocationByUserAtEpoch[_user].insert(_epoch, value, 0); + lqtyAllocationByUserAtEpoch[_user].insert(_epoch, _lqty, _offset, 0); } else { - lqtyAllocationByUserAtEpoch[_user].items[_epoch].value = value; + lqtyAllocationByUserAtEpoch[_user].items[_epoch].lqty = _lqty; + lqtyAllocationByUserAtEpoch[_user].items[_epoch].offset = _offset; } - emit ModifyLQTYAllocation(_user, _epoch, _lqty, _averageTimestamp); - } - - function _encodeLQTYAllocation(uint88 _lqty, uint120 _averageTimestamp) private pure returns (uint224) { - return EncodingDecodingLib.encodeLQTYAllocation(_lqty, _averageTimestamp); - } - - function _decodeLQTYAllocation(uint224 _value) private pure returns (uint88, uint120) { - return EncodingDecodingLib.decodeLQTYAllocation(_value); - } - - function _loadTotalLQTYAllocation(uint16 _epoch) private view returns (uint88, uint120) { - require(_epoch <= governance.epoch(), "No future Lookup"); - return _decodeLQTYAllocation(totalLQTYAllocationByEpoch.items[_epoch].value); - } - - function _loadLQTYAllocation(address _user, uint16 _epoch) private view returns (uint88, uint120) { - require(_epoch <= governance.epoch(), "No future Lookup"); - return _decodeLQTYAllocation(lqtyAllocationByUserAtEpoch[_user].items[_epoch].value); + emit ModifyLQTYAllocation(_user, _epoch, _lqty, _offset); } /// @inheritdoc IBribeInitiative - function getMostRecentUserEpoch(address _user) external view returns (uint16) { - uint16 mostRecentUserEpoch = lqtyAllocationByUserAtEpoch[_user].getHead(); + function getMostRecentUserEpoch(address _user) external view returns (uint256) { + uint256 mostRecentUserEpoch = lqtyAllocationByUserAtEpoch[_user].getHead(); return mostRecentUserEpoch; } /// @inheritdoc IBribeInitiative - function getMostRecentTotalEpoch() external view returns (uint16) { - uint16 mostRecentTotalEpoch = totalLQTYAllocationByEpoch.getHead(); + function getMostRecentTotalEpoch() external view returns (uint256) { + uint256 mostRecentTotalEpoch = totalLQTYAllocationByEpoch.getHead(); return mostRecentTotalEpoch; } function onAfterAllocateLQTY( - uint16 _currentEpoch, + uint256 _currentEpoch, address _user, - IGovernance.UserState calldata _userState, + IGovernance.UserState calldata, IGovernance.Allocation calldata _allocation, IGovernance.InitiativeState calldata _initiativeState ) external virtual onlyGovernance { - if (_currentEpoch == 0) return; - - uint16 mostRecentUserEpoch = lqtyAllocationByUserAtEpoch[_user].getHead(); - uint16 mostRecentTotalEpoch = totalLQTYAllocationByEpoch.getHead(); + uint256 mostRecentUserEpoch = lqtyAllocationByUserAtEpoch[_user].getHead(); + uint256 mostRecentTotalEpoch = totalLQTYAllocationByEpoch.getHead(); _setTotalLQTYAllocationByEpoch( _currentEpoch, _initiativeState.voteLQTY, - _initiativeState.averageStakingTimestampVoteLQTY, + _initiativeState.voteOffset, mostRecentTotalEpoch != _currentEpoch // Insert if current > recent ); @@ -250,11 +215,11 @@ contract BribeInitiative is IInitiative, IBribeInitiative { _user, _currentEpoch, _allocation.voteLQTY, - _userState.averageStakingTimestamp, + _allocation.voteOffset, mostRecentUserEpoch != _currentEpoch // Insert if user current > recent ); } /// @inheritdoc IInitiative - function onClaimForInitiative(uint16, uint256) external virtual override onlyGovernance {} + function onClaimForInitiative(uint256, uint256) external virtual override onlyGovernance {} } diff --git a/src/CurveV2GaugeRewards.sol b/src/CurveV2GaugeRewards.sol index 365e432f..dea21ee6 100644 --- a/src/CurveV2GaugeRewards.sol +++ b/src/CurveV2GaugeRewards.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity 0.8.24; import {ILiquidityGauge} from "./../src/interfaces/ILiquidityGauge.sol"; @@ -22,11 +22,10 @@ contract CurveV2GaugeRewards is BribeInitiative { /// @notice Governance transfers Bold, and we deposit it into the gauge /// @dev Doing this allows anyone to trigger the claim - function onClaimForInitiative(uint16, uint256 _bold) external override onlyGovernance { + function onClaimForInitiative(uint256, uint256 _bold) external override onlyGovernance { _depositIntoGauge(_bold); } - // TODO: If this is capped, we may need to donate here, so cap it here as well function _depositIntoGauge(uint256 amount) internal { uint256 total = amount + remainder; diff --git a/src/ForwardBribe.sol b/src/ForwardBribe.sol deleted file mode 100644 index d1cf4cca..00000000 --- a/src/ForwardBribe.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import {IERC20} from "openzeppelin/contracts/interfaces/IERC20.sol"; -import {SafeERC20} from "openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import {BribeInitiative} from "./BribeInitiative.sol"; - -contract ForwardBribe is BribeInitiative { - using SafeERC20 for IERC20; - - address public immutable receiver; - - constructor(address _governance, address _bold, address _bribeToken, address _receiver) - BribeInitiative(_governance, _bold, _bribeToken) - { - receiver = _receiver; - } - - function forwardBribe() external { - governance.claimForInitiative(address(this)); - - uint boldAmount = bold.balanceOf(address(this)); - uint bribeTokenAmount = bribeToken.balanceOf(address(this)); - - if (boldAmount != 0) bold.transfer(receiver, boldAmount); - if (bribeTokenAmount != 0) bribeToken.transfer(receiver, bribeTokenAmount); - } -} diff --git a/src/Governance.sol b/src/Governance.sol index d9c711c5..4aa58e74 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -1,26 +1,27 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity 0.8.24; import {IERC20} from "openzeppelin/contracts/interfaces/IERC20.sol"; import {SafeERC20} from "openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import {IGovernance} from "./interfaces/IGovernance.sol"; +import {IGovernance, UNREGISTERED_INITIATIVE} from "./interfaces/IGovernance.sol"; import {IInitiative} from "./interfaces/IInitiative.sol"; import {ILQTYStaking} from "./interfaces/ILQTYStaking.sol"; import {UserProxy} from "./UserProxy.sol"; import {UserProxyFactory} from "./UserProxyFactory.sol"; -import {add, max} from "./utils/Math.sol"; +import {add, sub, max} from "./utils/Math.sol"; import {_requireNoDuplicates, _requireNoNegatives} from "./utils/UniqueArray.sol"; -import {Multicall} from "./utils/Multicall.sol"; +import {MultiDelegateCall} from "./utils/MultiDelegateCall.sol"; import {WAD, PermitParams} from "./utils/Types.sol"; import {safeCallWithMinGas} from "./utils/SafeCallMinGas.sol"; import {Ownable} from "./utils/Ownable.sol"; +import {_lqtyToVotes} from "./utils/VotingPower.sol"; /// @title Governance: Modular Initiative based Governance -contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IGovernance { +contract Governance is MultiDelegateCall, UserProxyFactory, ReentrancyGuard, Ownable, IGovernance { using SafeERC20 for IERC20; uint256 constant MIN_GAS_TO_HOOK = 350_000; @@ -50,8 +51,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG /// @inheritdoc IGovernance uint256 public immutable UNREGISTRATION_THRESHOLD_FACTOR; /// @inheritdoc IGovernance - uint256 public immutable REGISTRATION_WARM_UP_PERIOD; - /// @inheritdoc IGovernance uint256 public immutable UNREGISTRATION_AFTER_EPOCHS; /// @inheritdoc IGovernance uint256 public immutable VOTING_THRESHOLD_FACTOR; @@ -73,12 +72,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG /// @inheritdoc IGovernance mapping(address => mapping(address => Allocation)) public lqtyAllocatedByUserToInitiative; /// @inheritdoc IGovernance - mapping(address => uint16) public override registeredInitiatives; - - uint16 constant UNREGISTERED_INITIATIVE = type(uint16).max; - - // 100 Million LQTY will be necessary to make the rounding error cause 1 second of loss per operation - uint120 public constant TIMESTAMP_PRECISION = 1e26; + mapping(address => uint256) public override registeredInitiatives; constructor( address _lqty, @@ -102,8 +96,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG // Unregistration must be X times above the `votingThreshold` require(_config.unregistrationThresholdFactor > WAD, "Gov: unregistration-config"); UNREGISTRATION_THRESHOLD_FACTOR = _config.unregistrationThresholdFactor; - - REGISTRATION_WARM_UP_PERIOD = _config.registrationWarmUpPeriod; UNREGISTRATION_AFTER_EPOCHS = _config.unregistrationAfterEpochs; // Voting threshold must be below 100% of votes @@ -112,6 +104,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG MIN_CLAIM = _config.minClaim; MIN_ACCRUAL = _config.minAccrual; + require(_config.epochStart <= block.timestamp, "Gov: cannot-start-in-future"); EPOCH_START = _config.epochStart; require(_config.epochDuration > 0, "Gov: epoch-duration-zero"); EPOCH_DURATION = _config.epochDuration; @@ -124,74 +117,28 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG } function registerInitialInitiatives(address[] memory _initiatives) public onlyOwner { - uint16 currentEpoch = epoch(); - for (uint256 i = 0; i < _initiatives.length; i++) { - initiativeStates[_initiatives[i]] = InitiativeState(0, 0, 0, 0, 0); - registeredInitiatives[_initiatives[i]] = currentEpoch; - - emit RegisterInitiative(_initiatives[i], msg.sender, currentEpoch); - } + // Register initial initiatives in the earliest possible epoch, which lets us make them votable immediately + // post-deployment if we so choose, by backdating the first epoch at least EPOCH_DURATION in the past. + registeredInitiatives[_initiatives[i]] = 1; - _renounceOwnership(); - } + bool success = safeCallWithMinGas( + _initiatives[i], MIN_GAS_TO_HOOK, 0, abi.encodeCall(IInitiative.onRegisterInitiative, (1)) + ); - function _averageAge(uint120 _currentTimestamp, uint120 _averageTimestamp) internal pure returns (uint120) { - if (_averageTimestamp == 0 || _currentTimestamp < _averageTimestamp) return 0; - return _currentTimestamp - _averageTimestamp; - } - - function _calculateAverageTimestamp( - uint120 _prevOuterAverageTimestamp, - uint120 _newInnerAverageTimestamp, - uint88 _prevLQTYBalance, - uint88 _newLQTYBalance - ) internal view returns (uint120) { - if (_newLQTYBalance == 0) return 0; - - // NOTE: Truncation - // NOTE: u32 -> u120 - // While we upscale the Timestamp, the system will stop working at type(uint32).max - // Because the rest of the type is used for precision - uint120 currentTime = uint120(uint32(block.timestamp)) * uint120(TIMESTAMP_PRECISION); - - uint120 prevOuterAverageAge = _averageAge(currentTime, _prevOuterAverageTimestamp); - uint120 newInnerAverageAge = _averageAge(currentTime, _newInnerAverageTimestamp); - - // 120 for timestamps = 2^32 * 1e18 | 2^32 * 1e26 - // 208 for voting power = 2^120 * 2^88 - // NOTE: 208 / X can go past u120! - // Therefore we keep `newOuterAverageAge` as u208 - uint208 newOuterAverageAge; - if (_prevLQTYBalance <= _newLQTYBalance) { - uint88 deltaLQTY = _newLQTYBalance - _prevLQTYBalance; - uint208 prevVotes = uint208(_prevLQTYBalance) * uint208(prevOuterAverageAge); - uint208 newVotes = uint208(deltaLQTY) * uint208(newInnerAverageAge); - uint208 votes = prevVotes + newVotes; - newOuterAverageAge = votes / _newLQTYBalance; - } else { - uint88 deltaLQTY = _prevLQTYBalance - _newLQTYBalance; - uint208 prevVotes = uint208(_prevLQTYBalance) * uint208(prevOuterAverageAge); - uint208 newVotes = uint208(deltaLQTY) * uint208(newInnerAverageAge); - uint208 votes = (prevVotes >= newVotes) ? prevVotes - newVotes : 0; - newOuterAverageAge = votes / _newLQTYBalance; + emit RegisterInitiative(_initiatives[i], msg.sender, 1, success ? HookStatus.Succeeded : HookStatus.Failed); } - if (newOuterAverageAge > currentTime) return 0; - return uint120(currentTime - newOuterAverageAge); + _renounceOwnership(); } /*////////////////////////////////////////////////////////////// STAKING //////////////////////////////////////////////////////////////*/ - function _updateUserTimestamp(uint88 _lqtyAmount) private returns (UserProxy) { + function _increaseUserVoteTrackers(uint256 _lqtyAmount) private returns (UserProxy) { require(_lqtyAmount > 0, "Governance: zero-lqty-amount"); - // Assert that we have resetted here - UserState memory userState = userStates[msg.sender]; - require(userState.allocatedLQTY == 0, "Governance: must-be-zero-allocation"); - address userProxyAddress = deriveUserProxyAddress(msg.sender); if (userProxyAddress.code.length == 0) { @@ -200,57 +147,101 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG UserProxy userProxy = UserProxy(payable(userProxyAddress)); - uint88 lqtyStaked = uint88(stakingV1.stakes(userProxyAddress)); + // update the vote power trackers + userStates[msg.sender].unallocatedLQTY += _lqtyAmount; + userStates[msg.sender].unallocatedOffset += block.timestamp * _lqtyAmount; + + return userProxy; + } - // update the average staked timestamp for LQTY staked by the user + /// @inheritdoc IGovernance + function depositLQTY(uint256 _lqtyAmount) external { + depositLQTY(_lqtyAmount, false, msg.sender); + } - // NOTE: Upscale user TS by `TIMESTAMP_PRECISION` - userState.averageStakingTimestamp = _calculateAverageTimestamp( - userState.averageStakingTimestamp, - uint120(block.timestamp) * uint120(TIMESTAMP_PRECISION), - lqtyStaked, - lqtyStaked + _lqtyAmount - ); - userStates[msg.sender] = userState; + function depositLQTY(uint256 _lqtyAmount, bool _doSendRewards, address _recipient) public nonReentrant { + UserProxy userProxy = _increaseUserVoteTrackers(_lqtyAmount); - emit DepositLQTY(msg.sender, _lqtyAmount); + (uint256 lusdReceived, uint256 lusdSent, uint256 ethReceived, uint256 ethSent) = + userProxy.stake(_lqtyAmount, msg.sender, _doSendRewards, _recipient); - return userProxy; + emit DepositLQTY(msg.sender, _recipient, _lqtyAmount, lusdReceived, lusdSent, ethReceived, ethSent); } /// @inheritdoc IGovernance - function depositLQTY(uint88 _lqtyAmount) external nonReentrant { - UserProxy userProxy = _updateUserTimestamp(_lqtyAmount); - userProxy.stake(_lqtyAmount, msg.sender); + function depositLQTYViaPermit(uint256 _lqtyAmount, PermitParams calldata _permitParams) external { + depositLQTYViaPermit(_lqtyAmount, _permitParams, false, msg.sender); } - /// @inheritdoc IGovernance - function depositLQTYViaPermit(uint88 _lqtyAmount, PermitParams calldata _permitParams) external nonReentrant { - UserProxy userProxy = _updateUserTimestamp(_lqtyAmount); - userProxy.stakeViaPermit(_lqtyAmount, msg.sender, _permitParams); + function depositLQTYViaPermit( + uint256 _lqtyAmount, + PermitParams calldata _permitParams, + bool _doSendRewards, + address _recipient + ) public nonReentrant { + UserProxy userProxy = _increaseUserVoteTrackers(_lqtyAmount); + + (uint256 lusdReceived, uint256 lusdSent, uint256 ethReceived, uint256 ethSent) = + userProxy.stakeViaPermit(_lqtyAmount, msg.sender, _permitParams, _doSendRewards, _recipient); + + emit DepositLQTY(msg.sender, _recipient, _lqtyAmount, lusdReceived, lusdSent, ethReceived, ethSent); } /// @inheritdoc IGovernance - function withdrawLQTY(uint88 _lqtyAmount) external nonReentrant { - // check that user has reset before changing lqty balance + function withdrawLQTY(uint256 _lqtyAmount) external { + withdrawLQTY(_lqtyAmount, true, msg.sender); + } + + function withdrawLQTY(uint256 _lqtyAmount, bool _doSendRewards, address _recipient) public nonReentrant { UserState storage userState = userStates[msg.sender]; - require(userState.allocatedLQTY == 0, "Governance: must-allocate-zero"); UserProxy userProxy = UserProxy(payable(deriveUserProxyAddress(msg.sender))); require(address(userProxy).code.length != 0, "Governance: user-proxy-not-deployed"); - uint88 lqtyStaked = uint88(stakingV1.stakes(address(userProxy))); + // check if user has enough unallocated lqty + require(_lqtyAmount <= userState.unallocatedLQTY, "Governance: insufficient-unallocated-lqty"); + + // Update the offset tracker + if (_lqtyAmount < userState.unallocatedLQTY) { + // The offset decrease is proportional to the partial lqty decrease + uint256 offsetDecrease = _lqtyAmount * userState.unallocatedOffset / userState.unallocatedLQTY; + userState.unallocatedOffset -= offsetDecrease; + } else { + // if _lqtyAmount == userState.unallocatedLqty, zero the offset tracker + userState.unallocatedOffset = 0; + } + + // Update the user's LQTY tracker + userState.unallocatedLQTY -= _lqtyAmount; - (uint256 accruedLUSD, uint256 accruedETH) = userProxy.unstake(_lqtyAmount, msg.sender); + ( + uint256 lqtyReceived, + uint256 lqtySent, + uint256 lusdReceived, + uint256 lusdSent, + uint256 ethReceived, + uint256 ethSent + ) = userProxy.unstake(_lqtyAmount, _doSendRewards, _recipient); - emit WithdrawLQTY(msg.sender, _lqtyAmount, accruedLUSD, accruedETH); + emit WithdrawLQTY(msg.sender, _recipient, lqtyReceived, lqtySent, lusdReceived, lusdSent, ethReceived, ethSent); } /// @inheritdoc IGovernance - function claimFromStakingV1(address _rewardRecipient) external returns (uint256 accruedLUSD, uint256 accruedETH) { + function claimFromStakingV1(address _rewardRecipient) external returns (uint256 lusdSent, uint256 ethSent) { address payable userProxyAddress = payable(deriveUserProxyAddress(msg.sender)); require(userProxyAddress.code.length != 0, "Governance: user-proxy-not-deployed"); - return UserProxy(userProxyAddress).unstake(0, _rewardRecipient); + + uint256 lqtyReceived; + uint256 lqtySent; + uint256 lusdReceived; + uint256 ethReceived; + + (lqtyReceived, lqtySent, lusdReceived, lusdSent, ethReceived, ethSent) = + UserProxy(userProxyAddress).unstake(0, true, _rewardRecipient); + + emit WithdrawLQTY( + msg.sender, _rewardRecipient, lqtyReceived, lqtySent, lusdReceived, lusdSent, ethReceived, ethSent + ); } /*////////////////////////////////////////////////////////////// @@ -258,33 +249,23 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG //////////////////////////////////////////////////////////////*/ /// @inheritdoc IGovernance - function epoch() public view returns (uint16) { - if (block.timestamp < EPOCH_START) { - return 0; - } - return uint16(((block.timestamp - EPOCH_START) / EPOCH_DURATION) + 1); + function epoch() public view returns (uint256) { + return ((block.timestamp - EPOCH_START) / EPOCH_DURATION) + 1; } /// @inheritdoc IGovernance - function epochStart() public view returns (uint32) { - uint16 currentEpoch = epoch(); - if (currentEpoch == 0) return 0; - return uint32(EPOCH_START + (currentEpoch - 1) * EPOCH_DURATION); + function epochStart() public view returns (uint256) { + return EPOCH_START + (epoch() - 1) * EPOCH_DURATION; } /// @inheritdoc IGovernance - function secondsWithinEpoch() public view returns (uint32) { - if (block.timestamp < EPOCH_START) return 0; - return uint32((block.timestamp - EPOCH_START) % EPOCH_DURATION); + function secondsWithinEpoch() public view returns (uint256) { + return (block.timestamp - EPOCH_START) % EPOCH_DURATION; } /// @inheritdoc IGovernance - function lqtyToVotes(uint88 _lqtyAmount, uint120 _currentTimestamp, uint120 _averageTimestamp) - public - pure - returns (uint208) - { - return uint208(_lqtyAmount) * uint208(_averageAge(_currentTimestamp, _averageTimestamp)); + function lqtyToVotes(uint256 _lqtyAmount, uint256 _timestamp, uint256 _offset) public pure returns (uint256) { + return _lqtyToVotes(_lqtyAmount, _timestamp, _offset); } /*////////////////////////////////////////////////////////////// @@ -294,22 +275,18 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG /// @inheritdoc IGovernance function getLatestVotingThreshold() public view returns (uint256) { uint256 snapshotVotes = votesSnapshot.votes; - /// @audit technically can be out of synch return calculateVotingThreshold(snapshotVotes); } - /// @dev Returns the most up to date voting threshold - /// In contrast to `getLatestVotingThreshold` this function updates the snapshot - /// This ensures that the value returned is always the latest + /// @inheritdoc IGovernance function calculateVotingThreshold() public returns (uint256) { (VoteSnapshot memory snapshot,) = _snapshotVotes(); return calculateVotingThreshold(snapshot.votes); } - /// @dev Utility function to compute the threshold votes without recomputing the snapshot - /// Note that `boldAccrued` is a cached value, this function works correctly only when called after an accrual + /// @inheritdoc IGovernance function calculateVotingThreshold(uint256 _votes) public view returns (uint256) { if (_votes == 0) return 0; @@ -321,7 +298,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG return max(_votes * VOTING_THRESHOLD_FACTOR / WAD, minVotes); } - // Snapshots votes for the previous epoch and accrues funds for the current epoch + // Snapshots votes at the end of the previous epoch + // Accrues funds until the first activity of the current epoch, which are valid throughout all of the current epoch function _snapshotVotes() internal returns (VoteSnapshot memory snapshot, GlobalState memory state) { bool shouldUpdate; (snapshot, state, shouldUpdate) = getTotalVotesAndState(); @@ -330,35 +308,29 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG votesSnapshot = snapshot; uint256 boldBalance = bold.balanceOf(address(this)); boldAccrued = (boldBalance < MIN_ACCRUAL) ? 0 : boldBalance; - emit SnapshotVotes(snapshot.votes, snapshot.forEpoch); + emit SnapshotVotes(snapshot.votes, snapshot.forEpoch, boldAccrued); } } - /// @notice Return the most up to date global snapshot and state as well as a flag to notify whether the state can be updated - /// This is a convenience function to always retrieve the most up to date state values + /// @inheritdoc IGovernance function getTotalVotesAndState() public view returns (VoteSnapshot memory snapshot, GlobalState memory state, bool shouldUpdate) { - uint16 currentEpoch = epoch(); + uint256 currentEpoch = epoch(); snapshot = votesSnapshot; state = globalState; if (snapshot.forEpoch < currentEpoch - 1) { shouldUpdate = true; - snapshot.votes = lqtyToVotes( - state.countedVoteLQTY, - uint120(epochStart()) * uint120(TIMESTAMP_PRECISION), - state.countedVoteLQTYAverageTimestamp - ); + snapshot.votes = lqtyToVotes(state.countedVoteLQTY, epochStart(), state.countedVoteOffset); snapshot.forEpoch = currentEpoch - 1; } } - // Snapshots votes for an initiative for the previous epoch but only count the votes - // if the received votes meet the voting threshold + // Snapshots votes for an initiative for the previous epoch function _snapshotVotesForInitiative(address _initiative) internal returns (InitiativeVoteSnapshot memory initiativeSnapshot, InitiativeState memory initiativeState) @@ -368,12 +340,13 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG if (shouldUpdate) { votesForInitiativeSnapshot[_initiative] = initiativeSnapshot; - emit SnapshotVotesForInitiative(_initiative, initiativeSnapshot.votes, initiativeSnapshot.forEpoch); + emit SnapshotVotesForInitiative( + _initiative, initiativeSnapshot.votes, initiativeSnapshot.vetos, initiativeSnapshot.forEpoch + ); } } - /// @dev Given an initiative address, return it's most up to date snapshot and state as well as a flag to notify whether the state can be updated - /// This is a convenience function to always retrieve the most up to date state values + /// @inheritdoc IGovernance function getInitiativeSnapshotAndState(address _initiative) public view @@ -384,19 +357,16 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG ) { // Get the storage data - uint16 currentEpoch = epoch(); + uint256 currentEpoch = epoch(); initiativeSnapshot = votesForInitiativeSnapshot[_initiative]; initiativeState = initiativeStates[_initiative]; if (initiativeSnapshot.forEpoch < currentEpoch - 1) { shouldUpdate = true; - uint120 start = uint120(epochStart()) * uint120(TIMESTAMP_PRECISION); - uint208 votes = - lqtyToVotes(initiativeState.voteLQTY, start, initiativeState.averageStakingTimestampVoteLQTY); - uint208 vetos = - lqtyToVotes(initiativeState.vetoLQTY, start, initiativeState.averageStakingTimestampVetoLQTY); - // NOTE: Upscaling to u224 is safe + uint256 start = epochStart(); + uint256 votes = lqtyToVotes(initiativeState.voteLQTY, start, initiativeState.voteOffset); + uint256 vetos = lqtyToVotes(initiativeState.vetoLQTY, start, initiativeState.vetoOffset); initiativeSnapshot.votes = votes; initiativeSnapshot.vetos = vetos; @@ -418,28 +388,11 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG FSM //////////////////////////////////////////////////////////////*/ - enum InitiativeStatus { - NONEXISTENT, - /// This Initiative Doesn't exist | This is never returned - WARM_UP, - /// This epoch was just registered - SKIP, - /// This epoch will result in no rewards and no unregistering - CLAIMABLE, - /// This epoch will result in claiming rewards - CLAIMED, - /// The rewards for this epoch have been claimed - UNREGISTERABLE, - /// Can be unregistered - DISABLED // It was already Unregistered - - } - /// @notice Given an inititive address, updates all snapshots and return the initiative state /// See the view version of `getInitiativeState` for the underlying logic on Initatives FSM function getInitiativeState(address _initiative) public - returns (InitiativeStatus status, uint16 lastEpochClaim, uint256 claimableAmount) + returns (InitiativeStatus status, uint256 lastEpochClaim, uint256 claimableAmount) { (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = @@ -454,15 +407,19 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG VoteSnapshot memory _votesSnapshot, InitiativeVoteSnapshot memory _votesForInitiativeSnapshot, InitiativeState memory _initiativeState - ) public view returns (InitiativeStatus status, uint16 lastEpochClaim, uint256 claimableAmount) { + ) public view returns (InitiativeStatus status, uint256 lastEpochClaim, uint256 claimableAmount) { + uint256 initiativeRegistrationEpoch = registeredInitiatives[_initiative]; + // == Non existent Condition == // - if (registeredInitiatives[_initiative] == 0) { + if (initiativeRegistrationEpoch == 0) { return (InitiativeStatus.NONEXISTENT, 0, 0); /// By definition it has zero rewards } + uint256 currentEpoch = epoch(); + // == Just Registered Condition == // - if (registeredInitiatives[_initiative] == epoch()) { + if (initiativeRegistrationEpoch == currentEpoch) { return (InitiativeStatus.WARM_UP, 0, 0); /// Was registered this week, cannot have rewards } @@ -471,13 +428,13 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG lastEpochClaim = initiativeStates[_initiative].lastEpochClaim; // == Disabled Condition == // - if (registeredInitiatives[_initiative] == UNREGISTERED_INITIATIVE) { + if (initiativeRegistrationEpoch == UNREGISTERED_INITIATIVE) { return (InitiativeStatus.DISABLED, lastEpochClaim, 0); /// By definition it has zero rewards } // == Already Claimed Condition == // - if (lastEpochClaim >= epoch() - 1) { + if (lastEpochClaim >= currentEpoch - 1) { // early return, we have already claimed return (InitiativeStatus.CLAIMED, lastEpochClaim, claimableAmount); } @@ -490,34 +447,24 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG // == Rewards Conditions (votes can be zero, logic is the same) == // // By definition if _votesForInitiativeSnapshot.votes > 0 then _votesSnapshot.votes > 0 + if ( + _votesForInitiativeSnapshot.votes > votingTheshold + && _votesForInitiativeSnapshot.votes > _votesForInitiativeSnapshot.vetos + ) { + uint256 claim = _votesForInitiativeSnapshot.votes * boldAccrued / _votesSnapshot.votes; - uint256 upscaledInitiativeVotes = uint256(_votesForInitiativeSnapshot.votes); - uint256 upscaledInitiativeVetos = uint256(_votesForInitiativeSnapshot.vetos); - uint256 upscaledTotalVotes = uint256(_votesSnapshot.votes); - - if (upscaledInitiativeVotes > votingTheshold && !(upscaledInitiativeVetos >= upscaledInitiativeVotes)) { - /// @audit 2^208 means we only have 2^48 left - /// Therefore we need to scale the value down by 4 orders of magnitude to make it fit - assert(upscaledInitiativeVotes * 1e14 / (VOTING_THRESHOLD_FACTOR / 1e4) > upscaledTotalVotes); - - // 34 times when using 0.03e18 -> 33.3 + 1-> 33 + 1 = 34 - uint256 CUSTOM_PRECISION = WAD / VOTING_THRESHOLD_FACTOR + 1; - - /// @audit Because of the updated timestamp, we can run into overflows if we multiply by `boldAccrued` - /// We use `CUSTOM_PRECISION` for this reason, a smaller multiplicative value - /// The change SHOULD be safe because we already check for `threshold` before getting into these lines - /// As an alternative, this line could be replaced by https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/FullMath.sol - uint256 claim = - upscaledInitiativeVotes * CUSTOM_PRECISION / upscaledTotalVotes * boldAccrued / CUSTOM_PRECISION; return (InitiativeStatus.CLAIMABLE, lastEpochClaim, claim); } // == Unregister Condition == // - // e.g. if `UNREGISTRATION_AFTER_EPOCHS` is 4, the 4th epoch flip that would result in SKIP, will result in the initiative being `UNREGISTERABLE` + // e.g. if `UNREGISTRATION_AFTER_EPOCHS` is 4, the initiative will become unregisterable after spending 4 epochs + // while being in one of the following conditions: + // - in `SKIP` state (not having received enough votes to cross the voting threshold) + // - in `CLAIMABLE` state (having received enough votes to cross the voting threshold) but never being claimed if ( - (_initiativeState.lastEpochClaim + UNREGISTRATION_AFTER_EPOCHS < epoch() - 1) - || upscaledInitiativeVetos > upscaledInitiativeVotes - && upscaledInitiativeVetos > votingTheshold * UNREGISTRATION_THRESHOLD_FACTOR / WAD + (_initiativeState.lastEpochClaim + UNREGISTRATION_AFTER_EPOCHS < currentEpoch - 1) + || _votesForInitiativeSnapshot.vetos > _votesForInitiativeSnapshot.votes + && _votesForInitiativeSnapshot.vetos > votingTheshold * UNREGISTRATION_THRESHOLD_FACTOR / WAD ) { return (InitiativeStatus.UNREGISTERABLE, lastEpochClaim, 0); } @@ -528,7 +475,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG /// @inheritdoc IGovernance function registerInitiative(address _initiative) external nonReentrant { - bold.safeTransferFrom(msg.sender, address(this), REGISTRATION_FEE); + uint256 currentEpoch = epoch(); + require(currentEpoch > 2, "Governance: registration-not-yet-enabled"); require(_initiative != address(0), "Governance: zero-address"); (InitiativeStatus status,,) = getInitiativeState(_initiative); @@ -538,38 +486,42 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG (VoteSnapshot memory snapshot,) = _snapshotVotes(); UserState memory userState = userStates[msg.sender]; + bold.safeTransferFrom(msg.sender, address(this), REGISTRATION_FEE); + // an initiative can be registered if the registrant has more voting power (LQTY * age) // than the registration threshold derived from the previous epoch's total global votes - uint256 upscaledSnapshotVotes = uint256(snapshot.votes); + uint256 upscaledSnapshotVotes = snapshot.votes; + + uint256 totalUserOffset = userState.allocatedOffset + userState.unallocatedOffset; require( - lqtyToVotes( - uint88(stakingV1.stakes(userProxyAddress)), - uint120(epochStart()) * uint120(TIMESTAMP_PRECISION), - userState.averageStakingTimestamp - ) >= upscaledSnapshotVotes * REGISTRATION_THRESHOLD_FACTOR / WAD, + // Check against the user's total voting power, so include both allocated and unallocated LQTY + lqtyToVotes(stakingV1.stakes(userProxyAddress), epochStart(), totalUserOffset) + >= upscaledSnapshotVotes * REGISTRATION_THRESHOLD_FACTOR / WAD, "Governance: insufficient-lqty" ); - uint16 currentEpoch = epoch(); - registeredInitiatives[_initiative] = currentEpoch; - /// @audit This ensures that the initiatives has UNREGISTRATION_AFTER_EPOCHS even after the first epoch - initiativeStates[_initiative].lastEpochClaim = epoch() - 1; - - emit RegisterInitiative(_initiative, msg.sender, currentEpoch); + /// This ensures that the initiatives has UNREGISTRATION_AFTER_EPOCHS even after the first epoch + initiativeStates[_initiative].lastEpochClaim = currentEpoch - 1; // Replaces try / catch | Enforces sufficient gas is passed - safeCallWithMinGas( + bool success = safeCallWithMinGas( _initiative, MIN_GAS_TO_HOOK, 0, abi.encodeCall(IInitiative.onRegisterInitiative, (currentEpoch)) ); + + emit RegisterInitiative( + _initiative, msg.sender, currentEpoch, success ? HookStatus.Succeeded : HookStatus.Failed + ); } struct ResetInitiativeData { address initiative; - int88 LQTYVotes; - int88 LQTYVetos; + int256 LQTYVotes; + int256 LQTYVetos; + int256 OffsetVotes; + int256 OffsetVetos; } /// @dev Resets an initiative and return the previous votes @@ -581,40 +533,39 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG { ResetInitiativeData[] memory cachedData = new ResetInitiativeData[](_initiativesToReset.length); - int88[] memory deltaLQTYVotes = new int88[](_initiativesToReset.length); - int88[] memory deltaLQTYVetos = new int88[](_initiativesToReset.length); + int256[] memory deltaLQTYVotes = new int256[](_initiativesToReset.length); + int256[] memory deltaLQTYVetos = new int256[](_initiativesToReset.length); + int256[] memory deltaOffsetVotes = new int256[](_initiativesToReset.length); + int256[] memory deltaOffsetVetos = new int256[](_initiativesToReset.length); // Prepare reset data for (uint256 i; i < _initiativesToReset.length; i++) { Allocation memory alloc = lqtyAllocatedByUserToInitiative[msg.sender][_initiativesToReset[i]]; - - // Must be below, else we cannot reset" - // Makes cast safe - /// @audit Check INVARIANT: property_ensure_user_alloc_cannot_dos - assert(alloc.voteLQTY <= uint88(type(int88).max)); - assert(alloc.vetoLQTY <= uint88(type(int88).max)); + require(alloc.voteLQTY > 0 || alloc.vetoLQTY > 0, "Governance: nothing to reset"); // Cache, used to enforce limits later cachedData[i] = ResetInitiativeData({ initiative: _initiativesToReset[i], - LQTYVotes: int88(alloc.voteLQTY), - LQTYVetos: int88(alloc.vetoLQTY) + LQTYVotes: int256(alloc.voteLQTY), + LQTYVetos: int256(alloc.vetoLQTY), + OffsetVotes: int256(alloc.voteOffset), + OffsetVetos: int256(alloc.vetoOffset) }); // -0 is still 0, so its fine to flip both - deltaLQTYVotes[i] = -int88(cachedData[i].LQTYVotes); - deltaLQTYVetos[i] = -int88(cachedData[i].LQTYVetos); + deltaLQTYVotes[i] = -(cachedData[i].LQTYVotes); + deltaLQTYVetos[i] = -(cachedData[i].LQTYVetos); + deltaOffsetVotes[i] = -(cachedData[i].OffsetVotes); + deltaOffsetVetos[i] = -(cachedData[i].OffsetVetos); } // RESET HERE || All initiatives will receive most updated data and 0 votes / vetos - _allocateLQTY(_initiativesToReset, deltaLQTYVotes, deltaLQTYVetos); + _allocateLQTY(_initiativesToReset, deltaLQTYVotes, deltaLQTYVetos, deltaOffsetVotes, deltaOffsetVetos); return cachedData; } - /// @notice Reset the allocations for the initiatives being passed, must pass all initiatives else it will revert - /// NOTE: If you reset at the last day of the epoch, you won't be able to vote again - /// Use `allocateLQTY` to reset and vote + /// @inheritdoc IGovernance function resetAllocations(address[] calldata _initiativesToReset, bool checkAll) external nonReentrant { _requireNoDuplicates(_initiativesToReset); _resetInitiatives(_initiativesToReset); @@ -633,19 +584,24 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG function allocateLQTY( address[] calldata _initiativesToReset, address[] calldata _initiatives, - int88[] calldata _absoluteLQTYVotes, - int88[] calldata _absoluteLQTYVetos + int256[] calldata _absoluteLQTYVotes, + int256[] calldata _absoluteLQTYVetos ) external nonReentrant { - require(_initiatives.length == _absoluteLQTYVotes.length, "Length"); - require(_absoluteLQTYVetos.length == _absoluteLQTYVotes.length, "Length"); + require( + _initiatives.length == _absoluteLQTYVotes.length && _absoluteLQTYVotes.length == _absoluteLQTYVetos.length, + "Governance: array-length-mismatch" + ); // To ensure the change is safe, enforce uniqueness - _requireNoDuplicates(_initiatives); _requireNoDuplicates(_initiativesToReset); + _requireNoDuplicates(_initiatives); // Explicit >= 0 checks for all values since we reset values below _requireNoNegatives(_absoluteLQTYVotes); _requireNoNegatives(_absoluteLQTYVetos); + // If the goal is to remove all votes from an initiative, including in _initiativesToReset is enough + _requireNoNOP(_absoluteLQTYVotes, _absoluteLQTYVetos); + _requireNoSimultaneousVoteAndVeto(_absoluteLQTYVotes, _absoluteLQTYVetos); // You MUST always reset ResetInitiativeData[] memory cachedData = _resetInitiatives(_initiativesToReset); @@ -653,6 +609,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG /// Invariant, 0 allocated = 0 votes UserState memory userState = userStates[msg.sender]; require(userState.allocatedLQTY == 0, "must be a reset"); + require(userState.unallocatedLQTY != 0, "Governance: insufficient-or-allocated-lqty"); // avoid div-by-zero // After cutoff you can only re-apply the same vote // Or vote less @@ -683,44 +640,86 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG } } + int256[] memory absoluteOffsetVotes = new int256[](_initiatives.length); + int256[] memory absoluteOffsetVetos = new int256[](_initiatives.length); + + // Calculate the offset portions that correspond to each LQTY vote and veto portion + // By recalculating `unallocatedLQTY` & `unallocatedOffset` after each step, we ensure that rounding error + // doesn't accumulate in `unallocatedOffset`. + // However, it should be noted that this makes the exact offset allocations dependent on the ordering of the + // `_initiatives` array. + for (uint256 x; x < _initiatives.length; x++) { + // Either _absoluteLQTYVotes[x] or _absoluteLQTYVetos[x] is guaranteed to be zero + (int256[] calldata lqtyAmounts, int256[] memory offsets) = _absoluteLQTYVotes[x] > 0 + ? (_absoluteLQTYVotes, absoluteOffsetVotes) + : (_absoluteLQTYVetos, absoluteOffsetVetos); + + uint256 lqtyAmount = uint256(lqtyAmounts[x]); + uint256 offset = userState.unallocatedOffset * lqtyAmount / userState.unallocatedLQTY; + + userState.unallocatedLQTY -= lqtyAmount; + userState.unallocatedOffset -= offset; + + offsets[x] = int256(offset); + } + // Vote here, all values are now absolute changes - _allocateLQTY(_initiatives, _absoluteLQTYVotes, _absoluteLQTYVetos); + _allocateLQTY(_initiatives, _absoluteLQTYVotes, _absoluteLQTYVetos, absoluteOffsetVotes, absoluteOffsetVetos); + } + + // Avoid "stack too deep" by placing these variables in memory + struct AllocateLQTYMemory { + VoteSnapshot votesSnapshot_; + GlobalState state; + UserState userState; + InitiativeVoteSnapshot votesForInitiativeSnapshot_; + InitiativeState initiativeState; + InitiativeState prevInitiativeState; + Allocation allocation; + uint256 currentEpoch; + int256 deltaLQTYVotes; + int256 deltaLQTYVetos; + int256 deltaOffsetVotes; + int256 deltaOffsetVetos; } /// @dev For each given initiative applies relative changes to the allocation - /// NOTE: Given the current usage the function either: Resets the value to 0, or sets the value to a new value - /// Review the flows as the function could be used in many ways, but it ends up being used in just those 2 ways + /// @dev Assumes that all the input arrays are of equal length + /// @dev NOTE: Given the current usage the function either: Resets the value to 0, or sets the value to a new value + /// Review the flows as the function could be used in many ways, but it ends up being used in just those 2 ways function _allocateLQTY( address[] memory _initiatives, - int88[] memory _deltaLQTYVotes, - int88[] memory _deltaLQTYVetos + int256[] memory _deltaLQTYVotes, + int256[] memory _deltaLQTYVetos, + int256[] memory _deltaOffsetVotes, + int256[] memory _deltaOffsetVetos ) internal { - require( - _initiatives.length == _deltaLQTYVotes.length && _initiatives.length == _deltaLQTYVetos.length, - "Governance: array-length-mismatch" - ); - - (VoteSnapshot memory votesSnapshot_, GlobalState memory state) = _snapshotVotes(); - uint16 currentEpoch = epoch(); - UserState memory userState = userStates[msg.sender]; + AllocateLQTYMemory memory vars; + (vars.votesSnapshot_, vars.state) = _snapshotVotes(); + vars.currentEpoch = epoch(); + vars.userState = userStates[msg.sender]; for (uint256 i = 0; i < _initiatives.length; i++) { address initiative = _initiatives[i]; - int88 deltaLQTYVotes = _deltaLQTYVotes[i]; - int88 deltaLQTYVetos = _deltaLQTYVetos[i]; + vars.deltaLQTYVotes = _deltaLQTYVotes[i]; + vars.deltaLQTYVetos = _deltaLQTYVetos[i]; + assert(vars.deltaLQTYVotes != 0 || vars.deltaLQTYVetos != 0); + + vars.deltaOffsetVotes = _deltaOffsetVotes[i]; + vars.deltaOffsetVetos = _deltaOffsetVetos[i]; /// === Check FSM === /// - // Can vote positively in SKIP, CLAIMABLE, CLAIMED and UNREGISTERABLE states + // Can vote positively in SKIP, CLAIMABLE and CLAIMED states // Force to remove votes if disabled // Can remove votes and vetos in every stage - (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = - _snapshotVotesForInitiative(initiative); + (vars.votesForInitiativeSnapshot_, vars.initiativeState) = _snapshotVotesForInitiative(initiative); - (InitiativeStatus status,,) = - getInitiativeState(initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState); + (InitiativeStatus status,,) = getInitiativeState( + initiative, vars.votesSnapshot_, vars.votesForInitiativeSnapshot_, vars.initiativeState + ); - if (deltaLQTYVotes > 0 || deltaLQTYVetos > 0) { - /// @audit You cannot vote on `unregisterable` but a vote may have been there + if (vars.deltaLQTYVotes > 0 || vars.deltaLQTYVetos > 0) { + /// You cannot vote on `unregisterable` but a vote may have been there require( status == InitiativeStatus.SKIP || status == InitiativeStatus.CLAIMABLE || status == InitiativeStatus.CLAIMED, @@ -729,124 +728,117 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG } if (status == InitiativeStatus.DISABLED) { - require(deltaLQTYVotes <= 0 && deltaLQTYVetos <= 0, "Must be a withdrawal"); + require(vars.deltaLQTYVotes <= 0 && vars.deltaLQTYVetos <= 0, "Must be a withdrawal"); } /// === UPDATE ACCOUNTING === /// // == INITIATIVE STATE == // // deep copy of the initiative's state before the allocation - InitiativeState memory prevInitiativeState = InitiativeState( - initiativeState.voteLQTY, - initiativeState.vetoLQTY, - initiativeState.averageStakingTimestampVoteLQTY, - initiativeState.averageStakingTimestampVetoLQTY, - initiativeState.lastEpochClaim - ); - - // update the average staking timestamp for the initiative based on the user's average staking timestamp - initiativeState.averageStakingTimestampVoteLQTY = _calculateAverageTimestamp( - initiativeState.averageStakingTimestampVoteLQTY, - userState.averageStakingTimestamp, - /// @audit This is wrong unless we enforce a reset on deposit and withdrawal - initiativeState.voteLQTY, - add(initiativeState.voteLQTY, deltaLQTYVotes) - ); - initiativeState.averageStakingTimestampVetoLQTY = _calculateAverageTimestamp( - initiativeState.averageStakingTimestampVetoLQTY, - userState.averageStakingTimestamp, - /// @audit This is wrong unless we enforce a reset on deposit and withdrawal - initiativeState.vetoLQTY, - add(initiativeState.vetoLQTY, deltaLQTYVetos) + vars.prevInitiativeState = InitiativeState( + vars.initiativeState.voteLQTY, + vars.initiativeState.voteOffset, + vars.initiativeState.vetoLQTY, + vars.initiativeState.vetoOffset, + vars.initiativeState.lastEpochClaim ); // allocate the voting and vetoing LQTY to the initiative - initiativeState.voteLQTY = add(initiativeState.voteLQTY, deltaLQTYVotes); - initiativeState.vetoLQTY = add(initiativeState.vetoLQTY, deltaLQTYVetos); + vars.initiativeState.voteLQTY = add(vars.initiativeState.voteLQTY, vars.deltaLQTYVotes); + vars.initiativeState.vetoLQTY = add(vars.initiativeState.vetoLQTY, vars.deltaLQTYVetos); + + // Update the initiative's vote and veto offsets + vars.initiativeState.voteOffset = add(vars.initiativeState.voteOffset, vars.deltaOffsetVotes); + vars.initiativeState.vetoOffset = add(vars.initiativeState.vetoOffset, vars.deltaOffsetVetos); // update the initiative's state - initiativeStates[initiative] = initiativeState; + initiativeStates[initiative] = vars.initiativeState; // == GLOBAL STATE == // - // TODO: Veto reducing total votes logic change - // TODO: Accounting invariants - // TODO: Let's say I want to cap the votes vs weights - // Then by definition, I add the effective LQTY - // And the effective TS - // I remove the previous one - // and add the next one - // Veto > Vote - // Reduce down by Vote (cap min) - // If Vote > Veto - // Increase by Veto - Veto (reduced max) - - // update the average staking timestamp for all counted voting LQTY - /// Discount previous only if the initiative was not unregistered - - /// @audit We update the state only for non-disabled initiaitives - /// Disabled initiaitves have had their totals subtracted already - /// Math is also non associative so we cannot easily compare values + /// We update the state only for non-disabled initiatives + /// Disabled initiatves have had their totals subtracted already if (status != InitiativeStatus.DISABLED) { - /// @audit Trophy: `test_property_sum_of_lqty_global_user_matches_0` - /// Removing votes from state desynchs the state until all users remove their votes from the initiative - /// The invariant that holds is: the one that removes the initiatives that have been unregistered - state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( - state.countedVoteLQTYAverageTimestamp, - prevInitiativeState.averageStakingTimestampVoteLQTY, - /// @audit We don't have a test that fails when this line is changed - state.countedVoteLQTY, - state.countedVoteLQTY - prevInitiativeState.voteLQTY - ); - assert(state.countedVoteLQTY >= prevInitiativeState.voteLQTY); - /// @audit INVARIANT: Never overflows - state.countedVoteLQTY -= prevInitiativeState.voteLQTY; - - state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( - state.countedVoteLQTYAverageTimestamp, - initiativeState.averageStakingTimestampVoteLQTY, - state.countedVoteLQTY, - state.countedVoteLQTY + initiativeState.voteLQTY - ); + assert(vars.state.countedVoteLQTY >= vars.prevInitiativeState.voteLQTY); + + // Remove old initative LQTY and offset from global count + vars.state.countedVoteLQTY -= vars.prevInitiativeState.voteLQTY; + vars.state.countedVoteOffset -= vars.prevInitiativeState.voteOffset; - state.countedVoteLQTY += initiativeState.voteLQTY; + // Add new initative LQTY and offset to global count + vars.state.countedVoteLQTY += vars.initiativeState.voteLQTY; + vars.state.countedVoteOffset += vars.initiativeState.voteOffset; } - // == USER ALLOCATION == // + // == USER ALLOCATION TO INITIATIVE == // - // allocate the voting and vetoing LQTY to the initiative - Allocation memory allocation = lqtyAllocatedByUserToInitiative[msg.sender][initiative]; - allocation.voteLQTY = add(allocation.voteLQTY, deltaLQTYVotes); - allocation.vetoLQTY = add(allocation.vetoLQTY, deltaLQTYVetos); - allocation.atEpoch = currentEpoch; - require(!(allocation.voteLQTY != 0 && allocation.vetoLQTY != 0), "Governance: vote-and-veto"); - lqtyAllocatedByUserToInitiative[msg.sender][initiative] = allocation; + // Record the vote and veto LQTY and offsets by user to initative + vars.allocation = lqtyAllocatedByUserToInitiative[msg.sender][initiative]; + // Update offsets + vars.allocation.voteOffset = add(vars.allocation.voteOffset, vars.deltaOffsetVotes); + vars.allocation.vetoOffset = add(vars.allocation.vetoOffset, vars.deltaOffsetVetos); - // == USER STATE == // + // Update votes and vetos + vars.allocation.voteLQTY = add(vars.allocation.voteLQTY, vars.deltaLQTYVotes); + vars.allocation.vetoLQTY = add(vars.allocation.vetoLQTY, vars.deltaLQTYVetos); - userState.allocatedLQTY = add(userState.allocatedLQTY, deltaLQTYVotes + deltaLQTYVetos); + vars.allocation.atEpoch = vars.currentEpoch; - emit AllocateLQTY(msg.sender, initiative, deltaLQTYVotes, deltaLQTYVetos, currentEpoch); + // Voting power allocated to initiatives should never be negative, else it might break reward allocation + // schemes such as `BribeInitiative` which distribute rewards in proportion to voting power allocated. + assert(vars.allocation.voteLQTY * block.timestamp >= vars.allocation.voteOffset); + assert(vars.allocation.vetoLQTY * block.timestamp >= vars.allocation.vetoOffset); - // Replaces try / catch | Enforces sufficient gas is passed - safeCallWithMinGas( - initiative, - MIN_GAS_TO_HOOK, - 0, - abi.encodeCall( - IInitiative.onAfterAllocateLQTY, (currentEpoch, msg.sender, userState, allocation, initiativeState) - ) + lqtyAllocatedByUserToInitiative[msg.sender][initiative] = vars.allocation; + + // == USER STATE == // + + // Remove from the user's unallocated LQTY and offset + vars.userState.unallocatedLQTY = + sub(vars.userState.unallocatedLQTY, (vars.deltaLQTYVotes + vars.deltaLQTYVetos)); + vars.userState.unallocatedOffset = + sub(vars.userState.unallocatedOffset, (vars.deltaOffsetVotes + vars.deltaOffsetVetos)); + + // Add to the user's allocated LQTY and offset + vars.userState.allocatedLQTY = + add(vars.userState.allocatedLQTY, (vars.deltaLQTYVotes + vars.deltaLQTYVetos)); + vars.userState.allocatedOffset = + add(vars.userState.allocatedOffset, (vars.deltaOffsetVotes + vars.deltaOffsetVetos)); + + HookStatus hookStatus; + + // See https://github.com/liquity/V2-gov/issues/125 + // A malicious initiative could try to dissuade voters from casting vetos by consuming as much gas as + // possible in the `onAfterAllocateLQTY` hook when detecting vetos. + // We deem that the risks of calling into malicous initiatives upon veto allocation far outweigh the + // benefits of notifying benevolent initiatives of vetos. + if (vars.allocation.vetoLQTY == 0) { + // Replaces try / catch | Enforces sufficient gas is passed + hookStatus = safeCallWithMinGas( + initiative, + MIN_GAS_TO_HOOK, + 0, + abi.encodeCall( + IInitiative.onAfterAllocateLQTY, + (vars.currentEpoch, msg.sender, vars.userState, vars.allocation, vars.initiativeState) + ) + ) ? HookStatus.Succeeded : HookStatus.Failed; + } else { + hookStatus = HookStatus.NotCalled; + } + + emit AllocateLQTY( + msg.sender, initiative, vars.deltaLQTYVotes, vars.deltaLQTYVetos, vars.currentEpoch, hookStatus ); } require( - userState.allocatedLQTY == 0 - || userState.allocatedLQTY <= uint88(stakingV1.stakes(deriveUserProxyAddress(msg.sender))), + vars.userState.allocatedLQTY <= stakingV1.stakes(deriveUserProxyAddress(msg.sender)), "Governance: insufficient-or-allocated-lqty" ); - globalState = state; - userStates[msg.sender] = userState; + globalState = vars.state; + userStates[msg.sender] = vars.userState; } /// @inheritdoc IGovernance @@ -858,42 +850,31 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG (InitiativeStatus status,,) = getInitiativeState(_initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState); - require(status != InitiativeStatus.NONEXISTENT, "Governance: initiative-not-registered"); - require(status != InitiativeStatus.WARM_UP, "Governance: initiative-in-warm-up"); require(status == InitiativeStatus.UNREGISTERABLE, "Governance: cannot-unregister-initiative"); // Remove weight from current state - uint16 currentEpoch = epoch(); + uint256 currentEpoch = epoch(); - /// @audit Invariant: Must only claim once or unregister // NOTE: Safe to remove | See `check_claim_soundness` assert(initiativeState.lastEpochClaim < currentEpoch - 1); - // recalculate the average staking timestamp for all counted voting LQTY if the initiative was counted in - /// @audit Trophy: `test_property_sum_of_lqty_global_user_matches_0` - // Removing votes from state desynchs the state until all users remove their votes from the initiative - - state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( - state.countedVoteLQTYAverageTimestamp, - initiativeState.averageStakingTimestampVoteLQTY, - state.countedVoteLQTY, - state.countedVoteLQTY - initiativeState.voteLQTY - ); assert(state.countedVoteLQTY >= initiativeState.voteLQTY); - /// RECON: Overflow + assert(state.countedVoteOffset >= initiativeState.voteOffset); + state.countedVoteLQTY -= initiativeState.voteLQTY; + state.countedVoteOffset -= initiativeState.voteOffset; globalState = state; - /// weeks * 2^16 > u32 so the contract will stop working before this is an issue + /// Epoch will never reach 2^256 - 1 registeredInitiatives[_initiative] = UNREGISTERED_INITIATIVE; - emit UnregisterInitiative(_initiative, currentEpoch); - // Replaces try / catch | Enforces sufficient gas is passed - safeCallWithMinGas( + bool success = safeCallWithMinGas( _initiative, MIN_GAS_TO_HOOK, 0, abi.encodeCall(IInitiative.onUnregisterInitiative, (currentEpoch)) ); + + emit UnregisterInitiative(_initiative, currentEpoch, success ? HookStatus.Succeeded : HookStatus.Failed); } /// @inheritdoc IGovernance @@ -911,7 +892,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG return 0; } - /// @audit INVARIANT: You can only claim for previous epoch + /// INVARIANT: You can only claim for previous epoch assert(votesSnapshot_.forEpoch == epoch() - 1); /// All unclaimed rewards are always recycled @@ -919,7 +900,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG /// If `lastEpochClaim` is older than epoch() - 1 it means the initiative couldn't claim any rewards this epoch initiativeStates[_initiative].lastEpochClaim = epoch() - 1; - // @audit INVARIANT, because of rounding errors the system can overpay + /// INVARIANT, because of rounding errors the system can overpay /// We upscale the timestamp to reduce the impact of the loss /// However this is still possible uint256 available = bold.balanceOf(address(this)); @@ -929,16 +910,33 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IG bold.safeTransfer(_initiative, claimableAmount); - emit ClaimForInitiative(_initiative, claimableAmount, votesSnapshot_.forEpoch); - // Replaces try / catch | Enforces sufficient gas is passed - safeCallWithMinGas( + bool success = safeCallWithMinGas( _initiative, MIN_GAS_TO_HOOK, 0, abi.encodeCall(IInitiative.onClaimForInitiative, (votesSnapshot_.forEpoch, claimableAmount)) ); + emit ClaimForInitiative( + _initiative, claimableAmount, votesSnapshot_.forEpoch, success ? HookStatus.Succeeded : HookStatus.Failed + ); + return claimableAmount; } + + function _requireNoNOP(int256[] memory _absoluteLQTYVotes, int256[] memory _absoluteLQTYVetos) internal pure { + for (uint256 i; i < _absoluteLQTYVotes.length; i++) { + require(_absoluteLQTYVotes[i] > 0 || _absoluteLQTYVetos[i] > 0, "Governance: voting nothing"); + } + } + + function _requireNoSimultaneousVoteAndVeto(int256[] memory _absoluteLQTYVotes, int256[] memory _absoluteLQTYVetos) + internal + pure + { + for (uint256 i; i < _absoluteLQTYVotes.length; i++) { + require(_absoluteLQTYVotes[i] == 0 || _absoluteLQTYVetos[i] == 0, "Governance: vote-and-veto"); + } + } } diff --git a/src/UniV4Donations.sol b/src/UniV4Donations.sol deleted file mode 100644 index 6666b18c..00000000 --- a/src/UniV4Donations.sol +++ /dev/null @@ -1,191 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import {IERC20} from "openzeppelin/contracts/interfaces/IERC20.sol"; -import {SafeERC20} from "openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; -import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; -import {PoolKey} from "v4-core/src/types/PoolKey.sol"; -import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; -import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol"; -import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; - -import {BaseHook, Hooks} from "./utils/BaseHook.sol"; -import {BribeInitiative} from "./BribeInitiative.sol"; - -contract UniV4Donations is BribeInitiative, BaseHook { - using SafeERC20 for IERC20; - using CurrencyLibrary for Currency; - using PoolIdLibrary for PoolKey; - - event DonateToPool(uint256 amount); - event RestartVesting(uint16 epoch, uint240 amount); - - uint256 public immutable VESTING_EPOCH_START; - uint256 public immutable VESTING_EPOCH_DURATION; - - address private immutable currency0; - address private immutable currency1; - uint24 private immutable fee; - int24 private immutable tickSpacing; - - struct Vesting { - uint240 amount; - uint16 epoch; - uint256 released; - } - - Vesting public vesting; - - constructor( - address _governance, - address _bold, - address _bribeToken, - uint256 _vestingEpochStart, - uint256 _vestingEpochDuration, - address _poolManager, - address _token, - uint24 _fee, - int24 _tickSpacing - ) BribeInitiative(_governance, _bold, _bribeToken) BaseHook(IPoolManager(_poolManager)) { - VESTING_EPOCH_START = _vestingEpochStart; - VESTING_EPOCH_DURATION = _vestingEpochDuration; - - if (uint256(uint160(address(_bold))) <= uint256(uint160(address(_token)))) { - currency0 = _bold; - currency1 = _token; - } else { - currency1 = _token; - currency0 = _bold; - } - fee = _fee; - tickSpacing = _tickSpacing; - } - - function vestingEpoch() public view returns (uint16) { - return uint16(((block.timestamp - VESTING_EPOCH_START) / VESTING_EPOCH_DURATION)) + 1; - } - - function vestingEpochStart() public view returns (uint256) { - return VESTING_EPOCH_START + ((vestingEpoch() - 1) * VESTING_EPOCH_DURATION); - } - - function _restartVesting(uint240 claimed) internal returns (Vesting memory) { - uint16 epoch = vestingEpoch(); - Vesting memory _vesting = vesting; - if (_vesting.epoch < epoch) { - _vesting.amount = claimed + _vesting.amount - uint240(_vesting.released); // roll over unclaimed amount - _vesting.epoch = epoch; - _vesting.released = 0; - vesting = _vesting; - emit RestartVesting(epoch, _vesting.amount); - } - return _vesting; - } - - /// @dev TO FIX - uint256 public received; - - /// @notice On claim we deposit the rewards - This is to prevent a griefing - function onClaimForInitiative(uint16, uint256 _bold) external override onlyGovernance { - received += _bold; - } - - function _donateToPool() internal returns (uint256) { - /// @audit TODO: Need to use storage value here I think - /// TODO: Test and fix release speed, which looks off - - // Claim again // NOTE: May be grifed - governance.claimForInitiative(address(this)); - - /// @audit Includes the queued rewards - uint256 toUse = received; - - // Reset - received = 0; - - // Rest of logic - Vesting memory _vesting = _restartVesting(uint240(toUse)); - uint256 amount = - (_vesting.amount * (block.timestamp - vestingEpochStart()) / VESTING_EPOCH_DURATION) - _vesting.released; - - if (amount != 0) { - PoolKey memory key = poolKey(); - - manager.donate(key, amount, 0, bytes("")); - manager.sync(key.currency0); - IERC20(Currency.unwrap(key.currency0)).safeTransfer(address(manager), amount); - manager.settle(key.currency0); - - vesting.released += amount; - - emit DonateToPool(amount); - } - - return amount; - } - - function donateToPool() public returns (uint256) { - return abi.decode(manager.unlock(abi.encode(address(this), poolKey())), (uint256)); - } - - function poolKey() public view returns (PoolKey memory key) { - key = PoolKey({ - currency0: Currency.wrap(currency0), - currency1: Currency.wrap(currency1), - fee: fee, - tickSpacing: tickSpacing, - hooks: IHooks(address(this)) - }); - } - - function getHookPermissions() public pure override returns (Hooks.Permissions memory) { - return Hooks.Permissions({ - beforeInitialize: false, - afterInitialize: true, - beforeAddLiquidity: false, - beforeRemoveLiquidity: false, - afterAddLiquidity: true, - afterRemoveLiquidity: false, - beforeSwap: false, - afterSwap: false, - beforeDonate: false, - afterDonate: false, - beforeSwapReturnDelta: false, - afterSwapReturnDelta: false, - afterAddLiquidityReturnDelta: false, - afterRemoveLiquidityReturnDelta: false - }); - } - - function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata) - external - view - override - onlyByManager - returns (bytes4) - { - require(PoolId.unwrap(poolKey().toId()) == PoolId.unwrap(key.toId()), "UniV4Donations: invalid-pool-id"); - return this.afterInitialize.selector; - } - - function afterAddLiquidity( - address, - PoolKey calldata key, - IPoolManager.ModifyLiquidityParams calldata, - BalanceDelta delta, - bytes calldata - ) external override onlyByManager returns (bytes4, BalanceDelta) { - require(PoolId.unwrap(poolKey().toId()) == PoolId.unwrap(key.toId()), "UniV4Donations: invalid-pool-id"); - _donateToPool(); - return (this.afterAddLiquidity.selector, delta); - } - - function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { - (address sender, PoolKey memory key) = abi.decode(data, (address, PoolKey)); - require(sender == address(this), "UniV4Donations: invalid-sender"); - require(PoolId.unwrap(poolKey().toId()) == PoolId.unwrap(key.toId()), "UniV4Donations: invalid-pool-id"); - return abi.encode(_donateToPool()); - } -} diff --git a/src/UserProxy.sol b/src/UserProxy.sol index 01df8665..2f1ffd7d 100644 --- a/src/UserProxy.sol +++ b/src/UserProxy.sol @@ -1,17 +1,14 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity 0.8.24; import {IERC20} from "openzeppelin/contracts/interfaces/IERC20.sol"; import {IERC20Permit} from "openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; -import {SafeERC20} from "openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IUserProxy} from "./interfaces/IUserProxy.sol"; import {ILQTYStaking} from "./interfaces/ILQTYStaking.sol"; import {PermitParams} from "./utils/Types.sol"; contract UserProxy is IUserProxy { - using SafeERC20 for IERC20; - /// @inheritdoc IUserProxy IERC20 public immutable lqty; /// @inheritdoc IUserProxy @@ -35,19 +32,36 @@ contract UserProxy is IUserProxy { } /// @inheritdoc IUserProxy - function stake(uint256 _amount, address _lqtyFrom) public onlyStakingV2 { - lqty.safeTransferFrom(_lqtyFrom, address(this), _amount); - lqty.approve(address(stakingV1), _amount); + function stake(uint256 _amount, address _lqtyFrom, bool _doSendRewards, address _recipient) + public + onlyStakingV2 + returns (uint256 lusdReceived, uint256 lusdSent, uint256 ethReceived, uint256 ethSent) + { + uint256 initialLUSDAmount = lusd.balanceOf(address(this)); + uint256 initialETHAmount = address(this).balance; + + lqty.transferFrom(_lqtyFrom, address(this), _amount); stakingV1.stake(_amount); - emit Stake(_amount, _lqtyFrom); + + uint256 lusdAmount = lusd.balanceOf(address(this)); + uint256 ethAmount = address(this).balance; + + lusdReceived = lusdAmount - initialLUSDAmount; + ethReceived = ethAmount - initialETHAmount; + + if (_doSendRewards) (lusdSent, ethSent) = _sendRewards(_recipient, lusdAmount, ethAmount); } /// @inheritdoc IUserProxy - function stakeViaPermit(uint256 _amount, address _lqtyFrom, PermitParams calldata _permitParams) - public - onlyStakingV2 - { + function stakeViaPermit( + uint256 _amount, + address _lqtyFrom, + PermitParams calldata _permitParams, + bool _doSendRewards, + address _recipient + ) external onlyStakingV2 returns (uint256 lusdReceived, uint256 lusdSent, uint256 ethReceived, uint256 ethSent) { require(_lqtyFrom == _permitParams.owner, "UserProxy: owner-not-sender"); + try IERC20Permit(address(lqty)).permit( _permitParams.owner, _permitParams.spender, @@ -57,33 +71,57 @@ contract UserProxy is IUserProxy { _permitParams.r, _permitParams.s ) {} catch {} - stake(_amount, _lqtyFrom); + + return stake(_amount, _lqtyFrom, _doSendRewards, _recipient); } /// @inheritdoc IUserProxy - function unstake(uint256 _amount, address _recipient) - public + function unstake(uint256 _amount, bool _doSendRewards, address _recipient) + external onlyStakingV2 - returns (uint256 lusdAmount, uint256 ethAmount) + returns ( + uint256 lqtyReceived, + uint256 lqtySent, + uint256 lusdReceived, + uint256 lusdSent, + uint256 ethReceived, + uint256 ethSent + ) { + uint256 initialLQTYAmount = lqty.balanceOf(address(this)); + uint256 initialLUSDAmount = lusd.balanceOf(address(this)); + uint256 initialETHAmount = address(this).balance; + stakingV1.unstake(_amount); - uint256 lqtyAmount = lqty.balanceOf(address(this)); - if (lqtyAmount > 0) lqty.safeTransfer(_recipient, lqtyAmount); - lusdAmount = lusd.balanceOf(address(this)); - if (lusdAmount > 0) lusd.safeTransfer(_recipient, lusdAmount); - ethAmount = address(this).balance; - if (ethAmount > 0) { - (bool success,) = payable(_recipient).call{value: ethAmount}(""); + lqtySent = lqty.balanceOf(address(this)); + uint256 lusdAmount = lusd.balanceOf(address(this)); + uint256 ethAmount = address(this).balance; + + lqtyReceived = lqtySent - initialLQTYAmount; + lusdReceived = lusdAmount - initialLUSDAmount; + ethReceived = ethAmount - initialETHAmount; + + if (lqtySent > 0) lqty.transfer(_recipient, lqtySent); + if (_doSendRewards) (lusdSent, ethSent) = _sendRewards(_recipient, lusdAmount, ethAmount); + } + + function _sendRewards(address _recipient, uint256 _lusdAmount, uint256 _ethAmount) + internal + returns (uint256 lusdSent, uint256 ethSent) + { + if (_lusdAmount > 0) lusd.transfer(_recipient, _lusdAmount); + if (_ethAmount > 0) { + (bool success,) = payable(_recipient).call{value: _ethAmount}(""); require(success, "UserProxy: eth-fail"); } - emit Unstake(_amount, _recipient, lusdAmount, ethAmount); + return (_lusdAmount, _ethAmount); } /// @inheritdoc IUserProxy - function staked() external view returns (uint88) { - return uint88(stakingV1.stakes(address(this))); + function staked() external view returns (uint256) { + return stakingV1.stakes(address(this)); } receive() external payable {} diff --git a/src/UserProxyFactory.sol b/src/UserProxyFactory.sol index 722f442b..28ba40a8 100644 --- a/src/UserProxyFactory.sol +++ b/src/UserProxyFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity 0.8.24; import {Clones} from "openzeppelin/contracts/proxy/Clones.sol"; diff --git a/src/interfaces/IBribeInitiative.sol b/src/interfaces/IBribeInitiative.sol index 4f349c9f..64053f9f 100644 --- a/src/interfaces/IBribeInitiative.sol +++ b/src/interfaces/IBribeInitiative.sol @@ -6,10 +6,10 @@ import {IERC20} from "openzeppelin/contracts/interfaces/IERC20.sol"; import {IGovernance} from "./IGovernance.sol"; interface IBribeInitiative { - event DepositBribe(address depositor, uint128 boldAmount, uint128 bribeTokenAmount, uint16 epoch); - event ModifyLQTYAllocation(address user, uint16 epoch, uint88 lqtyAllocated, uint120 averageTimestamp); - event ModifyTotalLQTYAllocation(uint16 epoch, uint88 totalLQTYAllocated, uint120 averageTimestamp); - event ClaimBribe(address user, uint16 epoch, uint256 boldAmount, uint256 bribeTokenAmount); + event DepositBribe(address depositor, uint256 boldAmount, uint256 bribeTokenAmount, uint256 epoch); + event ModifyLQTYAllocation(address user, uint256 epoch, uint256 lqtyAllocated, uint256 offset); + event ModifyTotalLQTYAllocation(uint256 epoch, uint256 totalLQTYAllocated, uint256 offset); + event ClaimBribe(address user, uint256 epoch, uint256 boldAmount, uint256 bribeTokenAmount); /// @notice Address of the governance contract /// @return governance Adress of the governance contract @@ -22,36 +22,45 @@ interface IBribeInitiative { function bribeToken() external view returns (IERC20 bribeToken); struct Bribe { - uint128 boldAmount; - uint128 bribeTokenAmount; // [scaled as 10 ** bribeToken.decimals()] + uint256 remainingBoldAmount; + uint256 remainingBribeTokenAmount; // [scaled as 10 ** bribeToken.decimals()] + uint256 claimedVotes; } /// @notice Amount of bribe tokens deposited for a given epoch /// @param _epoch Epoch at which the bribe was deposited - /// @return boldAmount Amount of BOLD tokens deposited - /// @return bribeTokenAmount Amount of bribe tokens deposited - function bribeByEpoch(uint16 _epoch) external view returns (uint128 boldAmount, uint128 bribeTokenAmount); + /// @return remainingBoldAmount Amount of BOLD tokens that haven't been claimed yet + /// @return remainingBribeTokenAmount Amount of bribe tokens that haven't been claimed yet + /// @return claimedVotes Sum of voting power of users who have already claimed their bribes + function bribeByEpoch(uint256 _epoch) + external + view + returns (uint256 remainingBoldAmount, uint256 remainingBribeTokenAmount, uint256 claimedVotes); /// @notice Check if a user has claimed bribes for a given epoch /// @param _user Address of the user /// @param _epoch Epoch at which the bribe may have been claimed by the user /// @return claimed If the user has claimed the bribe - function claimedBribeAtEpoch(address _user, uint16 _epoch) external view returns (bool claimed); + function claimedBribeAtEpoch(address _user, uint256 _epoch) external view returns (bool claimed); /// @notice Total LQTY allocated to the initiative at a given epoch + /// Voting power can be calculated as `totalLQTYAllocated * timestamp - offset` /// @param _epoch Epoch at which the LQTY was allocated /// @return totalLQTYAllocated Total LQTY allocated - function totalLQTYAllocatedByEpoch(uint16 _epoch) + /// @return offset Voting power offset + function totalLQTYAllocatedByEpoch(uint256 _epoch) external view - returns (uint88 totalLQTYAllocated, uint120 averageTimestamp); + returns (uint256 totalLQTYAllocated, uint256 offset); /// @notice LQTY allocated by a user to the initiative at a given epoch + /// Voting power can be calculated as `lqtyAllocated * timestamp - offset` /// @param _user Address of the user /// @param _epoch Epoch at which the LQTY was allocated by the user /// @return lqtyAllocated LQTY allocated by the user - function lqtyAllocatedByUserAtEpoch(address _user, uint16 _epoch) + /// @return offset Voting power offset + function lqtyAllocatedByUserAtEpoch(address _user, uint256 _epoch) external view - returns (uint88 lqtyAllocated, uint120 averageTimestamp); + returns (uint256 lqtyAllocated, uint256 offset); /// @notice Deposit bribe tokens for a given epoch /// @dev The caller has to approve this contract to spend the BOLD and bribe tokens. @@ -59,15 +68,15 @@ interface IBribeInitiative { /// @param _boldAmount Amount of BOLD tokens to deposit /// @param _bribeTokenAmount Amount of bribe tokens to deposit /// @param _epoch Epoch at which the bribe is deposited - function depositBribe(uint128 _boldAmount, uint128 _bribeTokenAmount, uint16 _epoch) external; + function depositBribe(uint256 _boldAmount, uint256 _bribeTokenAmount, uint256 _epoch) external; struct ClaimData { // Epoch at which the user wants to claim the bribes - uint16 epoch; + uint256 epoch; // Epoch at which the user updated the LQTY allocation for this initiative - uint16 prevLQTYAllocationEpoch; + uint256 prevLQTYAllocationEpoch; // Epoch at which the total LQTY allocation is updated for this initiative - uint16 prevTotalLQTYAllocationEpoch; + uint256 prevTotalLQTYAllocationEpoch; } /// @notice Claim bribes for a user @@ -80,8 +89,8 @@ interface IBribeInitiative { returns (uint256 boldAmount, uint256 bribeTokenAmount); /// @notice Given a user address return the last recorded epoch for their allocation - function getMostRecentUserEpoch(address _user) external view returns (uint16); + function getMostRecentUserEpoch(address _user) external view returns (uint256); /// @notice Return the last recorded epoch for the system - function getMostRecentTotalEpoch() external view returns (uint16); + function getMostRecentTotalEpoch() external view returns (uint256); } diff --git a/src/interfaces/IGovernance.sol b/src/interfaces/IGovernance.sol index 9d1fb76a..167aff3c 100644 --- a/src/interfaces/IGovernance.sol +++ b/src/interfaces/IGovernance.sol @@ -7,31 +7,80 @@ import {ILQTYStaking} from "./ILQTYStaking.sol"; import {PermitParams} from "../utils/Types.sol"; +uint256 constant UNREGISTERED_INITIATIVE = type(uint256).max; + interface IGovernance { - event DepositLQTY(address user, uint256 depositedLQTY); - event WithdrawLQTY(address user, uint256 withdrawnLQTY, uint256 accruedLUSD, uint256 accruedETH); + enum HookStatus { + Failed, + Succeeded, + NotCalled + } + + /// @notice Emitted when a user deposits LQTY + /// @param user The account depositing LQTY + /// @param rewardRecipient The account receiving the LUSD/ETH rewards earned from staking in V1, if claimed + /// @param lqtyAmount The amount of LQTY being deposited + /// @return lusdReceived Amount of LUSD tokens received as a side-effect of staking new LQTY + /// @return lusdSent Amount of LUSD tokens sent to `rewardRecipient` (may include previously received LUSD) + /// @return ethReceived Amount of ETH received as a side-effect of staking new LQTY + /// @return ethSent Amount of ETH sent to `rewardRecipient` (may include previously received ETH) + event DepositLQTY( + address indexed user, + address rewardRecipient, + uint256 lqtyAmount, + uint256 lusdReceived, + uint256 lusdSent, + uint256 ethReceived, + uint256 ethSent + ); - event SnapshotVotes(uint240 votes, uint16 forEpoch); - event SnapshotVotesForInitiative(address initiative, uint240 votes, uint16 forEpoch); + /// @notice Emitted when a user withdraws LQTY or claims V1 staking rewards + /// @param user The account withdrawing LQTY or claiming V1 staking rewards + /// @param recipient The account receiving the LQTY withdrawn, and if claimed, the LUSD/ETH rewards earned from staking in V1 + /// @return lqtyReceived Amount of LQTY tokens actually withdrawn (may be lower than the `_lqtyAmount` passed to `withdrawLQTY`) + /// @return lqtySent Amount of LQTY tokens sent to `recipient` (may include LQTY sent to the user's proxy from sources other than V1 staking) + /// @return lusdReceived Amount of LUSD tokens received as a side-effect of staking new LQTY + /// @return lusdSent Amount of LUSD tokens sent to `recipient` (may include previously received LUSD) + /// @return ethReceived Amount of ETH received as a side-effect of staking new LQTY + /// @return ethSent Amount of ETH sent to `recipient` (may include previously received ETH) + event WithdrawLQTY( + address indexed user, + address recipient, + uint256 lqtyReceived, + uint256 lqtySent, + uint256 lusdReceived, + uint256 lusdSent, + uint256 ethReceived, + uint256 ethSent + ); - event RegisterInitiative(address initiative, address registrant, uint16 atEpoch); - event UnregisterInitiative(address initiative, uint16 atEpoch); + event SnapshotVotes(uint256 votes, uint256 forEpoch, uint256 boldAccrued); + event SnapshotVotesForInitiative(address indexed initiative, uint256 votes, uint256 vetos, uint256 forEpoch); - event AllocateLQTY(address user, address initiative, int256 deltaVoteLQTY, int256 deltaVetoLQTY, uint16 atEpoch); - event ClaimForInitiative(address initiative, uint256 bold, uint256 forEpoch); + event RegisterInitiative(address initiative, address registrant, uint256 atEpoch, HookStatus hookStatus); + event UnregisterInitiative(address initiative, uint256 atEpoch, HookStatus hookStatus); + + event AllocateLQTY( + address indexed user, + address indexed initiative, + int256 deltaVoteLQTY, + int256 deltaVetoLQTY, + uint256 atEpoch, + HookStatus hookStatus + ); + event ClaimForInitiative(address indexed initiative, uint256 bold, uint256 forEpoch, HookStatus hookStatus); struct Configuration { - uint128 registrationFee; - uint128 registrationThresholdFactor; - uint128 unregistrationThresholdFactor; - uint16 registrationWarmUpPeriod; - uint16 unregistrationAfterEpochs; - uint128 votingThresholdFactor; - uint88 minClaim; - uint88 minAccrual; - uint32 epochStart; - uint32 epochDuration; - uint32 epochVotingCutoff; + uint256 registrationFee; + uint256 registrationThresholdFactor; + uint256 unregistrationThresholdFactor; + uint256 unregistrationAfterEpochs; + uint256 votingThresholdFactor; + uint256 minClaim; + uint256 minAccrual; + uint256 epochStart; + uint256 epochDuration; + uint256 epochVotingCutoff; } function registerInitialInitiatives(address[] memory _initiatives) external; @@ -71,9 +120,6 @@ interface IGovernance { /// @notice Multiple of the voting threshold in vetos that are necessary to unregister an initiative /// @return unregistrationThresholdFactor Unregistration threshold factor function UNREGISTRATION_THRESHOLD_FACTOR() external view returns (uint256 unregistrationThresholdFactor); - /// @notice Number of epochs an initiative has to exist before it can be unregistered - /// @return registrationWarmUpPeriod Number of epochs - function REGISTRATION_WARM_UP_PERIOD() external view returns (uint256 registrationWarmUpPeriod); /// @notice Number of epochs an initiative has to be inactive before it can be unregistered /// @return unregistrationAfterEpochs Number of epochs function UNREGISTRATION_AFTER_EPOCHS() external view returns (uint256 unregistrationAfterEpochs); @@ -86,21 +132,21 @@ interface IGovernance { function boldAccrued() external view returns (uint256 boldAccrued); struct VoteSnapshot { - uint240 votes; // Votes at epoch transition - uint16 forEpoch; // Epoch for which the votes are counted + uint256 votes; // Votes at epoch transition + uint256 forEpoch; // Epoch for which the votes are counted } struct InitiativeVoteSnapshot { - uint224 votes; // Votes at epoch transition - uint16 forEpoch; // Epoch for which the votes are counted - uint16 lastCountedEpoch; // Epoch at which which the votes where counted last in the global snapshot - uint224 vetos; // Vetos at epoch transition + uint256 votes; // Votes at epoch transition + uint256 forEpoch; // Epoch for which the votes are counted + uint256 lastCountedEpoch; // Epoch at which which the votes where counted last in the global snapshot + uint256 vetos; // Vetos at epoch transition } /// @notice Returns the vote count snapshot of the previous epoch /// @return votes Number of votes /// @return forEpoch Epoch for which the votes are counted - function votesSnapshot() external view returns (uint240 votes, uint16 forEpoch); + function votesSnapshot() external view returns (uint256 votes, uint256 forEpoch); /// @notice Returns the vote count snapshot for an initiative of the previous epoch /// @param _initiative Address of the initiative /// @return votes Number of votes @@ -109,95 +155,129 @@ interface IGovernance { function votesForInitiativeSnapshot(address _initiative) external view - returns (uint224 votes, uint16 forEpoch, uint16 lastCountedEpoch, uint224 vetos); + returns (uint256 votes, uint256 forEpoch, uint256 lastCountedEpoch, uint256 vetos); struct Allocation { - uint88 voteLQTY; // LQTY allocated vouching for the initiative - uint88 vetoLQTY; // LQTY vetoing the initiative - uint16 atEpoch; // Epoch at which the allocation was last updated + uint256 voteLQTY; // LQTY allocated vouching for the initiative + uint256 voteOffset; // Offset associated with LQTY vouching for the initiative + uint256 vetoLQTY; // LQTY vetoing the initiative + uint256 vetoOffset; // Offset associated with LQTY vetoing the initiative + uint256 atEpoch; // Epoch at which the allocation was last updated } struct UserState { - uint88 allocatedLQTY; // LQTY allocated by the user - uint120 averageStakingTimestamp; // Average timestamp at which LQTY was staked by the user + uint256 unallocatedLQTY; // LQTY deposited and unallocated + uint256 unallocatedOffset; // The offset sum corresponding to the unallocated LQTY + uint256 allocatedLQTY; // LQTY allocated by the user to initatives + uint256 allocatedOffset; // The offset sum corresponding to the allocated LQTY } struct InitiativeState { - uint88 voteLQTY; // LQTY allocated vouching for the initiative - uint88 vetoLQTY; // LQTY allocated vetoing the initiative - uint120 averageStakingTimestampVoteLQTY; // Average staking timestamp of the voting LQTY for the initiative - uint120 averageStakingTimestampVetoLQTY; // Average staking timestamp of the vetoing LQTY for the initiative - uint16 lastEpochClaim; + uint256 voteLQTY; // LQTY allocated vouching for the initiative + uint256 voteOffset; // Offset associated with LQTY vouching for to the initative + uint256 vetoLQTY; // LQTY allocated vetoing the initiative + uint256 vetoOffset; // Offset associated with LQTY veoting the initative + uint256 lastEpochClaim; } struct GlobalState { - uint88 countedVoteLQTY; // Total LQTY that is included in vote counting - uint120 countedVoteLQTYAverageTimestamp; // Average timestamp: derived initiativeAllocation.averageTimestamp + uint256 countedVoteLQTY; // Total LQTY that is included in vote counting + uint256 countedVoteOffset; // Offset associated with the counted vote LQTY } - /// TODO: Bold balance? Prob cheaper /// @notice Returns the user's state - /// @param _user Address of the user - /// @return allocatedLQTY LQTY allocated by the user - /// @return averageStakingTimestamp Average timestamp at which LQTY was staked (deposited) by the user - function userStates(address _user) external view returns (uint88 allocatedLQTY, uint120 averageStakingTimestamp); + /// @return unallocatedLQTY LQTY deposited and unallocated + /// @return unallocatedOffset Offset associated with unallocated LQTY + /// @return allocatedLQTY allocated by the user to initatives + /// @return allocatedOffset Offset associated with allocated LQTY + function userStates(address _user) + external + view + returns (uint256 unallocatedLQTY, uint256 unallocatedOffset, uint256 allocatedLQTY, uint256 allocatedOffset); /// @notice Returns the initiative's state /// @param _initiative Address of the initiative /// @return voteLQTY LQTY allocated vouching for the initiative + /// @return voteOffset Offset associated with voteLQTY /// @return vetoLQTY LQTY allocated vetoing the initiative - /// @return averageStakingTimestampVoteLQTY // Average staking timestamp of the voting LQTY for the initiative - /// @return averageStakingTimestampVetoLQTY // Average staking timestamp of the vetoing LQTY for the initiative + /// @return vetoOffset Offset associated with vetoLQTY /// @return lastEpochClaim // Last epoch at which rewards were claimed function initiativeStates(address _initiative) external view - returns ( - uint88 voteLQTY, - uint88 vetoLQTY, - uint120 averageStakingTimestampVoteLQTY, - uint120 averageStakingTimestampVetoLQTY, - uint16 lastEpochClaim - ); + returns (uint256 voteLQTY, uint256 voteOffset, uint256 vetoLQTY, uint256 vetoOffset, uint256 lastEpochClaim); /// @notice Returns the global state /// @return countedVoteLQTY Total LQTY that is included in vote counting - /// @return countedVoteLQTYAverageTimestamp Average timestamp: derived initiativeAllocation.averageTimestamp - function globalState() external view returns (uint88 countedVoteLQTY, uint120 countedVoteLQTYAverageTimestamp); + /// @return countedVoteOffset Offset associated with countedVoteLQTY + function globalState() external view returns (uint256 countedVoteLQTY, uint256 countedVoteOffset); /// @notice Returns the amount of voting and vetoing LQTY a user allocated to an initiative /// @param _user Address of the user /// @param _initiative Address of the initiative /// @return voteLQTY LQTY allocated vouching for the initiative - /// @return vetoLQTY LQTY allocated vetoing the initiative + /// @return voteOffset The offset associated with voteLQTY + /// @return vetoLQTY allocated vetoing the initiative + /// @return vetoOffset the offset associated with vetoLQTY /// @return atEpoch Epoch at which the allocation was last updated function lqtyAllocatedByUserToInitiative(address _user, address _initiative) external view - returns (uint88 voteLQTY, uint88 vetoLQTY, uint16 atEpoch); + returns (uint256 voteLQTY, uint256 voteOffset, uint256 vetoLQTY, uint256 vetoOffset, uint256 atEpoch); /// @notice Returns when an initiative was registered /// @param _initiative Address of the initiative - /// @return atEpoch Epoch at which the initiative was registered - function registeredInitiatives(address _initiative) external view returns (uint16 atEpoch); + /// @return atEpoch If `_initiative` is an active initiative, returns the epoch at which it was registered. + /// If `_initiative` hasn't been registered, returns 0. + /// If `_initiative` has been unregistered, returns `UNREGISTERED_INITIATIVE`. + function registeredInitiatives(address _initiative) external view returns (uint256 atEpoch); /*////////////////////////////////////////////////////////////// STAKING //////////////////////////////////////////////////////////////*/ /// @notice Deposits LQTY - /// @dev The caller has to approve this contract to spend the LQTY tokens + /// @dev The caller has to approve their `UserProxy` address to spend the LQTY tokens + /// @param _lqtyAmount Amount of LQTY to deposit + function depositLQTY(uint256 _lqtyAmount) external; + + /// @notice Deposits LQTY + /// @dev The caller has to approve their `UserProxy` address to spend the LQTY tokens /// @param _lqtyAmount Amount of LQTY to deposit - function depositLQTY(uint88 _lqtyAmount) external; + /// @param _doSendRewards If true, send rewards claimed from LQTY staking + /// @param _recipient Address to which the tokens should be sent + function depositLQTY(uint256 _lqtyAmount, bool _doSendRewards, address _recipient) external; + /// @notice Deposits LQTY via Permit /// @param _lqtyAmount Amount of LQTY to deposit /// @param _permitParams Permit parameters - function depositLQTYViaPermit(uint88 _lqtyAmount, PermitParams memory _permitParams) external; + function depositLQTYViaPermit(uint256 _lqtyAmount, PermitParams calldata _permitParams) external; + + /// @notice Deposits LQTY via Permit + /// @param _lqtyAmount Amount of LQTY to deposit + /// @param _permitParams Permit parameters + /// @param _doSendRewards If true, send rewards claimed from LQTY staking + /// @param _recipient Address to which the tokens should be sent + function depositLQTYViaPermit( + uint256 _lqtyAmount, + PermitParams calldata _permitParams, + bool _doSendRewards, + address _recipient + ) external; + /// @notice Withdraws LQTY and claims any accrued LUSD and ETH rewards from StakingV1 /// @param _lqtyAmount Amount of LQTY to withdraw - function withdrawLQTY(uint88 _lqtyAmount) external; + function withdrawLQTY(uint256 _lqtyAmount) external; + + /// @notice Withdraws LQTY and claims any accrued LUSD and ETH rewards from StakingV1 + /// @param _lqtyAmount Amount of LQTY to withdraw + /// @param _doSendRewards If true, send rewards claimed from LQTY staking + /// @param _recipient Address to which the tokens should be sent + function withdrawLQTY(uint256 _lqtyAmount, bool _doSendRewards, address _recipient) external; + /// @notice Claims staking rewards from StakingV1 without unstaking + /// @dev Note: in the unlikely event that the caller's `UserProxy` holds any LQTY tokens, they will also be sent to `_rewardRecipient` /// @param _rewardRecipient Address that will receive the rewards - /// @return accruedLUSD Amount of LUSD accrued - /// @return accruedETH Amount of ETH accrued - function claimFromStakingV1(address _rewardRecipient) external returns (uint256 accruedLUSD, uint256 accruedETH); + /// @return lusdSent Amount of LUSD tokens sent to `_rewardRecipient` (may include previously received LUSD) + /// @return ethSent Amount of ETH sent to `_rewardRecipient` (may include previously received ETH) + function claimFromStakingV1(address _rewardRecipient) external returns (uint256 lusdSent, uint256 ethSent); /*////////////////////////////////////////////////////////////// VOTING @@ -205,22 +285,47 @@ interface IGovernance { /// @notice Returns the current epoch number /// @return epoch Current epoch - function epoch() external view returns (uint16 epoch); + function epoch() external view returns (uint256 epoch); /// @notice Returns the timestamp at which the current epoch started /// @return epochStart Epoch start of the current epoch - function epochStart() external view returns (uint32 epochStart); + function epochStart() external view returns (uint256 epochStart); /// @notice Returns the number of seconds that have gone by since the current epoch started /// @return secondsWithinEpoch Seconds within the current epoch - function secondsWithinEpoch() external view returns (uint32 secondsWithinEpoch); - /// @notice Returns the number of votes per LQTY for a user - /// @param _lqtyAmount Amount of LQTY to convert to votes - /// @param _currentTimestamp Current timestamp - /// @param _averageTimestamp Average timestamp at which the LQTY was staked + function secondsWithinEpoch() external view returns (uint256 secondsWithinEpoch); + + /// @notice Returns the voting power for an entity (i.e. user or initiative) at a given timestamp + /// @param _lqtyAmount Amount of LQTY associated with the entity + /// @param _timestamp Timestamp at which to calculate voting power + /// @param _offset The entity's offset sum /// @return votes Number of votes - function lqtyToVotes(uint88 _lqtyAmount, uint120 _currentTimestamp, uint120 _averageTimestamp) + function lqtyToVotes(uint256 _lqtyAmount, uint256 _timestamp, uint256 _offset) external pure returns (uint256); + + /// @dev Returns the most up to date voting threshold + /// In contrast to `getLatestVotingThreshold` this function updates the snapshot + /// This ensures that the value returned is always the latest + function calculateVotingThreshold() external returns (uint256); + + /// @dev Utility function to compute the threshold votes without recomputing the snapshot + /// Note that `boldAccrued` is a cached value, this function works correctly only when called after an accrual + function calculateVotingThreshold(uint256 _votes) external view returns (uint256); + + /// @notice Return the most up to date global snapshot and state as well as a flag to notify whether the state can be updated + /// This is a convenience function to always retrieve the most up to date state values + function getTotalVotesAndState() external - pure - returns (uint208); + view + returns (VoteSnapshot memory snapshot, GlobalState memory state, bool shouldUpdate); + + /// @dev Given an initiative address, return it's most up to date snapshot and state as well as a flag to notify whether the state can be updated + /// This is a convenience function to always retrieve the most up to date state values + function getInitiativeSnapshotAndState(address _initiative) + external + view + returns ( + InitiativeVoteSnapshot memory initiativeSnapshot, + InitiativeState memory initiativeState, + bool shouldUpdate + ); /// @notice Voting threshold is the max. of either: /// - 4% of the total voting LQTY in the previous epoch @@ -237,6 +342,38 @@ interface IGovernance { external returns (VoteSnapshot memory voteSnapshot, InitiativeVoteSnapshot memory initiativeVoteSnapshot); + /*////////////////////////////////////////////////////////////// + FSM + //////////////////////////////////////////////////////////////*/ + + enum InitiativeStatus { + NONEXISTENT, + /// This Initiative Doesn't exist | This is never returned + WARM_UP, + /// This epoch was just registered + SKIP, + /// This epoch will result in no rewards and no unregistering + CLAIMABLE, + /// This epoch will result in claiming rewards + CLAIMED, + /// The rewards for this epoch have been claimed + UNREGISTERABLE, + /// Can be unregistered + DISABLED // It was already Unregistered + + } + + function getInitiativeState(address _initiative) + external + returns (InitiativeStatus status, uint256 lastEpochClaim, uint256 claimableAmount); + + function getInitiativeState( + address _initiative, + VoteSnapshot memory _votesSnapshot, + InitiativeVoteSnapshot memory _votesForInitiativeSnapshot, + InitiativeState memory _initiativeState + ) external view returns (InitiativeStatus status, uint256 lastEpochClaim, uint256 claimableAmount); + /// @notice Registers a new initiative /// @param _initiative Address of the initiative function registerInitiative(address _initiative) external; @@ -248,16 +385,21 @@ interface IGovernance { /// @notice Allocates the user's LQTY to initiatives /// @dev The user can only allocate to active initiatives (older than 1 epoch) and has to have enough unallocated /// LQTY available, the initiatives listed must be unique, and towards the end of the epoch a user can only maintain or reduce their votes - /// @param _resetInitiatives Addresses of the initiatives the caller was previously allocated to, must be reset to prevent desynch of voting power + /// @param _initiativesToReset Addresses of the initiatives the caller was previously allocated to, must be reset to prevent desynch of voting power /// @param _initiatives Addresses of the initiatives to allocate to, can match or be different from `_resetInitiatives` - /// @param _absoluteLQTYVotes Delta LQTY to allocate to the initiatives as votes - /// @param absoluteLQTYVetos Delta LQTY to allocate to the initiatives as vetos + /// @param _absoluteLQTYVotes LQTY to allocate to the initiatives as votes + /// @param _absoluteLQTYVetos LQTY to allocate to the initiatives as vetos function allocateLQTY( - address[] calldata _resetInitiatives, + address[] calldata _initiativesToReset, address[] memory _initiatives, - int88[] memory _absoluteLQTYVotes, - int88[] memory absoluteLQTYVetos + int256[] memory _absoluteLQTYVotes, + int256[] memory _absoluteLQTYVetos ) external; + /// @notice Deallocates the user's LQTY from initiatives + /// @param _initiativesToReset Addresses of initiatives to deallocate LQTY from + /// @param _checkAll When true, the call will revert if there is still some allocated LQTY left after deallocating + /// from all the addresses in `_initiativesToReset` + function resetAllocations(address[] calldata _initiativesToReset, bool _checkAll) external; /// @notice Splits accrued funds according to votes received between all initiatives /// @param _initiative Addresse of the initiative diff --git a/src/interfaces/IInitiative.sol b/src/interfaces/IInitiative.sol index ddb3179b..4a415c54 100644 --- a/src/interfaces/IInitiative.sol +++ b/src/interfaces/IInitiative.sol @@ -6,11 +6,11 @@ import {IGovernance} from "./IGovernance.sol"; interface IInitiative { /// @notice Callback hook that is called by Governance after the initiative was successfully registered /// @param _atEpoch Epoch at which the initiative is registered - function onRegisterInitiative(uint16 _atEpoch) external; + function onRegisterInitiative(uint256 _atEpoch) external; /// @notice Callback hook that is called by Governance after the initiative was unregistered /// @param _atEpoch Epoch at which the initiative is unregistered - function onUnregisterInitiative(uint16 _atEpoch) external; + function onUnregisterInitiative(uint256 _atEpoch) external; /// @notice Callback hook that is called by Governance after the LQTY allocation is updated by a user /// @param _currentEpoch Epoch at which the LQTY allocation is updated @@ -19,7 +19,7 @@ interface IInitiative { /// @param _allocation Allocation state from user to initiative /// @param _initiativeState Initiative state function onAfterAllocateLQTY( - uint16 _currentEpoch, + uint256 _currentEpoch, address _user, IGovernance.UserState calldata _userState, IGovernance.Allocation calldata _allocation, @@ -30,5 +30,5 @@ interface IInitiative { /// to the initiative /// @param _claimEpoch Epoch at which the claim was distributed /// @param _bold Amount of BOLD that was distributed - function onClaimForInitiative(uint16 _claimEpoch, uint256 _bold) external; + function onClaimForInitiative(uint256 _claimEpoch, uint256 _bold) external; } diff --git a/src/interfaces/ILQTY.sol b/src/interfaces/ILQTY.sol index b6451176..f3d986f9 100644 --- a/src/interfaces/ILQTY.sol +++ b/src/interfaces/ILQTY.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -interface ILQTY { +import {IERC20} from "openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Permit} from "openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; + +interface ILQTY is IERC20, IERC20Permit { function domainSeparator() external view returns (bytes32); } diff --git a/src/interfaces/ILQTYStaking.sol b/src/interfaces/ILQTYStaking.sol index e4bab790..4ebd4863 100644 --- a/src/interfaces/ILQTYStaking.sol +++ b/src/interfaces/ILQTYStaking.sol @@ -41,4 +41,6 @@ interface ILQTYStaking { function getPendingLUSDGain(address _user) external view returns (uint256); function stakes(address _user) external view returns (uint256); + + function totalLQTYStaked() external view returns (uint256); } diff --git a/src/interfaces/ILUSD.sol b/src/interfaces/ILUSD.sol new file mode 100644 index 00000000..d198040a --- /dev/null +++ b/src/interfaces/ILUSD.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IERC20} from "openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Permit} from "openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; + +interface ILUSD is IERC20, IERC20Permit { + function mint(address _account, uint256 _amount) external; +} diff --git a/src/interfaces/IMultiDelegateCall.sol b/src/interfaces/IMultiDelegateCall.sol new file mode 100644 index 00000000..6c042453 --- /dev/null +++ b/src/interfaces/IMultiDelegateCall.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IMultiDelegateCall { + /// @notice Call multiple functions of the contract while preserving `msg.sender` + /// @param inputs Function calls to perform, encoded using `abi.encodeCall()` or equivalent + /// @return returnValues Raw data returned by each call + function multiDelegateCall(bytes[] calldata inputs) external returns (bytes[] memory returnValues); +} diff --git a/src/interfaces/IMulticall.sol b/src/interfaces/IMulticall.sol deleted file mode 100644 index 8227fdc9..00000000 --- a/src/interfaces/IMulticall.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -/// Copied from: https://github.com/Uniswap/v3-periphery/blob/main/contracts/interfaces/IMulticall.sol -/// @title Multicall interface -/// @notice Enables calling multiple methods in a single call to the contract -interface IMulticall { - /// @notice Call multiple functions in the current contract and return the data from all of them if they all succeed - /// @dev The `msg.value` should not be trusted for any method callable from multicall. - /// @param data The encoded function data for each of the calls to make to this contract - /// @return results The results from each of the calls passed in via data - function multicall(bytes[] calldata data) external payable returns (bytes[] memory results); -} diff --git a/src/interfaces/IUserProxy.sol b/src/interfaces/IUserProxy.sol index 4169e93f..7da78371 100644 --- a/src/interfaces/IUserProxy.sol +++ b/src/interfaces/IUserProxy.sol @@ -8,9 +8,6 @@ import {ILQTYStaking} from "../interfaces/ILQTYStaking.sol"; import {PermitParams} from "../utils/Types.sol"; interface IUserProxy { - event Stake(uint256 amount, address lqtyFrom); - event Unstake(uint256 lqtyUnstaked, address indexed lqtyRecipient, uint256 lusdAmount, uint256 ethAmount); - /// @notice Address of the LQTY token /// @return lqty Address of the LQTY token function lqty() external view returns (IERC20 lqty); @@ -28,19 +25,56 @@ interface IUserProxy { /// @dev The LQTY tokens must be approved for transfer by the user /// @param _amount Amount of LQTY tokens to stake /// @param _lqtyFrom Address from which to transfer the LQTY tokens - function stake(uint256 _amount, address _lqtyFrom) external; + /// @param _doSendRewards If true, send rewards claimed from LQTY staking + /// @param _recipient Address to which the tokens should be sent + /// @return lusdReceived Amount of LUSD tokens received as a side-effect of staking new LQTY + /// @return lusdSent Amount of LUSD tokens sent to `_recipient` (may include previously received LUSD) + /// @return ethReceived Amount of ETH received as a side-effect of staking new LQTY + /// @return ethSent Amount of ETH sent to `_recipient` (may include previously received ETH) + function stake(uint256 _amount, address _lqtyFrom, bool _doSendRewards, address _recipient) + external + returns (uint256 lusdReceived, uint256 lusdSent, uint256 ethReceived, uint256 ethSent); + /// @notice Stakes a given amount of LQTY tokens in the V1 staking contract using a permit /// @param _amount Amount of LQTY tokens to stake /// @param _lqtyFrom Address from which to transfer the LQTY tokens /// @param _permitParams Parameters for the permit data - function stakeViaPermit(uint256 _amount, address _lqtyFrom, PermitParams calldata _permitParams) external; + /// @param _doSendRewards If true, send rewards claimed from LQTY staking + /// @param _recipient Address to which the tokens should be sent + /// @return lusdReceived Amount of LUSD tokens received as a side-effect of staking new LQTY + /// @return lusdSent Amount of LUSD tokens sent to `_recipient` (may include previously received LUSD) + /// @return ethReceived Amount of ETH received as a side-effect of staking new LQTY + /// @return ethSent Amount of ETH sent to `_recipient` (may include previously received ETH) + function stakeViaPermit( + uint256 _amount, + address _lqtyFrom, + PermitParams calldata _permitParams, + bool _doSendRewards, + address _recipient + ) external returns (uint256 lusdReceived, uint256 lusdSent, uint256 ethReceived, uint256 ethSent); + /// @notice Unstakes a given amount of LQTY tokens from the V1 staking contract and claims the accrued rewards /// @param _amount Amount of LQTY tokens to unstake + /// @param _doSendRewards If true, send rewards claimed from LQTY staking /// @param _recipient Address to which the tokens should be sent - /// @return lusdAmount Amount of LUSD tokens claimed - /// @return ethAmount Amount of ETH claimed - function unstake(uint256 _amount, address _recipient) external returns (uint256 lusdAmount, uint256 ethAmount); + /// @return lqtyReceived Amount of LQTY tokens actually unstaked (may be lower than `_amount`) + /// @return lqtySent Amount of LQTY tokens sent to `_recipient` (may include LQTY sent to the proxy from sources other than V1 staking) + /// @return lusdReceived Amount of LUSD tokens received as a side-effect of staking new LQTY + /// @return lusdSent Amount of LUSD tokens claimed (may include previously received LUSD) + /// @return ethReceived Amount of ETH received as a side-effect of staking new LQTY + /// @return ethSent Amount of ETH claimed (may include previously received ETH) + function unstake(uint256 _amount, bool _doSendRewards, address _recipient) + external + returns ( + uint256 lqtyReceived, + uint256 lqtySent, + uint256 lusdReceived, + uint256 lusdSent, + uint256 ethReceived, + uint256 ethSent + ); + /// @notice Returns the current amount LQTY staked by a user in the V1 staking contract /// @return staked Amount of LQTY tokens staked - function staked() external view returns (uint88); + function staked() external view returns (uint256); } diff --git a/src/utils/BaseHook.sol b/src/utils/BaseHook.sol deleted file mode 100644 index b2021986..00000000 --- a/src/utils/BaseHook.sol +++ /dev/null @@ -1,158 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import {Hooks} from "v4-core/src/libraries/Hooks.sol"; -import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; -import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; -import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; -import {PoolKey} from "v4-core/src/types/PoolKey.sol"; -import {BeforeSwapDelta} from "v4-core/src/types/BeforeSwapDelta.sol"; -import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol"; - -contract ImmutableState { - IPoolManager public immutable manager; - - constructor(IPoolManager _manager) { - manager = _manager; - } -} - -abstract contract SafeCallback is ImmutableState, IUnlockCallback { - error NotManager(); - - modifier onlyByManager() { - if (msg.sender != address(manager)) revert NotManager(); - _; - } - - /// @dev We force the onlyByManager modifier by exposing a virtual function after the onlyByManager check. - function unlockCallback(bytes calldata data) external onlyByManager returns (bytes memory) { - return _unlockCallback(data); - } - - function _unlockCallback(bytes calldata data) internal virtual returns (bytes memory); -} - -abstract contract BaseHook is IHooks, SafeCallback { - error NotSelf(); - error InvalidPool(); - error LockFailure(); - error HookNotImplemented(); - - constructor(IPoolManager _manager) ImmutableState(_manager) { - validateHookAddress(this); - } - - /// @dev Only this address may call this function - modifier selfOnly() { - if (msg.sender != address(this)) revert NotSelf(); - _; - } - - /// @dev Only pools with hooks set to this contract may call this function - modifier onlyValidPools(IHooks hooks) { - if (hooks != this) revert InvalidPool(); - _; - } - - function getHookPermissions() public pure virtual returns (Hooks.Permissions memory); - - // this function is virtual so that we can override it during testing, - // which allows us to deploy an implementation to any address - // and then etch the bytecode into the correct address - function validateHookAddress(BaseHook _this) internal pure virtual { - Hooks.validateHookPermissions(_this, getHookPermissions()); - } - - function _unlockCallback(bytes calldata data) internal virtual override returns (bytes memory) { - (bool success, bytes memory returnData) = address(this).call(data); - if (success) return returnData; - if (returnData.length == 0) revert LockFailure(); - // if the call failed, bubble up the reason - /// @solidity memory-safe-assembly - assembly { - revert(add(returnData, 32), mload(returnData)) - } - } - - function beforeInitialize(address, PoolKey calldata, uint160, bytes calldata) external virtual returns (bytes4) { - revert HookNotImplemented(); - } - - function afterInitialize(address, PoolKey calldata, uint160, int24, bytes calldata) - external - virtual - returns (bytes4) - { - revert HookNotImplemented(); - } - - function beforeAddLiquidity(address, PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, bytes calldata) - external - virtual - returns (bytes4) - { - revert HookNotImplemented(); - } - - function beforeRemoveLiquidity( - address, - PoolKey calldata, - IPoolManager.ModifyLiquidityParams calldata, - bytes calldata - ) external virtual returns (bytes4) { - revert HookNotImplemented(); - } - - function afterAddLiquidity( - address, - PoolKey calldata, - IPoolManager.ModifyLiquidityParams calldata, - BalanceDelta, - bytes calldata - ) external virtual returns (bytes4, BalanceDelta) { - revert HookNotImplemented(); - } - - function afterRemoveLiquidity( - address, - PoolKey calldata, - IPoolManager.ModifyLiquidityParams calldata, - BalanceDelta, - bytes calldata - ) external virtual returns (bytes4, BalanceDelta) { - revert HookNotImplemented(); - } - - function beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata) - external - virtual - returns (bytes4, BeforeSwapDelta, uint24) - { - revert HookNotImplemented(); - } - - function afterSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) - external - virtual - returns (bytes4, int128) - { - revert HookNotImplemented(); - } - - function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) - external - virtual - returns (bytes4) - { - revert HookNotImplemented(); - } - - function afterDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) - external - virtual - returns (bytes4) - { - revert HookNotImplemented(); - } -} diff --git a/src/utils/DoubleLinkedList.sol b/src/utils/DoubleLinkedList.sol index e91cb14f..f6fc97ab 100644 --- a/src/utils/DoubleLinkedList.sol +++ b/src/utils/DoubleLinkedList.sol @@ -6,13 +6,14 @@ pragma solidity ^0.8.24; /// and the tail is defined as the null item's next pointer ([tail][prev][item][next][head]) library DoubleLinkedList { struct Item { - uint224 value; - uint16 prev; - uint16 next; + uint256 lqty; + uint256 offset; + uint256 prev; + uint256 next; } struct List { - mapping(uint16 => Item) items; + mapping(uint256 => Item) items; } error IdIsZero(); @@ -22,46 +23,47 @@ library DoubleLinkedList { /// @notice Returns the head item id of the list /// @param list Linked list which contains the item /// @return _ Id of the head item - function getHead(List storage list) internal view returns (uint16) { + function getHead(List storage list) internal view returns (uint256) { return list.items[0].prev; } /// @notice Returns the tail item id of the list /// @param list Linked list which contains the item /// @return _ Id of the tail item - function getTail(List storage list) internal view returns (uint16) { + function getTail(List storage list) internal view returns (uint256) { return list.items[0].next; } - /// @notice Returns the item id which follows item `id`. Returns the head item id of the list if the `id` is 0. + /// @notice Returns the item id which follows item `id`. Returns the tail item id of the list if the `id` is 0. /// @param list Linked list which contains the items /// @param id Id of the current item /// @return _ Id of the current item's next item - function getNext(List storage list, uint16 id) internal view returns (uint16) { + function getNext(List storage list, uint256 id) internal view returns (uint256) { return list.items[id].next; } - /// @notice Returns the item id which precedes item `id`. Returns the tail item id of the list if the `id` is 0. + /// @notice Returns the item id which precedes item `id`. Returns the head item id of the list if the `id` is 0. /// @param list Linked list which contains the items /// @param id Id of the current item /// @return _ Id of the current item's previous item - function getPrev(List storage list, uint16 id) internal view returns (uint16) { + function getPrev(List storage list, uint256 id) internal view returns (uint256) { return list.items[id].prev; } /// @notice Returns the value of item `id` /// @param list Linked list which contains the item /// @param id Id of the item - /// @return _ Value of the item - function getValue(List storage list, uint16 id) internal view returns (uint224) { - return list.items[id].value; + /// @return LQTY associated with the item + /// @return Offset associated with the item's LQTY + function getLQTYAndOffset(List storage list, uint256 id) internal view returns (uint256, uint256) { + return (list.items[id].lqty, list.items[id].offset); } /// @notice Returns the item `id` /// @param list Linked list which contains the item /// @param id Id of the item /// @return _ Item - function getItem(List storage list, uint16 id) internal view returns (Item memory) { + function getItem(List storage list, uint256 id) internal view returns (Item memory) { return list.items[id]; } @@ -69,7 +71,7 @@ library DoubleLinkedList { /// @param list Linked list which should contain the item /// @param id Id of the item to check /// @return _ True if the list contains the item, false otherwise - function contains(List storage list, uint16 id) internal view returns (bool) { + function contains(List storage list, uint256 id) internal view returns (bool) { if (id == 0) revert IdIsZero(); return (list.items[id].prev != 0 || list.items[id].next != 0 || list.items[0].next == id); } @@ -79,16 +81,18 @@ library DoubleLinkedList { /// @dev This function should not be called with an `id` that is already in the list. /// @param list Linked list which contains the next item and into which the new item will be inserted /// @param id Id of the item to insert - /// @param value Value of the item to insert + /// @param lqty amount of LQTY + /// @param offset associated with the LQTY amount /// @param next Id of the item which should follow item `id` - function insert(List storage list, uint16 id, uint224 value, uint16 next) internal { + function insert(List storage list, uint256 id, uint256 lqty, uint256 offset, uint256 next) internal { if (contains(list, id)) revert ItemInList(); if (next != 0 && !contains(list, next)) revert ItemNotInList(); - uint16 prev = list.items[next].prev; + uint256 prev = list.items[next].prev; list.items[prev].next = id; list.items[next].prev = id; list.items[id].prev = prev; list.items[id].next = next; - list.items[id].value = value; + list.items[id].lqty = lqty; + list.items[id].offset = offset; } } diff --git a/src/utils/EncodingDecodingLib.sol b/src/utils/EncodingDecodingLib.sol deleted file mode 100644 index 79026859..00000000 --- a/src/utils/EncodingDecodingLib.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -library EncodingDecodingLib { - function encodeLQTYAllocation(uint88 _lqty, uint120 _averageTimestamp) internal pure returns (uint224) { - uint224 _value = (uint224(_lqty) << 120) | _averageTimestamp; - return _value; - } - - function decodeLQTYAllocation(uint224 _value) internal pure returns (uint88, uint120) { - return (uint88(_value >> 120), uint120(_value)); - } -} diff --git a/src/utils/Math.sol b/src/utils/Math.sol index dd608737..67a6c0ad 100644 --- a/src/utils/Math.sol +++ b/src/utils/Math.sol @@ -1,17 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -function add(uint88 a, int88 b) pure returns (uint88) { +function add(uint256 a, int256 b) pure returns (uint256) { if (b < 0) { return a - abs(b); } - return a + abs(b); + return a + uint256(b); +} + +function sub(uint256 a, int256 b) pure returns (uint256) { + if (b < 0) { + return a + abs(b); + } + return a - uint256(b); } function max(uint256 a, uint256 b) pure returns (uint256) { return a > b ? a : b; } -function abs(int88 a) pure returns (uint88) { - return a < 0 ? uint88(uint256(-int256(a))) : uint88(a); +function abs(int256 a) pure returns (uint256) { + return a < 0 ? uint256(-int256(a)) : uint256(a); } diff --git a/src/utils/MultiDelegateCall.sol b/src/utils/MultiDelegateCall.sol new file mode 100644 index 00000000..cd6b701b --- /dev/null +++ b/src/utils/MultiDelegateCall.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IMultiDelegateCall} from "../interfaces/IMultiDelegateCall.sol"; + +contract MultiDelegateCall is IMultiDelegateCall { + /// @inheritdoc IMultiDelegateCall + function multiDelegateCall(bytes[] calldata inputs) external returns (bytes[] memory returnValues) { + returnValues = new bytes[](inputs.length); + + for (uint256 i; i < inputs.length; ++i) { + (bool success, bytes memory returnData) = address(this).delegatecall(inputs[i]); + + if (!success) { + // Bubble up the revert + assembly { + revert( + add(32, returnData), // offset (skip first 32 bytes, where the size of the array is stored) + mload(returnData) // size + ) + } + } + + returnValues[i] = returnData; + } + } +} diff --git a/src/utils/Multicall.sol b/src/utils/Multicall.sol deleted file mode 100644 index f5752815..00000000 --- a/src/utils/Multicall.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.24; - -import {IMulticall} from "../interfaces/IMulticall.sol"; - -/// Copied from: https://github.com/Uniswap/v3-periphery/blob/main/contracts/base/Multicall.sol -/// @title Multicall -/// @notice Enables calling multiple methods in a single call to the contract -abstract contract Multicall is IMulticall { - /// @inheritdoc IMulticall - function multicall(bytes[] calldata data) public payable override returns (bytes[] memory results) { - results = new bytes[](data.length); - for (uint256 i = 0; i < data.length; i++) { - (bool success, bytes memory result) = address(this).delegatecall(data[i]); - - if (!success) { - // Next 5 lines from https://ethereum.stackexchange.com/a/83577 - if (result.length < 68) revert(); - assembly { - result := add(result, 0x04) - } - revert(abi.decode(result, (string))); - } - - results[i] = result; - } - } -} diff --git a/src/utils/SafeCallMinGas.sol b/src/utils/SafeCallMinGas.sol index 759d8be2..29e42290 100644 --- a/src/utils/SafeCallMinGas.sol +++ b/src/utils/SafeCallMinGas.sol @@ -16,8 +16,8 @@ function hasMinGas(uint256 _minGas, uint256 _reservedGas) view returns (bool) { function safeCallWithMinGas(address _target, uint256 _gas, uint256 _value, bytes memory _calldata) returns (bool success) { - /// @audit This is not necessary - /// But this is basically a worst case estimate of mem exp cost + operations before the call + /// This is not necessary + /// But this is basically a worst case estimate of mem exp cost + operations before the call require(hasMinGas(_gas, 1_000), "Must have minGas"); // dispatch message to recipient diff --git a/src/utils/UniqueArray.sol b/src/utils/UniqueArray.sol index 17812174..c7b47e71 100644 --- a/src/utils/UniqueArray.sol +++ b/src/utils/UniqueArray.sol @@ -5,6 +5,8 @@ pragma solidity ^0.8.24; /// @param arr - List to check for dups function _requireNoDuplicates(address[] calldata arr) pure { uint256 arrLength = arr.length; + if (arrLength == 0) return; + // only up to len - 1 (no j to check if i == len - 1) for (uint i; i < arrLength - 1;) { for (uint j = i + 1; j < arrLength;) { @@ -21,7 +23,7 @@ function _requireNoDuplicates(address[] calldata arr) pure { } } -function _requireNoNegatives(int88[] memory vals) pure { +function _requireNoNegatives(int256[] memory vals) pure { uint256 arrLength = vals.length; for (uint i; i < arrLength; i++) { diff --git a/src/utils/VotingPower.sol b/src/utils/VotingPower.sol new file mode 100644 index 00000000..23c70ff6 --- /dev/null +++ b/src/utils/VotingPower.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +function _lqtyToVotes(uint256 _lqtyAmount, uint256 _timestamp, uint256 _offset) pure returns (uint256) { + uint256 prod = _lqtyAmount * _timestamp; + return prod > _offset ? prod - _offset : 0; +} diff --git a/test/BribeInitiative.t.sol b/test/BribeInitiative.t.sol index 6d9e301d..02ee9691 100644 --- a/test/BribeInitiative.t.sol +++ b/test/BribeInitiative.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import {Test, console2} from "forge-std/Test.sol"; -import {MockERC20} from "forge-std/mocks/MockERC20.sol"; +import {Strings} from "openzeppelin/contracts/utils/Strings.sol"; import {IGovernance} from "../src/interfaces/IGovernance.sol"; import {IBribeInitiative} from "../src/interfaces/IBribeInitiative.sol"; @@ -10,30 +10,30 @@ import {IBribeInitiative} from "../src/interfaces/IBribeInitiative.sol"; import {Governance} from "../src/Governance.sol"; import {BribeInitiative} from "../src/BribeInitiative.sol"; +import {MockERC20Tester} from "./mocks/MockERC20Tester.sol"; import {MockStakingV1} from "./mocks/MockStakingV1.sol"; +import {MockStakingV1Deployer} from "./mocks/MockStakingV1Deployer.sol"; -contract BribeInitiativeTest is Test { - MockERC20 private lqty; - MockERC20 private lusd; - address private stakingV1; +contract BribeInitiativeTest is Test, MockStakingV1Deployer { + using Strings for uint256; + + MockERC20Tester private lqty; + MockERC20Tester private lusd; + MockStakingV1 private stakingV1; address private constant user1 = address(0xF977814e90dA44bFA03b6295A0616a897441aceC); address private constant user2 = address(0x10C9cff3c4Faa8A60cB8506a7A99411E6A199038); address private user3 = makeAddr("user3"); address private constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); - address private constant initiative = address(0x1); - address private constant initiative2 = address(0x2); - address private constant initiative3 = address(0x3); - - uint128 private constant REGISTRATION_FEE = 1e18; - uint128 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; - uint128 private constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; - uint16 private constant REGISTRATION_WARM_UP_PERIOD = 4; - uint16 private constant UNREGISTRATION_AFTER_EPOCHS = 4; - uint128 private constant VOTING_THRESHOLD_FACTOR = 0.04e18; - uint88 private constant MIN_CLAIM = 500e18; - uint88 private constant MIN_ACCRUAL = 1000e18; - uint32 private constant EPOCH_DURATION = 7 days; // 7 days - uint32 private constant EPOCH_VOTING_CUTOFF = 518400; + + uint256 private constant REGISTRATION_FEE = 1e18; + uint256 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; + uint256 private constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; + uint256 private constant UNREGISTRATION_AFTER_EPOCHS = 4; + uint256 private constant VOTING_THRESHOLD_FACTOR = 0.04e18; + uint256 private constant MIN_CLAIM = 500e18; + uint256 private constant MIN_ACCRUAL = 1000e18; + uint256 private constant EPOCH_DURATION = 7 days; // 7 days + uint256 private constant EPOCH_VOTING_CUTOFF = 518400; Governance private governance; address[] private initialInitiatives; @@ -41,45 +41,31 @@ contract BribeInitiativeTest is Test { BribeInitiative private bribeInitiative; function setUp() public { - lqty = deployMockERC20("Liquity", "LQTY", 18); - lusd = deployMockERC20("Liquity USD", "LUSD", 18); - - vm.store(address(lqty), keccak256(abi.encode(address(lusdHolder), 4)), bytes32(abi.encode(10_000_000e18))); - vm.store(address(lusd), keccak256(abi.encode(address(lusdHolder), 4)), bytes32(abi.encode(10_000_000e18))); - vm.store(address(lqty), keccak256(abi.encode(address(lusdHolder), 4)), bytes32(abi.encode(10_000_000e18))); - vm.store(address(lusd), keccak256(abi.encode(address(lusdHolder), 4)), bytes32(abi.encode(10_000_000e18))); + (stakingV1, lqty, lusd) = deployMockStakingV1(); + + lqty.mint(lusdHolder, 10_000_000e18); + lusd.mint(lusdHolder, 10_000_000e18); + + IGovernance.Configuration memory config = IGovernance.Configuration({ + registrationFee: REGISTRATION_FEE, + registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, + unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, + unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, + votingThresholdFactor: VOTING_THRESHOLD_FACTOR, + minClaim: MIN_CLAIM, + minAccrual: MIN_ACCRUAL, + epochStart: uint256(block.timestamp), + epochDuration: EPOCH_DURATION, + epochVotingCutoff: EPOCH_VOTING_CUTOFF + }); - stakingV1 = address(new MockStakingV1(address(lqty))); - - bribeInitiative = new BribeInitiative( - address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 1)), - address(lusd), - address(lqty) + governance = new Governance( + address(lqty), address(lusd), address(stakingV1), address(lusd), config, address(this), new address[](0) ); + bribeInitiative = new BribeInitiative(address(governance), address(lusd), address(lqty)); initialInitiatives.push(address(bribeInitiative)); - - governance = new Governance( - address(lqty), - address(lusd), - stakingV1, - address(lusd), - IGovernance.Configuration({ - registrationFee: REGISTRATION_FEE, - registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, - unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, - unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, - votingThresholdFactor: VOTING_THRESHOLD_FACTOR, - minClaim: MIN_CLAIM, - minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), - epochDuration: EPOCH_DURATION, - epochVotingCutoff: EPOCH_VOTING_CUTOFF - }), - address(this), - initialInitiatives - ); + governance.registerInitialInitiatives(initialInitiatives); vm.startPrank(lusdHolder); lqty.transfer(user1, 1_000_000e18); @@ -91,6 +77,11 @@ contract BribeInitiativeTest is Test { vm.stopPrank(); } + function test_bribeToken_cannot_be_BOLD() external { + vm.expectRevert("BribeInitiative: bribe-token-cannot-be-bold"); + new BribeInitiative({_governance: address(governance), _bold: address(lusd), _bribeToken: address(lusd)}); + } + // test total allocation vote case function test_totalLQTYAllocatedByEpoch_vote() public { // staking LQTY into governance for user1 in first epoch @@ -102,7 +93,7 @@ contract BribeInitiativeTest is Test { // allocate LQTY to the bribeInitiative _allocateLQTY(user1, 10e18, 0); // total LQTY allocated for this epoch should increase - (uint88 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 10e18); } @@ -116,7 +107,7 @@ contract BribeInitiativeTest is Test { // allocate LQTY to veto bribeInitiative _allocateLQTY(user1, 0, 10e18); // total LQTY allocated for this epoch should not increase - (uint88 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 0); } @@ -131,8 +122,8 @@ contract BribeInitiativeTest is Test { _allocateLQTY(user1, 5e18, 0); // total LQTY allocated for this epoch should increase - (uint88 totalLQTYAllocated1,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated1,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 totalLQTYAllocated1,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated1,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); assertEq(totalLQTYAllocated1, 5e18); assertEq(userLQTYAllocated1, 5e18); @@ -142,8 +133,8 @@ contract BribeInitiativeTest is Test { _allocateLQTY(user1, 5e18, 0); // total LQTY allocated for this epoch should not change - (uint88 totalLQTYAllocated2,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated2,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 totalLQTYAllocated2,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated2,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); assertEq(totalLQTYAllocated2, 5e18); assertEq(userLQTYAllocated1, 5e18); } @@ -156,14 +147,14 @@ contract BribeInitiativeTest is Test { // user1 allocates in first epoch _allocateLQTY(user1, 5e18, 0); - (uint88 totalLQTYAllocated1,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated1,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 totalLQTYAllocated1,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated1,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); assertEq(totalLQTYAllocated1, 5e18); assertEq(userLQTYAllocated1, 5e18); _allocateLQTY(user1, 5e18, 0); - (uint88 totalLQTYAllocated2,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated2,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 totalLQTYAllocated2,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated2,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); assertEq(totalLQTYAllocated2, 5e18); assertEq(userLQTYAllocated2, 5e18); } @@ -175,14 +166,14 @@ contract BribeInitiativeTest is Test { // user1 allocates in first epoch _allocateLQTY(user1, 5e18, 0); - (uint88 totalLQTYAllocated1,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated1,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 totalLQTYAllocated1,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated1,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); assertEq(totalLQTYAllocated1, 5e18); assertEq(userLQTYAllocated1, 5e18); console2.log("current governance epoch: ", governance.epoch()); // user's linked-list should be updated to have a value for the current epoch - (uint88 allocatedAtEpoch,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 allocatedAtEpoch,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); console2.log("allocatedAtEpoch: ", allocatedAtEpoch); } @@ -195,8 +186,8 @@ contract BribeInitiativeTest is Test { // user1 allocates in first epoch _allocateLQTY(user1, 10e18, 0); - (uint88 totalLQTYAllocated1,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated1,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 totalLQTYAllocated1,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated1,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); assertEq(totalLQTYAllocated1, 10e18); assertEq(userLQTYAllocated1, 10e18); @@ -205,8 +196,8 @@ contract BribeInitiativeTest is Test { // user allocations should be disjoint because they're in separate epochs _allocateLQTY(user2, 10e18, 0); - (uint88 totalLQTYAllocated2,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated2,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); + (uint256 totalLQTYAllocated2,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated2,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); assertEq(totalLQTYAllocated2, 20e18); assertEq(userLQTYAllocated2, 10e18); } @@ -220,14 +211,14 @@ contract BribeInitiativeTest is Test { // user1 allocates in first epoch _allocateLQTY(user1, 10e18, 0); - (uint88 totalLQTYAllocated1,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated1,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 totalLQTYAllocated1,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated1,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); assertEq(totalLQTYAllocated1, 10e18); assertEq(userLQTYAllocated1, 10e18); _allocateLQTY(user2, 10e18, 0); - (uint88 totalLQTYAllocated2,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated2,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); + (uint256 totalLQTYAllocated2,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated2,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); assertEq(totalLQTYAllocated2, 20e18); assertEq(userLQTYAllocated2, 10e18); } @@ -241,13 +232,13 @@ contract BribeInitiativeTest is Test { // user1 allocates in first epoch _allocateLQTY(user1, 10e18, 0); - (uint88 totalLQTYAllocated1,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 totalLQTYAllocated1,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated1, 10e18); // warp to the end of the epoch vm.warp(block.timestamp + (EPOCH_VOTING_CUTOFF - 1)); - (uint88 totalLQTYAllocated2,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 totalLQTYAllocated2,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated2, 10e18); } @@ -272,7 +263,7 @@ contract BribeInitiativeTest is Test { // lusdHolder deposits lqty and lusd bribes claimable in epoch 3 _depositBribe(1e18, 1e18, governance.epoch() + 1); - uint16 depositedBribe = governance.epoch() + 1; + uint256 depositedBribe = governance.epoch() + 1; // =========== epoch 3 ================== vm.warp(block.timestamp + EPOCH_DURATION); @@ -311,7 +302,7 @@ contract BribeInitiativeTest is Test { _depositBribe(1e18, 1e18, governance.epoch()); _allocateLQTY(user1, 1e18, 0); _allocateLQTY(user2, 1, 0); - _allocateLQTY(user2, 0, 0); + _resetAllocation(user2); // =========== epoch 2 ================== vm.warp(block.timestamp + EPOCH_DURATION); // Needs to cause rounding error @@ -321,8 +312,8 @@ contract BribeInitiativeTest is Test { // user should receive bribe from their allocated stake (uint256 boldAmount, uint256 bribeTokenAmount) = _claimBribe(user1, 2, 2, 2); - assertEq(boldAmount, 1e18); - assertEq(bribeTokenAmount, 1e18); + assertEq(boldAmount, 1e18, "BOLD amount mismatch"); + assertEq(bribeTokenAmount, 1e18, "Bribe token amount mismatch"); } // check that bribes deposited after user votes can be claimed @@ -354,13 +345,13 @@ contract BribeInitiativeTest is Test { assertEq(5, governance.epoch(), "not in epoch 5"); // check amount of bribes in epoch 3 - (uint128 boldAmountFromStorage, uint128 bribeTokenAmountFromStorage) = + (uint256 boldAmountFromStorage, uint256 bribeTokenAmountFromStorage,) = IBribeInitiative(bribeInitiative).bribeByEpoch(governance.epoch() - 2); assertEq(boldAmountFromStorage, 1e18, "boldAmountFromStorage != 1e18"); assertEq(bribeTokenAmountFromStorage, 1e18, "bribeTokenAmountFromStorage != 1e18"); // check amount of bribes in epoch 4 - (boldAmountFromStorage, bribeTokenAmountFromStorage) = + (boldAmountFromStorage, bribeTokenAmountFromStorage,) = IBribeInitiative(bribeInitiative).bribeByEpoch(governance.epoch() - 1); assertEq(boldAmountFromStorage, 1e18, "boldAmountFromStorage != 1e18"); assertEq(bribeTokenAmountFromStorage, 1e18, "bribeTokenAmountFromStorage != 1e18"); @@ -368,8 +359,8 @@ contract BribeInitiativeTest is Test { // user should receive bribe from their allocated stake for each epoch // user claims for epoch 3 - uint16 claimEpoch = governance.epoch() - 2; // claim for epoch 3 - uint16 prevAllocationEpoch = governance.epoch() - 2; // epoch 3 + uint256 claimEpoch = governance.epoch() - 2; // claim for epoch 3 + uint256 prevAllocationEpoch = governance.epoch() - 2; // epoch 3 (uint256 boldAmount, uint256 bribeTokenAmount) = _claimBribe(user1, claimEpoch, prevAllocationEpoch, prevAllocationEpoch); assertEq(boldAmount, 1e18); @@ -410,126 +401,82 @@ contract BribeInitiativeTest is Test { assertEq(4, governance.epoch(), "not in epoch 4"); // user claims for epoch 3 - uint16 claimEpoch = governance.epoch() - 1; // claim for epoch 3 - uint16 prevAllocationEpoch = governance.epoch() - 1; // epoch 3 + uint256 claimEpoch = governance.epoch() - 1; // claim for epoch 3 + uint256 prevAllocationEpoch = governance.epoch() - 1; // epoch 3 (uint256 boldAmount, uint256 bribeTokenAmount) = _claimBribe(user1, claimEpoch, prevAllocationEpoch, prevAllocationEpoch); - // calculate user share of total allocation for initiative for the given epoch as percentage - (uint88 userLqtyAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, 3); - (uint88 totalLqtyAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(3); - uint256 userShareOfTotalAllocated = uint256((userLqtyAllocated * 10_000) / totalLqtyAllocated); - console2.log("userLqtyAllocated: ", userLqtyAllocated); - console2.log("totalLqtyAllocated: ", totalLqtyAllocated); - - // calculate user received bribes as share of total bribes as percentage - (uint128 boldAmountForEpoch, uint128 bribeTokenAmountForEpoch) = bribeInitiative.bribeByEpoch(3); - uint256 userShareOfTotalBoldForEpoch = (boldAmount * 10_000) / uint256(boldAmountForEpoch); - uint256 userShareOfTotalBribeForEpoch = (bribeTokenAmount * 10_000) / uint256(bribeTokenAmountForEpoch); - - // check that they're equivalent - assertEq( - userShareOfTotalAllocated, - userShareOfTotalBoldForEpoch, - "userShareOfTotalAllocated != userShareOfTotalBoldForEpoch" - ); - assertEq( - userShareOfTotalAllocated, - userShareOfTotalBribeForEpoch, - "userShareOfTotalAllocated != userShareOfTotalBribeForEpoch" - ); + assertEq(boldAmount, 0.5e18, "wrong BOLD amount"); + assertEq(bribeTokenAmount, 0.5e18, "wrong bribe token amount"); } - function test_claimedBribes_fraction_fuzz(uint88 user1StakeAmount, uint88 user2StakeAmount, uint88 user3StakeAmount) - public - { + function test_claimedBribes_fraction_fuzz( + uint256[3] memory userStakeAmount, + uint256 boldAmount, + uint256 bribeTokenAmount + ) public { + address[3] memory user = [user1, user2, user3]; + assertEq(user.length, userStakeAmount.length, "user.length != userStakeAmount.length"); + // =========== epoch 1 ================== - user1StakeAmount = uint88(bound(uint256(user1StakeAmount), 1, lqty.balanceOf(user1))); - user2StakeAmount = uint88(bound(uint256(user2StakeAmount), 1, lqty.balanceOf(user2))); - user3StakeAmount = uint88(bound(uint256(user3StakeAmount), 1, lqty.balanceOf(user3))); + boldAmount = bound(boldAmount, 1, lusd.balanceOf(lusdHolder)); + bribeTokenAmount = bound(bribeTokenAmount, 1, lqty.balanceOf(lusdHolder)); // all users stake in epoch 1 - _stakeLQTY(user1, user1StakeAmount); - _stakeLQTY(user2, user2StakeAmount); - _stakeLQTY(user3, user3StakeAmount); + uint256 totalStakeAmount; + for (uint256 i = 0; i < user.length; ++i) { + totalStakeAmount += userStakeAmount[i] = bound(userStakeAmount[i], 1, lqty.balanceOf(user[i])); + _stakeLQTY(user[i], userStakeAmount[i]); + } // =========== epoch 2 ================== vm.warp(block.timestamp + EPOCH_DURATION); assertEq(2, governance.epoch(), "not in epoch 2"); // lusdHolder deposits lqty and lusd bribes claimable in epoch 3 - _depositBribe(1e18, 1e18, governance.epoch() + 1); + _depositBribe(boldAmount, bribeTokenAmount, governance.epoch() + 1); // =========== epoch 3 ================== vm.warp(block.timestamp + EPOCH_DURATION); assertEq(3, governance.epoch(), "not in epoch 3"); // users all vote on bribeInitiative - _allocateLQTY(user1, int88(user1StakeAmount), 0); - _allocateLQTY(user2, int88(user2StakeAmount), 0); - _allocateLQTY(user3, int88(user3StakeAmount), 0); + for (uint256 i = 0; i < user.length; ++i) { + _allocateLQTY(user[i], int256(userStakeAmount[i]), 0); + } // =========== epoch 4 ================== vm.warp(block.timestamp + EPOCH_DURATION); assertEq(4, governance.epoch(), "not in epoch 4"); // all users claim bribes for epoch 3 - uint16 claimEpoch = governance.epoch() - 1; // claim for epoch 3 - uint16 prevAllocationEpoch = governance.epoch() - 1; // epoch 3 - (uint256 boldAmount1, uint256 bribeTokenAmount1) = - _claimBribe(user1, claimEpoch, prevAllocationEpoch, prevAllocationEpoch); - (uint256 boldAmount2, uint256 bribeTokenAmount2) = - _claimBribe(user2, claimEpoch, prevAllocationEpoch, prevAllocationEpoch); - (uint256 boldAmount3, uint256 bribeTokenAmount3) = - _claimBribe(user3, claimEpoch, prevAllocationEpoch, prevAllocationEpoch); - - // calculate user share of total allocation for initiative for the given epoch as percentage - uint256 userShareOfTotalAllocated1 = _getUserShareOfAllocationAsPercentage(user1, 3); - uint256 userShareOfTotalAllocated2 = _getUserShareOfAllocationAsPercentage(user2, 3); - uint256 userShareOfTotalAllocated3 = _getUserShareOfAllocationAsPercentage(user3, 3); - - // calculate user received bribes as share of total bribes as percentage - (uint256 userShareOfTotalBoldForEpoch1, uint256 userShareOfTotalBribeForEpoch1) = - _getBribesAsPercentageOfTotal(3, boldAmount1, bribeTokenAmount1); - (uint256 userShareOfTotalBoldForEpoch2, uint256 userShareOfTotalBribeForEpoch2) = - _getBribesAsPercentageOfTotal(3, boldAmount2, bribeTokenAmount2); - (uint256 userShareOfTotalBoldForEpoch3, uint256 userShareOfTotalBribeForEpoch3) = - _getBribesAsPercentageOfTotal(3, boldAmount3, bribeTokenAmount3); - - // check that they're equivalent - // user1 - assertEq( - userShareOfTotalAllocated1, - userShareOfTotalBoldForEpoch1, - "userShareOfTotalAllocated1 != userShareOfTotalBoldForEpoch1" - ); - assertEq( - userShareOfTotalAllocated1, - userShareOfTotalBribeForEpoch1, - "userShareOfTotalAllocated1 != userShareOfTotalBribeForEpoch1" - ); - // user2 - assertEq( - userShareOfTotalAllocated2, - userShareOfTotalBoldForEpoch2, - "userShareOfTotalAllocated2 != userShareOfTotalBoldForEpoch2" - ); - assertEq( - userShareOfTotalAllocated2, - userShareOfTotalBribeForEpoch2, - "userShareOfTotalAllocated2 != userShareOfTotalBribeForEpoch2" - ); - // user3 - assertEq( - userShareOfTotalAllocated3, - userShareOfTotalBoldForEpoch3, - "userShareOfTotalAllocated3 != userShareOfTotalBoldForEpoch3" - ); - assertEq( - userShareOfTotalAllocated3, - userShareOfTotalBribeForEpoch3, - "userShareOfTotalAllocated3 != userShareOfTotalBribeForEpoch3" - ); + uint256 claimEpoch = governance.epoch() - 1; // claim for epoch 3 + uint256 prevAllocationEpoch = governance.epoch() - 1; // epoch 3 + uint256 totalClaimedBoldAmount; + uint256 totalClaimedBribeTokenAmount; + + for (uint256 i = 0; i < user.length; ++i) { + (uint256 claimedBoldAmount, uint256 claimedBribeTokenAmount) = + _claimBribe(user[i], claimEpoch, prevAllocationEpoch, prevAllocationEpoch); + + assertApproxEqAbs( + claimedBoldAmount, + boldAmount * userStakeAmount[i] / totalStakeAmount, + // we expect `claimedBoldAmount` to be within `idealAmount +/- 1` + // where `idealAmount = boldAmount * userStakeAmount[i] / totalStakeAmount`, + // however our calculation of `idealAmount` itself has a rounding error of `(-1, 0]`, + // so the total difference can add up to 2 + 2, + string.concat("wrong BOLD amount for user[", i.toString(), "]") + ); + + totalClaimedBoldAmount += claimedBoldAmount; + totalClaimedBribeTokenAmount += claimedBribeTokenAmount; + } + + // total + assertEq(totalClaimedBoldAmount, boldAmount, "there should be no BOLD dust left"); + assertEq(totalClaimedBribeTokenAmount, bribeTokenAmount, "there should be no bribe token dust left"); } // only users that voted receive bribe, vetoes shouldn't receive anything @@ -560,8 +507,8 @@ contract BribeInitiativeTest is Test { assertEq(4, governance.epoch(), "not in epoch 4"); // user claims for epoch 3 - uint16 claimEpoch = governance.epoch() - 1; // claim for epoch 3 - uint16 prevAllocationEpoch = governance.epoch() - 1; // epoch 3 + uint256 claimEpoch = governance.epoch() - 1; // claim for epoch 3 + uint256 prevAllocationEpoch = governance.epoch() - 1; // epoch 3 (uint256 boldAmount, uint256 bribeTokenAmount) = _claimBribe(user1, claimEpoch, prevAllocationEpoch, prevAllocationEpoch); assertEq(boldAmount, 1e18, "voter doesn't receive full bold bribe amount"); @@ -570,7 +517,7 @@ contract BribeInitiativeTest is Test { // user2 should receive no bribes if they try to claim claimEpoch = governance.epoch() - 1; // claim for epoch 3 prevAllocationEpoch = governance.epoch() - 1; // epoch 3 - (boldAmount, bribeTokenAmount) = _claimBribe(user2, claimEpoch, prevAllocationEpoch, prevAllocationEpoch); + (boldAmount, bribeTokenAmount) = _claimBribe(user2, claimEpoch, prevAllocationEpoch, prevAllocationEpoch, true); assertEq(boldAmount, 0, "vetoer receives bold bribe amount"); assertEq(bribeTokenAmount, 0, "vetoer receives bribe amount"); } @@ -604,15 +551,15 @@ contract BribeInitiativeTest is Test { console2.log("current epoch: ", governance.epoch()); // user should receive bribe from their allocated stake in epoch 2 - uint16 claimEpoch = governance.epoch() - 2; // claim for epoch 3 - uint16 prevAllocationEpoch = governance.epoch() - 2; // epoch 3 + uint256 claimEpoch = governance.epoch() - 2; // claim for epoch 3 + uint256 prevAllocationEpoch = governance.epoch() - 2; // epoch 3 (uint256 boldAmount, uint256 bribeTokenAmount) = _claimBribe(user1, claimEpoch, prevAllocationEpoch, prevAllocationEpoch); assertEq(boldAmount, 1e18); assertEq(bribeTokenAmount, 1e18); // decrease user allocation for the initiative - _allocateLQTY(user1, 0, 0); + _resetAllocation(user1); // check if user can still receive bribes after removing votes claimEpoch = governance.epoch() - 1; // claim for epoch 4 @@ -640,7 +587,7 @@ contract BribeInitiativeTest is Test { // user votes on bribeInitiative _allocateLQTY(user1, 1e18, 0); - (uint88 lqtyAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 lqtyAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); assertEq(lqtyAllocated, 1e18, "lqty doesn't immediately get allocated"); } @@ -668,8 +615,8 @@ contract BribeInitiativeTest is Test { // deposit bribe for Epoch + 2 _depositBribe(1e18, 1e18, governance.epoch() + 1); - (uint88 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); assertEq(totalLQTYAllocated, 5e17, "total allocation"); assertEq(userLQTYAllocated, 5e17, "user allocation"); @@ -686,7 +633,7 @@ contract BribeInitiativeTest is Test { _claimBribe(user1, governance.epoch(), governance.epoch() - 1, governance.epoch() - 1, true); // decrease user allocation for the initiative - _allocateLQTY(user1, 0, 0); + _resetAllocation(user1); (userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); (totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); @@ -703,8 +650,8 @@ contract BribeInitiativeTest is Test { lqty.approve(address(bribeInitiative), 1e18); lusd.approve(address(bribeInitiative), 1e18); - vm.expectRevert("BribeInitiative: only-future-epochs"); - bribeInitiative.depositBribe(1e18, 1e18, uint16(0)); + vm.expectRevert("BribeInitiative: now-or-future-epochs"); + bribeInitiative.depositBribe(1e18, 1e18, uint256(0)); vm.stopPrank(); } @@ -720,8 +667,8 @@ contract BribeInitiativeTest is Test { _allocateLQTY(user1, 1e18, 0); - (uint88 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); assertEq(totalLQTYAllocated, 1e18); assertEq(userLQTYAllocated, 1e18); @@ -751,8 +698,8 @@ contract BribeInitiativeTest is Test { _allocateLQTY(user1, 1e18, 0); - (uint88 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); assertEq(totalLQTYAllocated, 1e18); assertEq(userLQTYAllocated, 1e18); @@ -782,8 +729,8 @@ contract BribeInitiativeTest is Test { _allocateLQTY(user1, 1e18, 0); - (uint88 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); assertEq(totalLQTYAllocated, 1e18); assertEq(userLQTYAllocated, 1e18); @@ -814,8 +761,8 @@ contract BribeInitiativeTest is Test { _allocateLQTY(user1, 1e18, 0); - (uint88 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); assertEq(totalLQTYAllocated, 1e18); assertEq(userLQTYAllocated, 1e18); @@ -841,10 +788,10 @@ contract BribeInitiativeTest is Test { vm.warp(block.timestamp + EPOCH_DURATION); - _allocateLQTY(user1, 0, 0); + _tryAllocateNothing(user1); - (uint88 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); assertEq(totalLQTYAllocated, 0); assertEq(userLQTYAllocated, 0); @@ -857,7 +804,7 @@ contract BribeInitiativeTest is Test { epochs[0].epoch = governance.epoch() - 1; epochs[0].prevLQTYAllocationEpoch = governance.epoch() - 2; epochs[0].prevTotalLQTYAllocationEpoch = governance.epoch() - 2; - vm.expectRevert("BribeInitiative: invalid-prev-total-lqty-allocation-epoch"); + vm.expectRevert("BribeInitiative: total-lqty-allocation-zero"); (uint256 boldAmount, uint256 bribeTokenAmount) = bribeInitiative.claimBribes(epochs); vm.stopPrank(); @@ -875,10 +822,10 @@ contract BribeInitiativeTest is Test { vm.warp(block.timestamp + EPOCH_DURATION); - _allocateLQTY(user1, 0, 0); + _tryAllocateNothing(user1); - (uint88 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - (uint88 userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (uint256 totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint256 userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); assertEq(totalLQTYAllocated, 0); assertEq(userLQTYAllocated, 0); @@ -899,10 +846,86 @@ contract BribeInitiativeTest is Test { assertEq(bribeTokenAmount, 0); } + // See https://github.com/liquity/V2-gov/issues/106 + function test_VoterGetsTheirFairShareOfBribes() external { + uint256 bribeAmount = 10_000 ether; + uint256 voteAmount = 100_000 ether; + address otherInitiative = makeAddr("otherInitiative"); + + // Fast-forward to enable registration + vm.warp(block.timestamp + 2 * EPOCH_DURATION); + + vm.startPrank(lusdHolder); + { + // Register otherInitiative, so user1 has something else to vote on + lusd.approve(address(governance), REGISTRATION_FEE); + governance.registerInitiative(otherInitiative); + + // Deposit some bribes into bribeInitiative in next epoch + lusd.approve(address(bribeInitiative), bribeAmount); + lqty.approve(address(bribeInitiative), bribeAmount); + bribeInitiative.depositBribe(bribeAmount, bribeAmount, governance.epoch() + 1); + } + vm.stopPrank(); + + // Ensure otherInitiative can be voted on + vm.warp(block.timestamp + EPOCH_DURATION); + + address[] memory initiativesToReset = new address[](0); + address[] memory initiatives; + int256[] memory votes; + int256[] memory vetos; + + vm.startPrank(user1); + { + initiatives = new address[](2); + votes = new int256[](2); + vetos = new int256[](2); + + initiatives[0] = otherInitiative; + initiatives[1] = address(bribeInitiative); + votes[0] = int256(voteAmount); + votes[1] = int256(voteAmount); + + lqty.approve(governance.deriveUserProxyAddress(user1), 2 * voteAmount); + governance.depositLQTY(2 * voteAmount); + governance.allocateLQTY(initiativesToReset, initiatives, votes, vetos); + } + vm.stopPrank(); + + vm.startPrank(user2); + { + initiatives = new address[](1); + votes = new int256[](1); + vetos = new int256[](1); + + initiatives[0] = address(bribeInitiative); + votes[0] = int256(voteAmount); + + lqty.approve(governance.deriveUserProxyAddress(user2), voteAmount); + governance.depositLQTY(voteAmount); + governance.allocateLQTY(initiativesToReset, initiatives, votes, vetos); + } + vm.stopPrank(); + + // Fast-forward to next epoch, so previous epoch's bribes can be claimed + vm.warp(block.timestamp + EPOCH_DURATION); + + IBribeInitiative.ClaimData[] memory claimData = new IBribeInitiative.ClaimData[](1); + claimData[0].epoch = governance.epoch() - 1; + claimData[0].prevLQTYAllocationEpoch = governance.epoch() - 1; + claimData[0].prevTotalLQTYAllocationEpoch = governance.epoch() - 1; + + vm.prank(user1); + (uint256 lusdBribe, uint256 lqtyBribe) = bribeInitiative.claimBribes(claimData); + assertEqDecimal(lusdBribe, bribeAmount / 2, 18, "user1 didn't get their fair share of LUSD"); + assertEqDecimal(lqtyBribe, bribeAmount / 2, 18, "user1 didn't get their fair share of LQTY"); + } + /** * Helpers */ - function _stakeLQTY(address staker, uint88 amount) public { + function _stakeLQTY(address staker, uint256 amount) internal { vm.startPrank(staker); address userProxy = governance.deriveUserProxyAddress(staker); lqty.approve(address(userProxy), amount); @@ -910,67 +933,98 @@ contract BribeInitiativeTest is Test { vm.stopPrank(); } - function _allocateLQTY(address staker, int88 deltaVoteLQTYAmt, int88 deltaVetoLQTYAmt) public { + function _allocateLQTY(address staker, int256 absoluteVoteLQTYAmt, int256 absoluteVetoLQTYAmt) internal { vm.startPrank(staker); + address[] memory initiativesToReset; + (uint256 currentVote,, uint256 currentVeto,,) = + governance.lqtyAllocatedByUserToInitiative(staker, address(bribeInitiative)); + if (currentVote != 0 || currentVeto != 0) { + initiativesToReset = new address[](1); + initiativesToReset[0] = address(bribeInitiative); + } + address[] memory initiatives = new address[](1); initiatives[0] = address(bribeInitiative); - // voting in favor of the initiative with half of user1's stake - int88[] memory deltaVoteLQTY = new int88[](1); - deltaVoteLQTY[0] = deltaVoteLQTYAmt; + int256[] memory absoluteVoteLQTY = new int256[](1); + absoluteVoteLQTY[0] = absoluteVoteLQTYAmt; - int88[] memory deltaVetoLQTY = new int88[](1); - deltaVetoLQTY[0] = deltaVetoLQTYAmt; + int256[] memory absoluteVetoLQTY = new int256[](1); + absoluteVetoLQTY[0] = absoluteVetoLQTYAmt; - governance.allocateLQTY(initiatives, initiatives, deltaVoteLQTY, deltaVetoLQTY); + governance.allocateLQTY(initiativesToReset, initiatives, absoluteVoteLQTY, absoluteVetoLQTY); vm.stopPrank(); } - function _allocate(address staker, address initiative, int88 votes, int88 vetos) internal { + function _allocate(address staker, address initiative, int256 votes, int256 vetos) internal { vm.startPrank(staker); address[] memory initiatives = new address[](1); initiatives[0] = initiative; - int88[] memory deltaLQTYVotes = new int88[](1); - deltaLQTYVotes[0] = votes; - int88[] memory deltaLQTYVetos = new int88[](1); - deltaLQTYVetos[0] = vetos; + int256[] memory absoluteLQTYVotes = new int256[](1); + absoluteLQTYVotes[0] = votes; + int256[] memory absoluteLQTYVetos = new int256[](1); + absoluteLQTYVetos[0] = vetos; - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiatives, initiatives, absoluteLQTYVotes, absoluteLQTYVetos); vm.stopPrank(); } - function _depositBribe(uint128 boldAmount, uint128 bribeAmount, uint16 epoch) public { + function _tryAllocateNothing(address staker) internal { + vm.startPrank(staker); + address[] memory initiativesToReset; + + address[] memory initiatives = new address[](1); + initiatives[0] = address(bribeInitiative); + + int256[] memory absoluteVoteLQTY = new int256[](1); + int256[] memory absoluteVetoLQTY = new int256[](1); + + vm.expectRevert("Governance: voting nothing"); + governance.allocateLQTY(initiativesToReset, initiatives, absoluteVoteLQTY, absoluteVetoLQTY); + vm.stopPrank(); + } + + function _resetAllocation(address staker) internal { + vm.startPrank(staker); + address[] memory initiativesToReset = new address[](1); + initiativesToReset[0] = address(bribeInitiative); + + governance.resetAllocations(initiativesToReset, true); + vm.stopPrank(); + } + + function _depositBribe(uint256 boldAmount, uint256 bribeAmount, uint256 epoch) public { vm.startPrank(lusdHolder); - lqty.approve(address(bribeInitiative), boldAmount); - lusd.approve(address(bribeInitiative), bribeAmount); + lusd.approve(address(bribeInitiative), boldAmount); + lqty.approve(address(bribeInitiative), bribeAmount); bribeInitiative.depositBribe(boldAmount, bribeAmount, epoch); vm.stopPrank(); } - function _depositBribe(address _initiative, uint128 boldAmount, uint128 bribeAmount, uint16 epoch) public { + function _depositBribe(address _initiative, uint256 boldAmount, uint256 bribeAmount, uint256 epoch) public { vm.startPrank(lusdHolder); - lqty.approve(_initiative, boldAmount); - lusd.approve(_initiative, bribeAmount); + lusd.approve(_initiative, boldAmount); + lqty.approve(_initiative, bribeAmount); BribeInitiative(_initiative).depositBribe(boldAmount, bribeAmount, epoch); vm.stopPrank(); } function _claimBribe( address claimer, - uint16 epoch, - uint16 prevLQTYAllocationEpoch, - uint16 prevTotalLQTYAllocationEpoch + uint256 epoch, + uint256 prevLQTYAllocationEpoch, + uint256 prevTotalLQTYAllocationEpoch ) public returns (uint256 boldAmount, uint256 bribeTokenAmount) { return _claimBribe(claimer, epoch, prevLQTYAllocationEpoch, prevTotalLQTYAllocationEpoch, false); } function _claimBribe( address claimer, - uint16 epoch, - uint16 prevLQTYAllocationEpoch, - uint16 prevTotalLQTYAllocationEpoch, + uint256 epoch, + uint256 prevLQTYAllocationEpoch, + uint256 prevTotalLQTYAllocationEpoch, bool expectRevert ) public returns (uint256 boldAmount, uint256 bribeTokenAmount) { vm.startPrank(claimer); @@ -984,23 +1038,4 @@ contract BribeInitiativeTest is Test { (boldAmount, bribeTokenAmount) = bribeInitiative.claimBribes(epochs); vm.stopPrank(); } - - function _getUserShareOfAllocationAsPercentage(address user, uint16 epoch) - internal - returns (uint256 userShareOfTotalAllocated) - { - (uint88 userLqtyAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, epoch); - (uint88 totalLqtyAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(epoch); - userShareOfTotalAllocated = (uint256(userLqtyAllocated) * 10_000) / uint256(totalLqtyAllocated); - } - - function _getBribesAsPercentageOfTotal(uint16 epoch, uint256 userBoldAmount, uint256 userBribeTokenAmount) - internal - returns (uint256 userShareOfTotalBoldForEpoch, uint256 userShareOfTotalBribeForEpoch) - { - (uint128 boldAmountForEpoch, uint128 bribeTokenAmountForEpoch) = bribeInitiative.bribeByEpoch(epoch); - uint256 userShareOfTotalBoldForEpoch = (userBoldAmount * 10_000) / uint256(boldAmountForEpoch); - uint256 userShareOfTotalBribeForEpoch = (userBribeTokenAmount * 10_000) / uint256(bribeTokenAmountForEpoch); - return (userShareOfTotalBoldForEpoch, userShareOfTotalBribeForEpoch); - } } diff --git a/test/BribeInitiativeAllocate.t.sol b/test/BribeInitiativeAllocate.t.sol index 3653a68f..0e9deaee 100644 --- a/test/BribeInitiativeAllocate.t.sol +++ b/test/BribeInitiativeAllocate.t.sol @@ -1,959 +1,956 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {Test} from "forge-std/Test.sol"; -import {console} from "forge-std/console.sol"; -import {MockERC20} from "forge-std/mocks/MockERC20.sol"; - -import {Governance} from "../src/Governance.sol"; -import {BribeInitiative} from "../src/BribeInitiative.sol"; - -import {IGovernance} from "../src/interfaces/IGovernance.sol"; - -import {MockStakingV1} from "./mocks/MockStakingV1.sol"; -import {MockGovernance} from "./mocks/MockGovernance.sol"; - -// new epoch: -// no veto to no veto: insert new user allocation, add and sub from total allocation -// (prevVoteLQTY == 0 || prevVoteLQTY != 0) && _vetoLQTY == 0 - -// no veto to veto: insert new 0 user allocation, sub from total allocation -// (prevVoteLQTY == 0 || prevVoteLQTY != 0) && _vetoLQTY != 0 - -// veto to no veto: insert new user allocation, add to total allocation -// prevVoteLQTY == 0 && _vetoLQTY == 0 - -// veto to veto: insert new 0 user allocation, do nothing to total allocation -// prevVoteLQTY == 0 && _vetoLQTY != 0 - -// same epoch: -// no veto to no veto: update user allocation, add and sub from total allocation -// no veto to veto: set 0 user allocation, sub from total allocation -// veto to no veto: update user allocation, add to total allocation -// veto to veto: set 0 user allocation, do nothing to total allocation - -contract BribeInitiativeAllocateTest is Test { - MockERC20 private lqty; - MockERC20 private lusd; - address private stakingV1; - address private constant user = address(0xF977814e90dA44bFA03b6295A0616a897441aceC); - address private constant user2 = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); - address private constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); - - MockGovernance private governance; - BribeInitiative private bribeInitiative; - - function setUp() public { - lqty = deployMockERC20("Liquity", "LQTY", 18); - lusd = deployMockERC20("Liquity USD", "LUSD", 18); - - vm.store(address(lqty), keccak256(abi.encode(address(lusdHolder), 4)), bytes32(abi.encode(10000e18))); - vm.store(address(lusd), keccak256(abi.encode(address(lusdHolder), 4)), bytes32(abi.encode(10000e18))); - - stakingV1 = address(new MockStakingV1(address(lqty))); - - governance = new MockGovernance(); - - bribeInitiative = new BribeInitiative(address(governance), address(lusd), address(lqty)); - } - - function test_onAfterAllocateLQTY_newEpoch_NoVetoToNoVeto() public { - vm.startPrank(lusdHolder); - lqty.approve(address(bribeInitiative), 1000e18); - lusd.approve(address(bribeInitiative), 1000e18); - bribeInitiative.depositBribe(1000e18, 1000e18, governance.epoch() + 1); - vm.stopPrank(); - governance.setEpoch(1); - - vm.startPrank(address(governance)); - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: 1}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - } - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); - assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - - { - IGovernance.UserState memory userState2 = - IGovernance.UserState({allocatedLQTY: 1000e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation2 = - IGovernance.Allocation({voteLQTY: 1000e18, vetoLQTY: 0, atEpoch: 1}); - IGovernance.InitiativeState memory initiativeState2 = IGovernance.InitiativeState({ - voteLQTY: 1001e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState2, allocation2, initiativeState2); - } - - (uint88 totalLQTYAllocated2, uint120 totalAverageTimestamp2) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated2, 1001e18); - assertEq(totalAverageTimestamp2, block.timestamp); - (uint88 userLQTYAllocated2, uint120 userAverageTimestamp2) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated2, 1000e18); - assertEq(userAverageTimestamp2, block.timestamp); - - vm.startPrank(lusdHolder); - lqty.approve(address(bribeInitiative), 1000e18); - lusd.approve(address(bribeInitiative), 1000e18); - bribeInitiative.depositBribe(1000e18, 1000e18, governance.epoch() + 1); - vm.stopPrank(); - governance.setEpoch(2); - - vm.startPrank(address(governance)); - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 2000e18, averageStakingTimestamp: uint32(1)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 2000e18, vetoLQTY: 0, atEpoch: 2}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 2001e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(1), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - } - - (totalLQTYAllocated, totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 2001e18); - assertEq(totalAverageTimestamp, 1); - (userLQTYAllocated, userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated, 2000e18); - assertEq(userAverageTimestamp, 1); - - governance.setEpoch(3); - - vm.startPrank(address(user)); - - BribeInitiative.ClaimData[] memory claimData = new BribeInitiative.ClaimData[](1); - claimData[0].epoch = 2; - claimData[0].prevLQTYAllocationEpoch = 2; - claimData[0].prevTotalLQTYAllocationEpoch = 2; - (uint256 boldAmount, uint256 bribeTokenAmount) = bribeInitiative.claimBribes(claimData); - assertGt(boldAmount, 999e18); - assertGt(bribeTokenAmount, 999e18); - } - - function test_onAfterAllocateLQTY_newEpoch_NoVetoToVeto() public { - governance.setEpoch(1); - vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch - - vm.startPrank(address(governance)); - - // set user2 allocations like governance would using onAfterAllocateLQTY at epoch 1 - // sets avgTimestamp to current block.timestamp - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: 1}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); - assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - // set user2 allocations like governance would using onAfterAllocateLQTY at epoch 1 - // sets avgTimestamp to current block.timestamp - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: 1}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1001e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1001e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); - assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - // lusdHolder deposits bribes into the initiative - vm.startPrank(lusdHolder); - lqty.approve(address(bribeInitiative), 1000e18); - lusd.approve(address(bribeInitiative), 1000e18); - bribeInitiative.depositBribe(1000e18, 1000e18, governance.epoch() + 1); - vm.stopPrank(); - - governance.setEpoch(2); - vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts - - vm.startPrank(address(governance)); - - // set allocation in initiative for user in epoch 1 - // sets avgTimestamp to current block.timestamp - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: 1}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 0, - vetoLQTY: 1, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 0); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated, 0); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - // set allocation in initiative for user2 in epoch 1 - // sets avgTimestamp to current block.timestamp - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: 1}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 0, - vetoLQTY: 1, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 0); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); - assertEq(userLQTYAllocated, 0); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - governance.setEpoch(3); - vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to third epoch ts - - vm.startPrank(address(user)); - - BribeInitiative.ClaimData[] memory claimData = new BribeInitiative.ClaimData[](1); - claimData[0].epoch = 2; - claimData[0].prevLQTYAllocationEpoch = 2; - claimData[0].prevTotalLQTYAllocationEpoch = 2; - (uint256 boldAmount, uint256 bribeTokenAmount) = bribeInitiative.claimBribes(claimData); - assertEq(boldAmount, 0, "boldAmount nonzero"); - assertEq(bribeTokenAmount, 0, "bribeTokenAmount nonzero"); - } - - function test_onAfterAllocateLQTY_newEpoch_VetoToNoVeto() public { - governance.setEpoch(1); - vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch - - vm.startPrank(address(governance)); - - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); - assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - - IGovernance.UserState memory userStateVeto = - IGovernance.UserState({allocatedLQTY: 1000e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocationVeto = - IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1000e18, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeStateVeto = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 1000e18, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: uint32(block.timestamp), - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY( - governance.epoch(), user, userStateVeto, allocationVeto, initiativeStateVeto - ); - - (uint88 totalLQTYAllocatedAfterVeto, uint120 totalAverageTimestampAfterVeto) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocatedAfterVeto, 1e18); - assertEq(totalAverageTimestampAfterVeto, uint120(block.timestamp)); - (uint88 userLQTYAllocatedAfterVeto, uint120 userAverageTimestampAfterVeto) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocatedAfterVeto, 0); - assertEq(userAverageTimestampAfterVeto, uint120(block.timestamp)); - - governance.setEpoch(2); - vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts - - IGovernance.UserState memory userStateNewEpoch = - IGovernance.UserState({allocatedLQTY: 1, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocationNewEpoch = - IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeStateNewEpoch = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 1, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: uint32(block.timestamp), - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY( - governance.epoch(), user, userStateNewEpoch, allocationNewEpoch, initiativeStateNewEpoch - ); - - (uint88 totalLQTYAllocatedNewEpoch, uint120 totalAverageTimestampNewEpoch) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocatedNewEpoch, 1e18); - assertEq(totalAverageTimestampNewEpoch, uint120(block.timestamp)); - (uint88 userLQTYAllocatedNewEpoch, uint120 userAverageTimestampNewEpoch) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocatedNewEpoch, 0); - assertEq(userAverageTimestampNewEpoch, uint120(block.timestamp)); - - vm.startPrank(lusdHolder); - lqty.approve(address(bribeInitiative), 1000e18); - lusd.approve(address(bribeInitiative), 1000e18); - bribeInitiative.depositBribe(1000e18, 1000e18, governance.epoch() + 1); - vm.stopPrank(); - - vm.startPrank(address(governance)); - - governance.setEpoch(3); - vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to third epoch ts - - IGovernance.UserState memory userStateNewEpoch3 = - IGovernance.UserState({allocatedLQTY: 2000e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocationNewEpoch3 = - IGovernance.Allocation({voteLQTY: 2000e18, vetoLQTY: 0, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeStateNewEpoch3 = IGovernance.InitiativeState({ - voteLQTY: 2001e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY( - governance.epoch(), user, userStateNewEpoch3, allocationNewEpoch3, initiativeStateNewEpoch3 - ); - - (uint88 totalLQTYAllocatedNewEpoch3, uint120 totalAverageTimestampNewEpoch3) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocatedNewEpoch3, 2001e18); - assertEq(totalAverageTimestampNewEpoch3, uint120(block.timestamp)); - (uint88 userLQTYAllocatedNewEpoch3, uint120 userAverageTimestampNewEpoch3) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocatedNewEpoch3, 2000e18); - assertEq(userAverageTimestampNewEpoch3, uint120(block.timestamp)); - - governance.setEpoch(4); - vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to fourth epoch ts - - vm.startPrank(address(user)); - - BribeInitiative.ClaimData[] memory claimData = new BribeInitiative.ClaimData[](1); - claimData[0].epoch = 3; - claimData[0].prevLQTYAllocationEpoch = 3; - claimData[0].prevTotalLQTYAllocationEpoch = 3; - bribeInitiative.claimBribes(claimData); - } - - function test_onAfterAllocateLQTY_newEpoch_VetoToVeto() public { - governance.setEpoch(1); - - vm.startPrank(address(governance)); - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); - assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1000e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 1000e18, vetoLQTY: 0, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1001e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1001e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated, 1000e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - governance.setEpoch(2); - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated, 0); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - governance.setEpoch(3); - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated, 0); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - } - - function test_onAfterAllocateLQTY_sameEpoch_NoVetoToNoVeto() public { - vm.startPrank(lusdHolder); - lqty.approve(address(bribeInitiative), 1000e18); - lusd.approve(address(bribeInitiative), 1000e18); - bribeInitiative.depositBribe(1000e18, 1000e18, governance.epoch() + 1); - vm.stopPrank(); - - governance.setEpoch(1); - vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch - - vm.startPrank(address(governance)); - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); - assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1000e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 1000e18, vetoLQTY: 0, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1001e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1001e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated, 1000e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 2000e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 2000e18, vetoLQTY: 0, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 2001e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 2001e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated, 2000e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - governance.setEpoch(2); - vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts - - vm.startPrank(address(user)); - - BribeInitiative.ClaimData[] memory claimData = new BribeInitiative.ClaimData[](1); - claimData[0].epoch = 1; - claimData[0].prevLQTYAllocationEpoch = 1; - claimData[0].prevTotalLQTYAllocationEpoch = 1; - bribeInitiative.claimBribes(claimData); - } - - function test_onAfterAllocateLQTY_sameEpoch_NoVetoToVeto() public { - vm.startPrank(lusdHolder); - lqty.approve(address(bribeInitiative), 1000e18); - lusd.approve(address(bribeInitiative), 1000e18); - bribeInitiative.depositBribe(1000e18, 1000e18, governance.epoch() + 1); - vm.stopPrank(); - - governance.setEpoch(1); - vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch - - vm.startPrank(address(governance)); - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); - assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1000e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 1000e18, vetoLQTY: 0, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1001e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1001e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated, 1000e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated, 0); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - governance.setEpoch(2); - vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts - - vm.startPrank(address(user)); - - BribeInitiative.ClaimData[] memory claimData = new BribeInitiative.ClaimData[](1); - claimData[0].epoch = 1; - claimData[0].prevLQTYAllocationEpoch = 1; - claimData[0].prevTotalLQTYAllocationEpoch = 1; - (uint256 boldAmount, uint256 bribeTokenAmount) = bribeInitiative.claimBribes(claimData); - assertEq(boldAmount, 0); - assertEq(bribeTokenAmount, 0); - } - - function test_onAfterAllocateLQTY_sameEpoch_VetoToNoVeto() public { - vm.startPrank(lusdHolder); - lqty.approve(address(bribeInitiative), 1000e18); - lusd.approve(address(bribeInitiative), 1000e18); - bribeInitiative.depositBribe(1000e18, 1000e18, governance.epoch() + 1); - vm.stopPrank(); - - governance.setEpoch(1); - vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch - - vm.startPrank(address(governance)); - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); - assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1000e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 1000e18, vetoLQTY: 0, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1001e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1001e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated, 1000e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated, 0); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 2000e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 2000e18, vetoLQTY: 0, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 2001e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 2001e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated, 2000e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - governance.setEpoch(2); - vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts - - vm.startPrank(address(user)); - - BribeInitiative.ClaimData[] memory claimData = new BribeInitiative.ClaimData[](1); - claimData[0].epoch = 1; - claimData[0].prevLQTYAllocationEpoch = 1; - claimData[0].prevTotalLQTYAllocationEpoch = 1; - bribeInitiative.claimBribes(claimData); - } - - function test_onAfterAllocateLQTY_sameEpoch_VetoToVeto() public { - governance.setEpoch(1); - - vm.startPrank(address(governance)); - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); - assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1000e18, averageStakingTimestamp: uint32(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 1000e18, vetoLQTY: 0, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1001e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint32(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1001e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated, 1000e18); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1, averageStakingTimestamp: uint120(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint120(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated, 0); - assertEq(userAverageTimestamp, uint120(block.timestamp)); - } - - { - IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 2, averageStakingTimestamp: uint120(block.timestamp)}); - IGovernance.Allocation memory allocation = - IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 2, atEpoch: uint16(governance.epoch())}); - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ - voteLQTY: 1e18, - vetoLQTY: 0, - averageStakingTimestampVoteLQTY: uint120(block.timestamp), - averageStakingTimestampVetoLQTY: 0, - lastEpochClaim: 0 - }); - bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - - (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = - bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); - assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint120(block.timestamp)); - (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = - bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); - assertEq(userLQTYAllocated, 0); - assertEq(userAverageTimestamp, uint32(block.timestamp)); - } - } - - // function test_onAfterAllocateLQTY() public { - // governance.setEpoch(1); - - // vm.startPrank(address(governance)); - - // // first total deposit, first user deposit - // bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, 1000e18, 0); - // assertEq(bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()), 1000e18); - // assertEq(bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()), 1000e18); - - // // second total deposit, second user deposit - // bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, 1000e18, 0); - // assertEq(bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()), 1000e18); // should stay the same - // assertEq(bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()), 1000e18); // should stay the same - - // // third total deposit, first user deposit - // bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, 1000e18, 0); - // assertEq(bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()), 2000e18); - // assertEq(bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()), 1000e18); - - // vm.stopPrank(); - // } -} +// // SPDX-License-Identifier: UNLICENSED +// pragma solidity ^0.8.24; + +// import {Test} from "forge-std/Test.sol"; +// import {console} from "forge-std/console.sol"; + +// import {BribeInitiative} from "../src/BribeInitiative.sol"; + +// import {IGovernance} from "../src/interfaces/IGovernance.sol"; + +// import {MockERC20Tester} from "./mocks/MockERC20Tester.sol"; +// import {MockGovernance} from "./mocks/MockGovernance.sol"; + +// // new epoch: +// // no veto to no veto: insert new user allocation, add and sub from total allocation +// // (prevVoteLQTY == 0 || prevVoteLQTY != 0) && _vetoLQTY == 0 + +// // no veto to veto: insert new 0 user allocation, sub from total allocation +// // (prevVoteLQTY == 0 || prevVoteLQTY != 0) && _vetoLQTY != 0 + +// // veto to no veto: insert new user allocation, add to total allocation +// // prevVoteLQTY == 0 && _vetoLQTY == 0 + +// // veto to veto: insert new 0 user allocation, do nothing to total allocation +// // prevVoteLQTY == 0 && _vetoLQTY != 0 + +// // same epoch: +// // no veto to no veto: update user allocation, add and sub from total allocation +// // no veto to veto: set 0 user allocation, sub from total allocation +// // veto to no veto: update user allocation, add to total allocation +// // veto to veto: set 0 user allocation, do nothing to total allocation + +// contract BribeInitiativeAllocateTest is Test { +// MockERC20Tester private lqty; +// MockERC20Tester private lusd; +// address private constant user = address(0xF977814e90dA44bFA03b6295A0616a897441aceC); +// address private constant user2 = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); +// address private constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); + +// MockGovernance private governance; +// BribeInitiative private bribeInitiative; + +// function setUp() public { +// lqty = new MockERC20Tester("Liquity", "LQTY"); +// lusd = new MockERC20Tester("Liquity USD", "LUSD"); + +// lqty.mint(lusdHolder, 10000e18); +// lusd.mint(lusdHolder, 10000e18); + +// governance = new MockGovernance(); + +// bribeInitiative = new BribeInitiative(address(governance), address(lusd), address(lqty)); +// } + +// function test_onAfterAllocateLQTY_newEpoch_NoVetoToNoVeto() public { +// vm.startPrank(lusdHolder); +// lqty.approve(address(bribeInitiative), 1000e18); +// lusd.approve(address(bribeInitiative), 1000e18); +// bribeInitiative.depositBribe(1000e18, 1000e18, governance.epoch() + 1); +// vm.stopPrank(); +// governance.setEpoch(1); + +// vm.startPrank(address(governance)); + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: 1}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); +// } +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); +// assertEq(userLQTYAllocated, 1e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); + +// { +// IGovernance.UserState memory userState2 = +// IGovernance.UserState({allocatedLQTY: 1000e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation2 = +// IGovernance.Allocation({voteLQTY: 1000e18, vetoLQTY: 0, atEpoch: 1}); +// IGovernance.InitiativeState memory initiativeState2 = IGovernance.InitiativeState({ +// voteLQTY: 1001e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState2, allocation2, initiativeState2); +// } + +// (uint256 totalLQTYAllocated2, uint256 totalAverageTimestamp2) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated2, 1001e18); +// assertEq(totalAverageTimestamp2, block.timestamp); +// (uint256 userLQTYAllocated2, uint256 userAverageTimestamp2) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated2, 1000e18); +// assertEq(userAverageTimestamp2, block.timestamp); + +// vm.startPrank(lusdHolder); +// lqty.approve(address(bribeInitiative), 1000e18); +// lusd.approve(address(bribeInitiative), 1000e18); +// bribeInitiative.depositBribe(1000e18, 1000e18, governance.epoch() + 1); +// vm.stopPrank(); +// governance.setEpoch(2); + +// vm.startPrank(address(governance)); + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 2000e18, averageStakingTimestamp: uint256(1)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 2000e18, vetoLQTY: 0, atEpoch: 2}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 2001e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(1), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); +// } + +// (totalLQTYAllocated, totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 2001e18); +// assertEq(totalAverageTimestamp, 1); +// (userLQTYAllocated, userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated, 2000e18); +// assertEq(userAverageTimestamp, 1); + +// governance.setEpoch(3); + +// vm.startPrank(address(user)); + +// BribeInitiative.ClaimData[] memory claimData = new BribeInitiative.ClaimData[](1); +// claimData[0].epoch = 2; +// claimData[0].prevLQTYAllocationEpoch = 2; +// claimData[0].prevTotalLQTYAllocationEpoch = 2; +// (uint256 boldAmount, uint256 bribeTokenAmount) = bribeInitiative.claimBribes(claimData); +// assertGt(boldAmount, 999e18); +// assertGt(bribeTokenAmount, 999e18); +// } + +// function test_onAfterAllocateLQTY_newEpoch_NoVetoToVeto() public { +// governance.setEpoch(1); +// vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch + +// vm.startPrank(address(governance)); + +// // set user2 allocations like governance would using onAfterAllocateLQTY at epoch 1 +// // sets avgTimestamp to current block.timestamp +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: 1}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); +// assertEq(userLQTYAllocated, 1e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// // set user2 allocations like governance would using onAfterAllocateLQTY at epoch 1 +// // sets avgTimestamp to current block.timestamp +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: 1}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1001e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1001e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); +// assertEq(userLQTYAllocated, 1e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// // lusdHolder deposits bribes into the initiative +// vm.startPrank(lusdHolder); +// lqty.approve(address(bribeInitiative), 1000e18); +// lusd.approve(address(bribeInitiative), 1000e18); +// bribeInitiative.depositBribe(1000e18, 1000e18, governance.epoch() + 1); +// vm.stopPrank(); + +// governance.setEpoch(2); +// vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts + +// vm.startPrank(address(governance)); + +// // set allocation in initiative for user in epoch 1 +// // sets avgTimestamp to current block.timestamp +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: 1}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 0, +// vetoLQTY: 1, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 0); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated, 0); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// // set allocation in initiative for user2 in epoch 1 +// // sets avgTimestamp to current block.timestamp +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: 1}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 0, +// vetoLQTY: 1, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 0); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); +// assertEq(userLQTYAllocated, 0); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// governance.setEpoch(3); +// vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to third epoch ts + +// vm.startPrank(address(user)); + +// BribeInitiative.ClaimData[] memory claimData = new BribeInitiative.ClaimData[](1); +// claimData[0].epoch = 2; +// claimData[0].prevLQTYAllocationEpoch = 2; +// claimData[0].prevTotalLQTYAllocationEpoch = 2; +// vm.expectRevert("BribeInitiative: total-lqty-allocation-zero"); +// (uint256 boldAmount, uint256 bribeTokenAmount) = bribeInitiative.claimBribes(claimData); +// assertEq(boldAmount, 0, "boldAmount nonzero"); +// assertEq(bribeTokenAmount, 0, "bribeTokenAmount nonzero"); +// } + +// function test_onAfterAllocateLQTY_newEpoch_VetoToNoVeto() public { +// governance.setEpoch(1); +// vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch + +// vm.startPrank(address(governance)); + +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); +// assertEq(userLQTYAllocated, 1e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); + +// IGovernance.UserState memory userStateVeto = +// IGovernance.UserState({allocatedLQTY: 1000e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocationVeto = +// IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1000e18, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeStateVeto = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 1000e18, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: uint256(block.timestamp), +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY( +// governance.epoch(), user, userStateVeto, allocationVeto, initiativeStateVeto +// ); + +// (uint256 totalLQTYAllocatedAfterVeto, uint256 totalAverageTimestampAfterVeto) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocatedAfterVeto, 1e18); +// assertEq(totalAverageTimestampAfterVeto, uint256(block.timestamp)); +// (uint256 userLQTYAllocatedAfterVeto, uint256 userAverageTimestampAfterVeto) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocatedAfterVeto, 0); +// assertEq(userAverageTimestampAfterVeto, uint256(block.timestamp)); + +// governance.setEpoch(2); +// vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts + +// IGovernance.UserState memory userStateNewEpoch = +// IGovernance.UserState({allocatedLQTY: 1, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocationNewEpoch = +// IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeStateNewEpoch = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 1, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: uint256(block.timestamp), +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY( +// governance.epoch(), user, userStateNewEpoch, allocationNewEpoch, initiativeStateNewEpoch +// ); + +// (uint256 totalLQTYAllocatedNewEpoch, uint256 totalAverageTimestampNewEpoch) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocatedNewEpoch, 1e18); +// assertEq(totalAverageTimestampNewEpoch, uint256(block.timestamp)); +// (uint256 userLQTYAllocatedNewEpoch, uint256 userAverageTimestampNewEpoch) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocatedNewEpoch, 0); +// assertEq(userAverageTimestampNewEpoch, uint256(block.timestamp)); + +// vm.startPrank(lusdHolder); +// lqty.approve(address(bribeInitiative), 1000e18); +// lusd.approve(address(bribeInitiative), 1000e18); +// bribeInitiative.depositBribe(1000e18, 1000e18, governance.epoch() + 1); +// vm.stopPrank(); + +// vm.startPrank(address(governance)); + +// governance.setEpoch(3); +// vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to third epoch ts + +// IGovernance.UserState memory userStateNewEpoch3 = +// IGovernance.UserState({allocatedLQTY: 2000e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocationNewEpoch3 = +// IGovernance.Allocation({voteLQTY: 2000e18, vetoLQTY: 0, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeStateNewEpoch3 = IGovernance.InitiativeState({ +// voteLQTY: 2001e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY( +// governance.epoch(), user, userStateNewEpoch3, allocationNewEpoch3, initiativeStateNewEpoch3 +// ); + +// (uint256 totalLQTYAllocatedNewEpoch3, uint256 totalAverageTimestampNewEpoch3) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocatedNewEpoch3, 2001e18); +// assertEq(totalAverageTimestampNewEpoch3, uint256(block.timestamp)); +// (uint256 userLQTYAllocatedNewEpoch3, uint256 userAverageTimestampNewEpoch3) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocatedNewEpoch3, 2000e18); +// assertEq(userAverageTimestampNewEpoch3, uint256(block.timestamp)); + +// governance.setEpoch(4); +// vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to fourth epoch ts + +// vm.startPrank(address(user)); + +// BribeInitiative.ClaimData[] memory claimData = new BribeInitiative.ClaimData[](1); +// claimData[0].epoch = 3; +// claimData[0].prevLQTYAllocationEpoch = 3; +// claimData[0].prevTotalLQTYAllocationEpoch = 3; +// bribeInitiative.claimBribes(claimData); +// } + +// function test_onAfterAllocateLQTY_newEpoch_VetoToVeto() public { +// governance.setEpoch(1); + +// vm.startPrank(address(governance)); + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); +// assertEq(userLQTYAllocated, 1e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1000e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 1000e18, vetoLQTY: 0, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1001e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1001e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated, 1000e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// governance.setEpoch(2); + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated, 0); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// governance.setEpoch(3); + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated, 0); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } +// } + +// function test_onAfterAllocateLQTY_sameEpoch_NoVetoToNoVeto() public { +// vm.startPrank(lusdHolder); +// lqty.approve(address(bribeInitiative), 1000e18); +// lusd.approve(address(bribeInitiative), 1000e18); +// bribeInitiative.depositBribe(1000e18, 1000e18, governance.epoch() + 1); +// vm.stopPrank(); + +// governance.setEpoch(1); +// vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch + +// vm.startPrank(address(governance)); + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); +// assertEq(userLQTYAllocated, 1e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1000e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 1000e18, vetoLQTY: 0, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1001e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1001e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated, 1000e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 2000e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 2000e18, vetoLQTY: 0, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 2001e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 2001e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated, 2000e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// governance.setEpoch(2); +// vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts + +// vm.startPrank(address(user)); + +// BribeInitiative.ClaimData[] memory claimData = new BribeInitiative.ClaimData[](1); +// claimData[0].epoch = 1; +// claimData[0].prevLQTYAllocationEpoch = 1; +// claimData[0].prevTotalLQTYAllocationEpoch = 1; +// bribeInitiative.claimBribes(claimData); +// } + +// function test_onAfterAllocateLQTY_sameEpoch_NoVetoToVeto() public { +// vm.startPrank(lusdHolder); +// lqty.approve(address(bribeInitiative), 1000e18); +// lusd.approve(address(bribeInitiative), 1000e18); +// bribeInitiative.depositBribe(1000e18, 1000e18, governance.epoch() + 1); +// vm.stopPrank(); + +// governance.setEpoch(1); +// vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch + +// vm.startPrank(address(governance)); + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); +// assertEq(userLQTYAllocated, 1e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1000e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 1000e18, vetoLQTY: 0, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1001e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1001e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated, 1000e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated, 0); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// governance.setEpoch(2); +// vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts + +// vm.startPrank(address(user)); + +// BribeInitiative.ClaimData[] memory claimData = new BribeInitiative.ClaimData[](1); +// claimData[0].epoch = 1; +// claimData[0].prevLQTYAllocationEpoch = 1; +// claimData[0].prevTotalLQTYAllocationEpoch = 1; +// vm.expectRevert("BribeInitiative: lqty-allocation-zero"); +// (uint256 boldAmount, uint256 bribeTokenAmount) = bribeInitiative.claimBribes(claimData); +// assertEq(boldAmount, 0); +// assertEq(bribeTokenAmount, 0); +// } + +// function test_onAfterAllocateLQTY_sameEpoch_VetoToNoVeto() public { +// vm.startPrank(lusdHolder); +// lqty.approve(address(bribeInitiative), 1000e18); +// lusd.approve(address(bribeInitiative), 1000e18); +// bribeInitiative.depositBribe(1000e18, 1000e18, governance.epoch() + 1); +// vm.stopPrank(); + +// governance.setEpoch(1); +// vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch + +// vm.startPrank(address(governance)); + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); +// assertEq(userLQTYAllocated, 1e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1000e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 1000e18, vetoLQTY: 0, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1001e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1001e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated, 1000e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated, 0); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 2000e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 2000e18, vetoLQTY: 0, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 2001e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 2001e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated, 2000e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// governance.setEpoch(2); +// vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts + +// vm.startPrank(address(user)); + +// BribeInitiative.ClaimData[] memory claimData = new BribeInitiative.ClaimData[](1); +// claimData[0].epoch = 1; +// claimData[0].prevLQTYAllocationEpoch = 1; +// claimData[0].prevTotalLQTYAllocationEpoch = 1; +// bribeInitiative.claimBribes(claimData); +// } + +// function test_onAfterAllocateLQTY_sameEpoch_VetoToVeto() public { +// governance.setEpoch(1); + +// vm.startPrank(address(governance)); + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 1e18, vetoLQTY: 0, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); +// assertEq(userLQTYAllocated, 1e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1000e18, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 1000e18, vetoLQTY: 0, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1001e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1001e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated, 1000e18); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 1, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 1, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated, 0); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } + +// { +// IGovernance.UserState memory userState = +// IGovernance.UserState({allocatedLQTY: 2, averageStakingTimestamp: uint256(block.timestamp)}); +// IGovernance.Allocation memory allocation = +// IGovernance.Allocation({voteLQTY: 0, vetoLQTY: 2, atEpoch: uint256(governance.epoch())}); +// IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState({ +// voteLQTY: 1e18, +// vetoLQTY: 0, +// averageStakingTimestampVoteLQTY: uint256(block.timestamp), +// averageStakingTimestampVetoLQTY: 0, +// lastEpochClaim: 0 +// }); +// bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); + +// (uint256 totalLQTYAllocated, uint256 totalAverageTimestamp) = +// bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); +// assertEq(totalLQTYAllocated, 1e18); +// assertEq(totalAverageTimestamp, uint256(block.timestamp)); +// (uint256 userLQTYAllocated, uint256 userAverageTimestamp) = +// bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); +// assertEq(userLQTYAllocated, 0); +// assertEq(userAverageTimestamp, uint256(block.timestamp)); +// } +// } + +// // function test_onAfterAllocateLQTY() public { +// // governance.setEpoch(1); + +// // vm.startPrank(address(governance)); + +// // // first total deposit, first user deposit +// // bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, 1000e18, 0); +// // assertEq(bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()), 1000e18); +// // assertEq(bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()), 1000e18); + +// // // second total deposit, second user deposit +// // bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, 1000e18, 0); +// // assertEq(bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()), 1000e18); // should stay the same +// // assertEq(bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()), 1000e18); // should stay the same + +// // // third total deposit, first user deposit +// // bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, 1000e18, 0); +// // assertEq(bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()), 2000e18); +// // assertEq(bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()), 1000e18); + +// // vm.stopPrank(); +// // } +// } diff --git a/test/BribeInitiativeFireAndForget.t.sol b/test/BribeInitiativeFireAndForget.t.sol new file mode 100644 index 00000000..105bc509 --- /dev/null +++ b/test/BribeInitiativeFireAndForget.t.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {console2 as console} from "forge-std/console2.sol"; +import {Strings} from "openzeppelin/contracts/utils/Strings.sol"; +import {Math} from "openzeppelin/contracts/utils/math/Math.sol"; +import {IBribeInitiative} from "../src/interfaces/IBribeInitiative.sol"; +import {IGovernance} from "../src/interfaces/IGovernance.sol"; +import {BribeInitiative} from "../src/BribeInitiative.sol"; +import {Governance} from "../src/Governance.sol"; +import {MockERC20Tester} from "./mocks/MockERC20Tester.sol"; +import {MockStakingV1} from "./mocks/MockStakingV1.sol"; +import {MockStakingV1Deployer} from "./mocks/MockStakingV1Deployer.sol"; +import {Random} from "./util/Random.sol"; +import {UintArray} from "./util/UintArray.sol"; +import {StringFormatting} from "./util/StringFormatting.sol"; + +contract BribeInitiativeFireAndForgetTest is MockStakingV1Deployer { + using Random for Random.Context; + using UintArray for uint256[]; + using Strings for *; + using StringFormatting for *; + + uint32 constant START_TIME = 1732873631; + uint32 constant EPOCH_DURATION = 7 days; + uint32 constant EPOCH_VOTING_CUTOFF = 6 days; + + uint256 constant MAX_NUM_EPOCHS = 100; + uint256 constant MAX_VOTE = 1e6 ether; + uint128 constant MAX_BRIBE = 1e6 ether; + uint256 constant MAX_CLAIMS_PER_CALL = 10; + uint256 constant MEAN_TIME_BETWEEN_VOTES = 2 * EPOCH_DURATION; + uint256 constant VOTER_PROBABILITY = type(uint256).max / 10; + + address constant voter = address(uint160(uint256(keccak256("voter")))); + address constant other = address(uint160(uint256(keccak256("other")))); + address constant briber = address(uint160(uint256(keccak256("briber")))); + + IGovernance.Configuration config = IGovernance.Configuration({ + registrationFee: 0, + registrationThresholdFactor: 0, + unregistrationThresholdFactor: 4 ether, + unregistrationAfterEpochs: 4, + votingThresholdFactor: 1e4, // min value that doesn't result in division by zero + minClaim: 0, + minAccrual: 0, + epochStart: START_TIME - EPOCH_DURATION, + epochDuration: EPOCH_DURATION, + epochVotingCutoff: EPOCH_VOTING_CUTOFF + }); + + struct Vote { + uint256 epoch; + uint256 amount; + } + + MockStakingV1 stakingV1; + MockERC20Tester lqty; + MockERC20Tester lusd; + MockERC20Tester bold; + MockERC20Tester bryb; + Governance governance; + BribeInitiative bribeInitiative; + + mapping(address who => address[]) initiativesToReset; + mapping(address who => Vote) latestVote; + mapping(uint256 epoch => uint256) boldAtEpoch; + mapping(uint256 epoch => uint256) brybAtEpoch; + mapping(uint256 epoch => uint256) voteAtEpoch; // number of LQTY allocated by "voter" + mapping(uint256 epoch => uint256) toteAtEpoch; // number of LQTY allocated in total ("voter" + "other") + mapping(uint256 epoch => IBribeInitiative.ClaimData) claimDataAtEpoch; + IBribeInitiative.ClaimData[] claimData; + + function setUp() external { + vm.warp(START_TIME); + + vm.label(voter, "voter"); + vm.label(other, "other"); + vm.label(briber, "briber"); + + (stakingV1, lqty, lusd) = deployMockStakingV1(); + + bold = new MockERC20Tester("BOLD Stablecoin", "BOLD"); + vm.label(address(bold), "BOLD"); + + bryb = new MockERC20Tester("Bribe Token", "BRYB"); + vm.label(address(bryb), "BRYB"); + + governance = new Governance({ + _lqty: address(lqty), + _lusd: address(lusd), + _stakingV1: address(stakingV1), + _bold: address(bold), + _config: config, + _owner: address(this), + _initiatives: new address[](0) + }); + + bribeInitiative = + new BribeInitiative({_governance: address(governance), _bold: address(bold), _bribeToken: address(bryb)}); + + address[] memory initiatives = new address[](1); + initiatives[0] = address(bribeInitiative); + governance.registerInitialInitiatives(initiatives); + + address voterProxy = governance.deriveUserProxyAddress(voter); + vm.label(voterProxy, "voterProxy"); + + address otherProxy = governance.deriveUserProxyAddress(other); + vm.label(otherProxy, "otherProxy"); + + lqty.mint(voter, MAX_VOTE); + lqty.mint(other, MAX_VOTE); + + vm.startPrank(voter); + lqty.approve(voterProxy, MAX_VOTE); + governance.depositLQTY(MAX_VOTE); + vm.stopPrank(); + + vm.startPrank(other); + lqty.approve(otherProxy, MAX_VOTE); + governance.depositLQTY(MAX_VOTE); + vm.stopPrank(); + + vm.startPrank(briber); + bold.approve(address(bribeInitiative), type(uint256).max); + bryb.approve(address(bribeInitiative), type(uint256).max); + vm.stopPrank(); + } + + // Ridiculously slow on Github + /// forge-config: ci.fuzz.runs = 50 + function test_AbleToClaimBribesInAnyOrder_EvenFromEpochsWhereVoterStayedInactive(bytes32 seed) external { + Random.Context memory random = Random.init(seed); + uint256 startingEpoch = governance.epoch(); + uint256 lastEpoch = startingEpoch; + + for (uint256 i = startingEpoch; i < startingEpoch + MAX_NUM_EPOCHS; ++i) { + boldAtEpoch[i] = random.generate(MAX_BRIBE); + brybAtEpoch[i] = random.generate(MAX_BRIBE); + + bold.mint(briber, boldAtEpoch[i]); + bryb.mint(briber, brybAtEpoch[i]); + + vm.prank(briber); + bribeInitiative.depositBribe(uint128(boldAtEpoch[i]), uint128(brybAtEpoch[i]), i); + } + + for (;;) { + vm.warp(block.timestamp + random.generate(2 * MEAN_TIME_BETWEEN_VOTES)); + uint256 epoch = governance.epoch(); + + for (uint256 i = lastEpoch; i < epoch; ++i) { + voteAtEpoch[i] = latestVote[voter].amount; + toteAtEpoch[i] = latestVote[voter].amount + latestVote[other].amount; + claimDataAtEpoch[i].epoch = i; + claimDataAtEpoch[i].prevLQTYAllocationEpoch = latestVote[voter].epoch; + claimDataAtEpoch[i].prevTotalLQTYAllocationEpoch = + uint256(Math.max(latestVote[voter].epoch, latestVote[other].epoch)); + + console.log( + string.concat( + "epoch #", + i.toString(), + ": vote = ", + voteAtEpoch[i].decimal(), + ", tote = ", + toteAtEpoch[i].decimal() + ) + ); + } + + lastEpoch = epoch; + if (epoch >= startingEpoch + MAX_NUM_EPOCHS) break; + + (IGovernance.InitiativeStatus status,,) = governance.getInitiativeState(address(bribeInitiative)); + + if (status == IGovernance.InitiativeStatus.CLAIMABLE) { + governance.claimForInitiative(address(bribeInitiative)); + } + + if (status == IGovernance.InitiativeStatus.UNREGISTERABLE) { + governance.unregisterInitiative(address(bribeInitiative)); + break; + } + + address who = random.generate() < VOTER_PROBABILITY ? voter : other; + uint256 vote = governance.secondsWithinEpoch() <= EPOCH_VOTING_CUTOFF ? random.generate(MAX_VOTE) : 0; + + if (vote > 0 || latestVote[who].amount > 0) { + // can't reset when already reset + latestVote[who].epoch = epoch; + latestVote[who].amount = vote; + _vote(who, address(bribeInitiative), latestVote[who].amount); + } + } + + uint256[] memory epochPermutation = UintArray.seq(startingEpoch, lastEpoch + 1).permute(random); + uint256 start = 0; + uint256 expectedBold = 0; + uint256 expectedBryb = 0; + + while (start < epochPermutation.length) { + uint256 end = Math.min(start + random.generate(MAX_CLAIMS_PER_CALL), epochPermutation.length); + + for (uint256 i = start; i < end; ++i) { + if ( + voteAtEpoch[epochPermutation[i]] > 0 + && (boldAtEpoch[epochPermutation[i]] > 0 || brybAtEpoch[epochPermutation[i]] > 0) + ) { + claimData.push(claimDataAtEpoch[epochPermutation[i]]); + expectedBold += boldAtEpoch[epochPermutation[i]] * voteAtEpoch[epochPermutation[i]] + / toteAtEpoch[epochPermutation[i]]; + expectedBryb += brybAtEpoch[epochPermutation[i]] * voteAtEpoch[epochPermutation[i]] + / toteAtEpoch[epochPermutation[i]]; + } + } + + vm.prank(voter); + bribeInitiative.claimBribes(claimData); + delete claimData; + + assertEqDecimal(bold.balanceOf(voter), expectedBold, 18, "bold.balanceOf(voter) != expectedBold"); + assertEqDecimal(bryb.balanceOf(voter), expectedBryb, 18, "bryb.balanceOf(voter) != expectedBryb"); + + start = end; + } + } + + ///////////// + // Helpers // + ///////////// + + function _vote(address who, address initiative, uint256 vote) internal { + assertLeDecimal(vote, uint256(int256(type(int256).max)), 18, "vote > type(uint256).max"); + vm.startPrank(who); + + if (vote > 0) { + address[] memory initiatives = new address[](1); + int256[] memory votes = new int256[](1); + int256[] memory vetos = new int256[](1); + + initiatives[0] = initiative; + votes[0] = int256(uint256(vote)); + governance.allocateLQTY(initiativesToReset[who], initiatives, votes, vetos); + + if (initiativesToReset[who].length != 0) initiativesToReset[who].pop(); + initiativesToReset[who].push(initiative); + } else { + if (initiativesToReset[who].length != 0) { + governance.resetAllocations(initiativesToReset[who], true); + initiativesToReset[who].pop(); + } + } + + vm.stopPrank(); + } +} diff --git a/test/CurveV2GaugeRewards.t.sol b/test/CurveV2GaugeRewards.t.sol index bb0edec8..99ff6369 100644 --- a/test/CurveV2GaugeRewards.t.sol +++ b/test/CurveV2GaugeRewards.t.sol @@ -13,9 +13,7 @@ import {ILiquidityGauge} from "./../src/interfaces/ILiquidityGauge.sol"; import {CurveV2GaugeRewards} from "../src/CurveV2GaugeRewards.sol"; import {Governance} from "../src/Governance.sol"; -import {MockGovernance} from "./mocks/MockGovernance.sol"; - -contract CurveV2GaugeRewardsTest is Test { +contract ForkedCurveV2GaugeRewardsTest is Test { IERC20 private constant lqty = IERC20(address(0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D)); IERC20 private constant lusd = IERC20(address(0x5f98805A4E8be255a32880FDeC7F6728C6568bA0)); IERC20 private constant usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); @@ -28,11 +26,10 @@ contract CurveV2GaugeRewardsTest is Test { uint128 private constant REGISTRATION_FEE = 1e18; uint128 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; uint128 private constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; - uint16 private constant REGISTRATION_WARM_UP_PERIOD = 4; uint16 private constant UNREGISTRATION_AFTER_EPOCHS = 4; uint128 private constant VOTING_THRESHOLD_FACTOR = 0.04e18; - uint88 private constant MIN_CLAIM = 500e18; - uint88 private constant MIN_ACCRUAL = 1000e18; + uint256 private constant MIN_CLAIM = 500e18; + uint256 private constant MIN_ACCRUAL = 1000e18; uint32 private constant EPOCH_DURATION = 604800; uint32 private constant EPOCH_VOTING_CUTOFF = 518400; @@ -42,11 +39,26 @@ contract CurveV2GaugeRewardsTest is Test { ILiquidityGauge private gauge; CurveV2GaugeRewards private curveV2GaugeRewards; - address mockGovernance = address(0x123123); - function setUp() public { vm.createSelectFork(vm.rpcUrl("mainnet"), 20430000); + IGovernance.Configuration memory config = IGovernance.Configuration({ + registrationFee: REGISTRATION_FEE, + registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, + unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, + unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, + votingThresholdFactor: VOTING_THRESHOLD_FACTOR, + minClaim: MIN_CLAIM, + minAccrual: MIN_ACCRUAL, + epochStart: uint32(block.timestamp), + epochDuration: EPOCH_DURATION, + epochVotingCutoff: EPOCH_VOTING_CUTOFF + }); + + governance = new Governance( + address(lqty), address(lusd), stakingV1, address(lusd), config, address(this), initialInitiatives + ); + address[] memory _coins = new address[](2); _coins[0] = address(lusd); _coins[1] = address(usdc); @@ -70,37 +82,15 @@ contract CurveV2GaugeRewardsTest is Test { curveV2GaugeRewards = new CurveV2GaugeRewards( // address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 1)), - address(mockGovernance), + address(governance), address(lusd), address(lqty), address(gauge), 604800 ); - initialInitiatives = new address[](1); - initialInitiatives[0] = address(curveV2GaugeRewards); - - governance = new Governance( - address(lqty), - address(lusd), - stakingV1, - address(lusd), - IGovernance.Configuration({ - registrationFee: REGISTRATION_FEE, - registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, - unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, - unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, - votingThresholdFactor: VOTING_THRESHOLD_FACTOR, - minClaim: MIN_CLAIM, - minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), - epochDuration: EPOCH_DURATION, - epochVotingCutoff: EPOCH_VOTING_CUTOFF - }), - address(this), - initialInitiatives - ); + initialInitiatives.push(address(curveV2GaugeRewards)); + governance.registerInitialInitiatives(initialInitiatives); vm.startPrank(curveFactory.admin()); gauge.add_reward(address(lusd), address(curveV2GaugeRewards)); @@ -121,11 +111,11 @@ contract CurveV2GaugeRewardsTest is Test { } function test_claimAndDepositIntoGaugeFuzz(uint128 amt) public { - deal(address(lusd), mockGovernance, amt); + deal(address(lusd), address(governance), amt); vm.assume(amt > 604800); // Pretend a Proposal has passed - vm.startPrank(address(mockGovernance)); + vm.startPrank(address(governance)); lusd.transfer(address(curveV2GaugeRewards), amt); assertEq(lusd.balanceOf(address(curveV2GaugeRewards)), amt); @@ -136,10 +126,10 @@ contract CurveV2GaugeRewardsTest is Test { /// @dev If the amount rounds down below 1 per second it reverts function test_claimAndDepositIntoGaugeGrief() public { uint256 amt = 604800 - 1; - deal(address(lusd), mockGovernance, amt); + deal(address(lusd), address(governance), amt); // Pretend a Proposal has passed - vm.startPrank(address(mockGovernance)); + vm.startPrank(address(governance)); lusd.transfer(address(curveV2GaugeRewards), amt); assertEq(lusd.balanceOf(address(curveV2GaugeRewards)), amt); @@ -150,10 +140,10 @@ contract CurveV2GaugeRewardsTest is Test { /// @dev Fuzz test that shows that given a total = amt + dust, the dust is lost permanently function test_noDustGriefFuzz(uint128 amt, uint128 dust) public { uint256 total = uint256(amt) + uint256(dust); - deal(address(lusd), mockGovernance, total); + deal(address(lusd), address(governance), total); // Pretend a Proposal has passed - vm.startPrank(address(mockGovernance)); + vm.startPrank(address(governance)); // Dust amount lusd.transfer(address(curveV2GaugeRewards), amt); // Rest diff --git a/test/Deployment.t.sol b/test/Deployment.t.sol new file mode 100644 index 00000000..de6a3da7 --- /dev/null +++ b/test/Deployment.t.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IGovernance} from "../src/interfaces/IGovernance.sol"; +import {Governance} from "../src/Governance.sol"; +import {MockERC20Tester} from "./mocks/MockERC20Tester.sol"; +import {MockStakingV1} from "./mocks/MockStakingV1.sol"; +import {MockStakingV1Deployer} from "./mocks/MockStakingV1Deployer.sol"; + +// These tests demonstrate that by deploying `Governance` with `epochStart` set one `EPOCH_DURATION` in the past: +// - initial initiatives can immediately be voted on, +// - registration of new initiatives is disabled for one epoch. +// +// The reason we want to initially disable registration is that there's not vote snapshot to base the registration +// threshold upon, thus registration would otherwise be possible without having any LQTY staked. +contract DeploymentTest is MockStakingV1Deployer { + uint32 constant START_TIME = 1732873631; + uint32 constant EPOCH_DURATION = 7 days; + uint128 constant REGISTRATION_FEE = 1 ether; + + address constant deployer = address(uint160(uint256(keccak256("deployer")))); + address constant voter = address(uint160(uint256(keccak256("voter")))); + address constant registrant = address(uint160(uint256(keccak256("registrant")))); + address constant initialInitiative = address(uint160(uint256(keccak256("initialInitiative")))); + address constant newInitiative = address(uint160(uint256(keccak256("newInitiative")))); + + IGovernance.Configuration config = IGovernance.Configuration({ + registrationFee: REGISTRATION_FEE, + registrationThresholdFactor: 0.01 ether, + unregistrationThresholdFactor: 4 ether, + unregistrationAfterEpochs: 4, + votingThresholdFactor: 0.04 ether, + minClaim: 0, + minAccrual: 0, + epochStart: START_TIME - EPOCH_DURATION, + epochDuration: EPOCH_DURATION, + epochVotingCutoff: EPOCH_DURATION - 1 days + }); + + MockStakingV1 stakingV1; + MockERC20Tester lqty; + MockERC20Tester lusd; + MockERC20Tester bold; + Governance governance; + + address[] initiativesToReset; + address[] initiatives; + int256[] votes; + int256[] vetos; + + function setUp() external { + vm.warp(START_TIME); + + vm.label(deployer, "deployer"); + vm.label(voter, "voter"); + vm.label(registrant, "registrant"); + vm.label(initialInitiative, "initialInitiative"); + vm.label(newInitiative, "newInitiative"); + + (stakingV1, lqty, lusd) = deployMockStakingV1(); + bold = new MockERC20Tester("BOLD Stablecoin", "BOLD"); + + initiatives.push(initialInitiative); + + vm.prank(deployer); + governance = new Governance({ + _lqty: address(lqty), + _lusd: address(lusd), + _stakingV1: address(stakingV1), + _bold: address(bold), + _config: config, + _owner: deployer, + _initiatives: initiatives + }); + + vm.label(governance.deriveUserProxyAddress(voter), "voterProxy"); + } + + function test_AtStart_WeAreInEpoch2() external view { + assertEq(governance.epoch(), 2, "We should start in epoch #2"); + } + + function test_OneEpochLater_WeAreInEpoch3() external { + vm.warp(block.timestamp + EPOCH_DURATION); + assertEq(governance.epoch(), 3, "We should be in epoch #3"); + } + + function test_AtStart_CanVoteOnInitialInitiative() external { + _voteOnInitiative(); + + uint256 boldAccrued = 1 ether; + bold.mint(address(governance), boldAccrued); + vm.warp(block.timestamp + EPOCH_DURATION); + + governance.claimForInitiative(initialInitiative); + assertEqDecimal(bold.balanceOf(initialInitiative), boldAccrued, 18, "Initiative should have received BOLD"); + } + + function test_AtStart_CannotRegisterNewInitiative() external { + _registerNewInitiative({expectRevertReason: "Governance: registration-not-yet-enabled"}); + } + + function test_OneEpochLater_WhenNoOneVotedDuringEpoch2_CanRegisterNewInitiativeWithNoLQTY() external { + vm.warp(block.timestamp + EPOCH_DURATION); + _registerNewInitiative(); + } + + function test_OneEpochLater_WhenSomeoneVotedDuringEpoch2_CannotRegisterNewInitiativeWithNoLQTY() external { + _voteOnInitiative(); + vm.warp(block.timestamp + EPOCH_DURATION); + _registerNewInitiative({expectRevertReason: "Governance: insufficient-lqty"}); + _depositLQTY(); // Only LQTY deposited during previous epoch counts + _registerNewInitiative({expectRevertReason: "Governance: insufficient-lqty"}); + } + + function test_OneEpochLater_WhenSomeoneVotedDuringEpoch2_CanRegisterNewInitiativeWithSufficientLQTY() external { + _voteOnInitiative(); + _depositLQTY(); + vm.warp(block.timestamp + EPOCH_DURATION); + _registerNewInitiative(); + } + + ///////////// + // Helpers // + ///////////// + + function _voteOnInitiative() internal { + uint256 lqtyAmount = 1 ether; + lqty.mint(voter, lqtyAmount); + + votes.push(int256(lqtyAmount)); + vetos.push(0); + + vm.startPrank(voter); + lqty.approve(governance.deriveUserProxyAddress(voter), lqtyAmount); + governance.depositLQTY(lqtyAmount); + governance.allocateLQTY(initiativesToReset, initiatives, votes, vetos); + vm.stopPrank(); + + delete votes; + delete vetos; + } + + function _registerNewInitiative() internal { + _registerNewInitiative(""); + } + + function _registerNewInitiative(bytes memory expectRevertReason) internal { + bold.mint(registrant, REGISTRATION_FEE); + vm.startPrank(registrant); + bold.approve(address(governance), REGISTRATION_FEE); + if (expectRevertReason.length > 0) vm.expectRevert(expectRevertReason); + governance.registerInitiative(newInitiative); + vm.stopPrank(); + } + + function _depositLQTY() internal { + uint256 lqtyAmount = 1 ether; + lqty.mint(registrant, lqtyAmount); + vm.startPrank(registrant); + lqty.approve(governance.deriveUserProxyAddress(registrant), lqtyAmount); + governance.depositLQTY(lqtyAmount); + vm.stopPrank(); + } +} diff --git a/test/DoubleLinkedList.t.sol b/test/DoubleLinkedList.t.sol index 4164ff0c..2d84a86c 100644 --- a/test/DoubleLinkedList.t.sol +++ b/test/DoubleLinkedList.t.sol @@ -10,24 +10,24 @@ contract DoubleLinkedListWrapper { DoubleLinkedList.List list; - function getHead() public view returns (uint16) { + function getHead() public view returns (uint256) { return list.getHead(); } - function getTail() public view returns (uint16) { + function getTail() public view returns (uint256) { return list.getTail(); } - function getNext(uint16 id) public view returns (uint16) { + function getNext(uint256 id) public view returns (uint256) { return list.getNext(id); } - function getPrev(uint16 id) public view returns (uint16) { + function getPrev(uint256 id) public view returns (uint256) { return list.getPrev(id); } - function insert(uint16 id, uint16 next) public { - list.insert(id, 1, next); + function insert(uint256 id, uint256 next) public { + list.insert(id, 1, 1, next); } } diff --git a/test/E2E.t.sol b/test/E2E.t.sol index 6a41fbf3..eeabd7d7 100644 --- a/test/E2E.t.sol +++ b/test/E2E.t.sol @@ -2,23 +2,16 @@ pragma solidity ^0.8.24; import {Test, console2} from "forge-std/Test.sol"; -import {VmSafe} from "forge-std/Vm.sol"; import {console} from "forge-std/console.sol"; import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; import {IGovernance} from "../src/interfaces/IGovernance.sol"; -import {ILQTY} from "../src/interfaces/ILQTY.sol"; import {BribeInitiative} from "../src/BribeInitiative.sol"; import {Governance} from "../src/Governance.sol"; -import {UserProxy} from "../src/UserProxy.sol"; -import {PermitParams} from "../src/utils/Types.sol"; - -import {MockInitiative} from "./mocks/MockInitiative.sol"; - -contract E2ETests is Test { +contract ForkedE2ETests is Test { IERC20 private constant lqty = IERC20(address(0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D)); IERC20 private constant lusd = IERC20(address(0x5f98805A4E8be255a32880FDeC7F6728C6568bA0)); address private constant stakingV1 = address(0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d); @@ -26,16 +19,15 @@ contract E2ETests is Test { address private constant user2 = address(0x10C9cff3c4Faa8A60cB8506a7A99411E6A199038); address private constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); - uint128 private constant REGISTRATION_FEE = 1e18; - uint128 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; - uint128 private constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; - uint16 private constant REGISTRATION_WARM_UP_PERIOD = 4; - uint16 private constant UNREGISTRATION_AFTER_EPOCHS = 4; - uint128 private constant VOTING_THRESHOLD_FACTOR = 0.04e18; - uint88 private constant MIN_CLAIM = 500e18; - uint88 private constant MIN_ACCRUAL = 1000e18; - uint32 private constant EPOCH_DURATION = 604800; - uint32 private constant EPOCH_VOTING_CUTOFF = 518400; + uint256 private constant REGISTRATION_FEE = 1e18; + uint256 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; + uint256 private constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; + uint256 private constant UNREGISTRATION_AFTER_EPOCHS = 4; + uint256 private constant VOTING_THRESHOLD_FACTOR = 0.04e18; + uint256 private constant MIN_CLAIM = 500e18; + uint256 private constant MIN_ACCRUAL = 1000e18; + uint256 private constant EPOCH_DURATION = 604800; + uint256 private constant EPOCH_VOTING_CUTOFF = 518400; Governance private governance; address[] private initialInitiatives; @@ -47,77 +39,57 @@ contract E2ETests is Test { function setUp() public { vm.createSelectFork(vm.rpcUrl("mainnet"), 20430000); - baseInitiative1 = address( - new BribeInitiative( - address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 3)), - address(lusd), - address(lqty) - ) - ); + IGovernance.Configuration memory config = IGovernance.Configuration({ + registrationFee: REGISTRATION_FEE, + registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, + unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, + unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, + votingThresholdFactor: VOTING_THRESHOLD_FACTOR, + minClaim: MIN_CLAIM, + minAccrual: MIN_ACCRUAL, + epochStart: uint256(block.timestamp - EPOCH_DURATION), + /// @audit KEY + epochDuration: EPOCH_DURATION, + epochVotingCutoff: EPOCH_VOTING_CUTOFF + }); - baseInitiative2 = address( - new BribeInitiative( - address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 2)), - address(lusd), - address(lqty) - ) + governance = new Governance( + address(lqty), address(lusd), stakingV1, address(lusd), config, address(this), new address[](0) ); - baseInitiative3 = address( - new BribeInitiative( - address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 1)), - address(lusd), - address(lqty) - ) - ); + baseInitiative1 = address(new BribeInitiative(address(governance), address(lusd), address(lqty))); + baseInitiative2 = address(new BribeInitiative(address(governance), address(lusd), address(lqty))); + baseInitiative3 = address(new BribeInitiative(address(governance), address(lusd), address(lqty))); initialInitiatives.push(baseInitiative1); initialInitiatives.push(baseInitiative2); - governance = new Governance( - address(lqty), - address(lusd), - stakingV1, - address(lusd), - IGovernance.Configuration({ - registrationFee: REGISTRATION_FEE, - registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, - unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, - unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, - votingThresholdFactor: VOTING_THRESHOLD_FACTOR, - minClaim: MIN_CLAIM, - minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp - EPOCH_DURATION), - /// @audit KEY - epochDuration: EPOCH_DURATION, - epochVotingCutoff: EPOCH_VOTING_CUTOFF - }), - address(this), - initialInitiatives - ); + governance.registerInitialInitiatives(initialInitiatives); } - // forge test --match-test test_initialInitiativesCanBeVotedOnAtStart -vv function test_initialInitiativesCanBeVotedOnAtStart() public { /// @audit NOTE: In order for this to work, the constructor must set the start time a week behind - /// This will make the initiatives work on the first epoch + /// This will make the initiatives work immediately after deployment, on the second epoch vm.startPrank(user); - // Check that we can vote on the first epoch, right after deployment _deposit(1000e18); + // Check that we can vote right after deployment console.log("epoch", governance.epoch()); - _allocate(baseInitiative1, 1e18, 0); // Doesn't work due to cool down I think + _allocate(baseInitiative1, 1e18, 0); + _reset(baseInitiative1); + + // Registration not allowed initially, so skip one epoch + vm.warp(block.timestamp + EPOCH_DURATION); - // And for sanity, you cannot vote on new ones, they need to be added first deal(address(lusd), address(user), REGISTRATION_FEE); lusd.approve(address(governance), REGISTRATION_FEE); governance.registerInitiative(address(0x123123)); + // You cannot immediately vote on new ones vm.expectRevert(); _allocate(address(0x123123), 1e18, 0); - // Whereas in next week it will work + // Whereas in next epoch it will work vm.warp(block.timestamp + EPOCH_DURATION); _allocate(address(0x123123), 1e18, 0); } @@ -128,7 +100,7 @@ contract E2ETests is Test { // Check that we can vote on the first epoch, right after deployment _deposit(100_000_000e18); - console.log("epoch", governance.epoch()); + //console.log("epoch", governance.epoch()); _allocate(baseInitiative1, 100_000_000e18, 0); } @@ -148,11 +120,10 @@ contract E2ETests is Test { vm.warp(block.timestamp + EPOCH_DURATION); - console.log("epoch", governance.epoch()); + //console.log("epoch", governance.epoch()); _allocate(newInitiative, 100_000_000e18, 0); } - // forge test --match-test test_noVetoGriefAtEpochOne -vv function test_noVetoGriefAtEpochOne() public { /// @audit NOTE: In order for this to work, the constructor must set the start time a week behind /// This will make the initiatives work on the first epoch @@ -170,7 +141,6 @@ contract E2ETests is Test { governance.unregisterInitiative(baseInitiative1); } - // forge test --match-test test_deregisterIsSound -vv function test_deregisterIsSound() public { // Deregistration works as follows: // We stop voting @@ -181,15 +151,18 @@ contract E2ETests is Test { _deposit(1000e18); console.log("epoch", governance.epoch()); - _allocate(baseInitiative1, 1e18, 0); // Doesn't work due to cool down I think + _allocate(baseInitiative1, 1e18, 0); // And for sanity, you cannot vote on new ones, they need to be added first deal(address(lusd), address(user), REGISTRATION_FEE); lusd.approve(address(governance), REGISTRATION_FEE); + // Registration not allowed initially, so skip one epoch + vm.warp(block.timestamp + EPOCH_DURATION); + address newInitiative = address(0x123123); governance.registerInitiative(newInitiative); - assertEq(uint256(Governance.InitiativeStatus.WARM_UP), _getInitiativeStatus(newInitiative), "Cooldown"); + assertEq(uint256(IGovernance.InitiativeStatus.WARM_UP), _getInitiativeStatus(newInitiative), "Cooldown"); uint256 skipCount; @@ -198,25 +171,25 @@ contract E2ETests is Test { // Whereas in next week it will work vm.warp(block.timestamp + EPOCH_DURATION); // 1 ++skipCount; - assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + assertEq(uint256(IGovernance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); // Cooldown on epoch Staert vm.warp(block.timestamp + EPOCH_DURATION); // 2 ++skipCount; - assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + assertEq(uint256(IGovernance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); vm.warp(block.timestamp + EPOCH_DURATION); // 3 ++skipCount; - assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + assertEq(uint256(IGovernance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); vm.warp(block.timestamp + EPOCH_DURATION); // 3 ++skipCount; - assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + assertEq(uint256(IGovernance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); vm.warp(block.timestamp + EPOCH_DURATION); // 4 ++skipCount; assertEq( - uint256(Governance.InitiativeStatus.UNREGISTERABLE), _getInitiativeStatus(newInitiative), "UNREGISTERABLE" + uint256(IGovernance.InitiativeStatus.UNREGISTERABLE), _getInitiativeStatus(newInitiative), "UNREGISTERABLE" ); /// 4 + 1 ?? @@ -225,7 +198,9 @@ contract E2ETests is Test { // forge test --match-test test_unregisterWorksCorrectlyEvenAfterXEpochs -vv function test_unregisterWorksCorrectlyEvenAfterXEpochs(uint8 epochsInFuture) public { - vm.warp(block.timestamp + epochsInFuture * EPOCH_DURATION); + // Registration starts working after one epoch, so fast-forward at least one EPOCH_DURATION + vm.warp(block.timestamp + (uint32(1) + epochsInFuture) * EPOCH_DURATION); + vm.startPrank(user); // Check that we can vote on the first epoch, right after deployment _deposit(1000e18); @@ -238,8 +213,8 @@ contract E2ETests is Test { address newInitiative2 = address(0x1231234); governance.registerInitiative(newInitiative); governance.registerInitiative(newInitiative2); - assertEq(uint256(Governance.InitiativeStatus.WARM_UP), _getInitiativeStatus(newInitiative), "Cooldown"); - assertEq(uint256(Governance.InitiativeStatus.WARM_UP), _getInitiativeStatus(newInitiative2), "Cooldown"); + assertEq(uint256(IGovernance.InitiativeStatus.WARM_UP), _getInitiativeStatus(newInitiative), "Cooldown"); + assertEq(uint256(IGovernance.InitiativeStatus.WARM_UP), _getInitiativeStatus(newInitiative2), "Cooldown"); uint256 skipCount; @@ -250,7 +225,7 @@ contract E2ETests is Test { vm.warp(block.timestamp + EPOCH_DURATION); // 1 ++skipCount; - assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + assertEq(uint256(IGovernance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); _allocate(newInitiative2, 1e18, 0); @@ -259,33 +234,34 @@ contract E2ETests is Test { // Cooldown on epoch Staert vm.warp(block.timestamp + EPOCH_DURATION); // 2 ++skipCount; - assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + assertEq(uint256(IGovernance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); // 3rd Week of SKIP vm.warp(block.timestamp + EPOCH_DURATION); // 3 ++skipCount; - assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + assertEq(uint256(IGovernance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); // 4th Week of SKIP | If it doesn't get any rewards it will be UNREGISTERABLE vm.warp(block.timestamp + EPOCH_DURATION); // 3 ++skipCount; - assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + assertEq(uint256(IGovernance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); vm.warp(block.timestamp + EPOCH_DURATION); // 4 ++skipCount; assertEq( - uint256(Governance.InitiativeStatus.UNREGISTERABLE), _getInitiativeStatus(newInitiative), "UNREGISTERABLE" + uint256(IGovernance.InitiativeStatus.UNREGISTERABLE), _getInitiativeStatus(newInitiative), "UNREGISTERABLE" ); /// It was SKIP for 4 EPOCHS, it is now UNREGISTERABLE assertEq(skipCount, UNREGISTRATION_AFTER_EPOCHS + 1, "Skipped exactly UNREGISTRATION_AFTER_EPOCHS"); } - // forge test --match-test test_unregisterWorksCorrectlyEvenAfterXEpochs_andCanBeSavedAtLast -vv function test_unregisterWorksCorrectlyEvenAfterXEpochs_andCanBeSavedAtLast(uint8 epochsInFuture) public { - vm.warp(block.timestamp + epochsInFuture * EPOCH_DURATION); + // Registration starts working after one epoch, so fast-forward at least one EPOCH_DURATION + vm.warp(block.timestamp + (uint32(1) + epochsInFuture) * EPOCH_DURATION); + vm.startPrank(user); // Check that we can vote on the first epoch, right after deployment _deposit(1000e18); @@ -298,8 +274,8 @@ contract E2ETests is Test { address newInitiative2 = address(0x1231234); governance.registerInitiative(newInitiative); governance.registerInitiative(newInitiative2); - assertEq(uint256(Governance.InitiativeStatus.WARM_UP), _getInitiativeStatus(newInitiative), "Cooldown"); - assertEq(uint256(Governance.InitiativeStatus.WARM_UP), _getInitiativeStatus(newInitiative2), "Cooldown"); + assertEq(uint256(IGovernance.InitiativeStatus.WARM_UP), _getInitiativeStatus(newInitiative), "Cooldown"); + assertEq(uint256(IGovernance.InitiativeStatus.WARM_UP), _getInitiativeStatus(newInitiative2), "Cooldown"); uint256 skipCount; @@ -310,7 +286,7 @@ contract E2ETests is Test { vm.warp(block.timestamp + EPOCH_DURATION); // 1 ++skipCount; - assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + assertEq(uint256(IGovernance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); _allocate(newInitiative2, 1e18, 0); @@ -319,66 +295,61 @@ contract E2ETests is Test { // Cooldown on epoch Staert vm.warp(block.timestamp + EPOCH_DURATION); // 2 ++skipCount; - assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + assertEq(uint256(IGovernance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); // 3rd Week of SKIP vm.warp(block.timestamp + EPOCH_DURATION); // 3 ++skipCount; - assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + assertEq(uint256(IGovernance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); // 4th Week of SKIP | If it doesn't get any rewards it will be UNREGISTERABLE vm.warp(block.timestamp + EPOCH_DURATION); // 3 ++skipCount; - assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + assertEq(uint256(IGovernance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); // Allocating to it, saves it + _reset(newInitiative2); _allocate(newInitiative, 1e18, 0); vm.warp(block.timestamp + EPOCH_DURATION); // 4 ++skipCount; - assertEq(uint256(Governance.InitiativeStatus.CLAIMABLE), _getInitiativeStatus(newInitiative), "UNREGISTERABLE"); + assertEq(uint256(IGovernance.InitiativeStatus.CLAIMABLE), _getInitiativeStatus(newInitiative), "UNREGISTERABLE"); } - function _deposit(uint88 amt) internal { + function _deposit(uint256 amt) internal { address userProxy = governance.deployUserProxy(); lqty.approve(address(userProxy), amt); governance.depositLQTY(amt); } - function _allocate(address initiative, int88 votes, int88 vetos) internal { - address[] memory initiativesToDeRegister = new address[](5); - initiativesToDeRegister[0] = baseInitiative1; - initiativesToDeRegister[1] = baseInitiative2; - initiativesToDeRegister[2] = baseInitiative3; - initiativesToDeRegister[3] = address(0x123123); - initiativesToDeRegister[4] = address(0x1231234); - + function _allocate(address initiative, int256 votes, int256 vetos) internal { + address[] memory initiativesToReset; address[] memory initiatives = new address[](1); initiatives[0] = initiative; - int88[] memory deltaLQTYVotes = new int88[](1); - deltaLQTYVotes[0] = votes; - int88[] memory deltaLQTYVetos = new int88[](1); - deltaLQTYVetos[0] = vetos; + int256[] memory absoluteLQTYVotes = new int256[](1); + absoluteLQTYVotes[0] = votes; + int256[] memory absoluteLQTYVetos = new int256[](1); + absoluteLQTYVetos[0] = vetos; - governance.allocateLQTY(initiativesToDeRegister, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, absoluteLQTYVotes, absoluteLQTYVetos); } - function _allocate(address[] memory initiatives, int88[] memory votes, int88[] memory vetos) internal { - address[] memory initiativesToDeRegister = new address[](5); - initiativesToDeRegister[0] = baseInitiative1; - initiativesToDeRegister[1] = baseInitiative2; - initiativesToDeRegister[2] = baseInitiative3; - initiativesToDeRegister[3] = address(0x123123); - initiativesToDeRegister[4] = address(0x1231234); + function _allocate(address[] memory initiatives, int256[] memory votes, int256[] memory vetos) internal { + address[] memory initiativesToReset; + governance.allocateLQTY(initiativesToReset, initiatives, votes, vetos); + } - governance.allocateLQTY(initiativesToDeRegister, initiatives, votes, vetos); + function _reset(address initiative) internal { + address[] memory initiativesToReset = new address[](1); + initiativesToReset[0] = initiative; + governance.resetAllocations(initiativesToReset, false); } function _getInitiativeStatus(address _initiative) internal returns (uint256) { - (Governance.InitiativeStatus status,,) = governance.getInitiativeState(_initiative); + (IGovernance.InitiativeStatus status,,) = governance.getInitiativeState(_initiative); return uint256(status); } } diff --git a/test/EncodingDecoding.t.sol b/test/EncodingDecoding.t.sol deleted file mode 100644 index 49e205e0..00000000 --- a/test/EncodingDecoding.t.sol +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {Test, console2} from "forge-std/Test.sol"; - -import {EncodingDecodingLib} from "src/utils/EncodingDecodingLib.sol"; - -contract EncodingDecodingTest is Test { - // value -> encoding -> decoding -> value - function test_encoding_and_decoding_symmetrical(uint88 lqty, uint120 averageTimestamp) public { - uint224 encodedValue = EncodingDecodingLib.encodeLQTYAllocation(lqty, averageTimestamp); - (uint88 decodedLqty, uint120 decodedAverageTimestamp) = EncodingDecodingLib.decodeLQTYAllocation(encodedValue); - - assertEq(lqty, decodedLqty); - assertEq(averageTimestamp, decodedAverageTimestamp); - - // Redo - uint224 reEncoded = EncodingDecodingLib.encodeLQTYAllocation(decodedLqty, decodedAverageTimestamp); - (uint88 reDecodedLqty, uint120 reDecodedAverageTimestamp) = - EncodingDecodingLib.decodeLQTYAllocation(encodedValue); - - assertEq(reEncoded, encodedValue); - assertEq(reDecodedLqty, decodedLqty); - assertEq(reDecodedAverageTimestamp, decodedAverageTimestamp); - } - - // receive -> undo -> check -> redo -> compare - function test_receive_undo_compare(uint120 encodedValue) public { - _receive_undo_compare(encodedValue); - } - - // receive -> undo -> check -> redo -> compare - function _receive_undo_compare(uint224 encodedValue) public { - /// These values fail because we could pass a value that is bigger than intended - (uint88 decodedLqty, uint120 decodedAverageTimestamp) = EncodingDecodingLib.decodeLQTYAllocation(encodedValue); - - uint224 encodedValue2 = EncodingDecodingLib.encodeLQTYAllocation(decodedLqty, decodedAverageTimestamp); - (uint88 decodedLqty2, uint120 decodedAverageTimestamp2) = - EncodingDecodingLib.decodeLQTYAllocation(encodedValue2); - - assertEq(encodedValue, encodedValue2, "encoded values not equal"); - assertEq(decodedLqty, decodedLqty2, "decoded lqty not equal"); - assertEq(decodedAverageTimestamp, decodedAverageTimestamp2, "decoded timestamps not equal"); - } -} diff --git a/test/Governance.t.sol b/test/Governance.t.sol index e3f1ea3a..234e3595 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -5,10 +5,13 @@ import {Test, console2} from "forge-std/Test.sol"; import {VmSafe} from "forge-std/Vm.sol"; import {console} from "forge-std/console.sol"; -import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; +import {IERC20Errors} from "openzeppelin/contracts/interfaces/draft-IERC6093.sol"; +import {Strings} from "openzeppelin/contracts/utils/Strings.sol"; import {IGovernance} from "../src/interfaces/IGovernance.sol"; +import {ILUSD} from "../src/interfaces/ILUSD.sol"; import {ILQTY} from "../src/interfaces/ILQTY.sol"; +import {ILQTYStaking} from "../src/interfaces/ILQTYStaking.sol"; import {BribeInitiative} from "../src/BribeInitiative.sol"; import {Governance} from "../src/Governance.sol"; @@ -16,159 +19,98 @@ import {UserProxy} from "../src/UserProxy.sol"; import {PermitParams} from "../src/utils/Types.sol"; +import {MockERC20Tester} from "./mocks/MockERC20Tester.sol"; import {MockInitiative} from "./mocks/MockInitiative.sol"; +import {MockStakingV1} from "./mocks/MockStakingV1.sol"; +import {MockStakingV1Deployer} from "./mocks/MockStakingV1Deployer.sol"; +import "./constants.sol"; -contract GovernanceInternal is Governance { +contract GovernanceTester is Governance { constructor( address _lqty, address _lusd, address _stakingV1, address _bold, Configuration memory _config, + address _owner, address[] memory _initiatives - ) Governance(_lqty, _lusd, _stakingV1, _bold, _config, msg.sender, _initiatives) {} + ) Governance(_lqty, _lusd, _stakingV1, _bold, _config, _owner, _initiatives) {} - function averageAge(uint120 _currentTimestamp, uint120 _averageTimestamp) external pure returns (uint120) { - return _averageAge(_currentTimestamp, _averageTimestamp); + function tester_setVotesSnapshot(VoteSnapshot calldata _votesSnapshot) external { + votesSnapshot = _votesSnapshot; } - function calculateAverageTimestamp( - uint120 _prevOuterAverageTimestamp, - uint120 _newInnerAverageTimestamp, - uint88 _prevLQTYBalance, - uint88 _newLQTYBalance - ) external view returns (uint208) { - return _calculateAverageTimestamp( - _prevOuterAverageTimestamp, _newInnerAverageTimestamp, _prevLQTYBalance, _newLQTYBalance - ); + function tester_setVotesForInitiativeSnapshot( + address _initiative, + InitiativeVoteSnapshot calldata _votesForInitiativeSnapshot + ) external { + votesForInitiativeSnapshot[_initiative] = _votesForInitiativeSnapshot; + } + + function tester_setBoldAccrued(uint256 _boldAccrued) external { + boldAccrued = _boldAccrued; } } -contract GovernanceTest is Test { - IERC20 private constant lqty = IERC20(address(0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D)); - IERC20 private constant lusd = IERC20(address(0x5f98805A4E8be255a32880FDeC7F6728C6568bA0)); - address private constant stakingV1 = address(0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d); - address private constant user = address(0xF977814e90dA44bFA03b6295A0616a897441aceC); - address private constant user2 = address(0x10C9cff3c4Faa8A60cB8506a7A99411E6A199038); - address private constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); - - uint128 private constant REGISTRATION_FEE = 1e18; - uint128 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; - uint128 private constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; - uint16 private constant REGISTRATION_WARM_UP_PERIOD = 4; - uint16 private constant UNREGISTRATION_AFTER_EPOCHS = 4; - uint128 private constant VOTING_THRESHOLD_FACTOR = 0.04e18; - uint88 private constant MIN_CLAIM = 500e18; - uint88 private constant MIN_ACCRUAL = 1000e18; - uint32 private constant EPOCH_DURATION = 604800; +abstract contract GovernanceTest is Test { + using Strings for uint256; + + ILQTY internal lqty; + ILUSD internal lusd; + ILQTYStaking internal stakingV1; + + address internal constant user = address(0xF977814e90dA44bFA03b6295A0616a897441aceC); + address internal constant user2 = address(0x10C9cff3c4Faa8A60cB8506a7A99411E6A199038); + address internal constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); + + uint256 private constant REGISTRATION_FEE = 1e18; + uint256 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; + uint256 private constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; + uint256 private constant UNREGISTRATION_AFTER_EPOCHS = 4; + uint256 private constant VOTING_THRESHOLD_FACTOR = 0.04e18; + uint256 private constant MIN_CLAIM = 500e18; + uint256 private constant MIN_ACCRUAL = 1000e18; + uint256 private constant EPOCH_DURATION = 604800; uint32 private constant EPOCH_VOTING_CUTOFF = 518400; - Governance private governance; - GovernanceInternal private governanceInternal; + GovernanceTester private governance; address[] private initialInitiatives; address private baseInitiative2; address private baseInitiative3; address private baseInitiative1; - function setUp() public { - vm.createSelectFork(vm.rpcUrl("mainnet"), 20430000); - - baseInitiative1 = address( - new BribeInitiative( - address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 3)), - address(lusd), - address(lqty) - ) - ); + function _expectInsufficientAllowance() internal virtual; + function _expectInsufficientBalance() internal virtual; + + // When both allowance and balance are insufficient, LQTY fails on insufficient balance, unlike recent OZ ERC20 + function _expectInsufficientAllowanceAndBalance() internal virtual; + + function setUp() public virtual { + IGovernance.Configuration memory config = IGovernance.Configuration({ + registrationFee: REGISTRATION_FEE, + registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, + unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, + unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, + votingThresholdFactor: VOTING_THRESHOLD_FACTOR, + minClaim: MIN_CLAIM, + minAccrual: MIN_ACCRUAL, + epochStart: uint256(block.timestamp), + epochDuration: EPOCH_DURATION, + epochVotingCutoff: EPOCH_VOTING_CUTOFF + }); - baseInitiative2 = address( - new BribeInitiative( - address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 2)), - address(lusd), - address(lqty) - ) + governance = new GovernanceTester( + address(lqty), address(lusd), address(stakingV1), address(lusd), config, address(this), new address[](0) ); - baseInitiative3 = address( - new BribeInitiative( - address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 1)), - address(lusd), - address(lqty) - ) - ); + baseInitiative1 = address(new BribeInitiative(address(governance), address(lusd), address(lqty))); + baseInitiative2 = address(new BribeInitiative(address(governance), address(lusd), address(lqty))); + baseInitiative3 = address(new BribeInitiative(address(governance), address(lusd), address(lqty))); initialInitiatives.push(baseInitiative1); initialInitiatives.push(baseInitiative2); - - governance = new Governance( - address(lqty), - address(lusd), - stakingV1, - address(lusd), - IGovernance.Configuration({ - registrationFee: REGISTRATION_FEE, - registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, - unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, - unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, - votingThresholdFactor: VOTING_THRESHOLD_FACTOR, - minClaim: MIN_CLAIM, - minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), - epochDuration: EPOCH_DURATION, - epochVotingCutoff: EPOCH_VOTING_CUTOFF - }), - address(this), - initialInitiatives - ); - - governanceInternal = new GovernanceInternal( - address(lqty), - address(lusd), - stakingV1, - address(lusd), - IGovernance.Configuration({ - registrationFee: REGISTRATION_FEE, - registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, - unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, - unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, - votingThresholdFactor: VOTING_THRESHOLD_FACTOR, - minClaim: MIN_CLAIM, - minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), - epochDuration: EPOCH_DURATION, - epochVotingCutoff: EPOCH_VOTING_CUTOFF - }), - initialInitiatives - ); - } - - // should not revert under any input - function test_averageAge(uint120 _currentTimestamp, uint120 _timestamp) public { - uint120 averageAge = governanceInternal.averageAge(_currentTimestamp, _timestamp); - if (_timestamp == 0 || _currentTimestamp < _timestamp) { - assertEq(averageAge, 0); - } else { - assertEq(averageAge, _currentTimestamp - _timestamp); - } - } - - // should not revert under any input - function test_calculateAverageTimestamp( - uint32 _prevOuterAverageTimestamp, - uint32 _newInnerAverageTimestamp, - uint88 _prevLQTYBalance, - uint88 _newLQTYBalance - ) public { - uint32 highestTimestamp = (_prevOuterAverageTimestamp > _newInnerAverageTimestamp) - ? _prevOuterAverageTimestamp - : _newInnerAverageTimestamp; - if (highestTimestamp > block.timestamp) vm.warp(highestTimestamp); - governanceInternal.calculateAverageTimestamp( - _prevOuterAverageTimestamp, _newInnerAverageTimestamp, _prevLQTYBalance, _newLQTYBalance - ); + governance.registerInitialInitiatives(initialInitiatives); } // forge test --match-test test_depositLQTY_withdrawLQTY -vv @@ -183,57 +125,65 @@ contract GovernanceTest is Test { governance.depositLQTY(0); // should revert if the `_lqtyAmount` > `lqty.allowance(msg.sender, userProxy)` - vm.expectRevert("ERC20: transfer amount exceeds allowance"); + _expectInsufficientAllowance(); governance.depositLQTY(1e18); // should revert if the `_lqtyAmount` > `lqty.balanceOf(msg.sender)` - vm.expectRevert("ERC20: transfer amount exceeds balance"); - governance.depositLQTY(type(uint88).max); + _expectInsufficientAllowanceAndBalance(); + governance.depositLQTY(1e26); + + uint256 lqtyDeposit = 2e18; // should not revert if the user doesn't have a UserProxy deployed yet address userProxy = governance.deriveUserProxyAddress(user); - lqty.approve(address(userProxy), 1e18); + lqty.approve(address(userProxy), lqtyDeposit); // vm.expectEmit("DepositLQTY", abi.encode(user, 1e18)); - // deploy and deposit 1 LQTY - governance.depositLQTY(1e18); - assertEq(UserProxy(payable(userProxy)).staked(), 1e18); - (uint88 allocatedLQTY, uint120 averageStakingTimestamp) = governance.userStates(user); - assertEq(allocatedLQTY, 0); - // first deposit should have an averageStakingTimestamp if block.timestamp - assertEq(averageStakingTimestamp, block.timestamp * 1e26); + // deploy and deposit 2 LQTY + governance.depositLQTY(lqtyDeposit); + assertEq(UserProxy(payable(userProxy)).staked(), lqtyDeposit); + (uint256 unallocatedLQTY, uint256 unallocatedOffset,,) = governance.userStates(user); + assertEq(unallocatedLQTY, lqtyDeposit); + + uint256 expectedOffset1 = block.timestamp * lqtyDeposit; + // first deposit should have an unallocated offset of deposit * block.timestamp + assertEq(unallocatedOffset, expectedOffset1); vm.warp(block.timestamp + timeIncrease); - lqty.approve(address(userProxy), 1e18); - governance.depositLQTY(1e18); - assertEq(UserProxy(payable(userProxy)).staked(), 2e18); - (allocatedLQTY, averageStakingTimestamp) = governance.userStates(user); - assertEq(allocatedLQTY, 0); - // subsequent deposits should have a stake weighted average - assertEq(averageStakingTimestamp, (block.timestamp - timeIncrease / 2) * 1e26, "Avg ts"); + // Deposit again + lqty.approve(address(userProxy), lqtyDeposit); + governance.depositLQTY(lqtyDeposit); + assertEq(UserProxy(payable(userProxy)).staked(), lqtyDeposit * 2); + (unallocatedLQTY, unallocatedOffset,,) = governance.userStates(user); + assertEq(unallocatedLQTY, lqtyDeposit * 2); - // withdraw 0.5 half of LQTY + uint256 expectedOffset2 = expectedOffset1 + block.timestamp * lqtyDeposit; + // subsequent deposits should result in an increased unallocated offset + assertEq(unallocatedOffset, expectedOffset2, "unallocated offset"); + + // withdraw half of LQTY vm.warp(block.timestamp + timeIncrease); vm.startPrank(address(this)); vm.expectRevert("Governance: user-proxy-not-deployed"); - governance.withdrawLQTY(1e18); + governance.withdrawLQTY(lqtyDeposit); vm.stopPrank(); vm.startPrank(user); - governance.withdrawLQTY(1e18); - assertEq(UserProxy(payable(userProxy)).staked(), 1e18); - (allocatedLQTY, averageStakingTimestamp) = governance.userStates(user); - assertEq(allocatedLQTY, 0); - assertEq(averageStakingTimestamp, ((block.timestamp - timeIncrease) - timeIncrease / 2) * 1e26, "avg ts2"); + governance.withdrawLQTY(lqtyDeposit); + assertEq(UserProxy(payable(userProxy)).staked(), lqtyDeposit); + (unallocatedLQTY, unallocatedOffset,,) = governance.userStates(user); + assertEq(unallocatedLQTY, lqtyDeposit); + // Withdrawing half of the LQTY should also halve the offset, i.e. withdraw "proportionally" from all past deposits + assertEq(unallocatedOffset, expectedOffset2 / 2, "unallocated offset2"); // withdraw remaining LQTY - governance.withdrawLQTY(1e18); + governance.withdrawLQTY(lqtyDeposit); assertEq(UserProxy(payable(userProxy)).staked(), 0); - (allocatedLQTY, averageStakingTimestamp) = governance.userStates(user); - assertEq(allocatedLQTY, 0); - assertEq(averageStakingTimestamp, ((block.timestamp - timeIncrease) - timeIncrease / 2) * 1e26, "avg ts3"); + (unallocatedLQTY, unallocatedOffset,,) = governance.userStates(user); + assertEq(unallocatedLQTY, 0); + assertEq(unallocatedOffset, 0, "unallocated offset2"); vm.stopPrank(); } @@ -284,7 +234,7 @@ contract GovernanceTest is Test { permitParams.v = v; permitParams.r = r; - vm.expectRevert("ERC20: transfer amount exceeds allowance"); + _expectInsufficientAllowance(); governance.depositLQTYViaPermit(1e18, permitParams); permitParams.s = s; @@ -296,15 +246,15 @@ contract GovernanceTest is Test { vm.startPrank(wallet.addr); - vm.expectRevert("ERC20: transfer amount exceeds balance"); - governance.depositLQTYViaPermit(type(uint88).max, permitParams); + _expectInsufficientAllowanceAndBalance(); + governance.depositLQTYViaPermit(1e26, permitParams); // deploy and deposit 1 LQTY governance.depositLQTYViaPermit(1e18, permitParams); assertEq(UserProxy(payable(userProxy)).staked(), 1e18); - (uint88 allocatedLQTY, uint120 averageStakingTimestamp) = governance.userStates(wallet.addr); - assertEq(allocatedLQTY, 0); - assertEq(averageStakingTimestamp, block.timestamp * 1e26); + (uint256 unallocatedLQTY, uint256 unallocatedOffset,,) = governance.userStates(wallet.addr); + assertEq(unallocatedLQTY, 1e18); + assertEq(unallocatedOffset, 1e18 * block.timestamp); } function test_claimFromStakingV1() public { @@ -346,7 +296,7 @@ contract GovernanceTest is Test { // should not revert under any block.timestamp >= EPOCH_START function test_epoch_fuzz(uint32 _timestamp) public { - vm.warp(_timestamp); + vm.warp(governance.EPOCH_START() + _timestamp); governance.epoch(); } @@ -359,7 +309,7 @@ contract GovernanceTest is Test { // should not revert under any block.timestamp >= EPOCH_START function test_epochStart_fuzz(uint32 _timestamp) public { - vm.warp(_timestamp); + vm.warp(governance.EPOCH_START() + _timestamp); governance.epochStart(); } @@ -378,17 +328,17 @@ contract GovernanceTest is Test { // should not revert under any block.timestamp function test_secondsWithinEpoch_fuzz(uint32 _timestamp) public { - vm.warp(_timestamp); + vm.warp(governance.EPOCH_START() + _timestamp); governance.secondsWithinEpoch(); } // should not revert under any input - function test_lqtyToVotes(uint88 _lqtyAmount, uint120 _currentTimestamp, uint120 _averageTimestamp) public { - governance.lqtyToVotes(_lqtyAmount, _currentTimestamp, _averageTimestamp); + function test_lqtyToVotes(uint88 _lqtyAmount, uint32 _currentTimestamp, uint256 _offset) public { + governance.lqtyToVotes(_lqtyAmount, _currentTimestamp, _offset); } function test_getLatestVotingThreshold() public { - governance = new Governance( + governance = new GovernanceTester( address(lqty), address(lusd), address(stakingV1), @@ -397,12 +347,11 @@ contract GovernanceTest is Test { registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: MIN_CLAIM, minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), + epochStart: uint256(block.timestamp), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -414,24 +363,16 @@ contract GovernanceTest is Test { assertEq(governance.getLatestVotingThreshold(), 0); // check that votingThreshold is is high enough such that MIN_CLAIM is met - IGovernance.VoteSnapshot memory snapshot = IGovernance.VoteSnapshot(1e18, 1); - vm.store( - address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) - ); - (uint240 votes, uint16 forEpoch) = governance.votesSnapshot(); - assertEq(votes, 1e18); - assertEq(forEpoch, 1); + IGovernance.VoteSnapshot memory snapshot = IGovernance.VoteSnapshot(1e18, governance.epoch()); + governance.tester_setVotesSnapshot(snapshot); uint256 boldAccrued = 1000e18; - vm.store(address(governance), bytes32(uint256(1)), bytes32(abi.encode(boldAccrued))); - assertEq(governance.boldAccrued(), 1000e18); + governance.tester_setBoldAccrued(boldAccrued); assertEq(governance.getLatestVotingThreshold(), MIN_CLAIM / 1000); // check that votingThreshold is 4% of votes of previous epoch - governance = new Governance( + governance = new GovernanceTester( address(lqty), address(lusd), address(stakingV1), @@ -440,12 +381,11 @@ contract GovernanceTest is Test { registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: 10e18, minAccrual: 10e18, - epochStart: uint32(block.timestamp), + epochStart: uint256(block.timestamp), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -453,34 +393,30 @@ contract GovernanceTest is Test { initialInitiatives ); - snapshot = IGovernance.VoteSnapshot(10000e18, 1); - vm.store( - address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) - ); - (votes, forEpoch) = governance.votesSnapshot(); - assertEq(votes, 10000e18); - assertEq(forEpoch, 1); + snapshot = IGovernance.VoteSnapshot(10000e18, governance.epoch()); + governance.tester_setVotesSnapshot(snapshot); boldAccrued = 1000e18; - vm.store(address(governance), bytes32(uint256(1)), bytes32(abi.encode(boldAccrued))); - assertEq(governance.boldAccrued(), 1000e18); + governance.tester_setBoldAccrued(boldAccrued); assertEq(governance.getLatestVotingThreshold(), 10000e18 * 0.04); } // should not revert under any state function test_calculateVotingThreshold_fuzz( - uint128 _votes, - uint16 _forEpoch, - uint88 _boldAccrued, - uint128 _votingThresholdFactor, - uint88 _minClaim + uint256 _votes, + uint256 _forEpoch, + uint256 _boldAccrued, + uint256 _votingThresholdFactor, + uint256 _minClaim ) public { - _votingThresholdFactor = _votingThresholdFactor % 1e18; - /// Clamp to prevent misconfig - governance = new Governance( + _votes = bound(_votes, 0, type(uint128).max); + _forEpoch = bound(_forEpoch, 0, type(uint16).max); + _boldAccrued = bound(_boldAccrued, 0, 1e9 ether); + _votingThresholdFactor = bound(_votingThresholdFactor, 0, 1 ether - 1); + _minClaim = bound(_minClaim, 0, 1e9 ether); + + governance = new GovernanceTester( address(lqty), address(lusd), address(stakingV1), @@ -489,12 +425,11 @@ contract GovernanceTest is Test { registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: _votingThresholdFactor, minClaim: _minClaim, - minAccrual: type(uint88).max, - epochStart: uint32(block.timestamp), + minAccrual: type(uint256).max, + epochStart: uint256(block.timestamp), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -503,17 +438,8 @@ contract GovernanceTest is Test { ); IGovernance.VoteSnapshot memory snapshot = IGovernance.VoteSnapshot(_votes, _forEpoch); - vm.store( - address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) - ); - (uint240 votes, uint16 forEpoch) = governance.votesSnapshot(); - assertEq(votes, _votes); - assertEq(forEpoch, _forEpoch); - - vm.store(address(governance), bytes32(uint256(1)), bytes32(abi.encode(_boldAccrued))); - assertEq(governance.boldAccrued(), _boldAccrued); + governance.tester_setVotesSnapshot(snapshot); + governance.tester_setBoldAccrued(_boldAccrued); governance.getLatestVotingThreshold(); } @@ -523,17 +449,18 @@ contract GovernanceTest is Test { address userProxy = governance.deployUserProxy(); - IGovernance.VoteSnapshot memory snapshot = IGovernance.VoteSnapshot(1e18, 1); - vm.store( - address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) - ); - (uint240 votes,) = governance.votesSnapshot(); - assertEq(votes, 1e18); + vm.expectRevert("Governance: registration-not-yet-enabled"); + governance.registerInitiative(baseInitiative3); - // should revert if the `REGISTRATION_FEE` > `lqty.balanceOf(msg.sender)` - vm.expectRevert("ERC20: transfer amount exceeds balance"); + // Registration not allowed before epoch #3 + vm.warp(block.timestamp + 2 * EPOCH_DURATION); + assertEq(governance.epoch(), 3, "We should be in epoch #3"); + + IGovernance.VoteSnapshot memory snapshot = IGovernance.VoteSnapshot(1e18, governance.epoch()); + governance.tester_setVotesSnapshot(snapshot); + + // should revert if the `REGISTRATION_FEE` > `lusd.balanceOf(msg.sender)` + _expectInsufficientAllowanceAndBalance(); governance.registerInitiative(baseInitiative3); vm.startPrank(lusdHolder); @@ -548,20 +475,20 @@ contract GovernanceTest is Test { vm.expectRevert("Governance: insufficient-lqty"); governance.registerInitiative(baseInitiative3); - // should revert if the `REGISTRATION_FEE` > `lqty.allowance(msg.sender, governance)` - vm.expectRevert("ERC20: transfer amount exceeds allowance"); + // should revert if the `REGISTRATION_FEE` > `lusd.allowance(msg.sender, governance)` + _expectInsufficientAllowance(); governance.depositLQTY(1e18); lqty.approve(address(userProxy), 1e18); governance.depositLQTY(1e18); - vm.warp(block.timestamp + governance.EPOCH_DURATION()); + vm.warp(block.timestamp + EPOCH_DURATION); // should revert if `_initiative` is zero vm.expectRevert("Governance: zero-address"); governance.registerInitiative(address(0)); governance.registerInitiative(baseInitiative3); - uint16 atEpoch = governance.registeredInitiatives(baseInitiative3); + uint256 atEpoch = governance.registeredInitiatives(baseInitiative3); assertEq(atEpoch, governance.epoch()); // should revert if the initiative was already registered @@ -571,66 +498,105 @@ contract GovernanceTest is Test { vm.stopPrank(); } - // TODO: Broken: Fix it by simplifying most likely - // forge test --match-test test_unregisterInitiative -vv - function test_unregisterInitiative() public { + function test_RegistrationFeesAreUsedAsRewardInNextEpoch() external { + IGovernance.Configuration memory config = IGovernance.Configuration({ + registrationFee: REGISTRATION_FEE, + registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, + unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, + unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, + votingThresholdFactor: VOTING_THRESHOLD_FACTOR, + minClaim: 0, // ensure REGISTRATION_FEE is enough to make a claim + minAccrual: 0, + epochStart: uint256(block.timestamp) - EPOCH_DURATION, // ensure initial initiative can be voted on + epochDuration: EPOCH_DURATION, + epochVotingCutoff: EPOCH_VOTING_CUTOFF + }); + + governance = new GovernanceTester( + address(lqty), address(lusd), address(stakingV1), address(lusd), config, address(this), new address[](0) + ); + + baseInitiative1 = address(new BribeInitiative(address(governance), address(lusd), address(lqty))); + baseInitiative2 = address(new BribeInitiative(address(governance), address(lusd), address(lqty))); + + address[] memory initiatives = new address[](1); + initiatives[0] = baseInitiative1; + governance.registerInitialInitiatives(initiatives); + + // Send user enough LUSD to register a new initiative + vm.prank(lusdHolder); + lusd.transfer(user, REGISTRATION_FEE); + vm.startPrank(user); + { + uint256 lqtyAmount = 1 ether; - address userProxy = governance.deployUserProxy(); + lqty.approve(governance.deriveUserProxyAddress(user), lqtyAmount); + governance.depositLQTY(lqtyAmount); - IGovernance.VoteSnapshot memory snapshot = IGovernance.VoteSnapshot(1e18, 1); - vm.store( - address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) - ); - (uint240 votes, uint16 forEpoch) = governance.votesSnapshot(); - assertEq(votes, 1e18); - assertEq(forEpoch, 1); + address[] memory initiativesToReset; // left empty + int256[] memory votes = new int256[](1); + int256[] memory vetos = new int256[](1); // left zero + + // User votes some LQTY on baseInitiative1 + votes[0] = int256(lqtyAmount); + governance.allocateLQTY(initiativesToReset, initiatives, votes, vetos); + + // Jump into next epoch + vm.warp(governance.epochStart() + EPOCH_DURATION + 6 hours); + // Register new initiative + lusd.approve(address(governance), REGISTRATION_FEE); + governance.registerInitiative(baseInitiative2); + } vm.stopPrank(); + governance.claimForInitiative(baseInitiative1); + assertEqDecimal(lusd.balanceOf(baseInitiative1), 0, 18, "baseInitiative1 shouldn't have received LUSD yet"); + + // One epoch later + vm.warp(block.timestamp + EPOCH_DURATION); + + governance.claimForInitiative(baseInitiative1); + assertEqDecimal( + lusd.balanceOf(baseInitiative1), + REGISTRATION_FEE, + 18, + "baseInitiative1 should have received the registration fee" + ); + } + + // forge test --match-test test_unregisterInitiative -vv + function test_unregisterInitiative() public { vm.startPrank(lusdHolder); lusd.transfer(user, 1e18); vm.stopPrank(); vm.startPrank(user); - lusd.approve(address(governance), 1e18); - lqty.approve(address(userProxy), 1e18); - governance.depositLQTY(1e18); - vm.warp(block.timestamp + governance.EPOCH_DURATION()); - // should revert if the initiative isn't registered - vm.expectRevert("Governance: initiative-not-registered"); + vm.expectRevert("Governance: cannot-unregister-initiative"); governance.unregisterInitiative(baseInitiative3); + // Registration not allowed before epoch #3 + vm.warp(block.timestamp + 2 * EPOCH_DURATION); + assertEq(governance.epoch(), 3, "We should be in epoch #3"); + + lusd.approve(address(governance), 1e18); governance.registerInitiative(baseInitiative3); - uint16 atEpoch = governance.registeredInitiatives(baseInitiative3); - assertEq(atEpoch, governance.epoch()); // should revert if the initiative is still in the registration warm up period - vm.expectRevert("Governance: initiative-in-warm-up"); + vm.expectRevert("Governance: cannot-unregister-initiative"); /// @audit should fail due to not waiting enough time governance.unregisterInitiative(baseInitiative3); - vm.warp(block.timestamp + governance.EPOCH_DURATION()); + vm.warp(block.timestamp + EPOCH_DURATION); // should revert if the initiative is still active or the vetos don't meet the threshold vm.expectRevert("Governance: cannot-unregister-initiative"); governance.unregisterInitiative(baseInitiative3); - snapshot = IGovernance.VoteSnapshot(1e18, governance.epoch() - 1); - vm.store( - address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) - ); - (votes, forEpoch) = governance.votesSnapshot(); - assertEq(votes, 1e18); - assertEq(forEpoch, governance.epoch() - 1); - - vm.warp(block.timestamp + governance.EPOCH_DURATION() * UNREGISTRATION_AFTER_EPOCHS); + vm.warp(block.timestamp + EPOCH_DURATION * UNREGISTRATION_AFTER_EPOCHS); governance.unregisterInitiative(baseInitiative3); @@ -648,7 +614,6 @@ contract GovernanceTest is Test { } /// Used to demonstrate how composite voting could allow using more power than intended - // forge test --match-test test_crit_accounting_mismatch -vv function test_crit_accounting_mismatch() public { // User setup vm.startPrank(user); @@ -660,27 +625,26 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + governance.EPOCH_DURATION()); /// Setup and vote for 2 initiatives, 0.1% vs 99.9% + address[] memory initiativesToReset; address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int88[] memory deltaLQTYVotes = new int88[](2); + int256[] memory deltaLQTYVotes = new int256[](2); deltaLQTYVotes[0] = 1e18; deltaLQTYVotes[1] = 999e18; - int88[] memory deltaLQTYVetos = new int88[](2); + int256[] memory deltaLQTYVetos = new int256[](2); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); - (uint256 allocatedLQTY,) = governance.userStates(user); + (,, uint256 allocatedLQTY,) = governance.userStates(user); assertEq(allocatedLQTY, 1_000e18); - (uint88 voteLQTY1,, uint120 averageStakingTimestampVoteLQTY1,,) = governance.initiativeStates(baseInitiative1); + (uint256 voteLQTY1, uint256 voteOffset1,,,) = governance.initiativeStates(baseInitiative1); - (uint88 voteLQTY2,,,,) = governance.initiativeStates(baseInitiative2); + (uint256 voteLQTY2,,,,) = governance.initiativeStates(baseInitiative2); // Get power at time of vote - uint256 votingPower = governance.lqtyToVotes( - voteLQTY1, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY1 - ); + uint256 votingPower = governance.lqtyToVotes(voteLQTY1, block.timestamp, voteOffset1); assertGt(votingPower, 0, "Non zero power"); /// @audit TODO Fully digest and explain the bug @@ -700,9 +664,7 @@ contract GovernanceTest is Test { assertLt(initiativeVoteSnapshot1.votes, threshold, "it didn't get rewards"); uint256 votingPowerWithProjection = governance.lqtyToVotes( - voteLQTY1, - uint120(governance.epochStart() + governance.EPOCH_DURATION()), - averageStakingTimestampVoteLQTY1 + voteLQTY1, uint256(governance.epochStart() + governance.EPOCH_DURATION()), voteOffset1 ); assertLt(votingPower, threshold, "Current Power is not enough - Desynch A"); assertLt(votingPowerWithProjection, threshold, "Future Power is also not enough - Desynch B"); @@ -711,7 +673,6 @@ contract GovernanceTest is Test { // Same setup as above (but no need for bug) // Show that you cannot withdraw - // forge test --match-test test_canAlwaysRemoveAllocation -vv function test_canAlwaysRemoveAllocation() public { // User setup vm.startPrank(user); @@ -723,15 +684,16 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + governance.EPOCH_DURATION()); /// Setup and vote for 2 initiatives, 0.1% vs 99.9% + address[] memory initiativesToReset; address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int88[] memory deltaLQTYVotes = new int88[](2); + int256[] memory deltaLQTYVotes = new int256[](2); deltaLQTYVotes[0] = 1e18; deltaLQTYVotes[1] = 999e18; - int88[] memory deltaLQTYVetos = new int88[](2); + int256[] memory deltaLQTYVetos = new int256[](2); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); // Warp to end so we check the threshold against future threshold @@ -759,32 +721,28 @@ contract GovernanceTest is Test { address[] memory removeInitiatives = new address[](2); removeInitiatives[0] = baseInitiative1; removeInitiatives[1] = baseInitiative2; - int88[] memory removeDeltaLQTYVotes = new int88[](2); - // don't need to explicitly remove allocation because it already gets reset - removeDeltaLQTYVotes[0] = 0; - int88[] memory removeDeltaLQTYVetos = new int88[](2); - - governance.allocateLQTY(removeInitiatives, removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); + governance.resetAllocations(removeInitiatives, true); + int256[] memory removeDeltaLQTYVotes = new int256[](2); + int256[] memory removeDeltaLQTYVetos = new int256[](2); removeDeltaLQTYVotes[0] = -1e18; vm.expectRevert("Cannot be negative"); - governance.allocateLQTY(removeInitiatives, removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); address[] memory reAddInitiatives = new address[](1); reAddInitiatives[0] = baseInitiative1; - int88[] memory reAddDeltaLQTYVotes = new int88[](1); + int256[] memory reAddDeltaLQTYVotes = new int256[](1); reAddDeltaLQTYVotes[0] = 1e18; - int88[] memory reAddDeltaLQTYVetos = new int88[](1); + int256[] memory reAddDeltaLQTYVetos = new int256[](1); /// @audit This MUST revert, an initiative should not be re-votable once disabled vm.expectRevert("Governance: active-vote-fsm"); - governance.allocateLQTY(reAddInitiatives, reAddInitiatives, reAddDeltaLQTYVotes, reAddDeltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, reAddInitiatives, reAddDeltaLQTYVotes, reAddDeltaLQTYVetos); } // Used to identify an accounting bug where vote power could be added to global state // While initiative is unregistered - // forge test --match-test test_allocationRemovalTotalLqtyMathIsSound -vv function test_allocationRemovalTotalLqtyMathIsSound() public { vm.startPrank(user2); address userProxy_2 = governance.deployUserProxy(); @@ -802,18 +760,19 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + governance.EPOCH_DURATION()); /// Setup and vote for 2 initiatives, 0.1% vs 99.9% + address[] memory initiativesToReset; address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int88[] memory deltaLQTYVotes = new int88[](2); + int256[] memory deltaLQTYVotes = new int256[](2); deltaLQTYVotes[0] = 1e18; deltaLQTYVotes[1] = 999e18; - int88[] memory deltaLQTYVetos = new int88[](2); + int256[] memory deltaLQTYVetos = new int256[](2); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); vm.startPrank(user2); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); vm.startPrank(user); @@ -823,36 +782,34 @@ contract GovernanceTest is Test { // Get state here // Get initiative state - (uint88 b4_countedVoteLQTY, uint120 b4_countedVoteLQTYAverageTimestamp) = governance.globalState(); + (uint256 b4_countedVoteLQTY, uint256 b4_countedVoteOffset) = governance.globalState(); // I want to remove my allocation - address[] memory removeInitiatives = new address[](2); - removeInitiatives[0] = baseInitiative1; - removeInitiatives[1] = baseInitiative2; - int88[] memory removeDeltaLQTYVotes = new int88[](2); + initiativesToReset = new address[](2); + initiativesToReset[0] = baseInitiative1; + initiativesToReset[1] = baseInitiative2; // don't need to explicitly remove allocation because it already gets reset - removeDeltaLQTYVotes[0] = 0; - removeDeltaLQTYVotes[1] = 999e18; + address[] memory removeInitiatives = new address[](1); + removeInitiatives[0] = baseInitiative2; + int256[] memory removeDeltaLQTYVotes = new int256[](1); + removeDeltaLQTYVotes[0] = 999e18; - int88[] memory removeDeltaLQTYVetos = new int88[](2); + int256[] memory removeDeltaLQTYVetos = new int256[](1); - governance.allocateLQTY(removeInitiatives, removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); { // Get state here // TODO Get initiative state - (uint88 after_countedVoteLQTY, uint120 after_countedVoteLQTYAverageTimestamp) = governance.globalState(); + (uint256 after_countedVoteLQTY, uint256 after_countedVoteOffset) = governance.globalState(); assertEq(after_countedVoteLQTY, b4_countedVoteLQTY, "LQTY should not change"); - assertEq( - b4_countedVoteLQTYAverageTimestamp, after_countedVoteLQTYAverageTimestamp, "Avg TS should not change" - ); + assertEq(b4_countedVoteOffset, after_countedVoteOffset, "Offset should not change"); } } // Remove allocation but check accounting // Need to find bug in accounting code - // forge test --match-test test_addRemoveAllocation_accounting -vv function test_addRemoveAllocation_accounting() public { // User setup vm.startPrank(user); @@ -864,15 +821,16 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + governance.EPOCH_DURATION()); /// Setup and vote for 2 initiatives, 0.1% vs 99.9% + address[] memory initiativesToReset; address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int88[] memory deltaLQTYVotes = new int88[](2); + int256[] memory deltaLQTYVotes = new int256[](2); deltaLQTYVotes[0] = 1e18; deltaLQTYVotes[1] = 999e18; - int88[] memory deltaLQTYVetos = new int88[](2); + int256[] memory deltaLQTYVetos = new int256[](2); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); // Warp to end so we check the threshold against future threshold { @@ -894,9 +852,9 @@ contract GovernanceTest is Test { // Grab values b4 unregistering and b4 removing user allocation - (uint88 b4_countedVoteLQTY, uint120 b4_countedVoteLQTYAverageTimestamp) = governance.globalState(); - (uint88 b4_allocatedLQTY, uint120 b4_averageStakingTimestamp) = governance.userStates(user); - (uint88 b4_voteLQTY,,,,) = governance.initiativeStates(baseInitiative1); + (uint256 b4_countedVoteLQTY, uint256 b4_countedVoteOffset) = governance.globalState(); + (,, uint256 b4_allocatedLQTY, uint256 b4_allocatedOffset) = governance.userStates(user); + (uint256 b4_voteLQTY,,,,) = governance.initiativeStates(baseInitiative1); // Unregistering governance.unregisterInitiative(baseInitiative1); @@ -906,20 +864,20 @@ contract GovernanceTest is Test { // We expect the state to already have those removed // We expect the user to not have any changes - (uint88 after_countedVoteLQTY,) = governance.globalState(); + (uint256 after_countedVoteLQTY,) = governance.globalState(); assertEq(after_countedVoteLQTY, b4_countedVoteLQTY - b4_voteLQTY, "Global Lqty change after unregister"); assertEq(1e18, b4_voteLQTY, "sanity check"); - (uint88 after_allocatedLQTY, uint120 after_averageStakingTimestamp) = governance.userStates(user); + (,, uint256 after_allocatedLQTY, uint256 after_unallocatedOffset) = governance.userStates(user); // We expect no changes here ( - uint88 after_voteLQTY, - uint88 after_vetoLQTY, - uint120 after_averageStakingTimestampVoteLQTY, - uint120 after_averageStakingTimestampVetoLQTY, - uint16 after_lastEpochClaim + uint256 after_voteLQTY, + uint256 after_voteOffset, + uint256 after_vetoLQTY, + uint256 after_vetoOffset, + uint256 after_lastEpochClaim ) = governance.initiativeStates(baseInitiative1); assertEq(b4_voteLQTY, after_voteLQTY, "Initiative votes are the same"); @@ -928,37 +886,30 @@ contract GovernanceTest is Test { // User Votes // Initiative Votes - // I cannot address[] memory removeInitiatives = new address[](2); removeInitiatives[0] = baseInitiative1; removeInitiatives[1] = baseInitiative2; // all user initiatives previously allocated to need to be included for resetting - int88[] memory removeDeltaLQTYVotes = new int88[](2); - removeDeltaLQTYVotes[0] = 0; - removeDeltaLQTYVotes[1] = 0; - int88[] memory removeDeltaLQTYVetos = new int88[](2); /// @audit the next call MUST not revert - this is a critical bug - governance.allocateLQTY(removeInitiatives, removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); + governance.resetAllocations(removeInitiatives, true); // After user counts LQTY the { - (uint88 after_user_countedVoteLQTY, uint120 after_user_countedVoteLQTYAverageTimestamp) = - governance.globalState(); + (uint256 after_user_countedVoteLQTY, uint256 after_user_countedVoteOffset) = governance.globalState(); // The LQTY was already removed assertEq(after_user_countedVoteLQTY, 0, "Removal 1"); } // User State allocated LQTY changes by entire previous allocation amount - // Timestamp should not change { - (uint88 after_user_allocatedLQTY,) = governance.userStates(user); + (,, uint256 after_user_allocatedLQTY,) = governance.userStates(user); assertEq(after_user_allocatedLQTY, 0, "Removal 2"); } // Check user math only change is the LQTY amt // user was the only one allocated so since all alocations were reset, the initative lqty should be 0 { - (uint88 after_user_voteLQTY,,,,) = governance.initiativeStates(baseInitiative1); + (uint256 after_user_voteLQTY,,,,) = governance.initiativeStates(baseInitiative1); assertEq(after_user_voteLQTY, 0, "Removal 3"); } @@ -977,16 +928,17 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + governance.EPOCH_DURATION()); /// Setup and vote for 2 initiatives, 0.1% vs 99.9% + address[] memory initiativesToReset; address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int88[] memory deltaLQTYVotes = new int88[](2); + int256[] memory deltaLQTYVotes = new int256[](2); deltaLQTYVotes[0] = 1e18; deltaLQTYVotes[1] = 999e18; - int88[] memory deltaLQTYVetos = new int88[](2); + int256[] memory deltaLQTYVetos = new int256[](2); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); - (uint88 allocatedB4Test,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); + (uint256 allocatedB4Test,,,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); console.log("allocatedB4Test", allocatedB4Test); vm.warp(block.timestamp + governance.EPOCH_DURATION()); @@ -997,21 +949,21 @@ contract GovernanceTest is Test { address[] memory removeInitiatives = new address[](2); removeInitiatives[0] = baseInitiative1; removeInitiatives[1] = baseInitiative2; - int88[] memory removeDeltaLQTYVotes = new int88[](2); - removeDeltaLQTYVotes[0] = 0; - int88[] memory removeDeltaLQTYVetos = new int88[](2); - (uint88 allocatedB4Removal,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + (uint256 allocatedB4Removal,,,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); console.log("allocatedB4Removal", allocatedB4Removal); - governance.allocateLQTY(removeInitiatives, removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); - (uint88 allocatedAfterRemoval,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + governance.resetAllocations(removeInitiatives, true); + (uint256 allocatedAfterRemoval,,,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); console.log("allocatedAfterRemoval", allocatedAfterRemoval); - // @audit this test no longer reverts due to underflow because of resetting before each allocation - // vm.expectRevert(); - governance.allocateLQTY(removeInitiatives, removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); - (uint88 allocatedAfter,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + vm.expectRevert("Governance: nothing to reset"); + governance.resetAllocations(removeInitiatives, true); + int256[] memory removeDeltaLQTYVotes = new int256[](2); + int256[] memory removeDeltaLQTYVetos = new int256[](2); + vm.expectRevert("Governance: voting nothing"); + governance.allocateLQTY(initiativesToReset, removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); + (uint256 allocatedAfter,,,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); console.log("allocatedAfter", allocatedAfter); } @@ -1022,6 +974,24 @@ contract GovernanceTest is Test { /// Ensure that at the end you remove 100% function test_fuzz_canRemoveExtact() public {} + function test_allocateLQTY_revertsWhenInputArraysAreOfDifferentLengths() external { + address[] memory initiativesToReset = new address[](0); + address[][2] memory initiatives = [new address[](2), new address[](3)]; + int256[][2] memory votes = [new int256[](2), new int256[](3)]; + int256[][2] memory vetos = [new int256[](2), new int256[](3)]; + + for (uint256 i = 0; i < 2; ++i) { + for (uint256 j = 0; j < 2; ++j) { + for (uint256 k = 0; k < 2; ++k) { + if (i == j && j == k) continue; + + vm.expectRevert("Governance: array-length-mismatch"); + governance.allocateLQTY(initiativesToReset, initiatives[i], votes[j], vetos[k]); + } + } + } + } + function test_allocateLQTY_single() public { vm.startPrank(user); @@ -1030,48 +1000,41 @@ contract GovernanceTest is Test { lqty.approve(address(userProxy), 1e18); governance.depositLQTY(1e18); - (uint88 allocatedLQTY, uint120 averageStakingTimestampUser) = governance.userStates(user); + (,, uint256 allocatedLQTY, uint256 allocatedOffset) = governance.userStates(user); assertEq(allocatedLQTY, 0); - (uint88 countedVoteLQTY,) = governance.globalState(); + (uint256 countedVoteLQTY,) = governance.globalState(); assertEq(countedVoteLQTY, 0); + address[] memory initiativesToReset; address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int88[] memory deltaLQTYVotes = new int88[](1); + int256[] memory deltaLQTYVotes = new int256[](1); deltaLQTYVotes[0] = 1e18; //this should be 0 - int88[] memory deltaLQTYVetos = new int88[](1); + int256[] memory deltaLQTYVetos = new int256[](1); // should revert if the initiative has been registered in the current epoch vm.expectRevert("Governance: active-vote-fsm"); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); vm.warp(block.timestamp + governance.EPOCH_DURATION()); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); - (allocatedLQTY,) = governance.userStates(user); + (,, allocatedLQTY,) = governance.userStates(user); assertEq(allocatedLQTY, 1e18); - ( - uint88 voteLQTY, - uint88 vetoLQTY, - uint120 averageStakingTimestampVoteLQTY, - uint120 averageStakingTimestampVetoLQTY, - ) = governance.initiativeStates(baseInitiative1); + (uint256 voteLQTY, uint256 voteOffset, uint256 vetoLQTY, uint256 vetoOffset,) = + governance.initiativeStates(baseInitiative1); // should update the `voteLQTY` and `vetoLQTY` variables assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); - // should update the average staking timestamp for the initiative based on the average staking timestamp of the user's - // voting and vetoing LQTY - assertEq(averageStakingTimestampVoteLQTY, (block.timestamp - governance.EPOCH_DURATION()) * 1e26); - assertEq(averageStakingTimestampVoteLQTY, averageStakingTimestampUser); - assertEq(averageStakingTimestampVetoLQTY, 0); + // TODO: assertions re: initiative vote & veto offsets // should remove or add the initiatives voting LQTY from the counter (countedVoteLQTY,) = governance.globalState(); assertEq(countedVoteLQTY, 1e18); - uint16 atEpoch; - (voteLQTY, vetoLQTY, atEpoch) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + uint256 atEpoch; + (voteLQTY,, vetoLQTY,, atEpoch) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); // should update the allocation mapping from user to initiative assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); @@ -1079,7 +1042,7 @@ contract GovernanceTest is Test { assertGt(atEpoch, 0); // should snapshot the global and initiatives votes if there hasn't been a snapshot in the current epoch yet - (, uint16 forEpoch) = governance.votesSnapshot(); + (, uint256 forEpoch) = governance.votesSnapshot(); assertEq(forEpoch, governance.epoch() - 1); (, forEpoch,,) = governance.votesForInitiativeSnapshot(baseInitiative1); assertEq(forEpoch, governance.epoch() - 1); @@ -1095,53 +1058,55 @@ contract GovernanceTest is Test { lqty.approve(address(user2Proxy), 1e18); governance.depositLQTY(1e18); - (, uint120 averageAge) = governance.userStates(user2); - assertEq(governance.lqtyToVotes(1e18, uint120(block.timestamp) * uint120(1e26), averageAge), 0); + IGovernance.UserState memory user2State; + (user2State.unallocatedLQTY, user2State.unallocatedOffset, user2State.allocatedLQTY, user2State.allocatedOffset) + = governance.userStates(user2); + assertEq(user2State.allocatedLQTY, 0); + assertEq(user2State.allocatedOffset, 0); + assertEq( + governance.lqtyToVotes(user2State.unallocatedLQTY, uint256(block.timestamp), user2State.unallocatedOffset), + 0 + ); deltaLQTYVetos[0] = 1e18; vm.expectRevert("Governance: vote-and-veto"); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); deltaLQTYVetos[0] = 0; - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); // should update the user's allocated LQTY balance - (allocatedLQTY,) = governance.userStates(user2); + (,, allocatedLQTY,) = governance.userStates(user2); assertEq(allocatedLQTY, 1e18); - (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY,) = - governance.initiativeStates(baseInitiative1); + (voteLQTY, voteOffset, vetoLQTY, vetoOffset,) = governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 2e18); assertEq(vetoLQTY, 0); - assertEq(averageStakingTimestampVoteLQTY, (block.timestamp - governance.EPOCH_DURATION()) * 1e26); - assertGt(averageStakingTimestampVoteLQTY, averageStakingTimestampUser); - assertEq(averageStakingTimestampVetoLQTY, 0); + // TODO: assertions re: initiative vote + veto offsets // should revert if the user doesn't have enough unallocated LQTY available - vm.expectRevert("Governance: must-allocate-zero"); + vm.expectRevert("Governance: insufficient-unallocated-lqty"); governance.withdrawLQTY(1e18); vm.warp(block.timestamp + EPOCH_DURATION - governance.secondsWithinEpoch() - 1); // user can only unallocate after voting cutoff initiatives[0] = baseInitiative1; - deltaLQTYVotes[0] = 0; - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.resetAllocations(initiatives, true); - (allocatedLQTY,) = governance.userStates(user2); + (,, allocatedLQTY,) = governance.userStates(user2); assertEq(allocatedLQTY, 0); (countedVoteLQTY,) = governance.globalState(); console.log("countedVoteLQTY: ", countedVoteLQTY); assertEq(countedVoteLQTY, 1e18); - (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY,) = - governance.initiativeStates(baseInitiative1); + (voteLQTY, voteOffset, vetoLQTY, vetoOffset,) = governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); - assertEq(averageStakingTimestampVoteLQTY, averageStakingTimestampUser); - assertEq(averageStakingTimestampVetoLQTY, 0); + // TODO: assertion re: vote offset + assertEq(vetoOffset, 0); vm.stopPrank(); } @@ -1154,48 +1119,43 @@ contract GovernanceTest is Test { lqty.approve(address(userProxy), 1e18); governance.depositLQTY(1e18); - (uint88 allocatedLQTY, uint120 averageStakingTimestampUser) = governance.userStates(user); + (,, uint256 allocatedLQTY, uint256 allocatedOffset) = governance.userStates(user); assertEq(allocatedLQTY, 0); - (uint88 countedVoteLQTY,) = governance.globalState(); + (uint256 countedVoteLQTY,) = governance.globalState(); assertEq(countedVoteLQTY, 0); + address[] memory initiativesToReset; address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int88[] memory deltaLQTYVotes = new int88[](1); + int256[] memory deltaLQTYVotes = new int256[](1); deltaLQTYVotes[0] = 1e18; //this should be 0 - int88[] memory deltaLQTYVetos = new int88[](1); + int256[] memory deltaLQTYVetos = new int256[](1); // should revert if the initiative has been registered in the current epoch vm.expectRevert("Governance: active-vote-fsm"); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); vm.warp(block.timestamp + governance.EPOCH_DURATION()); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); - (allocatedLQTY,) = governance.userStates(user); + (,, allocatedLQTY,) = governance.userStates(user); assertEq(allocatedLQTY, 1e18); - ( - uint88 voteLQTY, - uint88 vetoLQTY, - uint120 averageStakingTimestampVoteLQTY, - uint120 averageStakingTimestampVetoLQTY, - ) = governance.initiativeStates(baseInitiative1); + (uint256 voteLQTY, uint256 voteOffset, uint256 vetoLQTY, uint256 vetoOffset,) = + governance.initiativeStates(baseInitiative1); // should update the `voteLQTY` and `vetoLQTY` variables assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); // should update the average staking timestamp for the initiative based on the average staking timestamp of the user's // voting and vetoing LQTY - assertEq(averageStakingTimestampVoteLQTY, (block.timestamp - governance.EPOCH_DURATION()) * 1e26, "TS"); - assertEq(averageStakingTimestampVoteLQTY, averageStakingTimestampUser); - assertEq(averageStakingTimestampVetoLQTY, 0); + // TODO: assertions re: vote + veto offsets // should remove or add the initiatives voting LQTY from the counter (countedVoteLQTY,) = governance.globalState(); assertEq(countedVoteLQTY, 1e18); - uint16 atEpoch; - (voteLQTY, vetoLQTY, atEpoch) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + uint256 atEpoch; + (voteLQTY,, vetoLQTY,, atEpoch) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); // should update the allocation mapping from user to initiative assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); @@ -1203,7 +1163,7 @@ contract GovernanceTest is Test { assertGt(atEpoch, 0); // should snapshot the global and initiatives votes if there hasn't been a snapshot in the current epoch yet - (, uint16 forEpoch) = governance.votesSnapshot(); + (, uint256 forEpoch) = governance.votesSnapshot(); assertEq(forEpoch, governance.epoch() - 1); (, forEpoch,,) = governance.votesForInitiativeSnapshot(baseInitiative1); assertEq(forEpoch, governance.epoch() - 1); @@ -1219,32 +1179,29 @@ contract GovernanceTest is Test { lqty.approve(address(user2Proxy), 1e18); governance.depositLQTY(1e18); - (, uint120 averageAge) = governance.userStates(user2); - assertEq(governance.lqtyToVotes(1e18, uint120(block.timestamp) * uint120(1e26), averageAge), 0); + (, uint256 unallocatedOffset,,) = governance.userStates(user2); + assertEq(governance.lqtyToVotes(1e18, block.timestamp, unallocatedOffset), 0); deltaLQTYVetos[0] = 1e18; vm.expectRevert("Governance: vote-and-veto"); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); deltaLQTYVetos[0] = 0; - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); // should update the user's allocated LQTY balance - (allocatedLQTY,) = governance.userStates(user2); + (,, allocatedLQTY,) = governance.userStates(user2); assertEq(allocatedLQTY, 1e18); - (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY,) = - governance.initiativeStates(baseInitiative1); + (voteLQTY, voteOffset, vetoLQTY, vetoOffset,) = governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 2e18); assertEq(vetoLQTY, 0); - assertEq(averageStakingTimestampVoteLQTY, (block.timestamp - governance.EPOCH_DURATION()) * 1e26, "TS 2"); - assertGt(averageStakingTimestampVoteLQTY, averageStakingTimestampUser); - assertEq(averageStakingTimestampVetoLQTY, 0); + // TODO: offset vote + veto assertions // should revert if the user doesn't have enough unallocated LQTY available - vm.expectRevert("Governance: must-allocate-zero"); + vm.expectRevert("Governance: insufficient-unallocated-lqty"); governance.withdrawLQTY(1e18); vm.warp(block.timestamp + EPOCH_DURATION - governance.secondsWithinEpoch() - 1); @@ -1254,7 +1211,7 @@ contract GovernanceTest is Test { // should only allow for unallocating votes or allocating vetos after the epoch voting cutoff // vm.expectRevert("Governance: epoch-voting-cutoff"); governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); - (allocatedLQTY,) = governance.userStates(msg.sender); + (,, allocatedLQTY,) = governance.userStates(msg.sender); // this no longer reverts but the user allocation doesn't increase either way assertEq(allocatedLQTY, 0, "user can allocate after voting cutoff"); @@ -1271,92 +1228,89 @@ contract GovernanceTest is Test { lqty.approve(address(userProxy), 2e18); governance.depositLQTY(2e18); - (uint88 allocatedLQTY,) = governance.userStates(user); + (,, uint256 allocatedLQTY,) = governance.userStates(user); assertEq(allocatedLQTY, 0); - (uint88 countedVoteLQTY,) = governance.globalState(); + (uint256 countedVoteLQTY,) = governance.globalState(); assertEq(countedVoteLQTY, 0); + address[] memory initiativesToReset; address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int88[] memory deltaLQTYVotes = new int88[](2); + int256[] memory deltaLQTYVotes = new int256[](2); deltaLQTYVotes[0] = 1e18; deltaLQTYVotes[1] = 1e18; - int88[] memory deltaLQTYVetos = new int88[](2); + int256[] memory deltaLQTYVetos = new int256[](2); vm.warp(block.timestamp + governance.EPOCH_DURATION()); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); - (allocatedLQTY,) = governance.userStates(user); + (,, allocatedLQTY,) = governance.userStates(user); assertEq(allocatedLQTY, 2e18); (countedVoteLQTY,) = governance.globalState(); assertEq(countedVoteLQTY, 2e18); - ( - uint88 voteLQTY, - uint88 vetoLQTY, - uint120 averageStakingTimestampVoteLQTY, - uint120 averageStakingTimestampVetoLQTY, - ) = governance.initiativeStates(baseInitiative1); + (uint256 voteLQTY, uint256 voteOffset, uint256 vetoLQTY, uint256 vetoOffset,) = + governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); - (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY,) = - governance.initiativeStates(baseInitiative2); + (voteLQTY, voteOffset, vetoLQTY, vetoOffset,) = governance.initiativeStates(baseInitiative2); assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); } - function test_allocateLQTY_fuzz_deltaLQTYVotes(uint88 _deltaLQTYVotes) public { - vm.assume(_deltaLQTYVotes > 0 && _deltaLQTYVotes < uint88(type(int88).max)); + function test_allocateLQTY_fuzz_deltaLQTYVotes(uint256 _deltaLQTYVotes) public { + _deltaLQTYVotes = bound(_deltaLQTYVotes, 1, 100e6 ether); vm.startPrank(user); address userProxy = governance.deployUserProxy(); - vm.store(address(lqty), keccak256(abi.encode(user, 0)), bytes32(abi.encode(uint256(_deltaLQTYVotes)))); + deal(address(lqty), user, _deltaLQTYVotes); lqty.approve(address(userProxy), _deltaLQTYVotes); governance.depositLQTY(_deltaLQTYVotes); + address[] memory initiativesToReset; address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int88[] memory deltaLQTYVotes = new int88[](1); - deltaLQTYVotes[0] = int88(uint88(_deltaLQTYVotes)); - int88[] memory deltaLQTYVetos = new int88[](1); + int256[] memory deltaLQTYVotes = new int256[](1); + deltaLQTYVotes[0] = int256(_deltaLQTYVotes); + int256[] memory deltaLQTYVetos = new int256[](1); vm.warp(block.timestamp + governance.EPOCH_DURATION()); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); vm.stopPrank(); } - function test_allocateLQTY_fuzz_deltaLQTYVetos(uint88 _deltaLQTYVetos) public { - vm.assume(_deltaLQTYVetos > 0 && _deltaLQTYVetos < uint88(type(int88).max)); + function test_allocateLQTY_fuzz_deltaLQTYVetos(uint256 _deltaLQTYVetos) public { + _deltaLQTYVetos = bound(_deltaLQTYVetos, 1, 100e6 ether); vm.startPrank(user); address userProxy = governance.deployUserProxy(); - vm.store(address(lqty), keccak256(abi.encode(user, 0)), bytes32(abi.encode(uint256(_deltaLQTYVetos)))); + deal(address(lqty), user, _deltaLQTYVetos); lqty.approve(address(userProxy), _deltaLQTYVetos); governance.depositLQTY(_deltaLQTYVetos); + address[] memory initiativesToReset; address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int88[] memory deltaLQTYVotes = new int88[](1); - int88[] memory deltaLQTYVetos = new int88[](1); - deltaLQTYVetos[0] = int88(uint88(_deltaLQTYVetos)); + int256[] memory deltaLQTYVotes = new int256[](1); + int256[] memory deltaLQTYVetos = new int256[](1); + deltaLQTYVetos[0] = int256(_deltaLQTYVetos); vm.warp(block.timestamp + governance.EPOCH_DURATION()); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); /// @audit needs overflow tests!! vm.stopPrank(); } - // forge test --match-test test_claimForInitiative -vv function test_claimForInitiative() public { vm.startPrank(user); @@ -1376,15 +1330,16 @@ contract GovernanceTest is Test { vm.startPrank(user); + address[] memory initiativesToReset; address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int88[] memory deltaVoteLQTY = new int88[](2); + int256[] memory deltaVoteLQTY = new int256[](2); deltaVoteLQTY[0] = 500e18; deltaVoteLQTY[1] = 500e18; - int88[] memory deltaVetoLQTY = new int88[](2); - governance.allocateLQTY(initiatives, initiatives, deltaVoteLQTY, deltaVetoLQTY); - (uint88 allocatedLQTY,) = governance.userStates(user); + int256[] memory deltaVetoLQTY = new int256[](2); + governance.allocateLQTY(initiativesToReset, initiatives, deltaVoteLQTY, deltaVetoLQTY); + (,, uint256 allocatedLQTY,) = governance.userStates(user); assertEq(allocatedLQTY, 1000e18); vm.warp(block.timestamp + governance.EPOCH_DURATION() + 1); @@ -1408,14 +1363,17 @@ contract GovernanceTest is Test { vm.startPrank(user); - console.log("here 1"); + initiativesToReset = new address[](2); + initiativesToReset[0] = baseInitiative1; + initiativesToReset[1] = baseInitiative2; + initiatives = new address[](1); initiatives[0] = baseInitiative1; - initiatives[1] = baseInitiative2; + deltaVoteLQTY = new int256[](1); + deltaVetoLQTY = new int256[](1); deltaVoteLQTY[0] = 495e18; + // @audit user can't deallocate because votes already get reset // deltaVoteLQTY[1] = -495e18; - deltaVoteLQTY[1] = 0; // @audit user can't deallocate because votes already get reset - governance.allocateLQTY(initiatives, initiatives, deltaVoteLQTY, deltaVetoLQTY); - console.log("here 2"); + governance.allocateLQTY(initiativesToReset, initiatives, deltaVoteLQTY, deltaVetoLQTY); vm.warp(block.timestamp + governance.EPOCH_DURATION() + 1); @@ -1424,10 +1382,10 @@ contract GovernanceTest is Test { assertEq(lusd.balanceOf(baseInitiative1), 15000e18); - (Governance.InitiativeStatus status,, uint256 claimable) = governance.getInitiativeState(baseInitiative2); + (IGovernance.InitiativeStatus status,, uint256 claimable) = governance.getInitiativeState(baseInitiative2); console.log("res", uint8(status)); console.log("claimable", claimable); - (uint224 votes,,, uint224 vetos) = governance.votesForInitiativeSnapshot(baseInitiative2); + (uint256 votes,,, uint256 vetos) = governance.votesForInitiativeSnapshot(baseInitiative2); console.log("snapshot votes", votes); console.log("snapshot vetos", vetos); @@ -1462,15 +1420,16 @@ contract GovernanceTest is Test { vm.startPrank(user); + address[] memory initiativesToReset; address[] memory initiatives = new address[](2); initiatives[0] = EOAInitiative; // attempt for an EOA initiatives[1] = baseInitiative2; - int88[] memory deltaVoteLQTY = new int88[](2); + int256[] memory deltaVoteLQTY = new int256[](2); deltaVoteLQTY[0] = 500e18; deltaVoteLQTY[1] = 500e18; - int88[] memory deltaVetoLQTY = new int88[](2); - governance.allocateLQTY(initiatives, initiatives, deltaVoteLQTY, deltaVetoLQTY); - (uint88 allocatedLQTY,) = governance.userStates(user); + int256[] memory deltaVetoLQTY = new int256[](2); + governance.allocateLQTY(initiativesToReset, initiatives, deltaVoteLQTY, deltaVetoLQTY); + (,, uint256 allocatedLQTY,) = governance.userStates(user); assertEq(allocatedLQTY, 1000e18); vm.warp(block.timestamp + governance.EPOCH_DURATION() + 1); @@ -1521,35 +1480,45 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + governance.EPOCH_DURATION()); - uint88 lqtyAmount = 1000e18; + uint256 lqtyAmount = 1000e18; uint256 lqtyBalance = lqty.balanceOf(user); lqty.approve(address(governance.deriveUserProxyAddress(user)), lqtyAmount); - bytes[] memory data = new bytes[](7); + bytes[] memory data = new bytes[](8); + address[] memory initiativesToReset; address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int88[] memory deltaVoteLQTY = new int88[](1); - deltaVoteLQTY[0] = int88(uint88(lqtyAmount)); - int88[] memory deltaVetoLQTY = new int88[](1); + int256[] memory deltaVoteLQTY = new int256[](1); + deltaVoteLQTY[0] = int256(uint256(lqtyAmount)); + int256[] memory deltaVetoLQTY = new int256[](1); - int88[] memory deltaVoteLQTY_ = new int88[](1); - deltaVoteLQTY_[0] = 0; + int256[] memory deltaVoteLQTY_ = new int256[](1); + deltaVoteLQTY_[0] = 1; data[0] = abi.encodeWithSignature("deployUserProxy()"); - data[1] = abi.encodeWithSignature("depositLQTY(uint88)", lqtyAmount); + data[1] = abi.encodeWithSignature("depositLQTY(uint256)", lqtyAmount); data[2] = abi.encodeWithSignature( - "allocateLQTY(address[],address[],int88[],int88[])", initiatives, initiatives, deltaVoteLQTY, deltaVetoLQTY + "allocateLQTY(address[],address[],int256[],int256[])", + initiativesToReset, + initiatives, + deltaVoteLQTY, + deltaVetoLQTY ); data[3] = abi.encodeWithSignature("userStates(address)", user); data[4] = abi.encodeWithSignature("snapshotVotesForInitiative(address)", baseInitiative1); data[5] = abi.encodeWithSignature( - "allocateLQTY(address[],address[],int88[],int88[])", initiatives, initiatives, deltaVoteLQTY_, deltaVetoLQTY + "allocateLQTY(address[],address[],int256[],int256[])", + initiatives, + initiatives, + deltaVoteLQTY_, + deltaVetoLQTY ); - data[6] = abi.encodeWithSignature("withdrawLQTY(uint88)", lqtyAmount); - bytes[] memory response = governance.multicall(data); + data[6] = abi.encodeWithSignature("resetAllocations(address[],bool)", initiatives, true); + data[7] = abi.encodeWithSignature("withdrawLQTY(uint256)", lqtyAmount); + bytes[] memory response = governance.multiDelegateCall(data); - (uint88 allocatedLQTY,) = abi.decode(response[3], (uint88, uint120)); + (,, uint256 allocatedLQTY,) = abi.decode(response[3], (uint256, uint256, uint256, uint256)); assertEq(allocatedLQTY, lqtyAmount); (IGovernance.VoteSnapshot memory votes, IGovernance.InitiativeVoteSnapshot memory votesForInitiative) = abi.decode(response[4], (IGovernance.VoteSnapshot, IGovernance.InitiativeVoteSnapshot)); @@ -1559,6 +1528,8 @@ contract GovernanceTest is Test { vm.stopPrank(); } + /* + * TODO function test_nonReentrant() public { MockInitiative mockInitiative = new MockInitiative(address(governance)); @@ -1566,129 +1537,88 @@ contract GovernanceTest is Test { address userProxy = governance.deployUserProxy(); - IGovernance.VoteSnapshot memory snapshot = IGovernance.VoteSnapshot(1e18, 1); - vm.store( - address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) - ); - (uint240 votes, uint16 forEpoch) = governance.votesSnapshot(); - assertEq(votes, 1e18); - assertEq(forEpoch, 1); + IGovernance.VoteSnapshot memory snapshot = IGovernance.VoteSnapshot(1e18, governance.epoch()); + governance.tester_setVotesSnapshot(snapshot); vm.startPrank(lusdHolder); lusd.transfer(user, 2e18); vm.stopPrank(); vm.startPrank(user); - lusd.approve(address(governance), 2e18); + vm.stopPrank(); lqty.approve(address(userProxy), 1e18); governance.depositLQTY(1e18); vm.warp(block.timestamp + governance.EPOCH_DURATION()); governance.registerInitiative(address(mockInitiative)); - uint16 atEpoch = governance.registeredInitiatives(address(mockInitiative)); + uint256 atEpoch = governance.registeredInitiatives(address(mockInitiative)); assertEq(atEpoch, governance.epoch()); vm.warp(block.timestamp + governance.EPOCH_DURATION()); address[] memory initiatives = new address[](1); initiatives[0] = address(mockInitiative); - int88[] memory deltaLQTYVotes = new int88[](1); - int88[] memory deltaLQTYVetos = new int88[](1); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + int256[] memory deltaLQTYVotes = new int256[](1); + int256[] memory deltaLQTYVetos = new int256[](1); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); // check that votingThreshold is is high enough such that MIN_CLAIM is met snapshot = IGovernance.VoteSnapshot(1, governance.epoch() - 1); - vm.store( - address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) - ); - (votes, forEpoch) = governance.votesSnapshot(); - assertEq(votes, 1); - assertEq(forEpoch, governance.epoch() - 1); + governance.tester_setVotesSnapshot(snapshot); IGovernance.InitiativeVoteSnapshot memory initiativeSnapshot = IGovernance.InitiativeVoteSnapshot(1, governance.epoch() - 1, governance.epoch() - 1, 0); - vm.store( - address(governance), - keccak256(abi.encode(address(mockInitiative), uint256(3))), - bytes32( - abi.encodePacked( - uint16(initiativeSnapshot.lastCountedEpoch), - uint16(initiativeSnapshot.forEpoch), - uint224(initiativeSnapshot.votes) - ) - ) - ); - (uint224 votes_, uint16 forEpoch_, uint16 lastCountedEpoch,) = - governance.votesForInitiativeSnapshot(address(mockInitiative)); - assertEq(votes_, 1); - assertEq(forEpoch_, governance.epoch() - 1); - assertEq(lastCountedEpoch, governance.epoch() - 1); + governance.tester_setVotesForInitiativeSnapshot(address(mockInitiative), initiativeSnapshot); governance.claimForInitiative(address(mockInitiative)); vm.warp(block.timestamp + governance.EPOCH_DURATION()); initiativeSnapshot = IGovernance.InitiativeVoteSnapshot(0, governance.epoch() - 1, 0, 0); - vm.store( - address(governance), - keccak256(abi.encode(address(mockInitiative), uint256(3))), - bytes32( - abi.encodePacked( - uint16(initiativeSnapshot.lastCountedEpoch), - uint16(initiativeSnapshot.forEpoch), - uint224(initiativeSnapshot.votes) - ) - ) - ); - (votes_, forEpoch_, lastCountedEpoch,) = governance.votesForInitiativeSnapshot(address(mockInitiative)); - assertEq(votes_, 0, "votes"); - assertEq(forEpoch_, governance.epoch() - 1, "forEpoch_"); - assertEq(lastCountedEpoch, 0, "lastCountedEpoch"); + governance.tester_setVotesForInitiativeSnapshot(address(mockInitiative), initiativeSnapshot); vm.warp(block.timestamp + governance.EPOCH_DURATION() * 4); governance.unregisterInitiative(address(mockInitiative)); } + */ // CS exploit PoC function test_allocateLQTY_overflow() public { vm.startPrank(user); + address[] memory initiativesToReset; address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int88[] memory deltaLQTYVotes = new int88[](2); - deltaLQTYVotes[0] = 0; - deltaLQTYVotes[1] = type(int88).max; - int88[] memory deltaLQTYVetos = new int88[](2); + int256[] memory deltaLQTYVotes = new int256[](2); + deltaLQTYVotes[0] = 1; + deltaLQTYVotes[1] = type(int256).max; + int256[] memory deltaLQTYVetos = new int256[](2); deltaLQTYVetos[0] = 0; deltaLQTYVetos[1] = 0; vm.warp(block.timestamp + governance.EPOCH_DURATION()); vm.expectRevert("Governance: insufficient-or-allocated-lqty"); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); deltaLQTYVotes[0] = 0; deltaLQTYVotes[1] = 0; - deltaLQTYVetos[0] = 0; - deltaLQTYVetos[1] = type(int88).max; + deltaLQTYVetos[0] = 1; + deltaLQTYVetos[1] = type(int256).max; vm.expectRevert("Governance: insufficient-or-allocated-lqty"); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); vm.stopPrank(); } function test_voting_power_increase() public { // =========== epoch 1 ================== - governance = new Governance( + governance = new GovernanceTester( address(lqty), address(lusd), address(stakingV1), @@ -1697,12 +1627,11 @@ contract GovernanceTest is Test { registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: MIN_CLAIM, minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), + epochStart: uint256(block.timestamp), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -1711,19 +1640,16 @@ contract GovernanceTest is Test { ); // 1. user stakes liquity - uint88 lqtyAmount = 1e18; + uint256 lqtyAmount = 1e18; _stakeLQTY(user, lqtyAmount); - (uint88 allocatedLQTY0, uint120 averageStakingTimestamp0) = governance.userStates(user); - uint240 currentUserPower0 = - governance.lqtyToVotes(allocatedLQTY0, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp0); + (,, uint256 allocatedLQTY0, uint256 allocatedOffset0) = governance.userStates(user); + uint256 currentUserPower0 = governance.lqtyToVotes(allocatedLQTY0, block.timestamp, allocatedOffset0); - (uint88 voteLQTY0,, uint120 averageStakingTimestampVoteLQTY0,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower0 = governance.lqtyToVotes( - voteLQTY0, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY0 - ); + (uint256 voteLQTY0, uint256 voteOffset0,,,) = governance.initiativeStates(baseInitiative1); + uint256 currentInitiativePower0 = governance.lqtyToVotes(voteLQTY0, block.timestamp, voteOffset0); - // (uint224 votes, uint16 forEpoch,,) = governance.votesForInitiativeSnapshot(baseInitiative1); + // (uint256 votes, uint256 forEpoch,,) = governance.votesForInitiativeSnapshot(baseInitiative1); // console2.log("votes0: ", votes); // =========== epoch 2 ================== @@ -1733,21 +1659,18 @@ contract GovernanceTest is Test { _allocateLQTY(user, lqtyAmount); // check user voting power for the current epoch - (uint88 allocatedLQTY1, uint120 averageStakingTimestamp1) = governance.userStates(user); - uint240 currentUserPower1 = - governance.lqtyToVotes(allocatedLQTY1, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp1); - // user's allocated lqty should immediately increase their voting power + (,, uint256 allocatedLQTY1, uint256 allocatedOffset1) = governance.userStates(user); + uint256 currentUserPower1 = governance.lqtyToVotes(allocatedLQTY1, block.timestamp, allocatedOffset1); + // user's allocated lqty should have non-zero voting power assertGt(currentUserPower1, 0, "current user voting power is 0"); // check initiative voting power for the current epoch - (uint88 voteLQTY1,, uint120 averageStakingTimestampVoteLQTY1,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower1 = governance.lqtyToVotes( - voteLQTY1, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY1 - ); + (uint256 voteLQTY1, uint256 votOffset1,,,) = governance.initiativeStates(baseInitiative1); + uint256 currentInitiativePower1 = governance.lqtyToVotes(voteLQTY1, block.timestamp, votOffset1); assertGt(currentInitiativePower1, 0, "current initiative voting power is 0"); assertEq(currentUserPower1, currentInitiativePower1, "initiative and user voting power should be equal"); - // (uint224 votes, uint16 forEpoch,,) = governance.votesForInitiativeSnapshot(baseInitiative1); + // (uint256 votes, uint256 forEpoch,,) = governance.votesForInitiativeSnapshot(baseInitiative1); // =========== epoch 2 (end) ================== // 3. warp to end of epoch 2 to see increase in voting power @@ -1756,22 +1679,19 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // user voting power should increase over a given chunk of time - (uint88 allocatedLQTY2, uint120 averageStakingTimestamp2) = governance.userStates(user); - uint240 currentUserPower2 = - governance.lqtyToVotes(allocatedLQTY2, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp2); + (,, uint256 allocatedLQTY2, uint256 allocatedOffset2) = governance.userStates(user); + uint256 currentUserPower2 = governance.lqtyToVotes(allocatedLQTY2, block.timestamp, allocatedOffset2); assertGt(currentUserPower2, currentUserPower1); // initiative voting power should increase over a given chunk of time - (uint88 voteLQTY2,, uint120 averageStakingTimestampVoteLQTY2,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower2 = governance.lqtyToVotes( - voteLQTY2, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY2 - ); + (uint256 voteLQTY2, uint256 voteOffset2,,,) = governance.initiativeStates(baseInitiative1); + uint256 currentInitiativePower2 = governance.lqtyToVotes(voteLQTY2, block.timestamp, voteOffset2); assertEq( currentUserPower2, currentInitiativePower2, "user power and initiative power should increase by same amount" ); // votes should only get counted in the next epoch after they were allocated - (uint224 votes, uint16 forEpoch,,) = governance.votesForInitiativeSnapshot(baseInitiative1); + (uint256 votes, uint256 forEpoch,,) = governance.votesForInitiativeSnapshot(baseInitiative1); assertEq(votes, 0, "votes get counted in epoch that they were allocated"); // =========== epoch 3 ================== @@ -1780,15 +1700,12 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // user voting power should increase - (uint88 allocatedLQTY3, uint120 averageStakingTimestamp3) = governance.userStates(user); - uint240 currentUserPower3 = - governance.lqtyToVotes(allocatedLQTY3, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp3); + (,, uint256 allocatedLQTY3, uint256 allocatedOffset) = governance.userStates(user); + uint256 currentUserPower3 = governance.lqtyToVotes(allocatedLQTY3, block.timestamp, allocatedOffset); // votes should match the voting power for the initiative and subsequently the user since they're the only one allocated - (uint88 voteLQTY3,, uint120 averageStakingTimestampVoteLQTY3,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower3 = governance.lqtyToVotes( - voteLQTY3, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY3 - ); + (uint256 voteLQTY3, uint256 voteOffset3,,,) = governance.initiativeStates(baseInitiative1); + uint256 currentInitiativePower3 = governance.lqtyToVotes(voteLQTY3, block.timestamp, voteOffset3); // votes should be counted in this epoch (votes, forEpoch,,) = governance.votesForInitiativeSnapshot(baseInitiative1); @@ -1799,17 +1716,14 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + EPOCH_DURATION - 1); governance.snapshotVotesForInitiative(baseInitiative1); - (uint88 allocatedLQTY4, uint120 averageStakingTimestamp4) = governance.userStates(user); - uint240 currentUserPower4 = - governance.lqtyToVotes(allocatedLQTY4, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp4); + (,, uint256 allocatedLQTY4, uint256 allocatedOffset4) = governance.userStates(user); + uint256 currentUserPower4 = governance.lqtyToVotes(allocatedLQTY4, block.timestamp, allocatedOffset4); - (uint88 voteLQTY4,, uint120 averageStakingTimestampVoteLQTY4,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower4 = governance.lqtyToVotes( - voteLQTY4, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY4 - ); + (uint256 voteLQTY4, uint256 voteOffset4,,,) = governance.initiativeStates(baseInitiative1); + uint256 currentInitiativePower4 = governance.lqtyToVotes(voteLQTY4, block.timestamp, voteOffset4); // checking if snapshotting at the end of an epoch increases the voting power - (uint224 votes2,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); + (uint256 votes2,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); assertEq(votes, votes2, "votes for an initiative snapshot increase in same epoch"); // =========== epoch 3 (end) ================== @@ -1818,7 +1732,7 @@ contract GovernanceTest is Test { // increase in user voting power and initiative voting power should be equivalent function test_voting_power_in_same_epoch_as_allocation() public { // =========== epoch 1 ================== - governance = new Governance( + governance = new GovernanceTester( address(lqty), address(lusd), address(stakingV1), @@ -1827,12 +1741,11 @@ contract GovernanceTest is Test { registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: MIN_CLAIM, minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), + epochStart: uint256(block.timestamp), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -1841,7 +1754,7 @@ contract GovernanceTest is Test { ); // 1. user stakes liquity - uint88 lqtyAmount = 1e18; + uint256 lqtyAmount = 1e18; _stakeLQTY(user, lqtyAmount); // =========== epoch 2 ================== @@ -1850,16 +1763,13 @@ contract GovernanceTest is Test { assertEq(2, governance.epoch(), "not in epoch 2"); // check user voting power before allocation at epoch start - (uint88 allocatedLQTY0, uint120 averageStakingTimestamp0) = governance.userStates(user); - uint240 currentUserPower0 = - governance.lqtyToVotes(allocatedLQTY0, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp0); + (,, uint256 allocatedLQTY0, uint256 allocatedOffset0) = governance.userStates(user); + uint256 currentUserPower0 = governance.lqtyToVotes(allocatedLQTY0, block.timestamp, allocatedOffset0); assertEq(currentUserPower0, 0, "user has voting power > 0"); // check initiative voting power before allocation at epoch start - (uint88 voteLQTY0,, uint120 averageStakingTimestampVoteLQTY0,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower0 = governance.lqtyToVotes( - voteLQTY0, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY0 - ); + (uint256 voteLQTY0, uint256 voteOffset0,,,) = governance.initiativeStates(baseInitiative1); + uint256 currentInitiativePower0 = governance.lqtyToVotes(voteLQTY0, block.timestamp, voteOffset0); assertEq(currentInitiativePower0, 0, "current initiative voting power is > 0"); _allocateLQTY(user, lqtyAmount); @@ -1868,16 +1778,13 @@ contract GovernanceTest is Test { assertEq(2, governance.epoch(), "not in epoch 2"); // check user voting power after allocation at epoch end - (uint88 allocatedLQTY1, uint120 averageStakingTimestamp1) = governance.userStates(user); - uint240 currentUserPower1 = - governance.lqtyToVotes(allocatedLQTY1, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp1); + (,, uint256 allocatedLQTY1, uint256 allocatedOffset1) = governance.userStates(user); + uint256 currentUserPower1 = governance.lqtyToVotes(allocatedLQTY1, block.timestamp, allocatedOffset1); assertGt(currentUserPower1, 0, "user has no voting power after allocation"); // check initiative voting power after allocation at epoch end - (uint88 voteLQTY1,, uint120 averageStakingTimestampVoteLQTY1,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower1 = governance.lqtyToVotes( - voteLQTY1, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY1 - ); + (uint256 voteLQTY1, uint256 voteOffset1,,,) = governance.initiativeStates(baseInitiative1); + uint256 currentInitiativePower1 = governance.lqtyToVotes(voteLQTY1, block.timestamp, voteOffset1); assertGt(currentInitiativePower1, 0, "initiative has no voting power after allocation"); // check that user and initiative voting power is equivalent at epoch end @@ -1887,16 +1794,13 @@ contract GovernanceTest is Test { assertEq(42, governance.epoch(), "not in epoch 42"); // get user voting power after multiple epochs - (uint88 allocatedLQTY2, uint120 averageStakingTimestamp2) = governance.userStates(user); - uint240 currentUserPower2 = - governance.lqtyToVotes(allocatedLQTY2, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp2); + (,, uint256 allocatedLQTY2, uint256 allocatedOffset2) = governance.userStates(user); + uint256 currentUserPower2 = governance.lqtyToVotes(allocatedLQTY2, block.timestamp, allocatedOffset2); assertGt(currentUserPower2, currentUserPower1, "user voting power doesn't increase"); // get initiative voting power after multiple epochs - (uint88 voteLQTY2,, uint120 averageStakingTimestampVoteLQTY2,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower2 = governance.lqtyToVotes( - voteLQTY2, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY2 - ); + (uint256 voteLQTY2, uint256 voteOffset2,,,) = governance.initiativeStates(baseInitiative1); + uint256 currentInitiativePower2 = governance.lqtyToVotes(voteLQTY2, block.timestamp, voteOffset2); assertGt(currentInitiativePower2, currentInitiativePower1, "initiative voting power doesn't increase"); // check that initiative and user voting always track each other @@ -1908,7 +1812,7 @@ contract GovernanceTest is Test { // |====== epoch 1=====|==== epoch 2 =====|==== epoch 3 ====| function test_voting_power_increase_in_an_epoch() public { // =========== epoch 1 ================== - governance = new Governance( + governance = new GovernanceTester( address(lqty), address(lusd), address(stakingV1), @@ -1917,12 +1821,11 @@ contract GovernanceTest is Test { registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: MIN_CLAIM, minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), + epochStart: uint256(block.timestamp), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -1931,7 +1834,7 @@ contract GovernanceTest is Test { ); // 1. user stakes lqty - uint88 lqtyAmount = 1e18; + uint256 lqtyAmount = 1e18; _stakeLQTY(user, lqtyAmount); // =========== epoch 2 (start) ================== @@ -1939,10 +1842,8 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + EPOCH_DURATION); // warp to second epoch // get initiative voting power at start of epoch - (uint88 voteLQTY0,, uint120 averageStakingTimestampVoteLQTY0,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower0 = governance.lqtyToVotes( - voteLQTY0, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY0 - ); + (uint256 voteLQTY0, uint256 voteOffset0,,,) = governance.initiativeStates(baseInitiative1); + uint256 currentInitiativePower0 = governance.lqtyToVotes(voteLQTY0, block.timestamp, voteOffset0); assertEq(currentInitiativePower0, 0, "initiative voting power is > 0"); _allocateLQTY(user, lqtyAmount); @@ -1953,23 +1854,21 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // get initiative voting power at time of snapshot - (uint88 voteLQTY1,, uint120 averageStakingTimestampVoteLQTY1,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower1 = governance.lqtyToVotes( - voteLQTY1, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY1 - ); + (uint256 voteLQTY1, uint256 voteOffset1,,,) = governance.initiativeStates(baseInitiative1); + uint256 currentInitiativePower1 = governance.lqtyToVotes(voteLQTY1, block.timestamp, voteOffset1); assertGt(currentInitiativePower1, 0, "initiative voting power is 0"); - uint240 deltaInitiativeVotingPower = currentInitiativePower1 - currentInitiativePower0; + uint256 deltaInitiativeVotingPower = currentInitiativePower1 - currentInitiativePower0; // 4. votes should be counted in this epoch - (uint224 votes,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); + (uint256 votes,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); assertEq(votes, deltaInitiativeVotingPower, "voting power should increase by amount user allocated"); } // checking that voting power calculated from lqtyAllocatedByUserToInitiative is equivalent to the voting power using values returned by userStates function test_voting_power_lqtyAllocatedByUserToInitiative() public { // =========== epoch 1 ================== - governance = new Governance( + governance = new GovernanceTester( address(lqty), address(lusd), address(stakingV1), @@ -1978,12 +1877,11 @@ contract GovernanceTest is Test { registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: MIN_CLAIM, minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), + epochStart: uint256(block.timestamp), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -1992,7 +1890,7 @@ contract GovernanceTest is Test { ); // 1. user stakes lqty - uint88 lqtyAmount = 1e18; + uint256 lqtyAmount = 1e18; _stakeLQTY(user, lqtyAmount); // =========== epoch 2 (start) ================== @@ -2002,12 +1900,10 @@ contract GovernanceTest is Test { _allocateLQTY(user, lqtyAmount); // get user voting power at start of epoch from lqtyAllocatedByUserToInitiative - (uint88 voteLQTY0,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); - (uint88 allocatedLQTY, uint120 averageStakingTimestamp) = governance.userStates(user); - uint240 currentInitiativePowerFrom1 = - governance.lqtyToVotes(voteLQTY0, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp); - uint240 currentInitiativePowerFrom2 = - governance.lqtyToVotes(allocatedLQTY, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp); + (uint256 voteLQTY, uint256 voteOffset,,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + (,, uint256 allocatedLQTY, uint256 allocatedOffset) = governance.userStates(user); + uint256 currentInitiativePowerFrom1 = governance.lqtyToVotes(voteLQTY, block.timestamp, voteOffset); + uint256 currentInitiativePowerFrom2 = governance.lqtyToVotes(allocatedLQTY, block.timestamp, allocatedOffset); assertEq( currentInitiativePowerFrom1, @@ -2016,10 +1912,10 @@ contract GovernanceTest is Test { ); } - // checking if allocating to a different initiative in a different epoch modifies the avgStakingTimestamp - function test_average_timestamp() public { + // checking if allocating to a different initiative in a different epoch modifies the allocated offset + function test_allocated_offset() public { // =========== epoch 1 ================== - governance = new Governance( + governance = new GovernanceTester( address(lqty), address(lusd), address(stakingV1), @@ -2028,12 +1924,11 @@ contract GovernanceTest is Test { registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: MIN_CLAIM, minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), + epochStart: uint256(block.timestamp), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -2042,7 +1937,7 @@ contract GovernanceTest is Test { ); // 1. user stakes lqty - uint88 lqtyAmount = 2e18; + uint256 lqtyAmount = 2e18; _stakeLQTY(user, lqtyAmount); // =========== epoch 2 (start) ================== @@ -2052,25 +1947,28 @@ contract GovernanceTest is Test { // user allocates to baseInitiative1 _allocateLQTY(user, 1e18); - // get user voting power at start of epoch 2 from lqtyAllocatedByUserToInitiative - (, uint120 averageStakingTimestamp1) = governance.userStates(user); + // get user voting power at start of epoch 2 + (,,, uint256 allocatedOffset1) = governance.userStates(user); // =========== epoch 3 (start) ================== // 3. user allocates to baseInitiative2 in epoch 3 vm.warp(block.timestamp + EPOCH_DURATION); // warp to third epoch - _allocateLQTYToInitiative(user, baseInitiative2, 1e18); + address[] memory initiativesToReset = new address[](1); + initiativesToReset[0] = address(baseInitiative1); + // this should reset all alloc to initiative1, and divert it to initative 2 + _allocateLQTYToInitiative(user, baseInitiative2, 1e18, initiativesToReset); - // get user voting power at start of epoch 3 from lqtyAllocatedByUserToInitiative - (, uint120 averageStakingTimestamp2) = governance.userStates(user); - assertEq(averageStakingTimestamp1, averageStakingTimestamp2); + // check offsets are equal + (,,, uint256 allocatedOffset2) = governance.userStates(user); + assertEq(allocatedOffset1, allocatedOffset2); } // checking if allocating to same initiative modifies the average timestamp // forge test --match-test test_average_timestamp_same_initiative -vv - function test_average_timestamp_same_initiative() public { + function test_offset_same_initiative() public { // =========== epoch 1 ================== - governance = new Governance( + governance = new GovernanceTester( address(lqty), address(lusd), address(stakingV1), @@ -2079,12 +1977,11 @@ contract GovernanceTest is Test { registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: MIN_CLAIM, minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), + epochStart: uint256(block.timestamp), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -2093,7 +1990,7 @@ contract GovernanceTest is Test { ); // 1. user stakes lqty - uint88 lqtyAmount = 2e18; + uint256 lqtyAmount = 2e18; _stakeLQTY(user, lqtyAmount); // =========== epoch 2 (start) ================== @@ -2103,9 +2000,9 @@ contract GovernanceTest is Test { // user allocates to baseInitiative1 _allocateLQTY(user, 1e18); - // get user voting power at start of epoch 2 from lqtyAllocatedByUserToInitiative - (, uint120 averageStakingTimestamp1) = governance.userStates(user); - console2.log("averageStakingTimestamp1: ", averageStakingTimestamp1); + // get user voting power at start of epoch 2 + (,,, uint256 allocatedOffset1) = governance.userStates(user); + console2.log("allocatedOffset1: ", allocatedOffset1); // =========== epoch 3 (start) ================== // 3. user allocates to baseInitiative1 in epoch 3 @@ -2113,15 +2010,15 @@ contract GovernanceTest is Test { _allocateLQTY(user, 1e18); - // get user voting power at start of epoch 3 from lqtyAllocatedByUserToInitiative - (, uint120 averageStakingTimestamp2) = governance.userStates(user); - assertEq(averageStakingTimestamp1, averageStakingTimestamp2, "average timestamps differ"); + // get user voting power at start of epoch 3 + (,,, uint256 allocatedOffset2) = governance.userStates(user); + assertEq(allocatedOffset2, allocatedOffset1, "offsets differ"); } // checking if allocating to same initiative modifies the average timestamp - function test_average_timestamp_allocate_same_initiative_fuzz(uint256 allocateAmount) public { + function test_offset_allocate_same_initiative_fuzz(uint256 allocateAmount) public { // =========== epoch 1 ================== - governance = new Governance( + governance = new GovernanceTester( address(lqty), address(lusd), address(stakingV1), @@ -2130,12 +2027,11 @@ contract GovernanceTest is Test { registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: MIN_CLAIM, minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), + epochStart: uint256(block.timestamp), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -2144,7 +2040,7 @@ contract GovernanceTest is Test { ); // 1. user stakes lqty - uint88 lqtyAmount = uint88(allocateAmount % lqty.balanceOf(user)); + uint256 lqtyAmount = uint256(allocateAmount % lqty.balanceOf(user)); vm.assume(lqtyAmount > 0); _stakeLQTY(user, lqtyAmount); @@ -2153,11 +2049,11 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + EPOCH_DURATION); // warp to second epoch // clamp lqtyAmount by half of what user staked - uint88 lqtyAmount2 = uint88(bound(allocateAmount, 1, lqtyAmount)); + uint256 lqtyAmount2 = uint256(bound(allocateAmount, 1, lqtyAmount)); _allocateLQTY(user, lqtyAmount2); - // get user voting power at start of epoch 2 from lqtyAllocatedByUserToInitiative - (, uint120 averageStakingTimestamp1) = governance.userStates(user); + // get user voting power at start of epoch 2 + (, uint256 unallocatedOffset1,, uint256 allocatedOffset1) = governance.userStates(user); // =========== epoch 3 (start) ================== // 3. user allocates to baseInitiative1 in epoch 3 @@ -2166,19 +2062,17 @@ contract GovernanceTest is Test { // clamp lqtyAmount by amount user staked vm.assume(lqtyAmount > lqtyAmount2); vm.assume(lqtyAmount - lqtyAmount2 > 1); - uint88 lqtyAmount3 = uint88(bound(allocateAmount, 1, lqtyAmount - lqtyAmount2)); + uint256 lqtyAmount3 = uint256(bound(allocateAmount, 1, lqtyAmount - lqtyAmount2)); _allocateLQTY(user, lqtyAmount3); // get user voting power at start of epoch 3 from lqtyAllocatedByUserToInitiative - (, uint120 averageStakingTimestamp2) = governance.userStates(user); - assertEq( - averageStakingTimestamp1, averageStakingTimestamp2, "averageStakingTimestamp1 != averageStakingTimestamp2" - ); + (, uint256 unallocatedOffset2,, uint256 allocatedOffset2) = governance.userStates(user); + assertEq(unallocatedOffset2 + allocatedOffset2, unallocatedOffset1 + allocatedOffset1, "offset2 != offset1"); } function test_voting_snapshot_start_vs_end_epoch() public { // =========== epoch 1 ================== - governance = new Governance( + governance = new GovernanceTester( address(lqty), address(lusd), address(stakingV1), @@ -2187,12 +2081,11 @@ contract GovernanceTest is Test { registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: MIN_CLAIM, minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), + epochStart: uint256(block.timestamp), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -2201,7 +2094,7 @@ contract GovernanceTest is Test { ); // 1. user stakes lqty - uint88 lqtyAmount = 1e18; + uint256 lqtyAmount = 1e18; _stakeLQTY(user, lqtyAmount); // =========== epoch 2 (start) ================== @@ -2209,15 +2102,13 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + EPOCH_DURATION); // warp to second epoch // get initiative voting power at start of epoch - (uint88 voteLQTY0,, uint120 averageStakingTimestampVoteLQTY0,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower0 = governance.lqtyToVotes( - voteLQTY0, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY0 - ); + (uint256 voteLQTY0, uint256 voteOffset0,,,) = governance.initiativeStates(baseInitiative1); + uint256 currentInitiativePower0 = governance.lqtyToVotes(voteLQTY0, block.timestamp, voteOffset0); assertEq(currentInitiativePower0, 0, "initiative voting power is > 0"); _allocateLQTY(user, lqtyAmount); - uint256 stateBeforeSnapshottingVotes = vm.snapshot(); + uint256 stateBeforeSnapshottingVotes = vm.snapshotState(); // =========== epoch 3 (start) ================== // 3a. warp to start of third epoch @@ -2226,20 +2117,18 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // get initiative voting power at start of epoch - (uint88 voteLQTY1,, uint120 averageStakingTimestampVoteLQTY1,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower1 = governance.lqtyToVotes( - voteLQTY1, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY1 - ); + (uint256 voteLQTY1, uint256 voteOffset1,,,) = governance.initiativeStates(baseInitiative1); + uint256 currentInitiativePower1 = governance.lqtyToVotes(voteLQTY1, block.timestamp, voteOffset1); // 4a. votes from snapshotting at begging of epoch - (uint224 votes,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); + (uint256 votes,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); console2.log("currentInitiativePower1: ", currentInitiativePower1); console2.log("votes: ", votes); // =========== epoch 3 (end) ================== // revert EVM to state before snapshotting - vm.revertTo(stateBeforeSnapshottingVotes); + vm.revertToState(stateBeforeSnapshottingVotes); // 3b. warp to end of third epoch vm.warp(block.timestamp + (EPOCH_DURATION * 2) - 1); @@ -2247,14 +2136,14 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // 4b. votes from snapshotting at end of epoch - (uint224 votes2,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); + (uint256 votes2,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); assertEq(votes, votes2, "votes from snapshot are dependent on time at snapshot"); } // checks that there's no difference to resulting voting power from allocating at start or end of epoch function test_voting_power_no_difference_in_allocating_start_or_end_of_epoch() public { // =========== epoch 1 ================== - governance = new Governance( + governance = new GovernanceTester( address(lqty), address(lusd), address(stakingV1), @@ -2263,12 +2152,11 @@ contract GovernanceTest is Test { registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: MIN_CLAIM, minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), + epochStart: uint256(block.timestamp), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -2277,10 +2165,10 @@ contract GovernanceTest is Test { ); // 1. user stakes liquity - uint88 lqtyAmount = 1e18; + uint256 lqtyAmount = 1e18; _stakeLQTY(user, lqtyAmount); - uint256 stateBeforeAllocation = vm.snapshot(); + uint256 stateBeforeAllocation = vm.snapshotState(); // =========== epoch 2 (start) ================== // 2a. user allocates at start of epoch 2 @@ -2294,7 +2182,7 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // get voting power from allocation in previous epoch - (uint224 votesFromAllocatingAtEpochStart,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); + (uint256 votesFromAllocatingAtEpochStart,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); // ======================================== // ===== revert to initial state ========== @@ -2302,7 +2190,7 @@ contract GovernanceTest is Test { // =============== epoch 1 =============== // revert EVM to state before allocation - vm.revertTo(stateBeforeAllocation); + vm.revertToState(stateBeforeAllocation); // =============== epoch 2 (end - just before cutoff) =============== // 2b. user allocates at end of epoch 2 @@ -2316,7 +2204,7 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // get voting power from allocation in previous epoch - (uint224 votesFromAllocatingAtEpochEnd,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); + (uint256 votesFromAllocatingAtEpochEnd,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); assertEq( votesFromAllocatingAtEpochStart, votesFromAllocatingAtEpochEnd, @@ -2327,7 +2215,7 @@ contract GovernanceTest is Test { // deallocating is correctly reflected in voting power for next epoch function test_voting_power_decreases_next_epoch() public { // =========== epoch 1 ================== - governance = new Governance( + governance = new GovernanceTester( address(lqty), address(lusd), address(stakingV1), @@ -2336,12 +2224,11 @@ contract GovernanceTest is Test { registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: MIN_CLAIM, minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), + epochStart: uint256(block.timestamp), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -2350,7 +2237,7 @@ contract GovernanceTest is Test { ); // 1. user stakes lqty - uint88 lqtyAmount = 1e18; + uint256 lqtyAmount = 1e18; _stakeLQTY(user, lqtyAmount); // =========== epoch 2 (start) ================== @@ -2366,7 +2253,7 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // 4. votes should be counted in this epoch - (uint224 votes,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); + (uint256 votes,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); assertGt(votes, 0, "voting power should increase"); _deAllocateLQTY(user, 0); @@ -2374,7 +2261,7 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // 5. votes should still be counted in this epoch - (uint224 votes2,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); + (uint256 votes2,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); assertGt(votes2, 0, "voting power should not decrease this epoch"); // =========== epoch 4 ================== @@ -2383,14 +2270,13 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // 6. votes should be decreased in this epoch - (uint224 votes3,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); + (uint256 votes3,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); assertEq(votes3, 0, "voting power should be decreased in this epoch"); } - // checking if deallocating changes the averageStakingTimestamp - function test_deallocating_decreases_avg_timestamp() public { + function test_deallocating_decreases_offset() public { // =========== epoch 1 ================== - governance = new Governance( + governance = new GovernanceTester( address(lqty), address(lusd), address(stakingV1), @@ -2399,12 +2285,11 @@ contract GovernanceTest is Test { registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: MIN_CLAIM, minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), + epochStart: uint256(block.timestamp), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -2413,7 +2298,7 @@ contract GovernanceTest is Test { ); // 1. user stakes lqty - uint88 lqtyAmount = 1e18; + uint256 lqtyAmount = 1e18; _stakeLQTY(user, lqtyAmount); // =========== epoch 2 (start) ================== @@ -2427,18 +2312,19 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + EPOCH_DURATION); governance.snapshotVotesForInitiative(baseInitiative1); - (, uint120 averageStakingTimestampBefore) = governance.userStates(user); + (,,, uint256 allocatedOffset) = governance.userStates(user); + assertGt(allocatedOffset, 0); _deAllocateLQTY(user, 0); - (, uint120 averageStakingTimestampAfter) = governance.userStates(user); - assertEq(averageStakingTimestampBefore, averageStakingTimestampAfter); + (,,, allocatedOffset) = governance.userStates(user); + assertEq(allocatedOffset, 0); } // vetoing shouldn't affect voting power of the initiative function test_vote_and_veto() public { // =========== epoch 1 ================== - governance = new Governance( + governance = new GovernanceTester( address(lqty), address(lusd), address(stakingV1), @@ -2447,12 +2333,11 @@ contract GovernanceTest is Test { registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: MIN_CLAIM, minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), + epochStart: uint256(block.timestamp), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -2461,7 +2346,7 @@ contract GovernanceTest is Test { ); // 1. user stakes lqty - uint88 lqtyAmount = 1e18; + uint256 lqtyAmount = 1e18; _stakeLQTY(user, lqtyAmount); // 1. user2 stakes lqty @@ -2483,16 +2368,260 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // voting power for initiative should be the same as votes from snapshot - (uint88 voteLQTY,, uint120 averageStakingTimestampVoteLQTY,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower = - governance.lqtyToVotes(voteLQTY, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY); + (uint256 voteLQTY, uint256 voteOffset,,,) = governance.initiativeStates(baseInitiative1); + uint256 currentInitiativePower = governance.lqtyToVotes(voteLQTY, block.timestamp, voteOffset); // 4. votes should not affect accounting for votes - (uint224 votes,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); + (uint256 votes,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); assertEq(votes, currentInitiativePower, "voting power of initiative should not be affected by vetos"); } - function _stakeLQTY(address staker, uint88 amount) internal { + struct StakingOp { + uint256 lqtyAmount; + uint256 waitTime; + } + + function test_NoDustInUnallocatedOffsetAfterAllocatingAllLQTY(uint256[3] memory _votes, StakingOp[4] memory _stakes) + external + { + address[] memory initiatives = new address[](_votes.length + 1); + + // Ensure initiatives can be registered + vm.warp(block.timestamp + 2 * EPOCH_DURATION); + + // Register as many initiatives as needed + vm.startPrank(lusdHolder); + for (uint256 i = 0; i < initiatives.length; ++i) { + initiatives[i] = makeAddr(string.concat("initiative", i.toString())); + lusd.approve(address(governance), REGISTRATION_FEE); + governance.registerInitiative(initiatives[i]); + } + vm.stopPrank(); + + // Ensure the new initiatives are votable + vm.warp(block.timestamp + EPOCH_DURATION); + + vm.startPrank(user); + { + // Don't wait too long or initiatives might time out + uint256 maxWaitTime = EPOCH_DURATION * UNREGISTRATION_AFTER_EPOCHS / _stakes.length; + address userProxy = governance.deriveUserProxyAddress(user); + uint256 lqtyBalance = lqty.balanceOf(user); + uint256 unallocatedLQTY_ = 0; + + for (uint256 i = 0; i < _stakes.length; ++i) { + _stakes[i].lqtyAmount = _bound(_stakes[i].lqtyAmount, 1, lqtyBalance - (_stakes.length - 1 - i)); + lqtyBalance -= _stakes[i].lqtyAmount; + unallocatedLQTY_ += _stakes[i].lqtyAmount; + + lqty.approve(userProxy, _stakes[i].lqtyAmount); + governance.depositLQTY(_stakes[i].lqtyAmount); + + _stakes[i].waitTime = _bound(_stakes[i].waitTime, 1, maxWaitTime); + vm.warp(block.timestamp + _stakes[i].waitTime); + } + + address[] memory initiativesToReset; // left empty + int256[] memory votes = new int256[](initiatives.length); + int256[] memory vetos = new int256[](initiatives.length); // left zero + + for (uint256 i = 0; i < initiatives.length - 1; ++i) { + uint256 vote = _bound(_votes[i], 1, unallocatedLQTY_ - (initiatives.length - 1 - i)); + unallocatedLQTY_ -= vote; + votes[i] = int256(vote); + } + + // Cast all remaining LQTY on the last initiative + votes[initiatives.length - 1] = int256(unallocatedLQTY_); + + vm.assume(governance.secondsWithinEpoch() < EPOCH_VOTING_CUTOFF); + governance.allocateLQTY(initiativesToReset, initiatives, votes, vetos); + } + vm.stopPrank(); + + (uint256 unallocatedLQTY, uint256 unallocatedOffset,,) = governance.userStates(user); + assertEqDecimal(unallocatedLQTY, 0, 18, "user should have no unallocated LQTY"); + assertEqDecimal(unallocatedOffset, 0, 18, "user should have no unallocated offset"); + } + + function test_WhenAllocatingTinyAmounts_VotingPowerDoesNotTurnNegativeDueToRoundingError( + uint256 initialVotingPower, + uint256 numInitiatives + ) external { + initialVotingPower = bound(initialVotingPower, 1, 20); + numInitiatives = bound(numInitiatives, 1, 20); + + address[] memory initiatives = new address[](numInitiatives); + + // Ensure initiatives can be registered + vm.warp(block.timestamp + 2 * EPOCH_DURATION); + + // Register as many initiatives as needed + vm.startPrank(lusdHolder); + for (uint256 i = 0; i < initiatives.length; ++i) { + initiatives[i] = makeAddr(string.concat("initiative", i.toString())); + lusd.approve(address(governance), REGISTRATION_FEE); + governance.registerInitiative(initiatives[i]); + } + vm.stopPrank(); + + // Ensure the new initiatives are votable + vm.warp(block.timestamp + EPOCH_DURATION); + + vm.startPrank(user); + { + address userProxy = governance.deriveUserProxyAddress(user); + lqty.approve(userProxy, type(uint256).max); + governance.depositLQTY(1); + + // By waiting `initialVotingPower` seconds while having 1 wei LQTY staked, + // we accrue exactly `initialVotingPower` + vm.warp(block.timestamp + initialVotingPower); + + governance.depositLQTY(1 ether); + + address[] memory initiativesToReset; // left empty + int256[] memory votes = new int256[](initiatives.length); + int256[] memory vetos = new int256[](initiatives.length); // left zero + + for (uint256 i = 0; i < initiatives.length; ++i) { + votes[i] = 1; + } + + governance.allocateLQTY(initiativesToReset, initiatives, votes, vetos); + } + vm.stopPrank(); + + (uint256 unallocatedLQTY, uint256 unallocatedOffset,,) = governance.userStates(user); + int256 votingPower = int256(unallocatedLQTY * block.timestamp) - int256(unallocatedOffset); + + // Even though we are allocating tiny amounts, each allocation + // reduces voting power by 1 (due to rounding), but not below zero + assertEq( + votingPower, + int256(initialVotingPower > numInitiatives ? initialVotingPower - numInitiatives : 0), + "voting power should stay non-negative" + ); + } + + // We find that a user's unallocated voting power can't be turned negative through manipulation, which is + // demonstrated in the next test. + // + // Whenever a user withdraws LQTY, they can lose more voting power than they should, due to rounding error in the + // calculation of their remaining offset: + // + // unallocatedOffset -= FLOOR(lqtyDecrease * unallocatedOffset / unallocatedLQTY) + // unallocatedLQTY -= lqtyDecrease + // + // For reference, unallocated voting power at time `t` is calculated as: + // + // unallocatedLQTY * t - unallocatedOffset + // + // The decrement of `unallocatedOffset` is rounded down, consequently `unallocatedOffset` is rounded up, in turn the + // voting power is rounded down. So when time a user has some relatively small positive unallocated voting power and + // a significant amount of unallocated LQTY, and withdraws a tiny amount of LQTY (corresponding to less than a unit + // of voting power), they lose a full unit of voting power. + // + // One might think that this can be done repeatedly in an attempt to manipulate unallocated voting power into + // negative range, thus being able to allocate negative voting power to an initiative (if done very close to the + // end of the present epoch), which would be bad as it would result in insolvency in initiatives that distribute + // rewards in proportion to voting power allocated by voters (such as `BribeInitiative`). + // + // However, we find that this manipulation stops being effective once unallocated voting power reaches zero. Having + // zero unallocated voting power means: + // + // unallocatedLQTY * t - unallocatedOffset = 0 + // unallocatedLQTY * t = unallocatedOffset + // + // Thus when unallocated voting power is zero, `unallocatedOffset` is a multiple of `unallocatedLQTY`, so there can + // be no more rounding error when re-calculating `unallocatedOffset` on withdrawals. + + function test_WhenWithdrawingTinyAmounts_VotingPowerDoesNotTurnNegativeDueToRoundingError( + uint256 initialVotingPower, + uint256 numWithdrawals + ) external { + initialVotingPower = bound(initialVotingPower, 1, 20); + numWithdrawals = bound(numWithdrawals, 1, 20); + + vm.startPrank(user); + { + address userProxy = governance.deriveUserProxyAddress(user); + lqty.approve(userProxy, type(uint256).max); + governance.depositLQTY(1); + + // By waiting `initialVotingPower` seconds while having 1 wei LQTY staked, + // we accrue exactly `initialVotingPower` + vm.warp(block.timestamp + initialVotingPower); + + governance.depositLQTY(1 ether); + + for (uint256 i = 0; i < numWithdrawals; ++i) { + governance.withdrawLQTY(1); + } + } + vm.stopPrank(); + + (uint256 unallocatedLQTY, uint256 unallocatedOffset,,) = governance.userStates(user); + int256 votingPower = int256(unallocatedLQTY * block.timestamp) - int256(unallocatedOffset); + + // Even though we are withdrawing tiny amounts, each withdrawal + // reduces voting power by 1 (due to rounding), but not below zero + assertEq( + votingPower, + int256(initialVotingPower > numWithdrawals ? initialVotingPower - numWithdrawals : 0), + "voting power should stay non-negative" + ); + } + + function test_Vote_Stake_Unvote() external { + address[] memory noInitiatives; + address[] memory initiatives = new address[](1); + int256[] memory noVotes; + int256[] memory votes = new int256[](1); + int256[] memory vetos = new int256[](1); + initiatives[0] = baseInitiative1; + + // Ensure the initial initiatives are active + vm.warp(block.timestamp + EPOCH_DURATION); + + // Have another user vote some on the initiative + vm.startPrank(user2); + { + address userProxy = governance.deriveUserProxyAddress(user2); + lqty.approve(userProxy, type(uint256).max); + + governance.depositLQTY(1 ether); + votes[0] = 1 ether; + governance.allocateLQTY(noInitiatives, initiatives, votes, vetos); + } + vm.stopPrank(); + + (uint256 voteLQTYBefore, uint256 voteOffsetBefore,,,) = governance.initiativeStates(baseInitiative1); + + vm.startPrank(user); + { + address userProxy = governance.deriveUserProxyAddress(user); + lqty.approve(userProxy, type(uint256).max); + + // Vote 1 LQTY + governance.depositLQTY(1 ether); + votes[0] = 1 ether; + governance.allocateLQTY(noInitiatives, initiatives, votes, vetos); + + vm.warp(block.timestamp + 1 days); + + // Increase stake then unvote 1 LQTY + governance.depositLQTY(1 ether); + governance.allocateLQTY(initiatives, noInitiatives, noVotes, noVotes); + } + vm.stopPrank(); + + (uint256 voteLQTYAfter, uint256 voteOffsetAfter,,,) = governance.initiativeStates(baseInitiative1); + assertEqDecimal(voteLQTYAfter, voteLQTYBefore, 18, "voteLQTYAfter != voteLQTYBefore"); + assertEqDecimal(voteOffsetAfter, voteOffsetBefore, 18, "voteOffsetAfter != voteOffsetBefore"); + } + + function _stakeLQTY(address staker, uint256 amount) internal { vm.startPrank(staker); address userProxy = governance.deriveUserProxyAddress(staker); lqty.approve(address(userProxy), amount); @@ -2501,74 +2630,124 @@ contract GovernanceTest is Test { vm.stopPrank(); } - function _allocateLQTY(address allocator, uint88 amount) internal { + function _allocateLQTY(address allocator, uint256 amount) internal { vm.startPrank(allocator); - // always pass all possible initiatives to deregister for simplicity - address[] memory initiativesToDeRegister = new address[](4); - initiativesToDeRegister[0] = baseInitiative1; - initiativesToDeRegister[1] = baseInitiative2; - initiativesToDeRegister[2] = baseInitiative3; - initiativesToDeRegister[3] = address(0x123123); + address[] memory initiativesToReset; + (uint256 currentVote,, uint256 currentVeto,,) = + governance.lqtyAllocatedByUserToInitiative(allocator, address(baseInitiative1)); + if (currentVote != 0 || currentVeto != 0) { + initiativesToReset = new address[](1); + initiativesToReset[0] = address(baseInitiative1); + } address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int88[] memory deltaLQTYVotes = new int88[](1); - deltaLQTYVotes[0] = int88(amount); - int88[] memory deltaLQTYVetos = new int88[](1); + int256[] memory deltaLQTYVotes = new int256[](1); + deltaLQTYVotes[0] = int256(amount); + int256[] memory deltaLQTYVetos = new int256[](1); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); vm.stopPrank(); } - function _allocateLQTYToInitiative(address allocator, address initiative, uint88 amount) internal { + function _allocateLQTYToInitiative( + address allocator, + address initiative, + uint256 amount, + address[] memory initiativesToReset + ) internal { vm.startPrank(allocator); - address[] memory initiativesToDeRegister = new address[](4); - initiativesToDeRegister[0] = baseInitiative1; - initiativesToDeRegister[1] = baseInitiative2; - initiativesToDeRegister[2] = baseInitiative3; - initiativesToDeRegister[3] = address(0x123123); - address[] memory initiatives = new address[](1); initiatives[0] = initiative; - int88[] memory deltaLQTYVotes = new int88[](1); - deltaLQTYVotes[0] = int88(amount); - int88[] memory deltaLQTYVetos = new int88[](1); + int256[] memory deltaLQTYVotes = new int256[](1); + deltaLQTYVotes[0] = int256(amount); + int256[] memory deltaLQTYVetos = new int256[](1); - governance.allocateLQTY(initiativesToDeRegister, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); vm.stopPrank(); } - function _veto(address allocator, uint88 amount) internal { + function _veto(address allocator, uint256 amount) internal { vm.startPrank(allocator); + address[] memory initiativesToReset; + (uint256 currentVote,, uint256 currentVeto,,) = + governance.lqtyAllocatedByUserToInitiative(allocator, address(baseInitiative1)); + if (currentVote != 0 || currentVeto != 0) { + initiativesToReset = new address[](1); + initiativesToReset[0] = address(baseInitiative1); + } + address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int88[] memory deltaLQTYVotes = new int88[](1); - int88[] memory deltaLQTYVetos = new int88[](1); - deltaLQTYVetos[0] = int88(amount); + int256[] memory deltaLQTYVotes = new int256[](1); + int256[] memory deltaLQTYVetos = new int256[](1); + deltaLQTYVetos[0] = int256(amount); - governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); vm.stopPrank(); } - function _deAllocateLQTY(address allocator, uint88 amount) internal { - vm.startPrank(allocator); - - address[] memory initiativesToDeRegister = new address[](4); - initiativesToDeRegister[0] = baseInitiative1; - initiativesToDeRegister[1] = baseInitiative2; - initiativesToDeRegister[2] = baseInitiative3; - initiativesToDeRegister[3] = address(0x123123); - + function _deAllocateLQTY(address allocator, uint256 amount) internal { address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int88[] memory deltaLQTYVotes = new int88[](1); - deltaLQTYVotes[0] = -int88(amount); - int88[] memory deltaLQTYVetos = new int88[](1); - governance.allocateLQTY(initiativesToDeRegister, initiatives, deltaLQTYVotes, deltaLQTYVetos); + vm.startPrank(allocator); + governance.resetAllocations(initiatives, true); vm.stopPrank(); } } + +contract MockedGovernanceTest is GovernanceTest, MockStakingV1Deployer { + function setUp() public override { + (MockStakingV1 mockStakingV1, MockERC20Tester mockLQTY, MockERC20Tester mockLUSD) = deployMockStakingV1(); + + mockLQTY.mint(user, 10_000e18); + mockLQTY.mint(user2, 1_000e18); + mockLUSD.mint(lusdHolder, 20_000e18); + + lqty = mockLQTY; + lusd = mockLUSD; + stakingV1 = mockStakingV1; + + super.setUp(); + } + + function _expectInsufficientAllowance() internal override { + vm.expectPartialRevert(IERC20Errors.ERC20InsufficientAllowance.selector); + } + + function _expectInsufficientBalance() internal override { + vm.expectPartialRevert(IERC20Errors.ERC20InsufficientBalance.selector); + } + + function _expectInsufficientAllowanceAndBalance() internal override { + _expectInsufficientAllowance(); + } +} + +contract ForkedGovernanceTest is GovernanceTest { + function setUp() public override { + vm.createSelectFork(vm.rpcUrl("mainnet"), 20430000); + + lqty = ILQTY(MAINNET_LQTY); + lusd = ILUSD(MAINNET_LUSD); + stakingV1 = ILQTYStaking(MAINNET_LQTY_STAKING); + + super.setUp(); + } + + function _expectInsufficientAllowance() internal override { + vm.expectRevert("ERC20: transfer amount exceeds allowance"); + } + + function _expectInsufficientBalance() internal override { + vm.expectRevert("ERC20: transfer amount exceeds balance"); + } + + function _expectInsufficientAllowanceAndBalance() internal override { + _expectInsufficientBalance(); + } +} diff --git a/test/GovernanceAttacks.t.sol b/test/GovernanceAttacks.t.sol index 5937894d..7629fd20 100644 --- a/test/GovernanceAttacks.t.sol +++ b/test/GovernanceAttacks.t.sol @@ -3,33 +3,38 @@ pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; -import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; - import {IGovernance} from "../src/interfaces/IGovernance.sol"; +import {ILUSD} from "../src/interfaces/ILUSD.sol"; +import {ILQTY} from "../src/interfaces/ILQTY.sol"; +import {ILQTYStaking} from "../src/interfaces/ILQTYStaking.sol"; import {Governance} from "../src/Governance.sol"; import {UserProxy} from "../src/UserProxy.sol"; import {MaliciousInitiative} from "./mocks/MaliciousInitiative.sol"; - -contract GovernanceTest is Test { - IERC20 private constant lqty = IERC20(address(0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D)); - IERC20 private constant lusd = IERC20(address(0x5f98805A4E8be255a32880FDeC7F6728C6568bA0)); - address private constant stakingV1 = address(0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d); - address private constant user = address(0xF977814e90dA44bFA03b6295A0616a897441aceC); - address private constant user2 = address(0x10C9cff3c4Faa8A60cB8506a7A99411E6A199038); - address private constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); - - uint128 private constant REGISTRATION_FEE = 1e18; - uint128 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; - uint128 private constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; - uint16 private constant REGISTRATION_WARM_UP_PERIOD = 4; - uint16 private constant UNREGISTRATION_AFTER_EPOCHS = 4; - uint128 private constant VOTING_THRESHOLD_FACTOR = 0.04e18; - uint88 private constant MIN_CLAIM = 500e18; - uint88 private constant MIN_ACCRUAL = 1000e18; - uint32 private constant EPOCH_DURATION = 604800; - uint32 private constant EPOCH_VOTING_CUTOFF = 518400; +import {MockERC20Tester} from "./mocks/MockERC20Tester.sol"; +import {MockStakingV1} from "./mocks/MockStakingV1.sol"; +import {MockStakingV1Deployer} from "./mocks/MockStakingV1Deployer.sol"; +import "./constants.sol"; + +abstract contract GovernanceAttacksTest is Test { + ILQTY internal lqty; + ILUSD internal lusd; + ILQTYStaking internal stakingV1; + + address internal constant user = address(0xF977814e90dA44bFA03b6295A0616a897441aceC); + address internal constant user2 = address(0x10C9cff3c4Faa8A60cB8506a7A99411E6A199038); + address internal constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); + + uint256 private constant REGISTRATION_FEE = 1e18; + uint256 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; + uint256 private constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; + uint256 private constant UNREGISTRATION_AFTER_EPOCHS = 4; + uint256 private constant VOTING_THRESHOLD_FACTOR = 0.04e18; + uint256 private constant MIN_CLAIM = 500e18; + uint256 private constant MIN_ACCRUAL = 1000e18; + uint256 private constant EPOCH_DURATION = 604800; + uint256 private constant EPOCH_VOTING_CUTOFF = 518400; Governance private governance; address[] private initialInitiatives; @@ -38,43 +43,34 @@ contract GovernanceTest is Test { MaliciousInitiative private maliciousInitiative2; MaliciousInitiative private eoaInitiative; - function setUp() public { - vm.createSelectFork(vm.rpcUrl("mainnet"), 20430000); - + function setUp() public virtual { maliciousInitiative1 = new MaliciousInitiative(); maliciousInitiative2 = new MaliciousInitiative(); eoaInitiative = MaliciousInitiative(address(0x123123123123)); initialInitiatives.push(address(maliciousInitiative1)); + IGovernance.Configuration memory config = IGovernance.Configuration({ + registrationFee: REGISTRATION_FEE, + registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, + unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, + unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, + votingThresholdFactor: VOTING_THRESHOLD_FACTOR, + minClaim: MIN_CLAIM, + minAccrual: MIN_ACCRUAL, + // backdate by 2 epochs to ensure new initiatives can be registered from the start + epochStart: uint256(block.timestamp - 2 * EPOCH_DURATION), + epochDuration: EPOCH_DURATION, + epochVotingCutoff: EPOCH_VOTING_CUTOFF + }); + governance = new Governance( - address(lqty), - address(lusd), - stakingV1, - address(lusd), - IGovernance.Configuration({ - registrationFee: REGISTRATION_FEE, - registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, - unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, - unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, - votingThresholdFactor: VOTING_THRESHOLD_FACTOR, - minClaim: MIN_CLAIM, - minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), - epochDuration: EPOCH_DURATION, - epochVotingCutoff: EPOCH_VOTING_CUTOFF - }), - address(this), - initialInitiatives + address(lqty), address(lusd), address(stakingV1), address(lusd), config, address(this), initialInitiatives ); } - // forge test --match-test test_all_revert_attacks_hardcoded -vv // All calls should never revert due to malicious initiative function test_all_revert_attacks_hardcoded() public { - vm.warp(block.timestamp + governance.EPOCH_DURATION()); - vm.startPrank(user); // should not revert if the user doesn't have a UserProxy deployed yet @@ -84,10 +80,10 @@ contract GovernanceTest is Test { // deploy and deposit 1 LQTY governance.depositLQTY(1e18); assertEq(UserProxy(payable(userProxy)).staked(), 1e18); - (uint88 allocatedLQTY, uint120 averageStakingTimestamp) = governance.userStates(user); + (,, uint256 allocatedLQTY, uint256 allocatedOffset) = governance.userStates(user); assertEq(allocatedLQTY, 0); - // first deposit should have an averageStakingTimestamp if block.timestamp - assertEq(averageStakingTimestamp, block.timestamp * 1e26); // TODO: Normalize + // First deposit should have an unallocated offset of timestamp * deposit + assertEq(allocatedOffset, 0); vm.stopPrank(); vm.startPrank(lusdHolder); @@ -139,46 +135,47 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + governance.EPOCH_DURATION()); - vm.startPrank(user); + address[] memory initiativesToReset; address[] memory initiatives = new address[](2); initiatives[0] = address(maliciousInitiative2); initiatives[1] = address(eoaInitiative); - int88[] memory deltaVoteLQTY = new int88[](2); + int256[] memory deltaVoteLQTY = new int256[](2); deltaVoteLQTY[0] = 5e17; deltaVoteLQTY[1] = 5e17; - int88[] memory deltaVetoLQTY = new int88[](2); + int256[] memory deltaVetoLQTY = new int256[](2); /// === Allocate LQTY REVERTS === /// uint256 allocateSnapshot = vm.snapshot(); + vm.startPrank(user); maliciousInitiative2.setRevertBehaviour( MaliciousInitiative.FunctionType.ALLOCATE, MaliciousInitiative.RevertType.THROW ); - governance.allocateLQTY(initiatives, initiatives, deltaVoteLQTY, deltaVetoLQTY); + governance.allocateLQTY(initiativesToReset, initiatives, deltaVoteLQTY, deltaVetoLQTY); vm.revertTo(allocateSnapshot); maliciousInitiative2.setRevertBehaviour( MaliciousInitiative.FunctionType.ALLOCATE, MaliciousInitiative.RevertType.OOG ); - governance.allocateLQTY(initiatives, initiatives, deltaVoteLQTY, deltaVetoLQTY); + governance.allocateLQTY(initiativesToReset, initiatives, deltaVoteLQTY, deltaVetoLQTY); vm.revertTo(allocateSnapshot); maliciousInitiative2.setRevertBehaviour( MaliciousInitiative.FunctionType.ALLOCATE, MaliciousInitiative.RevertType.RETURN_BOMB ); - governance.allocateLQTY(initiatives, initiatives, deltaVoteLQTY, deltaVetoLQTY); + governance.allocateLQTY(initiativesToReset, initiatives, deltaVoteLQTY, deltaVetoLQTY); vm.revertTo(allocateSnapshot); maliciousInitiative2.setRevertBehaviour( MaliciousInitiative.FunctionType.ALLOCATE, MaliciousInitiative.RevertType.REVERT_BOMB ); - governance.allocateLQTY(initiatives, initiatives, deltaVoteLQTY, deltaVetoLQTY); + governance.allocateLQTY(initiativesToReset, initiatives, deltaVoteLQTY, deltaVetoLQTY); vm.revertTo(allocateSnapshot); maliciousInitiative2.setRevertBehaviour( MaliciousInitiative.FunctionType.ALLOCATE, MaliciousInitiative.RevertType.NONE ); - governance.allocateLQTY(initiatives, initiatives, deltaVoteLQTY, deltaVetoLQTY); + governance.allocateLQTY(initiativesToReset, initiatives, deltaVoteLQTY, deltaVetoLQTY); vm.warp(block.timestamp + governance.EPOCH_DURATION() + 1); @@ -219,16 +216,15 @@ contract GovernanceTest is Test { /// === Unregister Reverts === /// vm.startPrank(user); - initiatives = new address[](3); - initiatives[0] = address(maliciousInitiative2); - initiatives[1] = address(eoaInitiative); - initiatives[2] = address(maliciousInitiative1); - deltaVoteLQTY = new int88[](3); - deltaVoteLQTY[0] = 0; - deltaVoteLQTY[1] = 0; - deltaVoteLQTY[2] = 5e17; - deltaVetoLQTY = new int88[](3); - governance.allocateLQTY(initiatives, initiatives, deltaVoteLQTY, deltaVetoLQTY); + initiativesToReset = new address[](2); + initiativesToReset[0] = address(maliciousInitiative2); + initiativesToReset[1] = address(eoaInitiative); + initiatives = new address[](1); + initiatives[0] = address(maliciousInitiative1); + deltaVoteLQTY = new int256[](1); + deltaVoteLQTY[0] = 5e17; + deltaVetoLQTY = new int256[](1); + governance.allocateLQTY(initiativesToReset, initiatives, deltaVoteLQTY, deltaVetoLQTY); (Governance.VoteSnapshot memory v, Governance.InitiativeVoteSnapshot memory initData) = governance.snapshotVotesForInitiative(address(maliciousInitiative2)); @@ -274,3 +270,30 @@ contract GovernanceTest is Test { governance.unregisterInitiative(address(eoaInitiative)); } } + +contract MockedGovernanceAttacksTest is GovernanceAttacksTest, MockStakingV1Deployer { + function setUp() public override { + (MockStakingV1 mockStakingV1, MockERC20Tester mockLQTY, MockERC20Tester mockLUSD) = deployMockStakingV1(); + + mockLQTY.mint(user, 1e18); + mockLUSD.mint(lusdHolder, 10_000e18); + + lqty = mockLQTY; + lusd = mockLUSD; + stakingV1 = mockStakingV1; + + super.setUp(); + } +} + +contract ForkedGovernanceAttacksTest is GovernanceAttacksTest { + function setUp() public override { + vm.createSelectFork(vm.rpcUrl("mainnet"), 20430000); + + lqty = ILQTY(MAINNET_LQTY); + lusd = ILUSD(MAINNET_LUSD); + stakingV1 = ILQTYStaking(MAINNET_LQTY_STAKING); + + super.setUp(); + } +} diff --git a/test/InitiativeHooks.t.sol b/test/InitiativeHooks.t.sol new file mode 100644 index 00000000..e5ae2c01 --- /dev/null +++ b/test/InitiativeHooks.t.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IGovernance} from "../src/interfaces/IGovernance.sol"; +import {IInitiative} from "../src/interfaces/IInitiative.sol"; +import {Governance} from "../src/Governance.sol"; +import {MockERC20Tester} from "./mocks/MockERC20Tester.sol"; +import {MockStakingV1} from "./mocks/MockStakingV1.sol"; +import {MockStakingV1Deployer} from "./mocks/MockStakingV1Deployer.sol"; + +contract MockInitiative is IInitiative { + struct OnAfterAllocateLQTYParams { + uint256 currentEpoch; + address user; + IGovernance.UserState userState; + IGovernance.Allocation allocation; + IGovernance.InitiativeState initiativeStat; + } + + OnAfterAllocateLQTYParams[] public onAfterAllocateLQTYCalls; + + function numOnAfterAllocateLQTYCalls() external view returns (uint256) { + return onAfterAllocateLQTYCalls.length; + } + + function onAfterAllocateLQTY( + uint256 _currentEpoch, + address _user, + IGovernance.UserState calldata _userState, + IGovernance.Allocation calldata _allocation, + IGovernance.InitiativeState calldata _initiativeState + ) external override { + onAfterAllocateLQTYCalls.push( + OnAfterAllocateLQTYParams(_currentEpoch, _user, _userState, _allocation, _initiativeState) + ); + } + + function onRegisterInitiative(uint256) external override {} + function onUnregisterInitiative(uint256) external override {} + function onClaimForInitiative(uint256, uint256) external override {} +} + +contract InitiativeHooksTest is MockStakingV1Deployer { + uint32 constant START_TIME = 1732873631; + uint32 constant EPOCH_DURATION = 7 days; + uint32 constant EPOCH_VOTING_CUTOFF = 6 days; + + IGovernance.Configuration config = IGovernance.Configuration({ + registrationFee: 0, + registrationThresholdFactor: 0, + unregistrationThresholdFactor: 4 ether, + unregistrationAfterEpochs: 4, + votingThresholdFactor: 0, + minClaim: 0, + minAccrual: 0, + epochStart: START_TIME - EPOCH_DURATION, + epochDuration: EPOCH_DURATION, + epochVotingCutoff: EPOCH_VOTING_CUTOFF + }); + + MockStakingV1 stakingV1; + MockERC20Tester lqty; + MockERC20Tester lusd; + MockERC20Tester bold; + Governance governance; + MockInitiative initiative; + address[] noInitiatives; // left empty + address[] initiatives; + int256[] votes; + int256[] vetos; + address voter; + + function setUp() external { + vm.warp(START_TIME); + + (stakingV1, lqty, lusd) = deployMockStakingV1(); + + bold = new MockERC20Tester("BOLD Stablecoin", "BOLD"); + vm.label(address(bold), "BOLD"); + + governance = new Governance({ + _lqty: address(lqty), + _lusd: address(lusd), + _stakingV1: address(stakingV1), + _bold: address(bold), + _config: config, + _owner: address(this), + _initiatives: new address[](0) + }); + + initiative = new MockInitiative(); + initiatives.push(address(initiative)); + governance.registerInitialInitiatives(initiatives); + + voter = makeAddr("voter"); + lqty.mint(voter, 1 ether); + + vm.startPrank(voter); + lqty.approve(governance.deriveUserProxyAddress(voter), type(uint256).max); + governance.depositLQTY(1 ether); + vm.stopPrank(); + + votes.push(); + vetos.push(); + } + + function test_OnAfterAllocateLQTY_IsCalled_WhenCastingVotes() external { + vm.startPrank(voter); + votes[0] = 123; + governance.allocateLQTY(noInitiatives, initiatives, votes, vetos); + vm.stopPrank(); + + assertEq(initiative.numOnAfterAllocateLQTYCalls(), 1, "onAfterAllocateLQTY should have been called once"); + (,,, IGovernance.Allocation memory allocation,) = initiative.onAfterAllocateLQTYCalls(0); + assertEq(allocation.voteLQTY, 123, "wrong voteLQTY 1"); + + vm.startPrank(voter); + votes[0] = 456; + governance.allocateLQTY(initiatives, initiatives, votes, vetos); + vm.stopPrank(); + + assertEq(initiative.numOnAfterAllocateLQTYCalls(), 3, "onAfterAllocateLQTY should have been called twice more"); + (,,, allocation,) = initiative.onAfterAllocateLQTYCalls(1); + assertEq(allocation.voteLQTY, 0, "wrong voteLQTY 2"); + (,,, allocation,) = initiative.onAfterAllocateLQTYCalls(2); + assertEq(allocation.voteLQTY, 456, "wrong voteLQTY 3"); + } + + function test_OnAfterAllocateLQTY_IsNotCalled_WhenCastingVetos() external { + vm.startPrank(voter); + vetos[0] = 123; + governance.allocateLQTY(noInitiatives, initiatives, votes, vetos); + vm.stopPrank(); + + assertEq(initiative.numOnAfterAllocateLQTYCalls(), 0, "onAfterAllocateLQTY should not have been called once"); + } + + function test_OnAfterAllocateLQTY_IsCalledOnceWithZeroVotes_WhenCastingVetosAfterHavingCastVotes() external { + vm.startPrank(voter); + votes[0] = 123; + governance.allocateLQTY(noInitiatives, initiatives, votes, vetos); + vm.stopPrank(); + + assertEq(initiative.numOnAfterAllocateLQTYCalls(), 1, "onAfterAllocateLQTY should have been called once"); + + vm.startPrank(voter); + votes[0] = 0; + vetos[0] = 456; + governance.allocateLQTY(initiatives, initiatives, votes, vetos); + vm.stopPrank(); + + assertEq(initiative.numOnAfterAllocateLQTYCalls(), 2, "onAfterAllocateLQTY should have been called once more"); + (,,, IGovernance.Allocation memory allocation,) = initiative.onAfterAllocateLQTYCalls(1); + assertEq(allocation.voteLQTY, 0, "wrong voteLQTY"); + } +} diff --git a/test/Math.t.sol b/test/Math.t.sol deleted file mode 100644 index 5464b175..00000000 --- a/test/Math.t.sol +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {Test} from "forge-std/Test.sol"; - -import {add, abs} from "src/utils/Math.sol"; -import {console} from "forge-std/console.sol"; - -contract AddComparer { - function libraryAdd(uint88 a, int88 b) public pure returns (uint88) { - return add(a, b); - } - // Differential test - // Verify that it will revert any time it overflows - // Verify we can never get a weird value - - function referenceAdd(uint88 a, int88 b) public pure returns (uint88) { - // Upscale both - int96 scaledA = int96(int256(uint256(a))); - int96 tempB = int96(b); - - int96 res = scaledA + tempB; - if (res < 0) { - revert("underflow"); - } - - if (res > int96(int256(uint256(type(uint88).max)))) { - revert("Too big"); - } - - return uint88(uint96(res)); - } -} - -contract AbsComparer { - function libraryAbs(int88 a) public pure returns (uint88) { - return abs(a); // by definition should fit, since input was int88 -> uint88 -> int88 - } - - event DebugEvent2(int256); - event DebugEvent(uint256); - - function referenceAbs(int88 a) public returns (uint88) { - int256 bigger = a; - uint256 ref = bigger < 0 ? uint256(-bigger) : uint256(bigger); - emit DebugEvent2(bigger); - emit DebugEvent(ref); - if (ref > type(uint88).max) { - revert("Too big"); - } - if (ref < type(uint88).min) { - revert("Too small"); - } - return uint88(ref); - } -} - -contract MathTests is Test { - // forge test --match-test test_math_fuzz_comparison -vv - function test_math_fuzz_comparison(uint88 a, int88 b) public { - vm.assume(a < uint88(type(int88).max)); - AddComparer tester = new AddComparer(); - - bool revertLib; - bool revertRef; - uint88 resultLib; - uint88 resultRef; - - try tester.libraryAdd(a, b) returns (uint88 x) { - resultLib = x; - } catch { - revertLib = true; - } - - try tester.referenceAdd(a, b) returns (uint88 x) { - resultRef = x; - } catch { - revertRef = true; - } - - // Negative overflow - if (revertLib == true && revertRef == false) { - // Check if we had a negative value - if (resultRef < 0) { - revertRef = true; - resultRef = uint88(0); - } - - // Check if we overflow on the positive - if (resultRef > uint88(type(int88).max)) { - // Overflow due to above limit - revertRef = true; - resultRef = uint88(0); - } - } - - assertEq(revertLib, revertRef, "Reverts"); // This breaks - assertEq(resultLib, resultRef, "Results"); // This should match excluding overflows - } - - /// @dev test that abs never incorrectly overflows - // forge test --match-test test_fuzz_abs_comparison -vv - /** - * [FAIL. Reason: reverts: false != true; counterexample: calldata=0x2c945365ffffffffffffffffffffffffffffffffffffffffff8000000000000000000000 args=[-154742504910672534362390528 [-1.547e26]]] - */ - function test_fuzz_abs_comparison(int88 a) public { - AbsComparer tester = new AbsComparer(); - - bool revertLib; - bool revertRef; - uint88 resultLib; - uint88 resultRef; - - try tester.libraryAbs(a) returns (uint88 x) { - resultLib = x; - } catch { - revertLib = true; - } - - try tester.referenceAbs(a) returns (uint88 x) { - resultRef = x; - } catch { - revertRef = true; - } - - assertEq(revertLib, revertRef, "reverts"); - assertEq(resultLib, resultRef, "results"); - } - - /// @dev Test that Abs never revert - /// It reverts on the smaller possible number - function test_fuzz_abs(int88 a) public { - /** - * Encountered 1 failing test in test/Math.t.sol:MathTests - * [FAIL. Reason: panic: arithmetic underflow or overflow (0x11); counterexample: calldata=0x804d552cffffffffffffffffffffffffffffffffffffffff800000000000000000000000 args=[-39614081257132168796771975168 [-3.961e28]]] test_fuzz_abs(int88) (runs: 0, μ: 0, ~: 0) - */ - /// @audit Reverts at the absolute minimum due to overflow as it will remain negative - abs(a); - } -} diff --git a/test/MultiDelegateCall.t.sol b/test/MultiDelegateCall.t.sol new file mode 100644 index 00000000..b09e359a --- /dev/null +++ b/test/MultiDelegateCall.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {stdError} from "forge-std/StdError.sol"; +import {MultiDelegateCall} from "../src/utils/MultiDelegateCall.sol"; + +contract Target is MultiDelegateCall { + error CustomError(string); + + function id(bytes calldata x) external pure returns (bytes calldata) { + return x; + } + + function revertWithMessage(string calldata message) external pure { + revert(message); + } + + function revertWithCustomError(string calldata message) external pure { + revert CustomError(message); + } + + function panicWithArithmeticError() external pure returns (int256) { + return -type(int256).min; + } +} + +contract MultiDelegateCallTest is Test { + function test_CallsAllInputsAndAggregatesResults() external { + Target target = new Target(); + + bytes[] memory inputValues = new bytes[](3); + inputValues[0] = abi.encode("asd", 123); + inputValues[1] = abi.encode("fgh", 456); + inputValues[2] = abi.encode("jkl", 789); + + bytes[] memory inputs = new bytes[](3); + inputs[0] = abi.encodeCall(target.id, (inputValues[0])); + inputs[1] = abi.encodeCall(target.id, (inputValues[1])); + inputs[2] = abi.encodeCall(target.id, (inputValues[2])); + + bytes[] memory returnValues = target.multiDelegateCall(inputs); + assertEq(returnValues.length, inputs.length, "returnValues.length != inputs.length"); + + assertEq(abi.decode(returnValues[0], (bytes)), inputValues[0], "returnValues[0]"); + assertEq(abi.decode(returnValues[1], (bytes)), inputValues[1], "returnValues[1]"); + assertEq(abi.decode(returnValues[2], (bytes)), inputValues[2], "returnValues[2]"); + } + + function test_StopsAtFirstRevertAndBubblesItUp() external { + Target target = new Target(); + + bytes[] memory inputs = new bytes[](3); + inputs[0] = abi.encodeCall(target.id, ("asd")); + inputs[1] = abi.encodeCall(target.revertWithMessage, ("fgh")); + inputs[2] = abi.encodeCall(target.revertWithMessage, ("jkl")); + + vm.expectRevert(bytes("fgh")); + target.multiDelegateCall(inputs); + } + + function test_CanBubbleCustomError() external { + Target target = new Target(); + + bytes[] memory inputs = new bytes[](3); + inputs[0] = abi.encodeCall(target.id, ("asd")); + inputs[1] = abi.encodeCall(target.revertWithCustomError, ("fgh")); + inputs[2] = abi.encodeCall(target.revertWithMessage, ("jkl")); + + vm.expectRevert(abi.encodeWithSelector(Target.CustomError.selector, "fgh")); + target.multiDelegateCall(inputs); + } + + function test_CanBubblePanic() external { + Target target = new Target(); + + bytes[] memory inputs = new bytes[](3); + inputs[0] = abi.encodeCall(target.id, ("asd")); + inputs[1] = abi.encodeCall(target.panicWithArithmeticError, ()); + inputs[2] = abi.encodeCall(target.revertWithMessage, ("jkl")); + + vm.expectRevert(stdError.arithmeticError); + target.multiDelegateCall(inputs); + } +} diff --git a/test/SafeCallWithMinGas.t.sol b/test/SafeCallWithMinGas.t.sol index 370d6f10..4e71d6d7 100644 --- a/test/SafeCallWithMinGas.t.sol +++ b/test/SafeCallWithMinGas.t.sol @@ -23,7 +23,8 @@ contract FallbackRecipient { contract SafeCallWithMinGasTests is Test { function test_basic_nonExistent(uint256 gas, uint256 value, bytes memory theData) public { - vm.assume(gas < 30_000_000); + gas = bound(gas, 0, 30_000_000); + // Call to non existent succeeds address nonExistent = address(0x123123123); assert(nonExistent.code.length == 0); @@ -32,8 +33,8 @@ contract SafeCallWithMinGasTests is Test { } function test_basic_contractData(uint256 gas, uint256 value, bytes memory theData) public { - vm.assume(gas < 30_000_000); - vm.assume(gas > 50_000 + theData.length * 2_100); + gas = bound(gas, 50_000 + theData.length * 2_100, 30_000_000); + /// @audit Approximation FallbackRecipient recipient = new FallbackRecipient(); // Call to non existent succeeds diff --git a/test/UniV4Donations.t.sol b/test/UniV4Donations.t.sol deleted file mode 100644 index 574ed2d7..00000000 --- a/test/UniV4Donations.t.sol +++ /dev/null @@ -1,250 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {Test} from "forge-std/Test.sol"; - -import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; - -import {IPoolManager, PoolManager, Deployers, TickMath, Hooks, IHooks} from "v4-core/test/utils/Deployers.sol"; -import {PoolModifyLiquidityTest} from "v4-core/src/test/PoolModifyLiquidityTest.sol"; - -import {IGovernance} from "../src/interfaces/IGovernance.sol"; - -import {UniV4Donations} from "../src/UniV4Donations.sol"; -import {Governance} from "../src/Governance.sol"; -import {BaseHook, Hooks} from "../src/utils/BaseHook.sol"; - -contract UniV4DonationsImpl is UniV4Donations { - constructor( - address _governance, - address _bold, - address _bribeToken, - uint256 _vestingEpochStart, - uint256 _vestingEpochDuration, - address _poolManager, - address _token, - uint24 _fee, - int24 _tickSpacing, - BaseHook addressToEtch - ) - UniV4Donations( - _governance, - _bold, - _bribeToken, - _vestingEpochStart, - _vestingEpochDuration, - _poolManager, - _token, - _fee, - _tickSpacing - ) - { - BaseHook.validateHookAddress(addressToEtch); - } - - // make this a no-op in testing - function validateHookAddress(BaseHook _this) internal pure override {} -} - -contract UniV4DonationsTest is Test, Deployers { - IERC20 private constant lqty = IERC20(address(0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D)); - IERC20 private constant lusd = IERC20(address(0x5f98805A4E8be255a32880FDeC7F6728C6568bA0)); - IERC20 private constant usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); - address private constant stakingV1 = address(0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d); - address private constant user = address(0xF977814e90dA44bFA03b6295A0616a897441aceC); - address private constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); - - uint128 private constant REGISTRATION_FEE = 1e18; - uint128 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; - uint128 private constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; - uint16 private constant REGISTRATION_WARM_UP_PERIOD = 4; - uint16 private constant UNREGISTRATION_AFTER_EPOCHS = 4; - uint128 private constant VOTING_THRESHOLD_FACTOR = 0.04e18; - uint88 private constant MIN_CLAIM = 500e18; - uint88 private constant MIN_ACCRUAL = 1000e18; - uint32 private constant EPOCH_DURATION = 604800; - uint32 private constant EPOCH_VOTING_CUTOFF = 518400; - - Governance private governance; - address[] private initialInitiatives; - - UniV4Donations private uniV4Donations = - UniV4Donations(address(uint160(Hooks.AFTER_INITIALIZE_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG))); - - int24 constant MAX_TICK_SPACING = 32767; - - function setUp() public { - vm.createSelectFork(vm.rpcUrl("mainnet"), 20430000); - - manager = new PoolManager(500000); - modifyLiquidityRouter = new PoolModifyLiquidityTest(manager); - - initialInitiatives = new address[](1); - initialInitiatives[0] = address(uniV4Donations); - - UniV4DonationsImpl impl = new UniV4DonationsImpl( - address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 1)), - address(lusd), - address(lqty), - block.timestamp, - EPOCH_DURATION, - address(manager), - address(usdc), - 400, - MAX_TICK_SPACING, - BaseHook(address(uniV4Donations)) - ); - - (, bytes32[] memory writes) = vm.accesses(address(impl)); - vm.etch(address(uniV4Donations), address(impl).code); - // for each storage key that was written during the hook implementation, copy the value over - unchecked { - for (uint256 i = 0; i < writes.length; i++) { - bytes32 slot = writes[i]; - vm.store(address(uniV4Donations), slot, vm.load(address(impl), slot)); - } - } - - governance = new Governance( - address(lqty), - address(lusd), - stakingV1, - address(lusd), - IGovernance.Configuration({ - registrationFee: REGISTRATION_FEE, - registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, - unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, - unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, - votingThresholdFactor: VOTING_THRESHOLD_FACTOR, - minClaim: MIN_CLAIM, - minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp), - epochDuration: EPOCH_DURATION, - epochVotingCutoff: EPOCH_VOTING_CUTOFF - }), - address(this), - initialInitiatives - ); - } - - function test_afterInitializeState() public { - manager.initialize(uniV4Donations.poolKey(), SQRT_PRICE_1_1, ZERO_BYTES); - } - - //// TODO: e2e test - With real governance and proposals - - function test_modifyPositionFuzz() public { - manager.initialize(uniV4Donations.poolKey(), SQRT_PRICE_1_1, ZERO_BYTES); - - vm.startPrank(lusdHolder); - lusd.transfer(address(uniV4Donations), 1000e18); - vm.stopPrank(); - - /// TODO: This is a mock call, we need a E2E test as well - vm.prank(address(governance)); - uniV4Donations.onClaimForInitiative(0, 1000e18); - - vm.startPrank(lusdHolder); - assertEq(uniV4Donations.donateToPool(), 0, "d"); - (uint240 amount, uint16 epoch, uint256 released) = uniV4Donations.vesting(); - assertEq(amount, 1000e18, "amt"); - assertEq(epoch, 1, "epoch"); - assertEq(released, 0, "released"); - - vm.warp(block.timestamp + uniV4Donations.VESTING_EPOCH_DURATION() / 2); - lusd.approve(address(modifyLiquidityRouter), type(uint256).max); - usdc.approve(address(modifyLiquidityRouter), type(uint256).max); - modifyLiquidityRouter.modifyLiquidity( - uniV4Donations.poolKey(), - IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 - ), - bytes("") - ); - (amount, epoch, released) = uniV4Donations.vesting(); - assertEq(amount, 1000e18); - assertEq(released, amount * 50 / 100); - assertEq(epoch, 1); - - vm.warp(block.timestamp + (uniV4Donations.VESTING_EPOCH_DURATION() / 2) - 1); - uint256 donated = uniV4Donations.donateToPool(); - assertGt(donated, amount * 49 / 100); - assertLt(donated, amount * 50 / 100); - (amount, epoch, released) = uniV4Donations.vesting(); - assertEq(amount, 1000e18); - assertEq(epoch, 1); - assertGt(released, amount * 99 / 100); - - vm.warp(block.timestamp + 1); - vm.mockCall(address(governance), abi.encode(IGovernance.claimForInitiative.selector), abi.encode(uint256(0))); - uniV4Donations.donateToPool(); - (amount, epoch, released) = uniV4Donations.vesting(); - assertLt(amount, 0.01e18); - assertEq(epoch, 2); - assertEq(released, 0); - - vm.stopPrank(); - } - - function test_modifyPositionFuzz(uint128 amt) public { - manager.initialize(uniV4Donations.poolKey(), SQRT_PRICE_1_1, ZERO_BYTES); - - deal(address(lusd), address(uniV4Donations), amt); - - /// TODO: This is a mock call, we need a E2E test as well - vm.prank(address(governance)); - uniV4Donations.onClaimForInitiative(0, amt); - - vm.startPrank(lusdHolder); - assertEq(uniV4Donations.donateToPool(), 0, "d"); - (uint240 amount, uint16 epoch, uint256 released) = uniV4Donations.vesting(); - assertEq(amount, amt, "amt"); - assertEq(epoch, 1, "epoch"); - assertEq(released, 0, "released"); - - vm.warp(block.timestamp + uniV4Donations.VESTING_EPOCH_DURATION() / 2); - lusd.approve(address(modifyLiquidityRouter), type(uint256).max); - usdc.approve(address(modifyLiquidityRouter), type(uint256).max); - modifyLiquidityRouter.modifyLiquidity( - uniV4Donations.poolKey(), - IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 - ), - bytes("") - ); - (amount, epoch, released) = uniV4Donations.vesting(); - assertEq(amount, amt); - assertEq(released, amount * 50 / 100); - assertEq(epoch, 1); - - vm.warp(block.timestamp + (uniV4Donations.VESTING_EPOCH_DURATION() / 2) - 1); - uint256 donated = uniV4Donations.donateToPool(); - assertGe(donated, amount * 49 / 100); - /// @audit Used to be Gt - assertLe(donated, amount * 50 / 100, "less than 50%"); - /// @audit Used to be Lt - (amount, epoch, released) = uniV4Donations.vesting(); - assertEq(amount, amt); - assertEq(epoch, 1); - assertGe(released, amount * 99 / 100); - /// @audit Used to be Gt - - vm.warp(block.timestamp + 1); - vm.mockCall(address(governance), abi.encode(IGovernance.claimForInitiative.selector), abi.encode(uint256(0))); - uniV4Donations.donateToPool(); - (amount, epoch, released) = uniV4Donations.vesting(); - - /// @audit Counterexample - // [FAIL. Reason: end results in dust: 1 > 0; counterexample: calldata=0x38b4b04f000000000000000000000000000000000000000000000000000000000000000c args=[12]] test_modifyPositionFuzz(uint128) (runs: 4, μ: 690381, ~: 690381) - if (amount > 1) { - assertLe(amount, amt / 100, "end results in dust"); - /// @audit Used to be Lt - } - - assertEq(epoch, 2); - assertEq(released, 0); - - vm.stopPrank(); - } -} diff --git a/test/UserProxy.t.sol b/test/UserProxy.t.sol index 17124d52..abc650f6 100644 --- a/test/UserProxy.t.sol +++ b/test/UserProxy.t.sol @@ -4,39 +4,45 @@ pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; import {VmSafe} from "forge-std/Vm.sol"; -import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; - import {ILQTY} from "../src/interfaces/ILQTY.sol"; +import {ILUSD} from "../src/interfaces/ILUSD.sol"; +import {ILQTYStaking} from "../src/interfaces/ILQTYStaking.sol"; import {UserProxyFactory} from "./../src/UserProxyFactory.sol"; import {UserProxy} from "./../src/UserProxy.sol"; import {PermitParams} from "../src/utils/Types.sol"; -contract UserProxyTest is Test { - IERC20 private constant lqty = IERC20(address(0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D)); - IERC20 private constant lusd = IERC20(address(0x5f98805A4E8be255a32880FDeC7F6728C6568bA0)); - address private constant stakingV1 = address(0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d); - address private constant user = address(0xF977814e90dA44bFA03b6295A0616a897441aceC); - address private constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); +import {MockERC20Tester} from "./mocks/MockERC20Tester.sol"; +import {MockStakingV1} from "./mocks/MockStakingV1.sol"; +import {MockStakingV1Deployer} from "./mocks/MockStakingV1Deployer.sol"; +import "./constants.sol"; + +abstract contract UserProxyTest is Test, MockStakingV1Deployer { + ILQTY internal lqty; + ILUSD internal lusd; + ILQTYStaking internal stakingV1; + + address internal constant user = address(0xF977814e90dA44bFA03b6295A0616a897441aceC); UserProxyFactory private userProxyFactory; UserProxy private userProxy; - function setUp() public { - vm.createSelectFork(vm.rpcUrl("mainnet"), 20430000); - - userProxyFactory = new UserProxyFactory(address(lqty), address(lusd), stakingV1); + function setUp() public virtual { + userProxyFactory = new UserProxyFactory(address(lqty), address(lusd), address(stakingV1)); userProxy = UserProxy(payable(userProxyFactory.deployUserProxy())); } + function _addLUSDGain(uint256 amount) internal virtual; + function _addETHGain(uint256 amount) internal virtual; + function test_stake() public { vm.startPrank(user); lqty.approve(address(userProxy), 1e18); vm.stopPrank(); vm.startPrank(address(userProxyFactory)); - userProxy.stake(1e18, user); + userProxy.stake(1e18, user, false, address(0)); vm.stopPrank(); } @@ -94,11 +100,11 @@ contract UserProxyTest is Test { // deposit 1 LQTY vm.startPrank(address(userProxyFactory)); vm.expectRevert(); - userProxy.stakeViaPermit(0.5e18, user, permitParams); - userProxy.stakeViaPermit(0.5e18, wallet.addr, permitParams); - userProxy.stakeViaPermit(0.5e18, wallet.addr, permitParams); + userProxy.stakeViaPermit(0.5e18, user, permitParams, false, address(0)); + userProxy.stakeViaPermit(0.5e18, wallet.addr, permitParams, false, address(0)); + userProxy.stakeViaPermit(0.5e18, wallet.addr, permitParams, false, address(0)); vm.expectRevert(); - userProxy.stakeViaPermit(1, wallet.addr, permitParams); + userProxy.stakeViaPermit(1, wallet.addr, permitParams, false, address(0)); vm.stopPrank(); } @@ -109,24 +115,83 @@ contract UserProxyTest is Test { vm.startPrank(address(userProxyFactory)); - userProxy.stake(1e18, user); + userProxy.stake(1e18, user, false, address(0)); - (uint256 lusdAmount, uint256 ethAmount) = userProxy.unstake(0, user); + (,, uint256 lusdAmount,, uint256 ethAmount,) = userProxy.unstake(0, true, user); assertEq(lusdAmount, 0); assertEq(ethAmount, 0); + vm.stopPrank(); + vm.warp(block.timestamp + 7 days); - uint256 ethBalance = uint256(vm.load(stakingV1, bytes32(uint256(3)))); - vm.store(stakingV1, bytes32(uint256(3)), bytes32(abi.encodePacked(ethBalance + 1e18))); + _addETHGain(stakingV1.totalLQTYStaked()); + _addLUSDGain(stakingV1.totalLQTYStaked()); - uint256 lusdBalance = uint256(vm.load(stakingV1, bytes32(uint256(4)))); - vm.store(stakingV1, bytes32(uint256(4)), bytes32(abi.encodePacked(lusdBalance + 1e18))); + vm.startPrank(address(userProxyFactory)); - (lusdAmount, ethAmount) = userProxy.unstake(1e18, user); + (,, lusdAmount,, ethAmount,) = userProxy.unstake(1e18, true, user); assertEq(lusdAmount, 1e18); assertEq(ethAmount, 1e18); vm.stopPrank(); } } + +contract MockedUserProxyTest is UserProxyTest { + MockERC20Tester private mockLQTY; + MockERC20Tester private mockLUSD; + MockStakingV1 private mockStakingV1; + + function setUp() public override { + (mockStakingV1, mockLQTY, mockLUSD) = deployMockStakingV1(); + mockLQTY.mint(user, 1e18); + + lqty = mockLQTY; + lusd = mockLUSD; + stakingV1 = mockStakingV1; + + super.setUp(); + } + + function _addLUSDGain(uint256 amount) internal override { + mockLUSD.mint(address(this), amount); + mockLUSD.approve(address(mockStakingV1), amount); + mockStakingV1.mock_addLUSDGain(amount); + } + + function _addETHGain(uint256 amount) internal override { + deal(address(this), address(this).balance + amount); + mockStakingV1.mock_addETHGain{value: amount}(); + } +} + +contract ForkedUserProxyTest is UserProxyTest { + function setUp() public override { + vm.createSelectFork(vm.rpcUrl("mainnet"), 20430000); + + lqty = ILQTY(MAINNET_LQTY); + lusd = ILUSD(MAINNET_LUSD); + stakingV1 = ILQTYStaking(MAINNET_LQTY_STAKING); + + super.setUp(); + } + + function _addLUSDGain(uint256 amount) internal override { + vm.prank(MAINNET_BORROWER_OPERATIONS); + stakingV1.increaseF_LUSD(amount); + + vm.prank(MAINNET_BORROWER_OPERATIONS); + lusd.mint(address(stakingV1), amount); + } + + function _addETHGain(uint256 amount) internal override { + deal(MAINNET_ACTIVE_POOL, MAINNET_ACTIVE_POOL.balance + amount); + vm.prank(MAINNET_ACTIVE_POOL); + (bool success,) = address(stakingV1).call{value: amount}(""); + assert(success); + + vm.prank(MAINNET_TROVE_MANAGER); + stakingV1.increaseF_ETH(amount); + } +} diff --git a/test/VotingPower.t.sol b/test/VotingPower.t.sol index bbfe0de8..42408b79 100644 --- a/test/VotingPower.t.sol +++ b/test/VotingPower.t.sol @@ -1,472 +1,466 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {Test, console2} from "forge-std/Test.sol"; -import {VmSafe} from "forge-std/Vm.sol"; -import {console} from "forge-std/console.sol"; - -import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; - -import {IGovernance} from "../src/interfaces/IGovernance.sol"; -import {ILQTY} from "../src/interfaces/ILQTY.sol"; - -import {BribeInitiative} from "../src/BribeInitiative.sol"; -import {Governance} from "../src/Governance.sol"; -import {UserProxy} from "../src/UserProxy.sol"; - -import {PermitParams} from "../src/utils/Types.sol"; - -import {MockInitiative} from "./mocks/MockInitiative.sol"; - -contract VotingPowerTest is Test { - IERC20 private constant lqty = IERC20(address(0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D)); - IERC20 private constant lusd = IERC20(address(0x5f98805A4E8be255a32880FDeC7F6728C6568bA0)); - address private constant stakingV1 = address(0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d); - address private constant user = address(0xF977814e90dA44bFA03b6295A0616a897441aceC); - address private constant user2 = address(0x10C9cff3c4Faa8A60cB8506a7A99411E6A199038); - address private constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); - - uint128 private constant REGISTRATION_FEE = 1e18; - uint128 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; - uint128 private constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; - uint16 private constant REGISTRATION_WARM_UP_PERIOD = 4; - uint16 private constant UNREGISTRATION_AFTER_EPOCHS = 4; - uint128 private constant VOTING_THRESHOLD_FACTOR = 0.04e18; - uint88 private constant MIN_CLAIM = 500e18; - uint88 private constant MIN_ACCRUAL = 1000e18; - uint32 private constant EPOCH_DURATION = 604800; - uint32 private constant EPOCH_VOTING_CUTOFF = 518400; - - Governance private governance; - address[] private initialInitiatives; - - address private baseInitiative2; - address private baseInitiative3; - address private baseInitiative1; - - function setUp() public { - vm.createSelectFork(vm.rpcUrl("mainnet"), 20430000); - - baseInitiative1 = address( - new BribeInitiative( - address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 3)), - address(lusd), - address(lqty) - ) - ); - - baseInitiative2 = address( - new BribeInitiative( - address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 2)), - address(lusd), - address(lqty) - ) - ); - - baseInitiative3 = address( - new BribeInitiative( - address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 1)), - address(lusd), - address(lqty) - ) - ); - - initialInitiatives.push(baseInitiative1); - initialInitiatives.push(baseInitiative2); - - governance = new Governance( - address(lqty), - address(lusd), - stakingV1, - address(lusd), - IGovernance.Configuration({ - registrationFee: REGISTRATION_FEE, - registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, - unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, - unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, - votingThresholdFactor: VOTING_THRESHOLD_FACTOR, - minClaim: MIN_CLAIM, - minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp - EPOCH_DURATION), - epochDuration: EPOCH_DURATION, - epochVotingCutoff: EPOCH_VOTING_CUTOFF - }), - address(this), - initialInitiatives - ); - } - - /// Compare with removing all and re-allocating all at the 2nd epoch - // forge test --match-test test_math_soundness -vv - function test_math_soundness() public { - // Given a Multiplier, I can wait 8 times more time - // Or use 8 times more amt - uint8 multiplier = 2; - - uint88 lqtyAmount = 1e18; - - uint256 powerInTheFuture = governance.lqtyToVotes(lqtyAmount, multiplier + 1, 1); - // Amt when delta is 1 - // 0 when delta is 0 - uint256 powerFromMoreDeposits = - governance.lqtyToVotes(lqtyAmount * multiplier, uint32(block.timestamp + 1), uint32(block.timestamp)); - - assertEq(powerInTheFuture, powerFromMoreDeposits, "Same result"); - } - - function test_math_soundness_fuzz(uint32 multiplier) public view { - vm.assume(multiplier < type(uint32).max - 1); - uint88 lqtyAmount = 1e10; - - uint256 powerInTheFuture = governance.lqtyToVotes(lqtyAmount, multiplier + 1, 1); - - // Amt when delta is 1 - // 0 when delta is 0 - uint256 powerFromMoreDeposits = - governance.lqtyToVotes(lqtyAmount * multiplier, uint32(block.timestamp + 1), uint32(block.timestamp)); - - assertEq(powerInTheFuture, powerFromMoreDeposits, "Same result"); - } - - function _averageAge(uint32 _currentTimestamp, uint32 _averageTimestamp) internal pure returns (uint32) { - if (_averageTimestamp == 0 || _currentTimestamp < _averageTimestamp) return 0; - return _currentTimestamp - _averageTimestamp; - } - - function _calculateAverageTimestamp( - uint32 _prevOuterAverageTimestamp, - uint32 _newInnerAverageTimestamp, - uint88 _prevLQTYBalance, - uint88 _newLQTYBalance - ) internal view returns (uint32) { - if (_newLQTYBalance == 0) return 0; - - uint32 prevOuterAverageAge = _averageAge(uint32(block.timestamp), _prevOuterAverageTimestamp); - uint32 newInnerAverageAge = _averageAge(uint32(block.timestamp), _newInnerAverageTimestamp); - - uint88 newOuterAverageAge; - if (_prevLQTYBalance <= _newLQTYBalance) { - uint88 deltaLQTY = _newLQTYBalance - _prevLQTYBalance; - uint240 prevVotes = uint240(_prevLQTYBalance) * uint240(prevOuterAverageAge); - uint240 newVotes = uint240(deltaLQTY) * uint240(newInnerAverageAge); - uint240 votes = prevVotes + newVotes; - newOuterAverageAge = uint32(votes / uint240(_newLQTYBalance)); - } else { - uint88 deltaLQTY = _prevLQTYBalance - _newLQTYBalance; - uint240 prevVotes = uint240(_prevLQTYBalance) * uint240(prevOuterAverageAge); - uint240 newVotes = uint240(deltaLQTY) * uint240(newInnerAverageAge); - uint240 votes = (prevVotes >= newVotes) ? prevVotes - newVotes : 0; - newOuterAverageAge = uint32(votes / uint240(_newLQTYBalance)); - } - - if (newOuterAverageAge > block.timestamp) return 0; - return uint32(block.timestamp - newOuterAverageAge); - } - - // This test prepares for comparing votes and vetos for state - // forge test --match-test test_we_can_compare_votes_and_vetos -vv - // function test_we_can_compare_votes_and_vetos() public { - /// TODO AUDIT Known bug with rounding math - // uint32 current_time = 123123123; - // vm.warp(current_time); - // // State at X - // // State made of X and Y - // uint32 time = current_time - 124; - // uint88 votes = 124; - // uint240 power = governance.lqtyToVotes(votes, current_time, time); - - // assertEq(power, (_averageAge(current_time, time)) * votes, "simple product"); - - // // if it's a simple product we have the properties of multiplication, we can get back the value by dividing the tiem - // uint88 resultingVotes = uint88(power / _averageAge(current_time, time)); - - // assertEq(resultingVotes, votes, "We can get it back"); - - // // If we can get it back, then we can also perform other operations like addition and subtraction - // // Easy when same TS +// // SPDX-License-Identifier: UNLICENSED +// pragma solidity ^0.8.24; + +// import {Test} from "forge-std/Test.sol"; +// import {console} from "forge-std/console.sol"; + +// import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; + +// import {IGovernance} from "../src/interfaces/IGovernance.sol"; +// import {ILQTYStaking} from "../src/interfaces/ILQTYStaking.sol"; + +// import {BribeInitiative} from "../src/BribeInitiative.sol"; +// import {Governance} from "../src/Governance.sol"; + +// import {MockERC20Tester} from "./mocks/MockERC20Tester.sol"; +// import {MockStakingV1} from "./mocks/MockStakingV1.sol"; +// import {MockStakingV1Deployer} from "./mocks/MockStakingV1Deployer.sol"; +// import "./constants.sol"; + +// abstract contract VotingPowerTest is Test { +// IERC20 internal lqty; +// IERC20 internal lusd; +// ILQTYStaking internal stakingV1; + +// address internal constant user = address(0xF977814e90dA44bFA03b6295A0616a897441aceC); +// address internal constant user2 = address(0x10C9cff3c4Faa8A60cB8506a7A99411E6A199038); +// address internal constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); + +// uint256 private constant REGISTRATION_FEE = 1e18; +// uint256 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; +// uint256 private constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; +// uint256 private constant UNREGISTRATION_AFTER_EPOCHS = 4; +// uint256 private constant VOTING_THRESHOLD_FACTOR = 0.04e18; +// uint256 private constant MIN_CLAIM = 500e18; +// uint256 private constant MIN_ACCRUAL = 1000e18; +// uint256 private constant EPOCH_DURATION = 604800; +// uint256 private constant EPOCH_VOTING_CUTOFF = 518400; + +// Governance private governance; +// address[] private initialInitiatives; +// address private baseInitiative1; + +// function setUp() public virtual { +// IGovernance.Configuration memory config = IGovernance.Configuration({ +// registrationFee: REGISTRATION_FEE, +// registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, +// unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, +// unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, +// votingThresholdFactor: VOTING_THRESHOLD_FACTOR, +// minClaim: MIN_CLAIM, +// minAccrual: MIN_ACCRUAL, +// epochStart: uint32(block.timestamp - EPOCH_DURATION), +// epochDuration: EPOCH_DURATION, +// epochVotingCutoff: EPOCH_VOTING_CUTOFF +// }); - // // // But how do we sum stuff with different TS? - // // // We need to sum the total and sum the % of average ts - // uint88 votes_2 = 15; - // uint32 time_2 = current_time - 15; +// governance = new Governance( +// address(lqty), address(lusd), address(stakingV1), address(lusd), config, address(this), new address[](0) +// ); + +// baseInitiative1 = address(new BribeInitiative(address(governance), address(lusd), address(lqty))); +// initialInitiatives.push(baseInitiative1); - // uint240 power_2 = governance.lqtyToVotes(votes_2, current_time, time_2); +// governance.registerInitialInitiatives(initialInitiatives); +// } - // uint240 total_power = power + power_2; +// /// Compare with removing all and re-allocating all at the 2nd epoch +// // forge test --match-test test_math_soundness -vv +// function test_math_soundness() public { +// // Given a Multiplier, I can wait 8 times more time +// // Or use 8 times more amt +// uint8 multiplier = 2; - // assertLe(total_power, uint240(type(uint88).max), "LT"); +// uint256 lqtyAmount = 1e18; - // uint88 total_liquity = votes + votes_2; +// uint256 powerInTheFuture = governance.lqtyToVotes(lqtyAmount, multiplier + 1, 1); +// // Amt when delta is 1 +// // 0 when delta is 0 +// uint256 powerFromMoreDeposits = +// governance.lqtyToVotes(lqtyAmount * multiplier, uint32(block.timestamp + 1), uint32(block.timestamp)); - // uint32 avgTs = _calculateAverageTimestamp(time, time_2, votes, total_liquity); +// assertEq(powerInTheFuture, powerFromMoreDeposits, "Same result"); +// } + +// function test_math_soundness_fuzz(uint32 multiplier) public view { +// vm.assume(multiplier < type(uint32).max - 1); +// uint256 lqtyAmount = 1e10; + +// uint256 powerInTheFuture = governance.lqtyToVotes(lqtyAmount, multiplier + 1, 1); + +// // Amt when delta is 1 +// // 0 when delta is 0 +// uint256 powerFromMoreDeposits = +// governance.lqtyToVotes(lqtyAmount * multiplier, uint32(block.timestamp + 1), uint32(block.timestamp)); - // console.log("votes", votes); - // console.log("time", current_time - time); - // console.log("power", power); +// assertEq(powerInTheFuture, powerFromMoreDeposits, "Same result"); +// } - // console.log("votes_2", votes_2); - // console.log("time_2", current_time - time_2); - // console.log("power_2", power_2); +// // This test prepares for comparing votes and vetos for state +// // forge test --match-test test_we_can_compare_votes_and_vetos -vv +// // function test_we_can_compare_votes_and_vetos() public { +// /// TODO AUDIT Known bug with rounding math +// // uint32 current_time = 123123123; +// // vm.warp(current_time); +// // // State at X +// // // State made of X and Y +// // uint32 time = current_time - 124; +// // uint256 votes = 124; +// // uint256 power = governance.lqtyToVotes(votes, current_time, time); - // uint256 total_power_from_avg = governance.lqtyToVotes(total_liquity, current_time, avgTs); +// // assertEq(power, (_averageAge(current_time, time)) * votes, "simple product"); - // console.log("total_liquity", total_liquity); - // console.log("avgTs", current_time - avgTs); - // console.log("total_power_from_avg", total_power_from_avg); +// // // if it's a simple product we have the properties of multiplication, we can get back the value by dividing the tiem +// // uint256 resultingVotes = uint256(power / _averageAge(current_time, time)); - // // Now remove the same math so we show that the rounding can be weaponized, let's see +// // assertEq(resultingVotes, votes, "We can get it back"); - // // WTF +// // // If we can get it back, then we can also perform other operations like addition and subtraction +// // // Easy when same TS - // // Prev, new, prev new - // // AVG TS is the prev outer - // // New Inner is time - // uint32 attacked_avg_ts = _calculateAverageTimestamp( - // avgTs, - // time_2, // User removes their time - // total_liquity, - // votes // Votes = total_liquity - Vote_2 - // ); +// // // // But how do we sum stuff with different TS? +// // // // We need to sum the total and sum the % of average ts +// // uint256 votes_2 = 15; +// // uint32 time_2 = current_time - 15; - // // NOTE: != time due to rounding error - // console.log("attacked_avg_ts", current_time - attacked_avg_ts); +// // uint256 power_2 = governance.lqtyToVotes(votes_2, current_time, time_2); - // // BASIC VOTING TEST - // // AFTER VOTING POWER IS X - // // AFTER REMOVING VOTING IS 0 +// // uint256 total_power = power + power_2; - // // Add a middle of random shit - // // Show that the math remains sound +// // assertLe(total_power, uint256(type(uint256).max), "LT"); - // // Off by 40 BPS????? WAYY TOO MUCH | SOMETHING IS WRONG +// // uint256 total_liquity = votes + votes_2; - // // It doesn't sum up exactly becasue of rounding errors - // // But we need the rounding error to be in favour of the protocol - // // And currently they are not - // assertEq(total_power, total_power_from_avg, "Sums up"); +// // uint32 avgTs = _calculateAverageTimestamp(time, time_2, votes, total_liquity); - // // From those we can find the average timestamp - // uint88 resultingReturnedVotes = uint88(total_power_from_avg / _averageAge(current_time, time)); - // assertEq(resultingReturnedVotes, total_liquity, "Lqty matches"); - // } +// // console.log("votes", votes); +// // console.log("time", current_time - time); +// // console.log("power", power); - // forge test --match-test test_crit_user_can_dilute_total_votes -vv - function test_crit_user_can_dilute_total_votes() public { - // User A deposits normaly - vm.startPrank(user); +// // console.log("votes_2", votes_2); +// // console.log("time_2", current_time - time_2); +// // console.log("power_2", power_2); - _stakeLQTY(user, 124); +// // uint256 total_power_from_avg = governance.lqtyToVotes(total_liquity, current_time, avgTs); - vm.warp(block.timestamp + 124 - 15); +// // console.log("total_liquity", total_liquity); +// // console.log("avgTs", current_time - avgTs); +// // console.log("total_power_from_avg", total_power_from_avg); - vm.startPrank(user2); - _stakeLQTY(user2, 15); +// // // Now remove the same math so we show that the rounding can be weaponized, let's see - vm.warp(block.timestamp + 15); +// // // WTF - vm.startPrank(user); - _allocate(address(baseInitiative1), 124, 0); - uint256 user1_avg = _getAverageTS(baseInitiative1); +// // // Prev, new, prev new +// // // AVG TS is the prev outer +// // // New Inner is time +// // uint32 attacked_avg_ts = _calculateAverageTimestamp( +// // avgTs, +// // time_2, // User removes their time +// // total_liquity, +// // votes // Votes = total_liquity - Vote_2 +// // ); - vm.startPrank(user2); - _allocate(address(baseInitiative1), 15, 0); - _allocate(address(baseInitiative1), 0, 0); +// // // NOTE: != time due to rounding error +// // console.log("attacked_avg_ts", current_time - attacked_avg_ts); - uint256 griefed_avg = _getAverageTS(baseInitiative1); +// // // BASIC VOTING TEST +// // // AFTER VOTING POWER IS X +// // // AFTER REMOVING VOTING IS 0 - uint256 vote_power_1 = governance.lqtyToVotes(124, uint32(block.timestamp), uint32(user1_avg)); - uint256 vote_power_2 = governance.lqtyToVotes(124, uint32(block.timestamp), uint32(griefed_avg)); +// // // Add a middle of random shit +// // // Show that the math remains sound - console.log("vote_power_1", vote_power_1); - console.log("vote_power_2", vote_power_2); +// // // Off by 40 BPS????? WAYY TOO MUCH | SOMETHING IS WRONG - // assertEq(user1_avg, griefed_avg, "same avg"); // BREAKS, OFF BY ONE +// // // It doesn't sum up exactly becasue of rounding errors +// // // But we need the rounding error to be in favour of the protocol +// // // And currently they are not +// // assertEq(total_power, total_power_from_avg, "Sums up"); - // Causes a loss of power of 1 second per time this is done +// // // From those we can find the average timestamp +// // uint256 resultingReturnedVotes = uint256(total_power_from_avg / _averageAge(current_time, time)); +// // assertEq(resultingReturnedVotes, total_liquity, "Lqty matches"); +// // } - vm.startPrank(user); - _allocate(address(baseInitiative1), 0, 0); +// // forge test --match-test test_crit_user_can_dilute_total_votes -vv +// // TODO: convert to an offset-based test +// // function test_crit_user_can_dilute_total_votes() public { +// // // User A deposits normaly +// // vm.startPrank(user); - uint256 final_avg = _getAverageTS(baseInitiative1); - console.log("final_avg", final_avg); +// // _stakeLQTY(user, 124); - // This is not an issue, except for bribes, bribes can get the last claimer DOSS - } +// // vm.warp(block.timestamp + 124 - 15); - // forge test --match-test test_can_we_spam_to_revert -vv - function test_can_we_spam_to_revert() public { - // User A deposits normaly - vm.startPrank(user); +// // vm.startPrank(user2); +// // _stakeLQTY(user2, 15); - _stakeLQTY(user, 124); +// // vm.warp(block.timestamp + 15); - vm.warp(block.timestamp + 124); +// // vm.startPrank(user); +// // _allocate(address(baseInitiative1), 124, 0); +// // uint256 user1_avg = _getAverageTS(baseInitiative1); - vm.startPrank(user2); - _stakeLQTY(user2, 15); +// // vm.startPrank(user2); +// // _allocate(address(baseInitiative1), 15, 0); +// // _reset(address(baseInitiative1)); - vm.startPrank(user); - _allocate(address(baseInitiative1), 124, 0); +// // uint256 griefed_avg = _getAverageTS(baseInitiative1); - vm.startPrank(user2); - _allocate(address(baseInitiative1), 15, 0); - _allocate(address(baseInitiative1), 0, 0); +// // uint256 vote_power_1 = governance.lqtyToVotes(124, uint32(block.timestamp), uint32(user1_avg)); +// // uint256 vote_power_2 = governance.lqtyToVotes(124, uint32(block.timestamp), uint32(griefed_avg)); - uint256 griefed_avg = _getAverageTS(baseInitiative1); - console.log("griefed_avg", griefed_avg); - console.log("block.timestamp", block.timestamp); +// // console.log("vote_power_1", vote_power_1); +// // console.log("vote_power_2", vote_power_2); - console.log("0?"); +// // // assertEq(user1_avg, griefed_avg, "same avg"); // BREAKS, OFF BY ONE - uint256 currentMagnifiedTs = uint120(block.timestamp) * uint120(1e26); +// // // Causes a loss of power of 1 second per time this is done - vm.startPrank(user2); - _allocate(address(baseInitiative1), 15, 0); - _allocate(address(baseInitiative1), 0, 0); +// // vm.startPrank(user); +// // _reset(address(baseInitiative1)); - uint256 ts = _getAverageTS(baseInitiative1); - uint256 delta = currentMagnifiedTs - ts; - console.log("griefed_avg", ts); - console.log("delta", delta); - console.log("currentMagnifiedTs", currentMagnifiedTs); +// // uint256 final_avg = _getAverageTS(baseInitiative1); +// // console.log("final_avg", final_avg); - console.log("0?"); - uint256 i; - while (i++ < 122) { - console.log("i", i); - _allocate(address(baseInitiative1), 15, 0); - _allocate(address(baseInitiative1), 0, 0); - } +// // // This is not an issue, except for bribes, bribes can get the last claimer DOSS +// // } - console.log("1?"); +// // forge test --match-test test_can_we_spam_to_revert -vv +// // function test_can_we_spam_to_revert() public { +// // // User A deposits normaly +// // vm.startPrank(user); - ts = _getAverageTS(baseInitiative1); - delta = currentMagnifiedTs - ts; - console.log("griefed_avg", ts); - console.log("delta", delta); - console.log("currentMagnifiedTs", currentMagnifiedTs); +// // _stakeLQTY(user, 124); - // One more time - _allocate(address(baseInitiative1), 15, 0); - _allocate(address(baseInitiative1), 0, 0); - _allocate(address(baseInitiative1), 15, 0); - _allocate(address(baseInitiative1), 0, 0); - _allocate(address(baseInitiative1), 15, 0); - _allocate(address(baseInitiative1), 0, 0); - _allocate(address(baseInitiative1), 15, 0); +// // vm.warp(block.timestamp + 124); - /// NOTE: Keep 1 wei to keep rounding error - _allocate(address(baseInitiative1), 1, 0); - - ts = _getAverageTS(baseInitiative1); - console.log("griefed_avg", ts); - - vm.startPrank(user); - _allocate(address(baseInitiative1), 0, 0); - _allocate(address(baseInitiative1), 124, 0); - - ts = _getAverageTS(baseInitiative1); - console.log("end_ts", ts); - } - - // forge test --match-test test_basic_reset_flow -vv - function test_basic_reset_flow() public { - vm.startPrank(user); - // =========== epoch 1 ================== - // 1. user stakes lqty - int88 lqtyAmount = 2e18; - _stakeLQTY(user, uint88(lqtyAmount / 2)); - - // user allocates to baseInitiative1 - _allocate(address(baseInitiative1), lqtyAmount / 2, 0); // 50% to it - (uint88 allocatedLQTY,) = governance.userStates(user); - assertEq(allocatedLQTY, uint88(lqtyAmount / 2), "half"); - - _allocate(address(baseInitiative1), lqtyAmount / 2, 0); // 50% to it - assertEq(allocatedLQTY, uint88(lqtyAmount / 2), "still half, the math is absolute now"); - } - - // forge test --match-test test_cutoff_logic -vv - function test_cutoff_logic() public { - vm.startPrank(user); - // =========== epoch 1 ================== - // 1. user stakes lqty - int88 lqtyAmount = 2e18; - _stakeLQTY(user, uint88(lqtyAmount)); - - // user allocates to baseInitiative1 - _allocate(address(baseInitiative1), lqtyAmount / 2, 0); // 50% to it - (uint88 allocatedLQTY,) = governance.userStates(user); - assertEq(allocatedLQTY, uint88(lqtyAmount / 2), "Half"); - - // Go to Cutoff - // See that you can reduce - // See that you can Veto as much as you want - vm.warp(block.timestamp + (EPOCH_DURATION) - governance.EPOCH_VOTING_CUTOFF() + 1); // warp to end of second epoch before the voting cutoff - - // Go to end of epoch, lazy math - while (!(governance.secondsWithinEpoch() > governance.EPOCH_VOTING_CUTOFF())) { - vm.warp(block.timestamp + 6 hours); - } - assertTrue( - governance.secondsWithinEpoch() > governance.EPOCH_VOTING_CUTOFF(), "We should not be able to vote more" - ); - - vm.expectRevert(); // cannot allocate more - _allocate(address(baseInitiative1), lqtyAmount, 0); - - // Can allocate less - _allocate(address(baseInitiative1), lqtyAmount / 2 - 1, 0); - - // Can Veto more than allocate - _allocate(address(baseInitiative1), 0, lqtyAmount); - } - - // Check if Flashloan can be used to cause issues? - // A flashloan would cause issues in the measure in which it breaks any specific property - // Or expectation - - // Remove votes - // Removing votes would force you to exclusively remove - // You can always remove at any time afacit - // Removing just updates that + the weights - // The weights are the avg time * the number - - function _getAverageTS(address initiative) internal view returns (uint256) { - (,, uint120 averageStakingTimestampVoteLQTY,,) = governance.initiativeStates(initiative); - - return averageStakingTimestampVoteLQTY; - } - - function _stakeLQTY(address _user, uint88 amount) internal { - address userProxy = governance.deriveUserProxyAddress(_user); - lqty.approve(address(userProxy), amount); - - governance.depositLQTY(amount); - } - - function _allocate(address initiative, int88 votes, int88 vetos) internal { - address[] memory initiativesToReset = new address[](3); - initiativesToReset[0] = baseInitiative1; - initiativesToReset[1] = baseInitiative2; - initiativesToReset[2] = baseInitiative3; - address[] memory initiatives = new address[](1); - initiatives[0] = initiative; - int88[] memory deltaLQTYVotes = new int88[](1); - deltaLQTYVotes[0] = votes; - int88[] memory deltaLQTYVetos = new int88[](1); - deltaLQTYVetos[0] = vetos; - - governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotes, deltaLQTYVetos); - } - - function _reset() internal { - address[] memory initiativesToReset = new address[](3); - initiativesToReset[0] = baseInitiative1; - initiativesToReset[1] = baseInitiative2; - initiativesToReset[2] = baseInitiative3; - - governance.resetAllocations(initiativesToReset, true); - } -} +// // vm.startPrank(user2); +// // _stakeLQTY(user2, 15); + +// // vm.startPrank(user); +// // _allocate(address(baseInitiative1), 124, 0); + +// // vm.startPrank(user2); +// // _allocate(address(baseInitiative1), 15, 0); +// // _reset(address(baseInitiative1)); + +// // uint256 griefed_avg = _getAverageTS(baseInitiative1); +// // console.log("griefed_avg", griefed_avg); +// // console.log("block.timestamp", block.timestamp); + +// // console.log("0?"); + +// // uint256 currentMagnifiedTs = uint256(block.timestamp) * uint256(1e26); + +// // vm.startPrank(user2); +// // _allocate(address(baseInitiative1), 15, 0); +// // _reset(address(baseInitiative1)); + +// // uint256 ts = _getAverageTS(baseInitiative1); +// // uint256 delta = currentMagnifiedTs - ts; +// // console.log("griefed_avg", ts); +// // console.log("delta", delta); +// // console.log("currentMagnifiedTs", currentMagnifiedTs); + +// // console.log("0?"); +// // uint256 i; +// // while (i++ < 122) { +// // console.log("i", i); +// // _allocate(address(baseInitiative1), 15, 0); +// // _reset(address(baseInitiative1)); +// // } + +// // console.log("1?"); + +// // ts = _getAverageTS(baseInitiative1); +// // delta = currentMagnifiedTs - ts; +// // console.log("griefed_avg", ts); +// // console.log("delta", delta); +// // console.log("currentMagnifiedTs", currentMagnifiedTs); + +// // // One more time +// // _allocate(address(baseInitiative1), 15, 0); +// // _reset(address(baseInitiative1)); +// // _allocate(address(baseInitiative1), 15, 0); +// // _reset(address(baseInitiative1)); +// // _allocate(address(baseInitiative1), 15, 0); +// // _reset(address(baseInitiative1)); +// // _allocate(address(baseInitiative1), 15, 0); + +// // /// NOTE: Keep 1 wei to keep rounding error +// // _allocate(address(baseInitiative1), 1, 0); + +// // ts = _getAverageTS(baseInitiative1); +// // console.log("griefed_avg", ts); + +// // vm.startPrank(user); +// // _reset(address(baseInitiative1)); +// // _allocate(address(baseInitiative1), 124, 0); + +// // ts = _getAverageTS(baseInitiative1); +// // console.log("end_ts", ts); +// // } + +// // forge test --match-test test_basic_reset_flow -vv +// function test_basic_reset_flow() public { +// vm.startPrank(user); +// // =========== epoch 1 ================== +// // 1. user stakes lqty +// int256 lqtyAmount = 2e18; +// _stakeLQTY(user, uint256(lqtyAmount / 2)); + +// // user allocates to baseInitiative1 +// _allocate(address(baseInitiative1), lqtyAmount / 2, 0); // 50% to it +// (,,uint256 allocatedLQTY,) = governance.userStates(user); +// assertEq(allocatedLQTY, uint256(lqtyAmount / 2), "half"); + +// _allocate(address(baseInitiative1), lqtyAmount / 2, 0); // 50% to it +// assertEq(allocatedLQTY, uint256(lqtyAmount / 2), "still half, the math is absolute now"); +// } + +// // forge test --match-test test_cutoff_logic -vv +// function test_cutoff_logic() public { +// vm.startPrank(user); +// // =========== epoch 1 ================== +// // 1. user stakes lqty +// int256 lqtyAmount = 2e18; +// _stakeLQTY(user, uint256(lqtyAmount)); + +// // user allocates to baseInitiative1 +// _allocate(address(baseInitiative1), lqtyAmount / 2, 0); // 50% to it +// (,,uint256 allocatedLQTY,) = governance.userStates(user); +// assertEq(allocatedLQTY, uint256(lqtyAmount / 2), "Half"); + +// // Go to Cutoff +// // See that you can reduce +// // See that you can Veto as much as you want +// vm.warp(block.timestamp + (EPOCH_DURATION) - governance.EPOCH_VOTING_CUTOFF() + 1); // warp to end of second epoch before the voting cutoff + +// // Go to end of epoch, lazy math +// while (!(governance.secondsWithinEpoch() > governance.EPOCH_VOTING_CUTOFF())) { +// vm.warp(block.timestamp + 6 hours); +// } +// assertTrue( +// governance.secondsWithinEpoch() > governance.EPOCH_VOTING_CUTOFF(), "We should not be able to vote more" +// ); + +// // Should fail to allocate more +// _tryAllocate(address(baseInitiative1), lqtyAmount, 0, "Cannot increase"); + +// // Can allocate less +// _allocate(address(baseInitiative1), lqtyAmount / 2 - 1, 0); + +// // Can Veto more than allocate +// _allocate(address(baseInitiative1), 0, lqtyAmount); +// } + +// // Check if Flashloan can be used to cause issues? +// // A flashloan would cause issues in the measure in which it breaks any specific property +// // Or expectation + +// // Remove votes +// // Removing votes would force you to exclusively remove +// // You can always remove at any time afacit +// // Removing just updates that + the weights +// // The weights are the avg time * the number + +// function _getInitiativeOffset(address initiative) internal view returns (uint256) { +// (,uint256 voteOffset,,,) = governance.initiativeStates(initiative); + +// return voteOffset; +// } + +// function _stakeLQTY(address _user, uint256 amount) internal { +// address userProxy = governance.deriveUserProxyAddress(_user); +// lqty.approve(address(userProxy), amount); + +// governance.depositLQTY(amount); +// } + +// // Helper function to get the current prank address +// function currentUser() external view returns (address) { +// return msg.sender; +// } + +// function _prepareAllocateParams(address initiative, int256 votes, int256 vetos) +// internal +// view +// returns ( +// address[] memory initiativesToReset, +// address[] memory initiatives, +// int256[] memory absoluteLQTYVotes, +// int256[] memory absoluteLQTYVetos +// ) +// { +// (uint256 currentVote, uint256 currentVeto,) = +// governance.lqtyAllocatedByUserToInitiative(this.currentUser(), address(initiative)); +// if (currentVote != 0 || currentVeto != 0) { +// initiativesToReset = new address[](1); +// initiativesToReset[0] = address(initiative); +// } + +// initiatives = new address[](1); +// initiatives[0] = initiative; +// absoluteLQTYVotes = new int256[](1); +// absoluteLQTYVotes[0] = votes; +// absoluteLQTYVetos = new int256[](1); +// absoluteLQTYVetos[0] = vetos; +// } + +// function _allocate(address initiative, int256 votes, int256 vetos) internal { +// ( +// address[] memory initiativesToReset, +// address[] memory initiatives, +// int256[] memory absoluteLQTYVotes, +// int256[] memory absoluteLQTYVetos +// ) = _prepareAllocateParams(initiative, votes, vetos); + +// governance.allocateLQTY(initiativesToReset, initiatives, absoluteLQTYVotes, absoluteLQTYVetos); +// } + +// function _tryAllocate(address initiative, int256 votes, int256 vetos, bytes memory expectedError) internal { +// ( +// address[] memory initiativesToReset, +// address[] memory initiatives, +// int256[] memory absoluteLQTYVotes, +// int256[] memory absoluteLQTYVetos +// ) = _prepareAllocateParams(initiative, votes, vetos); + +// vm.expectRevert(expectedError); +// governance.allocateLQTY(initiativesToReset, initiatives, absoluteLQTYVotes, absoluteLQTYVetos); +// } + +// function _reset(address initiative) internal { +// address[] memory initiativesToReset = new address[](1); +// initiativesToReset[0] = initiative; +// governance.resetAllocations(initiativesToReset, true); +// } +// } + +// contract MockedVotingPowerTest is VotingPowerTest, MockStakingV1Deployer { +// function setUp() public override { +// (MockStakingV1 mockStakingV1, MockERC20Tester mockLQTY, MockERC20Tester mockLUSD) = deployMockStakingV1(); +// mockLQTY.mint(user, 2e18); +// mockLQTY.mint(user2, 15); + +// lqty = mockLQTY; +// lusd = mockLUSD; +// stakingV1 = mockStakingV1; + +// super.setUp(); +// } +// } + +// contract ForkedVotingPowerTest is VotingPowerTest { +// function setUp() public override { +// vm.createSelectFork(vm.rpcUrl("mainnet"), 20430000); + +// lqty = IERC20(MAINNET_LQTY); +// lusd = IERC20(MAINNET_LUSD); +// stakingV1 = ILQTYStaking(MAINNET_LQTY_STAKING); + +// super.setUp(); +// } +// } diff --git a/test/constants.sol b/test/constants.sol new file mode 100644 index 00000000..81cb6fad --- /dev/null +++ b/test/constants.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +address constant MAINNET_USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; +address constant MAINNET_LQTY = 0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D; +address constant MAINNET_LUSD = 0x5f98805A4E8be255a32880FDeC7F6728C6568bA0; +address constant MAINNET_LQTY_STAKING = 0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d; +address constant MAINNET_ACTIVE_POOL = 0xDf9Eb223bAFBE5c5271415C75aeCD68C21fE3D7F; +address constant MAINNET_BORROWER_OPERATIONS = 0x24179CD81c9e782A4096035f7eC97fB8B783e007; +address constant MAINNET_TROVE_MANAGER = 0xA39739EF8b0231DbFA0DcdA07d7e29faAbCf4bb2; diff --git a/test/mocks/MaliciousInitiative.sol b/test/mocks/MaliciousInitiative.sol index 494d9284..ffb3e82a 100644 --- a/test/mocks/MaliciousInitiative.sol +++ b/test/mocks/MaliciousInitiative.sol @@ -29,16 +29,16 @@ contract MaliciousInitiative is IInitiative { } // Do stuff on each hook - function onRegisterInitiative(uint16) external view override { + function onRegisterInitiative(uint256) external view override { _performRevertBehaviour(revertBehaviours[FunctionType.REGISTER]); } - function onUnregisterInitiative(uint16) external view override { + function onUnregisterInitiative(uint256) external view override { _performRevertBehaviour(revertBehaviours[FunctionType.UNREGISTER]); } function onAfterAllocateLQTY( - uint16, + uint256, address, IGovernance.UserState calldata, IGovernance.Allocation calldata, @@ -47,7 +47,7 @@ contract MaliciousInitiative is IInitiative { _performRevertBehaviour(revertBehaviours[FunctionType.ALLOCATE]); } - function onClaimForInitiative(uint16, uint256) external view override { + function onClaimForInitiative(uint256, uint256) external view override { _performRevertBehaviour(revertBehaviours[FunctionType.CLAIM]); } diff --git a/test/mocks/MockERC20Tester.sol b/test/mocks/MockERC20Tester.sol index f239dca5..6425ed19 100644 --- a/test/mocks/MockERC20Tester.sol +++ b/test/mocks/MockERC20Tester.sol @@ -1,24 +1,37 @@ -// SPDX-License-Identifier: GPL-2.0 -pragma solidity ^0.8.0; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; -import {MockERC20} from "forge-std/mocks/MockERC20.sol"; +import {Ownable} from "openzeppelin/contracts/access/Ownable.sol"; +import {ERC20} from "openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IERC20} from "openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ERC20Permit} from "openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import {IERC20Permit} from "openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import {ILUSD} from "../../src/interfaces/ILUSD.sol"; +import {ILQTY} from "../../src/interfaces/ILQTY.sol"; -contract MockERC20Tester is MockERC20 { - address owner; +contract MockERC20Tester is ILUSD, ILQTY, ERC20Permit, Ownable { + mapping(address spender => bool) public mock_isWildcardSpender; - modifier onlyOwner() { - require(msg.sender == owner); - _; + constructor(string memory name, string memory symbol) ERC20Permit(name) ERC20(name, symbol) Ownable(msg.sender) {} + + // LUSD & LQTY expose this + function domainSeparator() external view returns (bytes32) { + return _domainSeparatorV4(); + } + + function nonces(address owner) public view virtual override(IERC20Permit, ERC20Permit) returns (uint256) { + return super.nonces(owner); } - constructor(address recipient, uint256 mintAmount, string memory name, string memory symbol, uint8 decimals) { - super.initialize(name, symbol, decimals); - _mint(recipient, mintAmount); + function allowance(address owner, address spender) public view virtual override(IERC20, ERC20) returns (uint256) { + return mock_isWildcardSpender[spender] ? type(uint256).max : super.allowance(owner, spender); + } - owner = msg.sender; + function mint(address account, uint256 value) external onlyOwner { + _mint(account, value); } - function mint(address to, uint256 amount) public onlyOwner { - _mint(to, amount); + function mock_setWildcardSpender(address spender, bool allowed) external onlyOwner { + mock_isWildcardSpender[spender] = allowed; } } diff --git a/test/mocks/MockGovernance.sol b/test/mocks/MockGovernance.sol index ee94c8c7..045f2ee7 100644 --- a/test/mocks/MockGovernance.sol +++ b/test/mocks/MockGovernance.sol @@ -2,33 +2,33 @@ pragma solidity ^0.8.24; contract MockGovernance { - uint16 private __epoch; + uint256 private __epoch; - uint32 public constant EPOCH_START = 0; - uint32 public constant EPOCH_DURATION = 7 days; + uint256 public constant EPOCH_START = 0; + uint256 public constant EPOCH_DURATION = 7 days; function claimForInitiative(address) external pure returns (uint256) { return 1000e18; } - function setEpoch(uint16 _epoch) external { + function setEpoch(uint256 _epoch) external { __epoch = _epoch; } - function epoch() external view returns (uint16) { + function epoch() external view returns (uint256) { return __epoch; } - function _averageAge(uint120 _currentTimestamp, uint120 _averageTimestamp) internal pure returns (uint120) { + function _averageAge(uint256 _currentTimestamp, uint256 _averageTimestamp) internal pure returns (uint256) { if (_averageTimestamp == 0 || _currentTimestamp < _averageTimestamp) return 0; return _currentTimestamp - _averageTimestamp; } - function lqtyToVotes(uint88 _lqtyAmount, uint120 _currentTimestamp, uint120 _averageTimestamp) + function lqtyToVotes(uint256 _lqtyAmount, uint256 _currentTimestamp, uint256 _averageTimestamp) public pure - returns (uint208) + returns (uint256) { - return uint208(_lqtyAmount) * uint208(_averageAge(_currentTimestamp, _averageTimestamp)); + return uint256(_lqtyAmount) * uint256(_averageAge(_currentTimestamp, _averageTimestamp)); } } diff --git a/test/mocks/MockInitiative.sol b/test/mocks/MockInitiative.sol index 47205589..91030dda 100644 --- a/test/mocks/MockInitiative.sol +++ b/test/mocks/MockInitiative.sol @@ -12,31 +12,31 @@ contract MockInitiative is IInitiative { } /// @inheritdoc IInitiative - function onRegisterInitiative(uint16) external virtual override { + function onRegisterInitiative(uint256) external virtual override { governance.registerInitiative(address(0)); } /// @inheritdoc IInitiative - function onUnregisterInitiative(uint16) external virtual override { + function onUnregisterInitiative(uint256) external virtual override { governance.unregisterInitiative(address(0)); } /// @inheritdoc IInitiative function onAfterAllocateLQTY( - uint16, + uint256, address, IGovernance.UserState calldata, IGovernance.Allocation calldata, IGovernance.InitiativeState calldata ) external virtual { address[] memory initiatives = new address[](0); - int88[] memory deltaLQTYVotes = new int88[](0); - int88[] memory deltaLQTYVetos = new int88[](0); + int256[] memory deltaLQTYVotes = new int256[](0); + int256[] memory deltaLQTYVetos = new int256[](0); governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); } /// @inheritdoc IInitiative - function onClaimForInitiative(uint16, uint256) external virtual override { + function onClaimForInitiative(uint256, uint256) external virtual override { governance.claimForInitiative(address(0)); } } diff --git a/test/mocks/MockStakingV1.sol b/test/mocks/MockStakingV1.sol index 12bac7ee..d17d3ac1 100644 --- a/test/mocks/MockStakingV1.sol +++ b/test/mocks/MockStakingV1.sol @@ -1,24 +1,104 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; +import {Ownable} from "openzeppelin/contracts/access/Ownable.sol"; +import {IERC20} from "openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Math} from "openzeppelin/contracts/utils/math/Math.sol"; +import {EnumerableSet} from "openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {ILQTYStaking} from "../../src/interfaces/ILQTYStaking.sol"; -contract MockStakingV1 { - IERC20 public immutable lqty; +contract MockStakingV1 is ILQTYStaking, Ownable { + using EnumerableSet for EnumerableSet.AddressSet; - mapping(address => uint256) public stakes; + IERC20 internal immutable _lqty; + IERC20 internal immutable _lusd; - constructor(address _lqty) { - lqty = IERC20(_lqty); + uint256 public totalLQTYStaked; + EnumerableSet.AddressSet internal _stakers; + mapping(address staker => uint256) public stakes; + mapping(address staker => uint256) internal _pendingLUSDGain; + mapping(address staker => uint256) internal _pendingETHGain; + + constructor(IERC20 lqty, IERC20 lusd) Ownable(msg.sender) { + _lqty = lqty; + _lusd = lusd; + } + + function _resetGains() internal returns (uint256 lusdGain, uint256 ethGain) { + lusdGain = _pendingLUSDGain[msg.sender]; + ethGain = _pendingETHGain[msg.sender]; + + _pendingLUSDGain[msg.sender] = 0; + _pendingETHGain[msg.sender] = 0; + } + + function _payoutGains(uint256 lusdGain, uint256 ethGain) internal { + _lusd.transfer(msg.sender, lusdGain); + (bool success,) = msg.sender.call{value: ethGain}(""); + require(success, "LQTYStaking: Failed to send accumulated ETHGain"); } - function stake(uint256 _LQTYamount) external { - stakes[msg.sender] += _LQTYamount; - lqty.transferFrom(msg.sender, address(this), _LQTYamount); + function stake(uint256 amount) external override { + require(amount > 0, "LQTYStaking: Amount must be non-zero"); + uint256 oldStake = stakes[msg.sender]; + (uint256 lusdGain, uint256 ethGain) = oldStake > 0 ? _resetGains() : (0, 0); + + stakes[msg.sender] += amount; + totalLQTYStaked += amount; + _stakers.add(msg.sender); + + _lqty.transferFrom(msg.sender, address(this), amount); + if (oldStake > 0) _payoutGains(lusdGain, ethGain); } - function unstake(uint256 _LQTYamount) external { - stakes[msg.sender] -= _LQTYamount; - lqty.transfer(msg.sender, _LQTYamount); + function unstake(uint256 amount) external override { + require(stakes[msg.sender] > 0, "LQTYStaking: User must have a non-zero stake"); + (uint256 lusdGain, uint256 ethGain) = _resetGains(); + + if (amount > 0) { + uint256 withdrawn = Math.min(amount, stakes[msg.sender]); + if ((stakes[msg.sender] -= withdrawn) == 0) _stakers.remove(msg.sender); + totalLQTYStaked -= withdrawn; + + _lqty.transfer(msg.sender, withdrawn); + } + + _payoutGains(lusdGain, ethGain); + } + + function getPendingLUSDGain(address user) external view override returns (uint256) { + return _pendingLUSDGain[user]; + } + + function getPendingETHGain(address user) external view override returns (uint256) { + return _pendingETHGain[user]; + } + + function setAddresses(address, address, address, address, address) external override {} + function increaseF_ETH(uint256) external override {} + function increaseF_LUSD(uint256) external override {} + + function mock_addLUSDGain(uint256 amount) external onlyOwner { + uint256 numStakers = _stakers.length(); + assert(numStakers == 0 || totalLQTYStaked > 0); + + for (uint256 i = 0; i < numStakers; ++i) { + address staker = _stakers.at(i); + assert(stakes[staker] > 0); + _pendingLUSDGain[staker] += amount * stakes[staker] / totalLQTYStaked; + } + + _lusd.transferFrom(msg.sender, address(this), amount); + } + + function mock_addETHGain() external payable onlyOwner { + uint256 numStakers = _stakers.length(); + assert(numStakers == 0 || totalLQTYStaked > 0); + + for (uint256 i = 0; i < numStakers; ++i) { + address staker = _stakers.at(i); + assert(stakes[staker] > 0); + _pendingETHGain[staker] += msg.value * stakes[staker] / totalLQTYStaked; + } } } diff --git a/test/mocks/MockStakingV1Deployer.sol b/test/mocks/MockStakingV1Deployer.sol new file mode 100644 index 00000000..c3ec94a3 --- /dev/null +++ b/test/mocks/MockStakingV1Deployer.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {MockERC20Tester} from "./MockERC20Tester.sol"; +import {MockStakingV1} from "./MockStakingV1.sol"; + +abstract contract MockStakingV1Deployer is Test { + function deployMockStakingV1() + internal + returns (MockStakingV1 stakingV1, MockERC20Tester lqty, MockERC20Tester lusd) + { + lqty = new MockERC20Tester("Liquity", "LQTY"); + vm.label(address(lqty), "LQTY"); + + lusd = new MockERC20Tester("Liquity USD", "LUSD"); + vm.label(address(lusd), "LUSD"); + + stakingV1 = new MockStakingV1(lqty, lusd); + + // Let stakingV1 spend anyone's LQTY without approval, like in the real LQTYStaking + lqty.mock_setWildcardSpender(address(stakingV1), true); + } +} diff --git a/test/recon/BeforeAfter.sol b/test/recon/BeforeAfter.sol index 4f52366a..c2a93fdd 100644 --- a/test/recon/BeforeAfter.sol +++ b/test/recon/BeforeAfter.sol @@ -9,12 +9,12 @@ import {Governance} from "src/Governance.sol"; abstract contract BeforeAfter is Setup, Asserts { struct Vars { - uint16 epoch; - mapping(address => Governance.InitiativeStatus) initiativeStatus; + uint256 epoch; + mapping(address => IGovernance.InitiativeStatus) initiativeStatus; // initiative => user => epoch => claimed - mapping(address => mapping(address => mapping(uint16 => bool))) claimedBribeForInitiativeAtEpoch; - mapping(address user => uint128 lqtyBalance) userLqtyBalance; - mapping(address user => uint128 lusdBalance) userLusdBalance; + mapping(address => mapping(address => mapping(uint256 => bool))) claimedBribeForInitiativeAtEpoch; + mapping(address user => uint256 lqtyBalance) userLqtyBalance; + mapping(address user => uint256 lusdBalance) userLusdBalance; } Vars internal _before; @@ -27,36 +27,36 @@ abstract contract BeforeAfter is Setup, Asserts { } function __before() internal { - uint16 currentEpoch = governance.epoch(); + uint256 currentEpoch = governance.epoch(); _before.epoch = currentEpoch; for (uint8 i; i < deployedInitiatives.length; i++) { address initiative = deployedInitiatives[i]; - (Governance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); + (IGovernance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); _before.initiativeStatus[initiative] = status; _before.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] = IBribeInitiative(initiative).claimedBribeAtEpoch(user, currentEpoch); } for (uint8 j; j < users.length; j++) { - _before.userLqtyBalance[users[j]] = uint128(lqty.balanceOf(user)); - _before.userLusdBalance[users[j]] = uint128(lusd.balanceOf(user)); + _before.userLqtyBalance[users[j]] = uint256(lqty.balanceOf(user)); + _before.userLusdBalance[users[j]] = uint256(lusd.balanceOf(user)); } } function __after() internal { - uint16 currentEpoch = governance.epoch(); + uint256 currentEpoch = governance.epoch(); _after.epoch = currentEpoch; for (uint8 i; i < deployedInitiatives.length; i++) { address initiative = deployedInitiatives[i]; - (Governance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); + (IGovernance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); _after.initiativeStatus[initiative] = status; _after.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] = IBribeInitiative(initiative).claimedBribeAtEpoch(user, currentEpoch); } for (uint8 j; j < users.length; j++) { - _after.userLqtyBalance[users[j]] = uint128(lqty.balanceOf(user)); - _after.userLusdBalance[users[j]] = uint128(lusd.balanceOf(user)); + _after.userLqtyBalance[users[j]] = uint256(lqty.balanceOf(user)); + _after.userLusdBalance[users[j]] = uint256(lusd.balanceOf(user)); } } } diff --git a/test/recon/CryticToFoundry.sol b/test/recon/CryticToFoundry.sol index 1660fb9c..3c3b2bdf 100644 --- a/test/recon/CryticToFoundry.sol +++ b/test/recon/CryticToFoundry.sol @@ -42,6 +42,7 @@ contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts { (,, uint256 votedPowerSum, uint256 govPower) = _getInitiativeStateAndGlobalState(); console.log("votedPowerSum", votedPowerSum); console.log("govPower", govPower); - assert(optimize_property_sum_of_initatives_matches_total_votes_insolvency()); + + assertTrue(optimize_property_sum_of_initatives_matches_total_votes_insolvency()); } } diff --git a/test/recon/Properties.sol b/test/recon/Properties.sol index b639746a..747a43c7 100644 --- a/test/recon/Properties.sol +++ b/test/recon/Properties.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-2.0 pragma solidity ^0.8.0; -import {BeforeAfter} from "./BeforeAfter.sol"; - // NOTE: OptimizationProperties imports Governance properties, to reuse a few fetchers import {OptimizationProperties} from "./properties/OptimizationProperties.sol"; import {BribeInitiativeProperties} from "./properties/BribeInitiativeProperties.sol"; diff --git a/test/recon/Setup.sol b/test/recon/Setup.sol index 446c4cc5..1182bd5d 100644 --- a/test/recon/Setup.sol +++ b/test/recon/Setup.sol @@ -7,20 +7,21 @@ import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {MockERC20Tester} from "../mocks/MockERC20Tester.sol"; import {MockStakingV1} from "../mocks/MockStakingV1.sol"; +import {MockStakingV1Deployer} from "../mocks/MockStakingV1Deployer.sol"; import {Governance} from "src/Governance.sol"; import {BribeInitiative} from "../../src/BribeInitiative.sol"; import {IBribeInitiative} from "../../src/interfaces/IBribeInitiative.sol"; import {IGovernance} from "src/interfaces/IGovernance.sol"; -abstract contract Setup is BaseSetup { +abstract contract Setup is BaseSetup, MockStakingV1Deployer { Governance governance; + MockStakingV1 internal stakingV1; MockERC20Tester internal lqty; MockERC20Tester internal lusd; IBribeInitiative internal initiative1; address internal user = address(this); address internal user2 = address(0x537C8f3d3E18dF5517a58B3fB9D9143697996802); // derived using makeAddrAndKey - address internal stakingV1; address internal userProxy; address[] internal users; address[] internal deployedInitiatives; @@ -28,18 +29,17 @@ abstract contract Setup is BaseSetup { bool internal claimedTwice; bool internal unableToClaim; - uint128 internal constant REGISTRATION_FEE = 1e18; - uint128 internal constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; - uint128 internal constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; - uint16 internal constant REGISTRATION_WARM_UP_PERIOD = 4; - uint16 internal constant UNREGISTRATION_AFTER_EPOCHS = 4; - uint128 internal constant VOTING_THRESHOLD_FACTOR = 0.04e18; - uint88 internal constant MIN_CLAIM = 500e18; - uint88 internal constant MIN_ACCRUAL = 1000e18; - uint32 internal constant EPOCH_DURATION = 604800; - uint32 internal constant EPOCH_VOTING_CUTOFF = 518400; + uint256 internal constant REGISTRATION_FEE = 1e18; + uint256 internal constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; + uint256 internal constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; + uint256 internal constant UNREGISTRATION_AFTER_EPOCHS = 4; + uint256 internal constant VOTING_THRESHOLD_FACTOR = 0.04e18; + uint256 internal constant MIN_CLAIM = 500e18; + uint256 internal constant MIN_ACCRUAL = 1000e18; + uint256 internal constant EPOCH_DURATION = 604800; + uint256 internal constant EPOCH_VOTING_CUTOFF = 518400; - uint120 magnifiedStartTS; + uint256 magnifiedStartTS; function setup() internal virtual override { vm.warp(block.timestamp + EPOCH_DURATION * 4); // Somehow Medusa goes back after the constructor @@ -47,28 +47,28 @@ abstract contract Setup is BaseSetup { users.push(user); users.push(user2); + (stakingV1, lqty, lusd) = deployMockStakingV1(); + uint256 initialMintAmount = type(uint88).max; - lqty = new MockERC20Tester(user, initialMintAmount, "Liquity", "LQTY", 18); - lusd = new MockERC20Tester(user, initialMintAmount, "Liquity USD", "LUSD", 18); + lqty.mint(user, initialMintAmount); lqty.mint(user2, initialMintAmount); + lusd.mint(user, initialMintAmount); - stakingV1 = address(new MockStakingV1(address(lqty))); governance = new Governance( address(lqty), address(lusd), - stakingV1, + address(stakingV1), address(lusd), // bold IGovernance.Configuration({ registrationFee: REGISTRATION_FEE, registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, votingThresholdFactor: VOTING_THRESHOLD_FACTOR, minClaim: MIN_CLAIM, minAccrual: MIN_ACCRUAL, - epochStart: uint32(block.timestamp - EPOCH_DURATION), - /// @audit will this work? + // backdate by 2 epochs to ensure new initiatives can be registered from the start + epochStart: uint256(block.timestamp - 2 * EPOCH_DURATION), epochDuration: EPOCH_DURATION, epochVotingCutoff: EPOCH_VOTING_CUTOFF }), @@ -91,7 +91,7 @@ abstract contract Setup is BaseSetup { governance.registerInitiative(address(initiative1)); - magnifiedStartTS = uint120(block.timestamp) * uint120(1e18); + magnifiedStartTS = uint256(block.timestamp) * uint256(1e18); } function _getDeployedInitiative(uint8 index) internal view returns (address initiative) { @@ -107,7 +107,7 @@ abstract contract Setup is BaseSetup { } function _getInitiativeStatus(address) internal returns (uint256) { - (Governance.InitiativeStatus status,,) = governance.getInitiativeState(_getDeployedInitiative(0)); + (IGovernance.InitiativeStatus status,,) = governance.getInitiativeState(_getDeployedInitiative(0)); return uint256(status); } } diff --git a/test/recon/TargetFunctions.sol b/test/recon/TargetFunctions.sol index f1a7335a..2d07e757 100644 --- a/test/recon/TargetFunctions.sol +++ b/test/recon/TargetFunctions.sol @@ -24,8 +24,8 @@ abstract contract TargetFunctions is GovernanceTargets, BribeInitiativeTargets { } // helper to simulate bold accrual in Governance contract - function helper_accrueBold(uint88 boldAmount) public withChecks { - boldAmount = uint88(boldAmount % lusd.balanceOf(user)); + function helper_accrueBold(uint256 boldAmount) public withChecks { + boldAmount = uint256(boldAmount % lusd.balanceOf(user)); // target contract is the user so it can transfer directly lusd.transfer(address(governance), boldAmount); } diff --git a/test/recon/properties/BribeInitiativeProperties.sol b/test/recon/properties/BribeInitiativeProperties.sol index 6c041115..07cb48c3 100644 --- a/test/recon/properties/BribeInitiativeProperties.sol +++ b/test/recon/properties/BribeInitiativeProperties.sol @@ -6,38 +6,38 @@ import {IBribeInitiative} from "../../../src/interfaces/IBribeInitiative.sol"; abstract contract BribeInitiativeProperties is BeforeAfter { function property_BI01() public { - uint16 currentEpoch = governance.epoch(); + uint256 currentEpoch = governance.epoch(); - for (uint8 i; i < deployedInitiatives.length; i++) { + for (uint256 i; i < deployedInitiatives.length; i++) { address initiative = deployedInitiatives[i]; - for (uint8 j; j < users.length; j++) { + for (uint256 j; j < users.length; j++) { // if the bool switches, the user has claimed their bribe for the epoch if ( _before.claimedBribeForInitiativeAtEpoch[initiative][users[j]][currentEpoch] != _after.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] ) { // calculate user balance delta of the bribe tokens - uint128 userLqtyBalanceDelta = _after.userLqtyBalance[users[j]] - _before.userLqtyBalance[users[j]]; - uint128 userLusdBalanceDelta = _after.userLusdBalance[users[j]] - _before.userLusdBalance[users[j]]; + uint256 userLqtyBalanceDelta = _after.userLqtyBalance[users[j]] - _before.userLqtyBalance[users[j]]; + uint256 userLusdBalanceDelta = _after.userLusdBalance[users[j]] - _before.userLusdBalance[users[j]]; // calculate balance delta as a percentage of the total bribe for this epoch // this is what user DOES receive - (uint128 bribeBoldAmount, uint128 bribeBribeTokenAmount) = + (uint256 bribeBoldAmount, uint256 bribeBribeTokenAmount,) = IBribeInitiative(initiative).bribeByEpoch(currentEpoch); - uint128 lqtyPercentageOfBribe = (userLqtyBalanceDelta * 10_000) / bribeBribeTokenAmount; - uint128 lusdPercentageOfBribe = (userLusdBalanceDelta * 10_000) / bribeBoldAmount; + uint256 lqtyPercentageOfBribe = (userLqtyBalanceDelta * 10_000) / bribeBribeTokenAmount; + uint256 lusdPercentageOfBribe = (userLusdBalanceDelta * 10_000) / bribeBoldAmount; // Shift right by 40 bits (128 - 88) to get the 88 most significant bits for needed downcasting to compare with lqty allocations - uint88 lqtyPercentageOfBribe88 = uint88(lqtyPercentageOfBribe >> 40); - uint88 lusdPercentageOfBribe88 = uint88(lusdPercentageOfBribe >> 40); + uint256 lqtyPercentageOfBribe88 = uint256(lqtyPercentageOfBribe >> 40); + uint256 lusdPercentageOfBribe88 = uint256(lusdPercentageOfBribe >> 40); // calculate user allocation percentage of total for this epoch // this is what user SHOULD receive - (uint88 lqtyAllocatedByUserAtEpoch,) = + (uint256 lqtyAllocatedByUserAtEpoch,) = IBribeInitiative(initiative).lqtyAllocatedByUserAtEpoch(users[j], currentEpoch); - (uint88 totalLQTYAllocatedAtEpoch,) = + (uint256 totalLQTYAllocatedAtEpoch,) = IBribeInitiative(initiative).totalLQTYAllocatedByEpoch(currentEpoch); - uint88 allocationPercentageOfTotal = + uint256 allocationPercentageOfTotal = (lqtyAllocatedByUserAtEpoch * 10_000) / totalLQTYAllocatedAtEpoch; // check that allocation percentage and received bribe percentage match @@ -61,12 +61,13 @@ abstract contract BribeInitiativeProperties is BeforeAfter { } function property_BI03() public { - for (uint8 i; i < deployedInitiatives.length; i++) { + for (uint256 i; i < deployedInitiatives.length; i++) { IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); - (uint88 voteLQTY,, uint16 epoch) = governance.lqtyAllocatedByUserToInitiative(user, deployedInitiatives[i]); + (uint256 voteLQTY,,,, uint256 epoch) = + governance.lqtyAllocatedByUserToInitiative(user, deployedInitiatives[i]); - try initiative.lqtyAllocatedByUserAtEpoch(user, epoch) returns (uint88 amt, uint120) { + try initiative.lqtyAllocatedByUserAtEpoch(user, epoch) returns (uint256 amt, uint256) { eq(voteLQTY, amt, "Allocation must match"); } catch { t(false, "Allocation doesn't match governance"); @@ -75,28 +76,28 @@ abstract contract BribeInitiativeProperties is BeforeAfter { } function property_BI04() public { - uint16 currentEpoch = governance.epoch(); - for (uint8 i; i < deployedInitiatives.length; i++) { + uint256 currentEpoch = governance.epoch(); + for (uint256 i; i < deployedInitiatives.length; i++) { IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); // NOTE: This doesn't revert in the future! - uint88 lastKnownLQTYAlloc = _getLastLQTYAllocationKnown(initiative, currentEpoch); + uint256 lastKnownLQTYAlloc = _getLastLQTYAllocationKnown(initiative, currentEpoch); // We compare when we don't get a revert (a change happened this epoch) - (uint88 voteLQTY,,,,) = governance.initiativeStates(deployedInitiatives[i]); + (uint256 voteLQTY,,,,) = governance.initiativeStates(deployedInitiatives[i]); eq(lastKnownLQTYAlloc, voteLQTY, "BI-04: Initiative Account matches governace"); } } - function _getLastLQTYAllocationKnown(IBribeInitiative initiative, uint16 targetEpoch) + function _getLastLQTYAllocationKnown(IBribeInitiative initiative, uint256 targetEpoch) internal view - returns (uint88) + returns (uint256) { - uint16 mostRecentTotalEpoch = initiative.getMostRecentTotalEpoch(); - (uint88 totalLQTYAllocatedAtEpoch,) = initiative.totalLQTYAllocatedByEpoch( + uint256 mostRecentTotalEpoch = initiative.getMostRecentTotalEpoch(); + (uint256 totalLQTYAllocatedAtEpoch,) = initiative.totalLQTYAllocatedByEpoch( (targetEpoch < mostRecentTotalEpoch) ? targetEpoch : mostRecentTotalEpoch ); return totalLQTYAllocatedAtEpoch; @@ -110,16 +111,16 @@ abstract contract BribeInitiativeProperties is BeforeAfter { // Dust cap check // function property_BI05() public { // // users can't claim for current epoch so checking for previous - // uint16 checkEpoch = governance.epoch() - 1; + // uint256 checkEpoch = governance.epoch() - 1; - // for (uint8 i; i < deployedInitiatives.length; i++) { + // for (uint256 i; i < deployedInitiatives.length; i++) { // address initiative = deployedInitiatives[i]; // // for any epoch: expected balance = Bribe - claimed bribes, actual balance = bribe token balance of initiative // // so if the delta between the expected and actual is > 0, dust is being collected // uint256 lqtyClaimedAccumulator; // uint256 lusdClaimedAccumulator; - // for (uint8 j; j < users.length; j++) { + // for (uint256 j; j < users.length; j++) { // // if the bool switches, the user has claimed their bribe for the epoch // if ( // _before.claimedBribeForInitiativeAtEpoch[initiative][user][checkEpoch] @@ -131,18 +132,18 @@ abstract contract BribeInitiativeProperties is BeforeAfter { // } // } - // (uint128 boldAmount, uint128 bribeTokenAmount) = IBribeInitiative(initiative).bribeByEpoch(checkEpoch); + // (uint256 boldAmount, uint256 bribeTokenAmount) = IBribeInitiative(initiative).bribeByEpoch(checkEpoch); // // shift 128 bit to the right to get the most significant bits of the accumulator (256 - 128 = 128) - // uint128 lqtyClaimedAccumulator128 = uint128(lqtyClaimedAccumulator >> 128); - // uint128 lusdClaimedAccumulator128 = uint128(lusdClaimedAccumulator >> 128); + // uint256 lqtyClaimedAccumulator128 = uint256(lqtyClaimedAccumulator >> 128); + // uint256 lusdClaimedAccumulator128 = uint256(lusdClaimedAccumulator >> 128); // // find delta between bribe and claimed amount (how much should be remaining in contract) - // uint128 lusdDelta = boldAmount - lusdClaimedAccumulator128; - // uint128 lqtyDelta = bribeTokenAmount - lqtyClaimedAccumulator128; + // uint256 lusdDelta = boldAmount - lusdClaimedAccumulator128; + // uint256 lqtyDelta = bribeTokenAmount - lqtyClaimedAccumulator128; - // uint128 initiativeLusdBalance = uint128(lusd.balanceOf(initiative) >> 128); - // uint128 initiativeLqtyBalance = uint128(lqty.balanceOf(initiative) >> 128); + // uint256 initiativeLusdBalance = uint256(lusd.balanceOf(initiative) >> 128); + // uint256 initiativeLqtyBalance = uint256(lqty.balanceOf(initiative) >> 128); // lte( // lusdDelta - initiativeLusdBalance, @@ -160,19 +161,19 @@ abstract contract BribeInitiativeProperties is BeforeAfter { function property_BI07() public { // sum user allocations for an epoch // check that this matches the total allocation for the epoch - for (uint8 i; i < deployedInitiatives.length; i++) { + for (uint256 i; i < deployedInitiatives.length; i++) { IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); - uint16 currentEpoch = initiative.getMostRecentTotalEpoch(); + uint256 currentEpoch = initiative.getMostRecentTotalEpoch(); - uint88 sumLqtyAllocated; - for (uint8 j; j < users.length; j++) { + uint256 sumLqtyAllocated; + for (uint256 j; j < users.length; j++) { // NOTE: We need to grab user latest - uint16 userEpoch = initiative.getMostRecentUserEpoch(users[j]); - (uint88 lqtyAllocated,) = initiative.lqtyAllocatedByUserAtEpoch(users[j], userEpoch); + uint256 userEpoch = initiative.getMostRecentUserEpoch(users[j]); + (uint256 lqtyAllocated,) = initiative.lqtyAllocatedByUserAtEpoch(users[j], userEpoch); sumLqtyAllocated += lqtyAllocated; } - (uint88 totalLQTYAllocated,) = initiative.totalLQTYAllocatedByEpoch(currentEpoch); + (uint256 totalLQTYAllocated,) = initiative.totalLQTYAllocatedByEpoch(currentEpoch); eq( sumLqtyAllocated, totalLQTYAllocated, @@ -182,20 +183,20 @@ abstract contract BribeInitiativeProperties is BeforeAfter { } function property_sum_of_votes_in_bribes_match() public { - uint16 currentEpoch = governance.epoch(); + uint256 currentEpoch = governance.epoch(); // sum user allocations for an epoch // check that this matches the total allocation for the epoch - for (uint8 i; i < deployedInitiatives.length; i++) { + for (uint256 i; i < deployedInitiatives.length; i++) { IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); uint256 sumOfPower; - for (uint8 j; j < users.length; j++) { - (uint88 lqtyAllocated, uint120 userTS) = initiative.lqtyAllocatedByUserAtEpoch(users[j], currentEpoch); - sumOfPower += governance.lqtyToVotes(lqtyAllocated, userTS, uint32(block.timestamp)); + for (uint256 j; j < users.length; j++) { + (uint256 lqtyAllocated, uint256 userTS) = initiative.lqtyAllocatedByUserAtEpoch(users[j], currentEpoch); + sumOfPower += governance.lqtyToVotes(lqtyAllocated, userTS, uint256(block.timestamp)); } - (uint88 totalLQTYAllocated, uint120 totalTS) = initiative.totalLQTYAllocatedByEpoch(currentEpoch); + (uint256 totalLQTYAllocated, uint256 totalTS) = initiative.totalLQTYAllocatedByEpoch(currentEpoch); - uint256 totalRecordedPower = governance.lqtyToVotes(totalLQTYAllocated, totalTS, uint32(block.timestamp)); + uint256 totalRecordedPower = governance.lqtyToVotes(totalLQTYAllocated, totalTS, uint256(block.timestamp)); gte(totalRecordedPower, sumOfPower, "property_sum_of_votes_in_bribes_match"); } @@ -203,14 +204,14 @@ abstract contract BribeInitiativeProperties is BeforeAfter { function property_BI08() public { // users can only claim for epoch that has already passed - uint16 checkEpoch = governance.epoch() - 1; + uint256 checkEpoch = governance.epoch() - 1; // use lqtyAllocatedByUserAtEpoch to determine if a user is allocated for an epoch // use claimedBribeForInitiativeAtEpoch to determine if user has claimed bribe for an epoch (would require the value changing from false -> true) - for (uint8 i; i < deployedInitiatives.length; i++) { + for (uint256 i; i < deployedInitiatives.length; i++) { IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); - for (uint8 j; j < users.length; j++) { - (uint88 lqtyAllocated,) = initiative.lqtyAllocatedByUserAtEpoch(users[j], checkEpoch); + for (uint256 j; j < users.length; j++) { + (uint256 lqtyAllocated,) = initiative.lqtyAllocatedByUserAtEpoch(users[j], checkEpoch); // check that user had no lqtyAllocated for the epoch and therefore shouldn't be able to claim for it if (lqtyAllocated == 0) { @@ -231,12 +232,12 @@ abstract contract BribeInitiativeProperties is BeforeAfter { // BI-09: User can’t be allocated for future epoch function property_BI09() public { // get one past current epoch in governance - uint16 checkEpoch = governance.epoch() + 1; + uint256 checkEpoch = governance.epoch() + 1; // check if any user is allocated for the epoch - for (uint8 i; i < deployedInitiatives.length; i++) { + for (uint256 i; i < deployedInitiatives.length; i++) { IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); - for (uint8 j; j < users.length; j++) { - (uint88 lqtyAllocated,) = initiative.lqtyAllocatedByUserAtEpoch(users[j], checkEpoch); + for (uint256 j; j < users.length; j++) { + (uint256 lqtyAllocated,) = initiative.lqtyAllocatedByUserAtEpoch(users[j], checkEpoch); eq(lqtyAllocated, 0, "BI-09: User cannot be allocated for future epoch"); } @@ -245,14 +246,14 @@ abstract contract BribeInitiativeProperties is BeforeAfter { // BI-10: totalLQTYAllocatedByEpoch ≥ lqtyAllocatedByUserAtEpoch function property_BI10() public { - uint16 checkEpoch = governance.epoch(); + uint256 checkEpoch = governance.epoch(); // check each user allocation for the epoch against the total for the epoch - for (uint8 i; i < deployedInitiatives.length; i++) { + for (uint256 i; i < deployedInitiatives.length; i++) { IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); - for (uint8 j; j < users.length; j++) { - (uint88 lqtyAllocated,) = initiative.lqtyAllocatedByUserAtEpoch(users[j], checkEpoch); - (uint88 totalLQTYAllocated,) = initiative.totalLQTYAllocatedByEpoch(checkEpoch); + for (uint256 j; j < users.length; j++) { + (uint256 lqtyAllocated,) = initiative.lqtyAllocatedByUserAtEpoch(users[j], checkEpoch); + (uint256 totalLQTYAllocated,) = initiative.totalLQTYAllocatedByEpoch(checkEpoch); gte(totalLQTYAllocated, lqtyAllocated, "BI-10: totalLQTYAllocatedByEpoch >= lqtyAllocatedByUserAtEpoch"); } diff --git a/test/recon/properties/GovernanceProperties.sol b/test/recon/properties/GovernanceProperties.sol index 99e40b4f..df12eb01 100644 --- a/test/recon/properties/GovernanceProperties.sol +++ b/test/recon/properties/GovernanceProperties.sol @@ -23,23 +23,23 @@ abstract contract GovernanceProperties is BeforeAfter { address initiative = deployedInitiatives[i]; // Hardcoded Allowed FSM - if (_before.initiativeStatus[initiative] == Governance.InitiativeStatus.UNREGISTERABLE) { + if (_before.initiativeStatus[initiative] == IGovernance.InitiativeStatus.UNREGISTERABLE) { // ALLOW TO SET DISABLE - if (_after.initiativeStatus[initiative] == Governance.InitiativeStatus.DISABLED) { + if (_after.initiativeStatus[initiative] == IGovernance.InitiativeStatus.DISABLED) { return; } } - if (_before.initiativeStatus[initiative] == Governance.InitiativeStatus.CLAIMABLE) { + if (_before.initiativeStatus[initiative] == IGovernance.InitiativeStatus.CLAIMABLE) { // ALLOW TO CLAIM - if (_after.initiativeStatus[initiative] == Governance.InitiativeStatus.CLAIMED) { + if (_after.initiativeStatus[initiative] == IGovernance.InitiativeStatus.CLAIMED) { return; } } - if (_before.initiativeStatus[initiative] == Governance.InitiativeStatus.NONEXISTENT) { + if (_before.initiativeStatus[initiative] == IGovernance.InitiativeStatus.NONEXISTENT) { // Registered -> SKIP is ok - if (_after.initiativeStatus[initiative] == Governance.InitiativeStatus.WARM_UP) { + if (_after.initiativeStatus[initiative] == IGovernance.InitiativeStatus.WARM_UP) { return; } } @@ -63,7 +63,7 @@ abstract contract GovernanceProperties is BeforeAfter { address userProxyAddress = governance.deriveUserProxyAddress(users[i]); uint256 stake = MockStakingV1(stakingV1).stakes(userProxyAddress); - (uint88 user_allocatedLQTY,) = governance.userStates(users[i]); + (,, uint256 user_allocatedLQTY,) = governance.userStates(users[i]); lte(user_allocatedLQTY, stake, "User can never allocated more than stake"); } } @@ -113,15 +113,12 @@ abstract contract GovernanceProperties is BeforeAfter { } function _getGlobalLQTYAndUserSum() internal returns (uint256, uint256) { - ( - uint88 totalCountedLQTY, - // uint32 after_user_countedVoteLQTYAverageTimestamp // TODO: How do we do this? - ) = governance.globalState(); + (uint256 totalCountedLQTY,) = governance.globalState(); uint256 totalUserCountedLQTY; for (uint256 i; i < users.length; i++) { // Only sum up user votes - (uint88 user_voteLQTY,) = _getAllUserAllocations(users[i], true); + (uint256 user_voteLQTY,) = _getAllUserAllocations(users[i], true); totalUserCountedLQTY += user_voteLQTY; } @@ -132,9 +129,9 @@ abstract contract GovernanceProperties is BeforeAfter { function property_ensure_user_alloc_cannot_dos() public { for (uint256 i; i < users.length; i++) { // Only sum up user votes - (uint88 user_voteLQTY,) = _getAllUserAllocations(users[i], false); + (uint256 user_voteLQTY,) = _getAllUserAllocations(users[i], false); - lte(user_voteLQTY, uint88(type(int88).max), "User can never allocate more than int88"); + lte(user_voteLQTY, uint256(type(int256).max), "User can never allocate more than int256"); } } @@ -147,14 +144,14 @@ abstract contract GovernanceProperties is BeforeAfter { uint256 totalInitiativesCountedVoteLQTY; uint256 totalInitiativesCountedVetoLQTY; for (uint256 i; i < deployedInitiatives.length; i++) { - (uint88 voteLQTY, uint88 vetoLQTY,,,) = governance.initiativeStates(deployedInitiatives[i]); + (uint256 voteLQTY, uint256 vetoLQTY,,,) = governance.initiativeStates(deployedInitiatives[i]); totalInitiativesCountedVoteLQTY += voteLQTY; totalInitiativesCountedVetoLQTY += vetoLQTY; } uint256 totalUserCountedLQTY; for (uint256 i; i < users.length; i++) { - (uint88 user_allocatedLQTY,) = governance.userStates(users[i]); + (,, uint256 user_allocatedLQTY,) = governance.userStates(users[i]); totalUserCountedLQTY += user_allocatedLQTY; } @@ -169,14 +166,14 @@ abstract contract GovernanceProperties is BeforeAfter { // For each user, for each initiative, allocation is correct function property_sum_of_user_initiative_allocations() public { for (uint256 i; i < deployedInitiatives.length; i++) { - (uint88 initiative_voteLQTY, uint88 initiative_vetoLQTY,,,) = + (uint256 initiative_voteLQTY, uint256 initiative_vetoLQTY,,,) = governance.initiativeStates(deployedInitiatives[i]); // Grab all users and sum up their participations uint256 totalUserVotes; uint256 totalUserVetos; for (uint256 j; j < users.length; j++) { - (uint88 vote_allocated, uint88 veto_allocated) = _getUserAllocation(users[j], deployedInitiatives[i]); + (uint256 vote_allocated, uint256 veto_allocated) = _getUserAllocation(users[j], deployedInitiatives[i]); totalUserVotes += vote_allocated; totalUserVetos += veto_allocated; } @@ -234,22 +231,18 @@ abstract contract GovernanceProperties is BeforeAfter { function _getUserVotesSumAndInitiativesVotes() internal returns (VotesSumAndInitiativeSum[] memory) { VotesSumAndInitiativeSum[] memory acc = new VotesSumAndInitiativeSum[](deployedInitiatives.length); for (uint256 i; i < deployedInitiatives.length; i++) { - uint240 userWeightAccumulatorForInitiative; + uint256 userWeightAccumulatorForInitiative; for (uint256 j; j < users.length; j++) { - (uint88 userVoteLQTY,,) = governance.lqtyAllocatedByUserToInitiative(users[j], deployedInitiatives[i]); - // TODO: double check that okay to use this average timestamp - (, uint120 averageStakingTimestamp) = governance.userStates(users[j]); + (uint256 userVoteLQTY, uint256 userVoteOffset,,,) = + governance.lqtyAllocatedByUserToInitiative(users[j], deployedInitiatives[i]); // add the weight calculated for each user's allocation to the accumulator - userWeightAccumulatorForInitiative += governance.lqtyToVotes( - userVoteLQTY, uint120(block.timestamp) * uint120(1e18), averageStakingTimestamp - ); + userWeightAccumulatorForInitiative += + governance.lqtyToVotes(userVoteLQTY, uint256(block.timestamp), userVoteOffset); } - (uint88 initiativeVoteLQTY,, uint120 initiativeAverageStakingTimestampVoteLQTY,,) = + (uint256 initiativeVoteLQTY, uint256 initiativeVoteOffset,,,) = governance.initiativeStates(deployedInitiatives[i]); - uint240 initiativeWeight = governance.lqtyToVotes( - initiativeVoteLQTY, uint120(block.timestamp) * uint120(1e18), initiativeAverageStakingTimestampVoteLQTY - ); + uint256 initiativeWeight = governance.lqtyToVotes(initiativeVoteLQTY, block.timestamp, initiativeVoteOffset); acc[i].userSum = userWeightAccumulatorForInitiative; acc[i].initiativeWeight = initiativeWeight; @@ -261,9 +254,9 @@ abstract contract GovernanceProperties is BeforeAfter { function property_allocations_are_never_dangerously_high() public { for (uint256 i; i < deployedInitiatives.length; i++) { for (uint256 j; j < users.length; j++) { - (uint88 vote_allocated, uint88 veto_allocated) = _getUserAllocation(users[j], deployedInitiatives[i]); - lte(vote_allocated, uint88(type(int88).max), "Vote is never above int88.max"); - lte(veto_allocated, uint88(type(int88).max), "Veto is Never above int88.max"); + (uint256 vote_allocated, uint256 veto_allocated) = _getUserAllocation(users[j], deployedInitiatives[i]); + lte(vote_allocated, uint256(type(int256).max), "Vote is never above int256.max"); + lte(veto_allocated, uint256(type(int256).max), "Veto is Never above int256.max"); } } } @@ -298,7 +291,7 @@ abstract contract GovernanceProperties is BeforeAfter { } function _getInitiativeStateAndGlobalState() internal returns (uint256, uint256, uint256, uint256) { - (uint88 totalCountedLQTY, uint120 global_countedVoteLQTYAverageTimestamp) = governance.globalState(); + (uint256 totalCountedLQTY, uint256 global_countedVoteOffset) = governance.globalState(); // Can sum via projection I guess @@ -307,32 +300,20 @@ abstract contract GovernanceProperties is BeforeAfter { uint256 allocatedLQTYSum; uint256 votedPowerSum; for (uint256 i; i < deployedInitiatives.length; i++) { - ( - uint88 voteLQTY, - uint88 vetoLQTY, - uint120 averageStakingTimestampVoteLQTY, - uint120 averageStakingTimestampVetoLQTY, - ) = governance.initiativeStates(deployedInitiatives[i]); + (uint256 voteLQTY, uint256 voteOffset, uint256 vetoLQTY, uint256 vetoOffset,) = + governance.initiativeStates(deployedInitiatives[i]); // Conditional, only if not DISABLED - (Governance.InitiativeStatus status,,) = governance.getInitiativeState(deployedInitiatives[i]); + (IGovernance.InitiativeStatus status,,) = governance.getInitiativeState(deployedInitiatives[i]); // Conditionally add based on state - if (status != Governance.InitiativeStatus.DISABLED) { + if (status != IGovernance.InitiativeStatus.DISABLED) { allocatedLQTYSum += voteLQTY; // Sum via projection - votedPowerSum += governance.lqtyToVotes( - voteLQTY, - uint120(block.timestamp) * uint120(governance.TIMESTAMP_PRECISION()), - averageStakingTimestampVoteLQTY - ); + votedPowerSum += governance.lqtyToVotes(voteLQTY, block.timestamp, voteOffset); } } - uint256 govPower = governance.lqtyToVotes( - totalCountedLQTY, - uint120(block.timestamp) * uint120(governance.TIMESTAMP_PRECISION()), - global_countedVoteLQTYAverageTimestamp - ); + uint256 govPower = governance.lqtyToVotes(totalCountedLQTY, block.timestamp, global_countedVoteOffset); return (allocatedLQTYSum, totalCountedLQTY, votedPowerSum, govPower); } @@ -346,14 +327,14 @@ abstract contract GovernanceProperties is BeforeAfter { // In the next epoch it can either be SKIP or UNREGISTERABLE address initiative = _getDeployedInitiative(initiativeIndex); - (Governance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); - if (status == Governance.InitiativeStatus.SKIP) { + (IGovernance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); + if (status == IGovernance.InitiativeStatus.SKIP) { vm.warp(block.timestamp + governance.EPOCH_DURATION()); - (Governance.InitiativeStatus newStatus,,) = governance.getInitiativeState(initiative); + (IGovernance.InitiativeStatus newStatus,,) = governance.getInitiativeState(initiative); t( uint256(status) == uint256(newStatus) - || uint256(newStatus) == uint256(Governance.InitiativeStatus.UNREGISTERABLE) - || uint256(newStatus) == uint256(Governance.InitiativeStatus.CLAIMABLE), + || uint256(newStatus) == uint256(IGovernance.InitiativeStatus.UNREGISTERABLE) + || uint256(newStatus) == uint256(IGovernance.InitiativeStatus.CLAIMABLE), "Either SKIP or UNREGISTERABLE or CLAIMABLE" ); } @@ -362,16 +343,16 @@ abstract contract GovernanceProperties is BeforeAfter { function check_warmup_unregisterable_consistency(uint8 initiativeIndex) public { // Status after MUST NOT be UNREGISTERABLE address initiative = _getDeployedInitiative(initiativeIndex); - (Governance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); + (IGovernance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); - if (status == Governance.InitiativeStatus.WARM_UP) { + if (status == IGovernance.InitiativeStatus.WARM_UP) { vm.warp(block.timestamp + governance.EPOCH_DURATION()); - (Governance.InitiativeStatus newStatus,,) = governance.getInitiativeState(initiative); + (IGovernance.InitiativeStatus newStatus,,) = governance.getInitiativeState(initiative); // Next status must be SKIP, because by definition it has // Received no votes (cannot) // Must not be UNREGISTERABLE - t(uint256(newStatus) == uint256(Governance.InitiativeStatus.SKIP), "Must be SKIP"); + t(uint256(newStatus) == uint256(IGovernance.InitiativeStatus.SKIP), "Must be SKIP"); } } @@ -385,10 +366,10 @@ abstract contract GovernanceProperties is BeforeAfter { // In the next epoch it will remain UNREGISTERABLE address initiative = _getDeployedInitiative(initiativeIndex); - (Governance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); - if (status == Governance.InitiativeStatus.UNREGISTERABLE) { + (IGovernance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); + if (status == IGovernance.InitiativeStatus.UNREGISTERABLE) { vm.warp(block.timestamp + governance.EPOCH_DURATION()); - (Governance.InitiativeStatus newStatus,,) = governance.getInitiativeState(initiative); + (IGovernance.InitiativeStatus newStatus,,) = governance.getInitiativeState(initiative); t(uint256(status) == uint256(newStatus), "UNREGISTERABLE must remain UNREGISTERABLE"); } } @@ -399,12 +380,12 @@ abstract contract GovernanceProperties is BeforeAfter { // Check if initiative is claimable // If it is assert the check for (uint256 i; i < deployedInitiatives.length; i++) { - (Governance.InitiativeStatus status,,) = governance.getInitiativeState(deployedInitiatives[i]); + (IGovernance.InitiativeStatus status,,) = governance.getInitiativeState(deployedInitiatives[i]); (, Governance.InitiativeState memory initiativeState,) = governance.getInitiativeSnapshotAndState(deployedInitiatives[i]); - if (status == Governance.InitiativeStatus.CLAIMABLE) { + if (status == IGovernance.InitiativeStatus.CLAIMABLE) { t(governance.epoch() > 0, "Can never be claimable in epoch 0!"); // Overflow Check, also flags misconfiguration // Normal check t(initiativeState.lastEpochClaim < governance.epoch() - 1, "Cannot be CLAIMABLE, should be CLAIMED"); @@ -425,7 +406,7 @@ abstract contract GovernanceProperties is BeforeAfter { uint256 claimableSum; for (uint256 i; i < deployedInitiatives.length; i++) { // NOTE: Non view so it accrues state - (Governance.InitiativeStatus status,, uint256 claimableAmount) = + (IGovernance.InitiativeStatus status,, uint256 claimableAmount) = governance.getInitiativeState(deployedInitiatives[i]); claimableSum += claimableAmount; @@ -456,20 +437,23 @@ abstract contract GovernanceProperties is BeforeAfter { function _getUserAllocation(address theUser, address initiative) internal view - returns (uint88 votes, uint88 vetos) + returns (uint256 votes, uint256 vetos) { - (votes, vetos,) = governance.lqtyAllocatedByUserToInitiative(theUser, initiative); + (votes, vetos,,,) = governance.lqtyAllocatedByUserToInitiative(theUser, initiative); } - function _getAllUserAllocations(address theUser, bool skipDisabled) internal returns (uint88 votes, uint88 vetos) { + function _getAllUserAllocations(address theUser, bool skipDisabled) + internal + returns (uint256 votes, uint256 vetos) + { for (uint256 i; i < deployedInitiatives.length; i++) { - (uint88 allocVotes, uint88 allocVetos,) = + (uint256 allocVotes, uint256 allocVetos,,,) = governance.lqtyAllocatedByUserToInitiative(theUser, deployedInitiatives[i]); if (skipDisabled) { - (Governance.InitiativeStatus status,,) = governance.getInitiativeState(deployedInitiatives[i]); + (IGovernance.InitiativeStatus status,,) = governance.getInitiativeState(deployedInitiatives[i]); // Conditionally add based on state - if (status != Governance.InitiativeStatus.DISABLED) { + if (status != IGovernance.InitiativeStatus.DISABLED) { votes += allocVotes; vetos += allocVetos; } @@ -483,9 +467,9 @@ abstract contract GovernanceProperties is BeforeAfter { function property_alloc_deposit_reset_is_idempotent( uint8 initiativesIndex, - uint96 deltaLQTYVotes, - uint96 deltaLQTYVetos, - uint88 lqtyAmount + uint256 deltaLQTYVotes, + uint256 deltaLQTYVetos, + uint256 lqtyAmount ) public withChecks { address targetInitiative = _getDeployedInitiative(initiativesIndex); @@ -493,69 +477,60 @@ abstract contract GovernanceProperties is BeforeAfter { // TODO: prob unnecessary // Cause we always reset anyway { - int88[] memory zeroes = new int88[](deployedInitiatives.length); + int256[] memory zeroes = new int256[](deployedInitiatives.length); governance.allocateLQTY(deployedInitiatives, deployedInitiatives, zeroes, zeroes); } // GET state and initiative data before allocation - (uint88 totalCountedLQTY, uint120 user_countedVoteLQTYAverageTimestamp) = governance.globalState(); - ( - uint88 voteLQTY, - uint88 vetoLQTY, - uint120 averageStakingTimestampVoteLQTY, - uint120 averageStakingTimestampVetoLQTY, - ) = governance.initiativeStates(targetInitiative); + (uint256 totalCountedLQTY, uint256 user_countedVoteOffset) = governance.globalState(); + (uint256 voteLQTY, uint256 voteOffset, uint256 vetoLQTY, uint256 vetoOffset,) = + governance.initiativeStates(targetInitiative); // Allocate { - uint96 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user)).staked(); + uint256 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user)).staked(); address[] memory initiatives = new address[](1); initiatives[0] = targetInitiative; - int88[] memory deltaLQTYVotesArray = new int88[](1); - deltaLQTYVotesArray[0] = int88(uint88(deltaLQTYVotes % stakedAmount)); - int88[] memory deltaLQTYVetosArray = new int88[](1); - deltaLQTYVetosArray[0] = int88(uint88(deltaLQTYVetos % stakedAmount)); + int256[] memory deltaLQTYVotesArray = new int256[](1); + deltaLQTYVotesArray[0] = int256(uint256(deltaLQTYVotes % stakedAmount)); + int256[] memory deltaLQTYVetosArray = new int256[](1); + deltaLQTYVetosArray[0] = int256(uint256(deltaLQTYVetos % stakedAmount)); governance.allocateLQTY(deployedInitiatives, initiatives, deltaLQTYVotesArray, deltaLQTYVetosArray); } // Deposit (Changes total LQTY an hopefully also changes ts) { - (, uint120 averageStakingTimestamp1) = governance.userStates(user); + (, uint256 unallocatedOffset1,,) = governance.userStates(user); - lqtyAmount = uint88(lqtyAmount % lqty.balanceOf(user)); + lqtyAmount = uint256(lqtyAmount % lqty.balanceOf(user)); governance.depositLQTY(lqtyAmount); - (, uint120 averageStakingTimestamp2) = governance.userStates(user); + (, uint256 unallocatedOffset2,,) = governance.userStates(user); - require(averageStakingTimestamp2 > averageStakingTimestamp1, "Must have changed"); + require(unallocatedOffset2 > unallocatedOffset1, "Must have changed"); } // REMOVE STUFF to remove the user data { - int88[] memory zeroes = new int88[](deployedInitiatives.length); + int256[] memory zeroes = new int256[](deployedInitiatives.length); governance.allocateLQTY(deployedInitiatives, deployedInitiatives, zeroes, zeroes); } // Check total allocation and initiative allocation { - (uint88 after_totalCountedLQTY, uint120 after_user_countedVoteLQTYAverageTimestamp) = - governance.globalState(); - ( - uint88 after_voteLQTY, - uint88 after_vetoLQTY, - uint120 after_averageStakingTimestampVoteLQTY, - uint120 after_averageStakingTimestampVetoLQTY, - ) = governance.initiativeStates(targetInitiative); + (uint256 after_totalCountedLQTY, uint256 after_user_countedVoteOffset) = governance.globalState(); + (uint256 after_voteLQTY, uint256 after_voteOffset, uint256 after_vetoLQTY, uint256 after_vetoOffset,) = + governance.initiativeStates(targetInitiative); eq(voteLQTY, after_voteLQTY, "Same vote"); eq(vetoLQTY, after_vetoLQTY, "Same veto"); - eq(averageStakingTimestampVoteLQTY, after_averageStakingTimestampVoteLQTY, "Same ts vote"); - eq(averageStakingTimestampVetoLQTY, after_averageStakingTimestampVetoLQTY, "Same ts veto"); + eq(voteOffset, after_voteOffset, "Same vote offset"); + eq(vetoOffset, after_vetoOffset, "Same veto offset"); eq(totalCountedLQTY, after_totalCountedLQTY, "Same total LQTY"); - eq(user_countedVoteLQTYAverageTimestamp, after_user_countedVoteLQTYAverageTimestamp, "Same total ts"); + eq(user_countedVoteOffset, after_user_countedVoteOffset, "Same total ts"); } } } diff --git a/test/recon/properties/OptimizationProperties.sol b/test/recon/properties/OptimizationProperties.sol index 6c6c8d2f..acf535f6 100644 --- a/test/recon/properties/OptimizationProperties.sol +++ b/test/recon/properties/OptimizationProperties.sol @@ -1,10 +1,8 @@ // SPDX-License-Identifier: GPL-2.0 pragma solidity ^0.8.0; -import {BeforeAfter} from "../BeforeAfter.sol"; import {Governance} from "src/Governance.sol"; import {IGovernance} from "src/interfaces/IGovernance.sol"; -import {MockStakingV1} from "test/mocks/MockStakingV1.sol"; import {vm} from "@chimera/Hevm.sol"; import {IUserProxy} from "src/interfaces/IUserProxy.sol"; import {GovernanceProperties} from "./GovernanceProperties.sol"; @@ -47,7 +45,7 @@ abstract contract OptimizationProperties is GovernanceProperties { uint256 claimableSum; for (uint256 i; i < deployedInitiatives.length; i++) { // NOTE: Non view so it accrues state - (Governance.InitiativeStatus status,, uint256 claimableAmount) = + (IGovernance.InitiativeStatus status,, uint256 claimableAmount) = governance.getInitiativeState(deployedInitiatives[i]); claimableSum += claimableAmount; @@ -70,7 +68,7 @@ abstract contract OptimizationProperties is GovernanceProperties { uint256 claimableSum; for (uint256 i; i < deployedInitiatives.length; i++) { // NOTE: Non view so it accrues state - (Governance.InitiativeStatus status,, uint256 claimableAmount) = + (IGovernance.InitiativeStatus status,, uint256 claimableAmount) = governance.getInitiativeState(deployedInitiatives[i]); claimableSum += claimableAmount; diff --git a/test/recon/properties/RevertProperties.sol b/test/recon/properties/RevertProperties.sol index 6d73d5e6..e7d46ec3 100644 --- a/test/recon/properties/RevertProperties.sol +++ b/test/recon/properties/RevertProperties.sol @@ -9,13 +9,10 @@ import {IBribeInitiative} from "src/interfaces/IBribeInitiative.sol"; // The are view functions that should never revert abstract contract RevertProperties is BeforeAfter { function property_computingGlobalPowerNeverReverts() public { - (uint88 totalCountedLQTY, uint120 global_countedVoteLQTYAverageTimestamp) = governance.globalState(); + (uint256 totalCountedLQTY, uint256 global_countedVoteOffset) = governance.globalState(); - try governance.lqtyToVotes( - totalCountedLQTY, - uint120(block.timestamp) * uint120(governance.TIMESTAMP_PRECISION()), - global_countedVoteLQTYAverageTimestamp - ) {} catch { + try governance.lqtyToVotes(totalCountedLQTY, block.timestamp, global_countedVoteOffset) {} + catch { t(false, "Should never revert"); } } @@ -23,21 +20,13 @@ abstract contract RevertProperties is BeforeAfter { function property_summingInitiativesPowerNeverReverts() public { uint256 votedPowerSum; for (uint256 i; i < deployedInitiatives.length; i++) { - ( - uint88 voteLQTY, - uint88 vetoLQTY, - uint120 averageStakingTimestampVoteLQTY, - uint120 averageStakingTimestampVetoLQTY, - ) = governance.initiativeStates(deployedInitiatives[i]); + (uint256 voteLQTY, uint256 voteOffset, uint256 vetoLQTY, uint256 vetoOffset,) = + governance.initiativeStates(deployedInitiatives[i]); // Sum via projection uint256 prevSum = votedPowerSum; unchecked { - try governance.lqtyToVotes( - voteLQTY, - uint120(block.timestamp) * uint120(governance.TIMESTAMP_PRECISION()), - averageStakingTimestampVoteLQTY - ) returns (uint208 res) { + try governance.lqtyToVotes(voteLQTY, block.timestamp, voteOffset) returns (uint256 res) { votedPowerSum += res; } catch { t(false, "Should never revert"); diff --git a/test/recon/properties/SynchProperties.sol b/test/recon/properties/SynchProperties.sol index 1414c64c..2cbc62e5 100644 --- a/test/recon/properties/SynchProperties.sol +++ b/test/recon/properties/SynchProperties.sol @@ -8,34 +8,33 @@ import {IBribeInitiative} from "src/interfaces/IBribeInitiative.sol"; abstract contract SynchProperties is BeforeAfter { // Properties that ensure that the states are synched - // Go through each initiative // Go through each user - // Ensure that a non zero vote uses the user latest TS + // Ensure that a non zero vote uses the user latest offset // This ensures that the math is correct in removal and addition - function property_initiative_ts_matches_user_when_non_zero() public { + // TODO: check whether this property really holds for offsets, since they are sums + function property_initiative_offset_matches_user_when_non_zero() public { // For all strategies for (uint256 i; i < deployedInitiatives.length; i++) { for (uint256 j; j < users.length; j++) { - (uint88 votes,, uint16 epoch) = + (uint256 votes,,,, uint256 epoch) = governance.lqtyAllocatedByUserToInitiative(users[j], deployedInitiatives[i]); // Grab epoch from initiative - (uint88 lqtyAllocatedByUserAtEpoch, uint120 ts) = + (uint256 lqtyAllocatedByUserAtEpoch, uint256 allocOffset) = IBribeInitiative(deployedInitiatives[i]).lqtyAllocatedByUserAtEpoch(users[j], epoch); - // Check that TS matches (only for votes) + // Check that votes match eq(lqtyAllocatedByUserAtEpoch, votes, "Votes must match at all times"); if (votes != 0) { // if we're voting and the votes are different from 0 - // then we check user TS - (, uint120 averageStakingTimestamp) = governance.userStates(users[j]); + // then we check user offset + (,,, uint256 allocatedOffset) = governance.userStates(users[j]); - eq(averageStakingTimestamp, ts, "Timestamp must be most recent when it's non zero"); + eq(allocatedOffset, allocOffset, "Offsets must match"); } else { - // NOTE: If votes are zero the TS is passed, but it is not a useful value - // This is left here as a note for the reviewer + // NOTE: If votes are zero the offset is zero } } } diff --git a/test/recon/properties/TsProperties.sol b/test/recon/properties/TsProperties.sol index 1fa84773..cd56e113 100644 --- a/test/recon/properties/TsProperties.sol +++ b/test/recon/properties/TsProperties.sol @@ -9,17 +9,17 @@ import {IBribeInitiative} from "src/interfaces/IBribeInitiative.sol"; abstract contract TsProperties is BeforeAfter { // Properties that ensure that a user TS is somewhat sound - function property_user_ts_is_always_greater_than_start() public { + function property_user_offset_is_always_greater_than_start() public { for (uint256 i; i < users.length; i++) { - (uint88 user_allocatedLQTY, uint120 userTs) = governance.userStates(users[i]); + (,, uint256 user_allocatedLQTY, uint256 userAllocatedOffset) = governance.userStates(users[i]); if (user_allocatedLQTY > 0) { - gte(userTs, magnifiedStartTS, "User ts must always be GTE than start"); + gte(userAllocatedOffset, magnifiedStartTS, "User ts must always be GTE than start"); } } } - function property_global_ts_is_always_greater_than_start() public { - (uint88 totalCountedLQTY, uint120 globalTs) = governance.globalState(); + function property_global_offset_is_always_greater_than_start() public { + (uint256 totalCountedLQTY, uint256 globalTs) = governance.globalState(); if (totalCountedLQTY > 0) { gte(globalTs, magnifiedStartTS, "Global ts must always be GTE than start"); diff --git a/test/recon/targets/BribeInitiativeTargets.sol b/test/recon/targets/BribeInitiativeTargets.sol index 694c7e0e..4ab6be9d 100644 --- a/test/recon/targets/BribeInitiativeTargets.sol +++ b/test/recon/targets/BribeInitiativeTargets.sol @@ -15,24 +15,24 @@ abstract contract BribeInitiativeTargets is Test, BaseTargetFunctions, Propertie // NOTE: initiatives that get called here are deployed but not necessarily registered - function initiative_depositBribe(uint128 boldAmount, uint128 bribeTokenAmount, uint16 epoch, uint8 initiativeIndex) + function initiative_depositBribe(uint256 boldAmount, uint256 bribeTokenAmount, uint256 epoch, uint8 initiativeIndex) public withChecks { IBribeInitiative initiative = IBribeInitiative(_getDeployedInitiative(initiativeIndex)); // clamp token amounts using user balance - boldAmount = uint128(boldAmount % lusd.balanceOf(user)); - bribeTokenAmount = uint128(bribeTokenAmount % lqty.balanceOf(user)); + boldAmount = uint256(boldAmount % lusd.balanceOf(user)); + bribeTokenAmount = uint256(bribeTokenAmount % lqty.balanceOf(user)); lusd.approve(address(initiative), boldAmount); lqty.approve(address(initiative), bribeTokenAmount); - (uint128 boldAmountB4, uint128 bribeTokenAmountB4) = IBribeInitiative(initiative).bribeByEpoch(epoch); + (uint256 boldAmountB4, uint256 bribeTokenAmountB4,) = IBribeInitiative(initiative).bribeByEpoch(epoch); initiative.depositBribe(boldAmount, bribeTokenAmount, epoch); - (uint128 boldAmountAfter, uint128 bribeTokenAmountAfter) = IBribeInitiative(initiative).bribeByEpoch(epoch); + (uint256 boldAmountAfter, uint256 bribeTokenAmountAfter,) = IBribeInitiative(initiative).bribeByEpoch(epoch); eq(boldAmountB4 + boldAmount, boldAmountAfter, "Bold amount tracking is sound"); eq(bribeTokenAmountB4 + bribeTokenAmount, bribeTokenAmountAfter, "Bribe amount tracking is sound"); @@ -40,10 +40,10 @@ abstract contract BribeInitiativeTargets is Test, BaseTargetFunctions, Propertie // Canaries are no longer necessary // function canary_bribeWasThere(uint8 initiativeIndex) public { - // uint16 epoch = governance.epoch(); + // uint256 epoch = governance.epoch(); // IBribeInitiative initiative = IBribeInitiative(_getDeployedInitiative(initiativeIndex)); - // (uint128 boldAmount, uint128 bribeTokenAmount) = initiative.bribeByEpoch(epoch); + // (uint256 boldAmount, uint256 bribeTokenAmount) = initiative.bribeByEpoch(epoch); // t(boldAmount == 0 && bribeTokenAmount == 0, "A bribe was found"); // } @@ -55,15 +55,15 @@ abstract contract BribeInitiativeTargets is Test, BaseTargetFunctions, Propertie function clamped_claimBribes(uint8 initiativeIndex) public { IBribeInitiative initiative = IBribeInitiative(_getDeployedInitiative(initiativeIndex)); - uint16 userEpoch = initiative.getMostRecentUserEpoch(user); - uint16 stateEpoch = initiative.getMostRecentTotalEpoch(); + uint256 userEpoch = initiative.getMostRecentUserEpoch(user); + uint256 stateEpoch = initiative.getMostRecentTotalEpoch(); initiative_claimBribes(governance.epoch() - 1, userEpoch, stateEpoch, initiativeIndex); } function initiative_claimBribes( - uint16 epoch, - uint16 prevAllocationEpoch, - uint16 prevTotalAllocationEpoch, + uint256 epoch, + uint256 prevAllocationEpoch, + uint256 prevTotalAllocationEpoch, uint8 initiativeIndex ) public withChecks { IBribeInitiative initiative = IBribeInitiative(_getDeployedInitiative(initiativeIndex)); @@ -92,14 +92,14 @@ abstract contract BribeInitiativeTargets is Test, BaseTargetFunctions, Propertie // NOTE: This is not a full check, but a sufficient check for some cases /// Specifically we may have to look at the user last epoch /// And see if we need to port over that balance from then - (uint88 lqtyAllocated,) = initiative.lqtyAllocatedByUserAtEpoch(user, epoch); + (uint256 lqtyAllocated,) = initiative.lqtyAllocatedByUserAtEpoch(user, epoch); bool claimedBribe = initiative.claimedBribeAtEpoch(user, epoch); if (initiative.getMostRecentTotalEpoch() != prevTotalAllocationEpoch) { return; // We are in a edge case } // Check if there are bribes - (uint128 boldAmount, uint128 bribeTokenAmount) = initiative.bribeByEpoch(epoch); + (uint256 boldAmount, uint256 bribeTokenAmount,) = initiative.bribeByEpoch(epoch); bool bribeWasThere; if (boldAmount != 0 || bribeTokenAmount != 0) { bribeWasThere = true; diff --git a/test/recon/targets/GovernanceTargets.sol b/test/recon/targets/GovernanceTargets.sol index d8ef2244..e5eb583d 100644 --- a/test/recon/targets/GovernanceTargets.sol +++ b/test/recon/targets/GovernanceTargets.sol @@ -9,7 +9,7 @@ import {console2} from "forge-std/Test.sol"; import {Properties} from "../Properties.sol"; import {MaliciousInitiative} from "../../mocks/MaliciousInitiative.sol"; import {BribeInitiative} from "src/BribeInitiative.sol"; -import {Governance} from "src/Governance.sol"; +import {IGovernance} from "src/interfaces/IGovernance.sol"; import {ILQTYStaking} from "src/interfaces/ILQTYStaking.sol"; import {IInitiative} from "src/interfaces/IInitiative.sol"; import {IUserProxy} from "src/interfaces/IUserProxy.sol"; @@ -20,26 +20,34 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { // clamps to a single initiative to ensure coverage in case both haven't been registered yet function governance_allocateLQTY_clamped_single_initiative( uint8 initiativesIndex, - uint96 deltaLQTYVotes, - uint96 deltaLQTYVetos + uint256 deltaLQTYVotes, + uint256 deltaLQTYVetos ) public withChecks { - uint96 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user)).staked(); // clamp using the user's staked balance - + uint256 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user)).staked(); // clamp using the user's staked balance + + address initiative = _getDeployedInitiative(initiativesIndex); + address[] memory initiativesToReset; + (uint256 currentVote,, uint256 currentVeto,,) = + governance.lqtyAllocatedByUserToInitiative(user, address(initiative)); + if (currentVote != 0 || currentVeto != 0) { + initiativesToReset = new address[](1); + initiativesToReset[0] = address(initiative); + } address[] memory initiatives = new address[](1); - initiatives[0] = _getDeployedInitiative(initiativesIndex); - int88[] memory deltaLQTYVotesArray = new int88[](1); - deltaLQTYVotesArray[0] = int88(uint88(deltaLQTYVotes % (stakedAmount + 1))); - int88[] memory deltaLQTYVetosArray = new int88[](1); - deltaLQTYVetosArray[0] = int88(uint88(deltaLQTYVetos % (stakedAmount + 1))); + initiatives[0] = initiative; + int256[] memory deltaLQTYVotesArray = new int256[](1); + deltaLQTYVotesArray[0] = int256(uint256(deltaLQTYVotes % (stakedAmount + 1))); + int256[] memory deltaLQTYVetosArray = new int256[](1); + deltaLQTYVetosArray[0] = int256(uint256(deltaLQTYVetos % (stakedAmount + 1))); // User B4 - // (uint88 b4_user_allocatedLQTY,) = governance.userStates(user); // TODO + // (uint256 b4_user_allocatedLQTY,) = governance.userStates(user); // TODO // StateB4 - (uint88 b4_global_allocatedLQTY,) = governance.globalState(); + (uint256 b4_global_allocatedLQTY,) = governance.globalState(); - (Governance.InitiativeStatus status,,) = governance.getInitiativeState(initiatives[0]); + (IGovernance.InitiativeStatus status,,) = governance.getInitiativeState(initiatives[0]); - try governance.allocateLQTY(deployedInitiatives, initiatives, deltaLQTYVotesArray, deltaLQTYVetosArray) { + try governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotesArray, deltaLQTYVetosArray) { t(deltaLQTYVotesArray[0] == 0 || deltaLQTYVetosArray[0] == 0, "One alloc must be zero"); } catch { // t(false, "Clamped allocated should not revert"); // TODO: Consider adding overflow check here @@ -53,10 +61,10 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { // If Initiative was anything else // Global state and user state accounting should change - // (uint88 after_user_allocatedLQTY,) = governance.userStates(user); // TODO - (uint88 after_global_allocatedLQTY,) = governance.globalState(); + // (uint256 after_user_allocatedLQTY,) = governance.userStates(user); // TODO + (uint256 after_global_allocatedLQTY,) = governance.globalState(); - if (status == Governance.InitiativeStatus.DISABLED) { + if (status == IGovernance.InitiativeStatus.DISABLED) { // NOTE: It could be 0 lte(after_global_allocatedLQTY, b4_global_allocatedLQTY, "Alloc can only be strictly decreasing"); } @@ -64,22 +72,30 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { function governance_allocateLQTY_clamped_single_initiative_2nd_user( uint8 initiativesIndex, - uint96 deltaLQTYVotes, - uint96 deltaLQTYVetos + uint256 deltaLQTYVotes, + uint256 deltaLQTYVetos ) public withChecks { - uint96 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user2)).staked(); // clamp using the user's staked balance - + uint256 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user2)).staked(); // clamp using the user's staked balance + + address initiative = _getDeployedInitiative(initiativesIndex); + address[] memory initiativesToReset; + (uint256 currentVote,, uint256 currentVeto,,) = + governance.lqtyAllocatedByUserToInitiative(user2, address(initiative)); + if (currentVote != 0 || currentVeto != 0) { + initiativesToReset = new address[](1); + initiativesToReset[0] = address(initiative); + } address[] memory initiatives = new address[](1); - initiatives[0] = _getDeployedInitiative(initiativesIndex); - int88[] memory deltaLQTYVotesArray = new int88[](1); - deltaLQTYVotesArray[0] = int88(uint88(deltaLQTYVotes % stakedAmount)); - int88[] memory deltaLQTYVetosArray = new int88[](1); - deltaLQTYVetosArray[0] = int88(uint88(deltaLQTYVetos % stakedAmount)); + initiatives[0] = initiative; + int256[] memory deltaLQTYVotesArray = new int256[](1); + deltaLQTYVotesArray[0] = int256(uint256(deltaLQTYVotes % stakedAmount)); + int256[] memory deltaLQTYVetosArray = new int256[](1); + deltaLQTYVetosArray[0] = int256(uint256(deltaLQTYVetos % stakedAmount)); require(stakedAmount > 0, "0 stake"); vm.prank(user2); - governance.allocateLQTY(deployedInitiatives, initiatives, deltaLQTYVotesArray, deltaLQTYVetosArray); + governance.allocateLQTY(initiativesToReset, initiatives, deltaLQTYVotesArray, deltaLQTYVetosArray); } function governance_resetAllocations() public { @@ -94,55 +110,53 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { // TODO: if userState.allocatedLQTY != 0 deposit and withdraw must always revert // Resetting never fails and always resets - function property_resetting_never_reverts() public withChecks { - int88[] memory zeroes = new int88[](deployedInitiatives.length); - - try governance.allocateLQTY(deployedInitiatives, deployedInitiatives, zeroes, zeroes) {} + function property_resetting_never_reverts(address[] memory initiativesToReset) public withChecks { + try governance.resetAllocations(initiativesToReset, true) {} catch { t(false, "must never revert"); } - (uint88 user_allocatedLQTY,) = governance.userStates(user); + (,, uint256 user_allocatedLQTY,) = governance.userStates(user); eq(user_allocatedLQTY, 0, "User has 0 allocated on a reset"); } - function depositTsIsRational(uint88 lqtyAmount) public withChecks { - uint88 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user)).staked(); // clamp using the user's staked balance + function offsetIsRational(uint256 lqtyAmount) public withChecks { + uint256 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user)).staked(); // clamp using the user's staked balance // Deposit on zero if (stakedAmount == 0) { - lqtyAmount = uint88(lqtyAmount % lqty.balanceOf(user)); + lqtyAmount = uint256(lqtyAmount % lqty.balanceOf(user)); governance.depositLQTY(lqtyAmount); - // assert that user TS is now * WAD - (, uint120 ts) = governance.userStates(user); - eq(ts, block.timestamp * 1e26, "User TS is scaled by WAD"); + // assert that user's offset TS is now * deposited LQTY + (, uint256 offset,,) = governance.userStates(user); + eq(offset, block.timestamp * lqtyAmount, "User unallocated offset is now * lqty deposited"); } else { // Make sure the TS can never bo before itself - (, uint120 ts_b4) = governance.userStates(user); - lqtyAmount = uint88(lqtyAmount % lqty.balanceOf(user)); + (, uint256 offset_b4,,) = governance.userStates(user); + lqtyAmount = uint256(lqtyAmount % lqty.balanceOf(user)); governance.depositLQTY(lqtyAmount); - (, uint120 ts_after) = governance.userStates(user); + (, uint256 offset_after,,) = governance.userStates(user); - gte(ts_after, ts_b4, "User TS must always increase"); + gte(offset_after, offset_b4, "User unallocated offset must always increase"); } } - function depositMustFailOnNonZeroAlloc(uint88 lqtyAmount) public withChecks { - (uint88 user_allocatedLQTY,) = governance.userStates(user); + function depositMustFailOnNonZeroAlloc(uint256 lqtyAmount) public withChecks { + (uint256 user_allocatedLQTY,,,) = governance.userStates(user); require(user_allocatedLQTY != 0, "0 alloc"); - lqtyAmount = uint88(lqtyAmount % lqty.balanceOf(user)); + lqtyAmount = uint256(lqtyAmount % lqty.balanceOf(user)); try governance.depositLQTY(lqtyAmount) { t(false, "Deposit Must always revert when user is not reset"); } catch {} } - function withdrwaMustFailOnNonZeroAcc(uint88 _lqtyAmount) public withChecks { - (uint88 user_allocatedLQTY,) = governance.userStates(user); + function withdrwaMustFailOnNonZeroAcc(uint256 _lqtyAmount) public withChecks { + (uint256 user_allocatedLQTY,,,) = governance.userStates(user); require(user_allocatedLQTY != 0); @@ -155,7 +169,7 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { // For every initiative, make ghost values and ensure they match // For all operations, you also need to add the VESTED AMT? - function governance_allocateLQTY(int88[] memory _deltaLQTYVotes, int88[] memory _deltaLQTYVetos) + function governance_allocateLQTY(int256[] memory _deltaLQTYVotes, int256[] memory _deltaLQTYVetos) public withChecks { @@ -203,25 +217,25 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { governance.deployUserProxy(); } - function governance_depositLQTY(uint88 lqtyAmount) public withChecks { - lqtyAmount = uint88(lqtyAmount % lqty.balanceOf(user)); + function governance_depositLQTY(uint256 lqtyAmount) public withChecks { + lqtyAmount = uint256(lqtyAmount % lqty.balanceOf(user)); governance.depositLQTY(lqtyAmount); } - function governance_depositLQTY_2(uint88 lqtyAmount) public withChecks { + function governance_depositLQTY_2(uint256 lqtyAmount) public withChecks { // Deploy and approve since we don't do it in constructor vm.prank(user2); try governance.deployUserProxy() returns (address proxy) { vm.prank(user2); - lqty.approve(proxy, type(uint88).max); + lqty.approve(proxy, type(uint256).max); } catch {} - lqtyAmount = uint88(lqtyAmount % lqty.balanceOf(user2)); + lqtyAmount = uint256(lqtyAmount % lqty.balanceOf(user2)); vm.prank(user2); governance.depositLQTY(lqtyAmount); } - function governance_depositLQTYViaPermit(uint88 _lqtyAmount) public withChecks { + function governance_depositLQTYViaPermit(uint256 _lqtyAmount) public withChecks { // Get the current block timestamp for the deadline uint256 deadline = block.timestamp + 1 hours; @@ -259,12 +273,12 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { governance.unregisterInitiative(initiative); } - function governance_withdrawLQTY(uint88 _lqtyAmount) public withChecks { + function governance_withdrawLQTY(uint256 _lqtyAmount) public withChecks { governance.withdrawLQTY(_lqtyAmount); } - function governance_withdrawLQTY_shouldRevertWhenClamped(uint88 _lqtyAmount) public withChecks { - uint88 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user)).staked(); // clamp using the user's staked balance + function governance_withdrawLQTY_shouldRevertWhenClamped(uint256 _lqtyAmount) public withChecks { + uint256 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user)).staked(); // clamp using the user's staked balance // Ensure we have 0 votes try governance.resetAllocations(deployedInitiatives, true) {} diff --git a/test/recon/trophies/SecondTrophiesToFoundry.sol b/test/recon/trophies/SecondTrophiesToFoundry.sol index f46bd61b..b7c9d45c 100644 --- a/test/recon/trophies/SecondTrophiesToFoundry.sol +++ b/test/recon/trophies/SecondTrophiesToFoundry.sol @@ -170,17 +170,17 @@ contract SecondTrophiesToFoundry is Test, TargetFunctions, FoundryAsserts { governance_registerInitiative(1); _loginitiative_and_state(); // 7 - property_sum_of_initatives_matches_total_votes_strict(); + property_sum_of_initatives_matches_total_votes_bounded(); vm.roll(block.number + 3); vm.warp(block.timestamp + 449572); governance_allocateLQTY_clamped_single_initiative(1, 330671315851182842292, 0); _loginitiative_and_state(); // 8 - property_sum_of_initatives_matches_total_votes_strict(); + property_sum_of_initatives_matches_total_votes_bounded(); - governance_resetAllocations(); // NOTE: This leaves 1 vote from user2, and removes the votes from user1 + // governance_resetAllocations(); // user 1 has nothing to reset _loginitiative_and_state(); // In lack of reset, we have 2 wei error | With reset the math is off by 7x - property_sum_of_initatives_matches_total_votes_strict(); + property_sum_of_initatives_matches_total_votes_bounded(); console.log("time 0", block.timestamp); vm.warp(block.timestamp + 231771); @@ -194,17 +194,16 @@ contract SecondTrophiesToFoundry is Test, TargetFunctions, FoundryAsserts { property_sum_of_user_voting_weights_bounded(); property_sum_of_lqty_global_user_matches(); - /// === BROKEN === /// - // property_sum_of_initatives_matches_total_votes_strict(); // THIS IS THE BROKEN PROPERTY + property_sum_of_initatives_matches_total_votes_bounded(); (IGovernance.VoteSnapshot memory snapshot,,) = governance.getTotalVotesAndState(); uint256 initiativeVotesSum; for (uint256 i; i < deployedInitiatives.length; i++) { (IGovernance.InitiativeVoteSnapshot memory initiativeSnapshot,,) = governance.getInitiativeSnapshotAndState(deployedInitiatives[i]); - (Governance.InitiativeStatus status,,) = governance.getInitiativeState(deployedInitiatives[i]); + (IGovernance.InitiativeStatus status,,) = governance.getInitiativeState(deployedInitiatives[i]); - // if (status != Governance.InitiativeStatus.DISABLED) { + // if (status != IGovernance.InitiativeStatus.DISABLED) { // FIX: Only count total if initiative is not disabled initiativeVotesSum += initiativeSnapshot.votes; // } @@ -225,7 +224,6 @@ contract SecondTrophiesToFoundry is Test, TargetFunctions, FoundryAsserts { console.log("snapshot.votes", snapshot.votes); console.log("state.countedVoteLQTY", state.countedVoteLQTY); - console.log("state.countedVoteLQTYAverageTimestamp", state.countedVoteLQTYAverageTimestamp); for (uint256 i; i < deployedInitiatives.length; i++) { ( @@ -234,16 +232,12 @@ contract SecondTrophiesToFoundry is Test, TargetFunctions, FoundryAsserts { ) = governance.getInitiativeSnapshotAndState(deployedInitiatives[i]); console.log("initiativeState.voteLQTY", initiativeState.voteLQTY); - console.log( - "initiativeState.averageStakingTimestampVoteLQTY", initiativeState.averageStakingTimestampVoteLQTY - ); assertEq(snapshot.forEpoch, initiativeSnapshot.forEpoch, "No desynch"); console.log("initiativeSnapshot.votes", initiativeSnapshot.votes); } } - // forge test --match-test test_property_BI07_4 -vv function test_property_BI07_4() public { vm.warp(block.timestamp + 562841); @@ -255,7 +249,8 @@ contract SecondTrophiesToFoundry is Test, TargetFunctions, FoundryAsserts { vm.roll(block.number + 1); - governance_allocateLQTY_clamped_single_initiative_2nd_user(0, 1, 0); + uint8 initiativesIndex = 0; + governance_allocateLQTY_clamped_single_initiative_2nd_user(initiativesIndex, 1, 0); vm.warp(block.timestamp + 403427); @@ -265,7 +260,10 @@ contract SecondTrophiesToFoundry is Test, TargetFunctions, FoundryAsserts { // Doesn't check latest alloc for each user // Property is broken due to wrong spec // For each user you need to grab the latest via the Governance.allocatedByUser - property_resetting_never_reverts(); + address[] memory initiativesToReset = new address[](1); + initiativesToReset[0] = _getDeployedInitiative(initiativesIndex); + vm.startPrank(user2); + property_resetting_never_reverts(initiativesToReset); property_BI07(); } diff --git a/test/recon/trophies/TrophiesToFoundry.sol b/test/recon/trophies/TrophiesToFoundry.sol index c26a8632..ca8382c1 100644 --- a/test/recon/trophies/TrophiesToFoundry.sol +++ b/test/recon/trophies/TrophiesToFoundry.sol @@ -34,14 +34,14 @@ contract TrophiesToFoundry is Test, TargetFunctions, FoundryAsserts { // uint256 state = _getInitiativeStatus(_getDeployedInitiative(0)); // assertEq(state, 5, "Should not be this tbh"); // // check_unregisterable_consistecy(0); - // uint16 epoch = _getLastEpochClaim(_getDeployedInitiative(0)); + // uint256 epoch = _getLastEpochClaim(_getDeployedInitiative(0)); // console.log(epoch + governance.UNREGISTRATION_AFTER_EPOCHS() < governance.epoch() - 1); // vm.warp(block.timestamp + governance.EPOCH_DURATION()); // uint256 newState = _getInitiativeStatus(_getDeployedInitiative(0)); - // uint16 lastEpochClaim = _getLastEpochClaim(_getDeployedInitiative(0)); + // uint256 lastEpochClaim = _getLastEpochClaim(_getDeployedInitiative(0)); // console.log("governance.UNREGISTRATION_AFTER_EPOCHS()", governance.UNREGISTRATION_AFTER_EPOCHS()); // console.log("governance.epoch()", governance.epoch()); @@ -54,8 +54,8 @@ contract TrophiesToFoundry is Test, TargetFunctions, FoundryAsserts { // assertEq(newState, state, "??"); // } - function _getLastEpochClaim(address _initiative) internal returns (uint16) { - (, uint16 epoch,) = governance.getInitiativeState(_initiative); + function _getLastEpochClaim(address _initiative) internal returns (uint256) { + (, uint256 epoch,) = governance.getInitiativeState(_initiative); return epoch; } } diff --git a/test/util/Random.sol b/test/util/Random.sol new file mode 100644 index 00000000..e5ebc3b8 --- /dev/null +++ b/test/util/Random.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +function bound(uint256 x, uint256 min, uint256 max) pure returns (uint256) { + require(min <= max, "min > max"); + return min == 0 && max == type(uint256).max ? x : min + x % (max - min + 1); +} + +library Random { + struct Context { + bytes32 seed; + } + + function init(bytes32 seed) internal pure returns (Random.Context memory c) { + init(c, seed); + } + + function init(Context memory c, bytes32 seed) internal pure { + c.seed = seed; + } + + function generate(Context memory c) internal pure returns (uint256) { + return generate(c, 0, type(uint256).max); + } + + function generate(Context memory c, uint256 max) internal pure returns (uint256) { + return generate(c, 0, max); + } + + function generate(Context memory c, uint256 min, uint256 max) internal pure returns (uint256) { + c.seed = keccak256(abi.encode(c.seed)); + return bound(uint256(c.seed), min, max); + } +} diff --git a/test/util/StringFormatting.sol b/test/util/StringFormatting.sol new file mode 100644 index 00000000..c2aac481 --- /dev/null +++ b/test/util/StringFormatting.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Strings} from "openzeppelin/contracts/utils/Strings.sol"; + +library StringFormatting { + using Strings for uint256; + using StringFormatting for uint256; + using StringFormatting for string; + using StringFormatting for bytes; + + bytes1 constant GROUP_SEPARATOR = "_"; + string constant DECIMAL_SEPARATOR = "."; + string constant DECIMAL_UNIT = " ether"; + + uint256 constant GROUP_DIGITS = 3; + uint256 constant DECIMALS = 18; + uint256 constant ONE = 10 ** DECIMALS; + + function equals(string memory a, string memory b) internal pure returns (bool) { + return keccak256(bytes(a)) == keccak256(bytes(b)); + } + + function toString(bytes memory str) internal pure returns (string memory) { + return string(str); + } + + function toString(bool b) internal pure returns (string memory) { + return b ? "true" : "false"; + } + + function decimal(int256 n) internal pure returns (string memory) { + if (n == type(int256).max) { + return "type(int256).max"; + } else if (n == type(int256).min) { + return "type(int256).min"; + } else if (n < 0) { + return string.concat("-", uint256(-n).decimal()); + } else { + return uint256(n).decimal(); + } + } + + function decimal(uint256 n) internal pure returns (string memory) { + if (n == type(uint256).max) { + return "type(uint256).max"; + } + + uint256 integerPart = n / ONE; + uint256 fractionalPart = n % ONE; + + if (fractionalPart == 0) { + return string.concat(integerPart.groupRight(), DECIMAL_UNIT); + } else { + return string.concat( + integerPart.groupRight(), + DECIMAL_SEPARATOR, + (ONE + fractionalPart).toString().slice(1).trimEnd("0"), + DECIMAL_UNIT + ); + } + } + + function groupRight(uint256 n) internal pure returns (string memory) { + return n.toString().groupRight(); + } + + function groupRight(string memory str) internal pure returns (string memory) { + return bytes(str).groupRight().toString(); + } + + function groupRight(bytes memory str) internal pure returns (bytes memory ret) { + uint256 length = str.length; + if (length == 0) return ""; + + uint256 retLength = length + (length - 1) / GROUP_DIGITS; + ret = new bytes(retLength); + + uint256 j = 1; + for (uint256 i = 1; i <= retLength; ++i) { + if (i % (GROUP_DIGITS + 1) == 0) { + ret[retLength - i] = GROUP_SEPARATOR; + } else { + ret[retLength - i] = str[length - j++]; + } + } + } + + function slice(string memory str, int256 start) internal pure returns (string memory) { + return bytes(str).slice(start).toString(); + } + + function slice(string memory str, int256 start, int256 end) internal pure returns (string memory) { + return bytes(str).slice(start, end).toString(); + } + + function slice(bytes memory str, int256 start) internal pure returns (bytes memory) { + return str.slice(start, int256(str.length)); + } + + // Should only be used on ASCII strings + function slice(bytes memory str, int256 start, int256 end) internal pure returns (bytes memory ret) { + uint256 uStart = uint256(start < 0 ? int256(str.length) + start : start); + uint256 uEnd = uint256(end < 0 ? int256(str.length) + end : end); + assert(0 <= uStart && uStart <= uEnd && uEnd <= str.length); + + ret = new bytes(uEnd - uStart); + + for (uint256 i = uStart; i < uEnd; ++i) { + ret[i - uStart] = str[i]; + } + } + + function trimEnd(string memory str, bytes1 char) internal pure returns (string memory) { + return bytes(str).trimEnd(char).toString(); + } + + function trimEnd(bytes memory str, bytes1 char) internal pure returns (bytes memory) { + uint256 end; + for (end = str.length; end > 0 && str[end - 1] == char; --end) {} + return str.slice(0, int256(end)); + } + + function join(string[] memory strs, string memory sep) internal pure returns (string memory ret) { + if (strs.length == 0) return ""; + + ret = strs[0]; + for (uint256 i = 1; i < strs.length; ++i) { + ret = string.concat(ret, sep, strs[i]); + } + } +} diff --git a/test/util/UintArray.sol b/test/util/UintArray.sol new file mode 100644 index 00000000..855ff00d --- /dev/null +++ b/test/util/UintArray.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Random} from "./Random.sol"; + +library UintArray { + using Random for Random.Context; + + function seq(uint256 last) internal pure returns (uint256[] memory) { + return seq(0, last); + } + + function seq(uint256 first, uint256 last) internal pure returns (uint256[] memory array) { + require(first <= last, "first > last"); + return seq(new uint256[](last - first), first); + } + + function seq(uint256[] memory array) internal pure returns (uint256[] memory) { + return seq(array, 0); + } + + function seq(uint256[] memory array, uint256 first) internal pure returns (uint256[] memory) { + for (uint256 i = 0; i < array.length; ++i) { + array[i] = first + i; + } + + return array; + } + + function slice(uint256[] memory array) internal pure returns (uint256[] memory) { + return slice(array, uint256(0), array.length); + } + + function slice(uint256[] memory array, uint256 start) internal pure returns (uint256[] memory) { + return slice(array, start, array.length); + } + + function slice(uint256[] memory array, int256 start) internal pure returns (uint256[] memory) { + return slice(array, start, array.length); + } + + function slice(uint256[] memory array, uint256 start, int256 end) internal pure returns (uint256[] memory) { + return slice(array, start, uint256(end < 0 ? int256(array.length) + end : end)); + } + + function slice(uint256[] memory array, int256 start, uint256 end) internal pure returns (uint256[] memory) { + return slice(array, uint256(start < 0 ? int256(array.length) + start : start), end); + } + + function slice(uint256[] memory array, int256 start, int256 end) internal pure returns (uint256[] memory) { + return slice( + array, + uint256(start < 0 ? int256(array.length) + start : start), + uint256(end < 0 ? int256(array.length) + end : end) + ); + } + + function slice(uint256[] memory array, uint256 start, uint256 end) internal pure returns (uint256[] memory ret) { + require(start <= end, "start > end"); + require(end <= array.length, "end > array.length"); + + ret = new uint256[](end - start); + + for (uint256 i = start; i < end; ++i) { + ret[i - start] = array[i]; + } + } + + function permute(uint256[] memory array, bytes32 seed) internal pure returns (uint256[] memory) { + return permute(array, Random.init(seed)); + } + + function permute(uint256[] memory array, Random.Context memory random) internal pure returns (uint256[] memory) { + for (uint256 i = 0; i < array.length - 1; ++i) { + uint256 j = random.generate(i, array.length - 1); + (array[i], array[j]) = (array[j], array[i]); + } + + return array; + } +}