diff --git a/Invariants.MD b/Invariants.MD new file mode 100644 index 00000000..c97ae35c --- /dev/null +++ b/Invariants.MD @@ -0,0 +1,10 @@ +Snapshot Solvency + + uint256 claim = _votesForInitiativeSnapshot.votes * boldAccrued / _votesSnapshot.votes; +For each initiative this is what the value is +If the initiative is "Claimable" this is what it receives +The call never reverts +The sum of claims is less than the boldAccrued + +Veto consistency + diff --git a/ToFix.MD b/ToFix.MD new file mode 100644 index 00000000..36cba9e8 --- /dev/null +++ b/ToFix.MD @@ -0,0 +1,31 @@ +- Add properties check to ensure that the math is sound <- HUGE, let's add it now + +A vote is: User TS * Votes +So an allocation should use that +We need to remove the data from the valid allocation +And not from a random one + +I think the best test is to simply store the contribution done +And see whether removing it is idempotent + +We would need a ton of work to make it even better + + +Specifically, if a user removes their votes, we need to see that reflect correctly +Because that's key + +- From there, try fixing with a reset on deposit and withdraw + +- Add a test that checks every: initiative, user allocation, ensure they are zero after a deposit and a withdrawal +- Add a test that checks every: X, ensure they use the correct TS + +- From there, reason around the deeper rounding errors + + + +Optimizations +Put the data in the storage +Remove all castings that are not safe +Invariant test it + +-- \ No newline at end of file diff --git a/echidna.yaml b/echidna.yaml index fc2a82f3..21c707d2 100644 --- a/echidna.yaml +++ b/echidna.yaml @@ -1,8 +1,10 @@ -testMode: "assertion" -prefix: "crytic_" +testMode: "property" +prefix: "optimize_" coverage: true corpusDir: "echidna" balanceAddr: 0x1043561a8829300000 balanceContract: 0x1043561a8829300000 filterFunctions: [] -cryticArgs: ["--foundry-compile-all"] \ No newline at end of file +cryticArgs: ["--foundry-compile-all"] + +shrinkLimit: 100000 diff --git a/src/BribeInitiative.sol b/src/BribeInitiative.sol index 140a35d0..fe6c4c0f 100644 --- a/src/BribeInitiative.sol +++ b/src/BribeInitiative.sol @@ -45,19 +45,19 @@ contract BribeInitiative is IInitiative, IBribeInitiative { } /// @inheritdoc IBribeInitiative - function totalLQTYAllocatedByEpoch(uint16 _epoch) external view returns (uint88, uint32) { + function totalLQTYAllocatedByEpoch(uint16 _epoch) external view returns (uint88, uint120) { return _loadTotalLQTYAllocation(_epoch); } /// @inheritdoc IBribeInitiative - function lqtyAllocatedByUserAtEpoch(address _user, uint16 _epoch) external view returns (uint88, uint32) { + function lqtyAllocatedByUserAtEpoch(address _user, uint16 _epoch) external view returns (uint88, uint120) { return _loadLQTYAllocation(_user, _epoch); } /// @inheritdoc IBribeInitiative function depositBribe(uint128 _boldAmount, uint128 _bribeTokenAmount, uint16 _epoch) external { uint16 epoch = governance.epoch(); - require(_epoch >= epoch, "BribeInitiative: only-future-epochs"); + require(_epoch >= epoch, "BribeInitiative: now-or-future-epochs"); Bribe memory bribe = bribeByEpoch[_epoch]; bribe.boldAmount += _boldAmount; @@ -70,6 +70,8 @@ contract BribeInitiative is IInitiative, IBribeInitiative { bribeToken.safeTransferFrom(msg.sender, address(this), _bribeTokenAmount); } + uint256 constant TIMESTAMP_PRECISION = 1e26; + function _claimBribe( address _user, uint16 _epoch, @@ -98,11 +100,24 @@ contract BribeInitiative is IInitiative, IBribeInitiative { "BribeInitiative: invalid-prev-total-lqty-allocation-epoch" ); - (uint88 totalLQTY, uint32 totalAverageTimestamp) = _decodeLQTYAllocation(totalLQTYAllocation.value); - uint240 totalVotes = governance.lqtyToVotes(totalLQTY, block.timestamp, totalAverageTimestamp); + (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); + + uint240 totalVotes = governance.lqtyToVotes(totalLQTY, scaledEpochEnd, totalAverageTimestamp); if (totalVotes != 0) { - (uint88 lqty, uint32 averageTimestamp) = _decodeLQTYAllocation(lqtyAllocation.value); - uint240 votes = governance.lqtyToVotes(lqty, block.timestamp, averageTimestamp); + (uint88 lqty, uint120 averageTimestamp) = _decodeLQTYAllocation(lqtyAllocation.value); + + /// @audit Governance Invariant + assert(averageTimestamp <= scaledEpochEnd); + + uint240 votes = governance.lqtyToVotes(lqty, scaledEpochEnd, averageTimestamp); boldAmount = uint256(bribe.boldAmount) * uint256(votes) / uint256(totalVotes); bribeTokenAmount = uint256(bribe.bribeTokenAmount) * uint256(votes) / uint256(totalVotes); } @@ -126,6 +141,9 @@ 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) { @@ -133,6 +151,7 @@ contract BribeInitiative is IInitiative, IBribeInitiative { } bold.safeTransfer(msg.sender, boldAmount); } + if (bribeTokenAmount != 0) { uint256 max = bribeToken.balanceOf(address(this)); if (bribeTokenAmount > max) { @@ -148,10 +167,10 @@ contract BribeInitiative is IInitiative, IBribeInitiative { /// @inheritdoc IInitiative function onUnregisterInitiative(uint16) external virtual override onlyGovernance {} - function _setTotalLQTYAllocationByEpoch(uint16 _epoch, uint88 _lqty, uint32 _averageTimestamp, bool _insert) + function _setTotalLQTYAllocationByEpoch(uint16 _epoch, uint88 _lqty, uint120 _averageTimestamp, bool _insert) private { - uint224 value = (uint224(_lqty) << 32) | _averageTimestamp; + uint224 value = _encodeLQTYAllocation(_lqty, _averageTimestamp); if (_insert) { totalLQTYAllocationByEpoch.insert(_epoch, value, 0); } else { @@ -164,10 +183,10 @@ contract BribeInitiative is IInitiative, IBribeInitiative { address _user, uint16 _epoch, uint88 _lqty, - uint32 _averageTimestamp, + uint120 _averageTimestamp, bool _insert ) private { - uint224 value = (uint224(_lqty) << 32) | _averageTimestamp; + uint224 value = _encodeLQTYAllocation(_lqty, _averageTimestamp); if (_insert) { lqtyAllocationByUserAtEpoch[_user].insert(_epoch, value, 0); } else { @@ -176,20 +195,20 @@ contract BribeInitiative is IInitiative, IBribeInitiative { emit ModifyLQTYAllocation(_user, _epoch, _lqty, _averageTimestamp); } - function _encodeLQTYAllocation(uint88 _lqty, uint32 _averageTimestamp) private pure returns (uint224) { + function _encodeLQTYAllocation(uint88 _lqty, uint120 _averageTimestamp) private pure returns (uint224) { return EncodingDecodingLib.encodeLQTYAllocation(_lqty, _averageTimestamp); } - function _decodeLQTYAllocation(uint224 _value) private pure returns (uint88, uint32) { + function _decodeLQTYAllocation(uint224 _value) private pure returns (uint88, uint120) { return EncodingDecodingLib.decodeLQTYAllocation(_value); } - function _loadTotalLQTYAllocation(uint16 _epoch) private view returns (uint88, uint32) { + 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, uint32) { + 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); } diff --git a/src/CurveV2GaugeRewards.sol b/src/CurveV2GaugeRewards.sol index 5d9d9824..365e432f 100644 --- a/src/CurveV2GaugeRewards.sol +++ b/src/CurveV2GaugeRewards.sol @@ -26,16 +26,23 @@ contract CurveV2GaugeRewards is BribeInitiative { _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; + // For small donations queue them into the contract - if (amount < duration * 1000) { + if (total < duration * 1000) { remainder += amount; return; } - uint256 total = amount + remainder; remainder = 0; + uint256 available = bold.balanceOf(address(this)); + if (available < total) { + total = available; // Cap due to rounding error causing a bit more bold being given away + } + bold.approve(address(gauge), total); gauge.deposit_reward_token(address(bold), total, duration); diff --git a/src/Governance.sol b/src/Governance.sol index 89b0fc8b..6a9a9af6 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -13,7 +13,7 @@ import {UserProxy} from "./UserProxy.sol"; import {UserProxyFactory} from "./UserProxyFactory.sol"; import {add, max} from "./utils/Math.sol"; -import {_requireNoDuplicates} from "./utils/UniqueArray.sol"; +import {_requireNoDuplicates, _requireNoNegatives} from "./utils/UniqueArray.sol"; import {Multicall} from "./utils/Multicall.sol"; import {WAD, PermitParams} from "./utils/Types.sol"; import {safeCallWithMinGas} from "./utils/SafeCallMinGas.sol"; @@ -76,6 +76,9 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance 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; + constructor( address _lqty, address _lusd, @@ -118,48 +121,62 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance } } - function _averageAge(uint32 _currentTimestamp, uint32 _averageTimestamp) internal pure returns (uint32) { + function _averageAge(uint120 _currentTimestamp, uint120 _averageTimestamp) internal pure returns (uint120) { if (_averageTimestamp == 0 || _currentTimestamp < _averageTimestamp) return 0; return _currentTimestamp - _averageTimestamp; } function _calculateAverageTimestamp( - uint32 _prevOuterAverageTimestamp, - uint32 _newInnerAverageTimestamp, + uint120 _prevOuterAverageTimestamp, + uint120 _newInnerAverageTimestamp, uint88 _prevLQTYBalance, uint88 _newLQTYBalance - ) internal view returns (uint32) { + ) internal view returns (uint120) { if (_newLQTYBalance == 0) return 0; - uint32 prevOuterAverageAge = _averageAge(uint32(block.timestamp), _prevOuterAverageTimestamp); - uint32 newInnerAverageAge = _averageAge(uint32(block.timestamp), _newInnerAverageTimestamp); + // 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); - uint88 newOuterAverageAge; + // 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; - uint240 prevVotes = uint240(_prevLQTYBalance) * uint240(prevOuterAverageAge); - uint240 newVotes = uint240(deltaLQTY) * uint240(newInnerAverageAge); - uint240 votes = prevVotes + newVotes; - newOuterAverageAge = uint32(votes / uint240(_newLQTYBalance)); + uint208 prevVotes = uint208(_prevLQTYBalance) * uint208(prevOuterAverageAge); + uint208 newVotes = uint208(deltaLQTY) * uint208(newInnerAverageAge); + uint208 votes = prevVotes + newVotes; + newOuterAverageAge = votes / _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)); + uint208 prevVotes = uint208(_prevLQTYBalance) * uint208(prevOuterAverageAge); + uint208 newVotes = uint208(deltaLQTY) * uint208(newInnerAverageAge); + uint208 votes = (prevVotes >= newVotes) ? prevVotes - newVotes : 0; + newOuterAverageAge = votes / _newLQTYBalance; } - if (newOuterAverageAge > block.timestamp) return 0; - return uint32(block.timestamp - newOuterAverageAge); + if (newOuterAverageAge > currentTime) return 0; + return uint120(currentTime - newOuterAverageAge); } /*////////////////////////////////////////////////////////////// STAKING //////////////////////////////////////////////////////////////*/ - function _deposit(uint88 _lqtyAmount) private returns (UserProxy) { + function _updateUserTimestamp(uint88 _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) { @@ -171,9 +188,13 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance uint88 lqtyStaked = uint88(stakingV1.stakes(userProxyAddress)); // update the average staked timestamp for LQTY staked by the user - UserState memory userState = userStates[msg.sender]; + + // NOTE: Upscale user TS by `TIMESTAMP_PRECISION` userState.averageStakingTimestamp = _calculateAverageTimestamp( - userState.averageStakingTimestamp, uint32(block.timestamp), lqtyStaked, lqtyStaked + _lqtyAmount + userState.averageStakingTimestamp, + uint120(block.timestamp) * uint120(TIMESTAMP_PRECISION), + lqtyStaked, + lqtyStaked + _lqtyAmount ); userStates[msg.sender] = userState; @@ -184,28 +205,27 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @inheritdoc IGovernance function depositLQTY(uint88 _lqtyAmount) external nonReentrant { - UserProxy userProxy = _deposit(_lqtyAmount); + UserProxy userProxy = _updateUserTimestamp(_lqtyAmount); userProxy.stake(_lqtyAmount, msg.sender); } /// @inheritdoc IGovernance function depositLQTYViaPermit(uint88 _lqtyAmount, PermitParams calldata _permitParams) external nonReentrant { - UserProxy userProxy = _deposit(_lqtyAmount); + UserProxy userProxy = _updateUserTimestamp(_lqtyAmount); userProxy.stakeViaPermit(_lqtyAmount, msg.sender, _permitParams); } /// @inheritdoc IGovernance function withdrawLQTY(uint88 _lqtyAmount) external nonReentrant { + // check that user has reset before changing lqty balance + 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))); - UserState storage userState = userStates[msg.sender]; - - // check if user has enough unallocated lqty - require(_lqtyAmount <= lqtyStaked - userState.allocatedLQTY, "Governance: insufficient-unallocated-lqty"); - (uint256 accruedLUSD, uint256 accruedETH) = userProxy.unstake(_lqtyAmount, msg.sender); emit WithdrawLQTY(msg.sender, _lqtyAmount, accruedLUSD, accruedETH); @@ -244,12 +264,12 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance } /// @inheritdoc IGovernance - function lqtyToVotes(uint88 _lqtyAmount, uint256 _currentTimestamp, uint32 _averageTimestamp) + function lqtyToVotes(uint88 _lqtyAmount, uint120 _currentTimestamp, uint120 _averageTimestamp) public pure - returns (uint240) + returns (uint208) { - return uint240(_lqtyAmount) * _averageAge(uint32(_currentTimestamp), _averageTimestamp); + return uint208(_lqtyAmount) * uint208(_averageAge(_currentTimestamp, _averageTimestamp)); } /*////////////////////////////////////////////////////////////// @@ -313,7 +333,11 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance if (snapshot.forEpoch < currentEpoch - 1) { shouldUpdate = true; - snapshot.votes = lqtyToVotes(state.countedVoteLQTY, epochStart(), state.countedVoteLQTYAverageTimestamp); + snapshot.votes = lqtyToVotes( + state.countedVoteLQTY, + uint120(epochStart()) * uint120(TIMESTAMP_PRECISION), + state.countedVoteLQTYAverageTimestamp + ); snapshot.forEpoch = currentEpoch - 1; } } @@ -352,13 +376,14 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance if (initiativeSnapshot.forEpoch < currentEpoch - 1) { shouldUpdate = true; - uint32 start = epochStart(); - uint240 votes = + uint120 start = uint120(epochStart()) * uint120(TIMESTAMP_PRECISION); + uint208 votes = lqtyToVotes(initiativeState.voteLQTY, start, initiativeState.averageStakingTimestampVoteLQTY); - uint240 vetos = + uint208 vetos = lqtyToVotes(initiativeState.vetoLQTY, start, initiativeState.averageStakingTimestampVetoLQTY); - initiativeSnapshot.votes = uint224(votes); - initiativeSnapshot.vetos = uint224(vetos); + // NOTE: Upscaling to u224 is safe + initiativeSnapshot.votes = votes; + initiativeSnapshot.vetos = vetos; initiativeSnapshot.forEpoch = currentEpoch - 1; } @@ -450,11 +475,25 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // == 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.vetos >= _votesForInitiativeSnapshot.votes) - ) { - 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); } @@ -462,8 +501,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // e.g. if `UNREGISTRATION_AFTER_EPOCHS` is 4, the 4th epoch flip that would result in SKIP, will result in the initiative being `UNREGISTERABLE` if ( (_initiativeState.lastEpochClaim + UNREGISTRATION_AFTER_EPOCHS < epoch() - 1) - || _votesForInitiativeSnapshot.vetos > _votesForInitiativeSnapshot.votes - && _votesForInitiativeSnapshot.vetos > votingTheshold * UNREGISTRATION_THRESHOLD_FACTOR / WAD + || upscaledInitiativeVetos > upscaledInitiativeVotes + && upscaledInitiativeVetos > votingTheshold * UNREGISTRATION_THRESHOLD_FACTOR / WAD ) { return (InitiativeStatus.UNREGISTERABLE, lastEpochClaim, 0); } @@ -486,9 +525,14 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // 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); require( - lqtyToVotes(uint88(stakingV1.stakes(userProxyAddress)), epochStart(), userState.averageStakingTimestamp) - >= snapshot.votes * REGISTRATION_THRESHOLD_FACTOR / WAD, + lqtyToVotes( + uint88(stakingV1.stakes(userProxyAddress)), + uint120(epochStart()) * uint120(TIMESTAMP_PRECISION), + userState.averageStakingTimestamp + ) >= upscaledSnapshotVotes * REGISTRATION_THRESHOLD_FACTOR / WAD, "Governance: insufficient-lqty" ); @@ -496,6 +540,9 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance 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); // Replaces try / catch | Enforces sufficient gas is passed @@ -528,7 +575,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // Must be below, else we cannot reset" // Makes cast safe - /// @audit INVARIANT: property_ensure_user_alloc_cannot_dos + /// @audit Check INVARIANT: property_ensure_user_alloc_cannot_dos assert(alloc.voteLQTY <= uint88(type(int88).max)); assert(alloc.vetoLQTY <= uint88(type(int88).max)); @@ -550,6 +597,23 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance 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 + function resetAllocations(address[] calldata _initiativesToReset, bool checkAll) external nonReentrant { + _requireNoDuplicates(_initiativesToReset); + _resetInitiatives(_initiativesToReset); + + // NOTE: In most cases, the check will pass + // But if you allocate too many initiatives, we may run OOG + // As such the check is optional here + // All other calls to the system enforce this + // So it's recommended that your last call to `resetAllocations` passes the check + if (checkAll) { + require(userStates[msg.sender].allocatedLQTY == 0, "Governance: must be a reset"); + } + } + /// @inheritdoc IGovernance function allocateLQTY( address[] calldata _initiativesToReset, @@ -564,6 +628,10 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance _requireNoDuplicates(_initiatives); _requireNoDuplicates(_initiativesToReset); + // Explicit >= 0 checks for all values since we reset values below + _requireNoNegatives(_absoluteLQTYVotes); + _requireNoNegatives(_absoluteLQTYVetos); + // You MUST always reset ResetInitiativeData[] memory cachedData = _resetInitiatives(_initiativesToReset); @@ -637,7 +705,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance getInitiativeState(initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState); if (deltaLQTYVotes > 0 || deltaLQTYVetos > 0) { - /// @audit FSM CHECK, note that the original version allowed voting on `Unregisterable` Initiatives | This fixes it + /// @audit You cannot vote on `unregisterable` but a vote may have been there require( status == InitiativeStatus.SKIP || status == InitiativeStatus.CLAIMABLE || status == InitiativeStatus.CLAIMED, @@ -665,12 +733,14 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance 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) ); @@ -699,6 +769,9 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // 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 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 @@ -713,16 +786,16 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance assert(state.countedVoteLQTY >= prevInitiativeState.voteLQTY); /// @audit INVARIANT: Never overflows state.countedVoteLQTY -= prevInitiativeState.voteLQTY; - } - /// Add current - state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( - state.countedVoteLQTYAverageTimestamp, - initiativeState.averageStakingTimestampVoteLQTY, - state.countedVoteLQTY, - state.countedVoteLQTY + initiativeState.voteLQTY - ); - state.countedVoteLQTY += initiativeState.voteLQTY; + state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( + state.countedVoteLQTYAverageTimestamp, + initiativeState.averageStakingTimestampVoteLQTY, + state.countedVoteLQTY, + state.countedVoteLQTY + initiativeState.voteLQTY + ); + + state.countedVoteLQTY += initiativeState.voteLQTY; + } // == USER ALLOCATION == // @@ -810,10 +883,12 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @inheritdoc IGovernance function claimForInitiative(address _initiative) external nonReentrant returns (uint256) { + // Accrue and update state (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(_initiative); + // Compute values on accrued state (InitiativeStatus status,, uint256 claimableAmount) = getInitiativeState(_initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState); @@ -829,6 +904,14 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// 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 + /// We upscale the timestamp to reduce the impact of the loss + /// However this is still possible + uint256 available = bold.balanceOf(address(this)); + if (claimableAmount > available) { + claimableAmount = available; + } + bold.safeTransfer(_initiative, claimableAmount); emit ClaimForInitiative(_initiative, claimableAmount, votesSnapshot_.forEpoch); diff --git a/src/interfaces/IBribeInitiative.sol b/src/interfaces/IBribeInitiative.sol index dd58e931..061bdf3a 100644 --- a/src/interfaces/IBribeInitiative.sol +++ b/src/interfaces/IBribeInitiative.sol @@ -7,8 +7,8 @@ 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, uint32 averageTimestamp); - event ModifyTotalLQTYAllocation(uint16 epoch, uint88 totalLQTYAllocated, uint32 averageTimestamp); + 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); /// @notice Address of the governance contract @@ -43,7 +43,7 @@ interface IBribeInitiative { function totalLQTYAllocatedByEpoch(uint16 _epoch) external view - returns (uint88 totalLQTYAllocated, uint32 averageTimestamp); + returns (uint88 totalLQTYAllocated, uint120 averageTimestamp); /// @notice LQTY allocated by a user to the initiative at a given epoch /// @param _user Address of the user /// @param _epoch Epoch at which the LQTY was allocated by the user @@ -51,7 +51,7 @@ interface IBribeInitiative { function lqtyAllocatedByUserAtEpoch(address _user, uint16 _epoch) external view - returns (uint88 lqtyAllocated, uint32 averageTimestamp); + returns (uint88 lqtyAllocated, uint120 averageTimestamp); /// @notice Deposit bribe tokens for a given epoch /// @dev The caller has to approve this contract to spend the BOLD and bribe tokens. diff --git a/src/interfaces/IGovernance.sol b/src/interfaces/IGovernance.sol index 834c6d12..d25f3e95 100644 --- a/src/interfaces/IGovernance.sol +++ b/src/interfaces/IGovernance.sol @@ -117,20 +117,20 @@ interface IGovernance { struct UserState { uint88 allocatedLQTY; // LQTY allocated by the user - uint32 averageStakingTimestamp; // Average timestamp at which LQTY was staked by the user + uint120 averageStakingTimestamp; // Average timestamp at which LQTY was staked by the user } struct InitiativeState { uint88 voteLQTY; // LQTY allocated vouching for the initiative uint88 vetoLQTY; // LQTY allocated vetoing the initiative - uint32 averageStakingTimestampVoteLQTY; // Average staking timestamp of the voting LQTY for the initiative - uint32 averageStakingTimestampVetoLQTY; // Average staking timestamp of the vetoing LQTY for 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; } struct GlobalState { uint88 countedVoteLQTY; // Total LQTY that is included in vote counting - uint32 countedVoteLQTYAverageTimestamp; // Average timestamp: derived initiativeAllocation.averageTimestamp + uint120 countedVoteLQTYAverageTimestamp; // Average timestamp: derived initiativeAllocation.averageTimestamp } /// TODO: Bold balance? Prob cheaper @@ -138,7 +138,7 @@ interface IGovernance { /// @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, uint32 averageStakingTimestamp); + function userStates(address _user) external view returns (uint88 allocatedLQTY, uint120 averageStakingTimestamp); /// @notice Returns the initiative's state /// @param _initiative Address of the initiative /// @return voteLQTY LQTY allocated vouching for the initiative @@ -152,14 +152,14 @@ interface IGovernance { returns ( uint88 voteLQTY, uint88 vetoLQTY, - uint32 averageStakingTimestampVoteLQTY, - uint32 averageStakingTimestampVetoLQTY, + uint120 averageStakingTimestampVoteLQTY, + uint120 averageStakingTimestampVetoLQTY, uint16 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, uint32 countedVoteLQTYAverageTimestamp); + function globalState() external view returns (uint88 countedVoteLQTY, uint120 countedVoteLQTYAverageTimestamp); /// @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 @@ -215,10 +215,10 @@ interface IGovernance { /// @param _currentTimestamp Current timestamp /// @param _averageTimestamp Average timestamp at which the LQTY was staked /// @return votes Number of votes - function lqtyToVotes(uint88 _lqtyAmount, uint256 _currentTimestamp, uint32 _averageTimestamp) + function lqtyToVotes(uint88 _lqtyAmount, uint120 _currentTimestamp, uint120 _averageTimestamp) external pure - returns (uint240); + returns (uint208); /// @notice Voting threshold is the max. of either: /// - 4% of the total voting LQTY in the previous epoch diff --git a/src/utils/EncodingDecodingLib.sol b/src/utils/EncodingDecodingLib.sol index 5173b166..79026859 100644 --- a/src/utils/EncodingDecodingLib.sol +++ b/src/utils/EncodingDecodingLib.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.24; library EncodingDecodingLib { - function encodeLQTYAllocation(uint88 _lqty, uint32 _averageTimestamp) internal pure returns (uint224) { - uint224 _value = (uint224(_lqty) << 32) | _averageTimestamp; + 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, uint32) { - return (uint88(_value >> 32), uint32(_value)); + function decodeLQTYAllocation(uint224 _value) internal pure returns (uint88, uint120) { + return (uint88(_value >> 120), uint120(_value)); } } diff --git a/src/utils/UniqueArray.sol b/src/utils/UniqueArray.sol index d2809205..17812174 100644 --- a/src/utils/UniqueArray.sol +++ b/src/utils/UniqueArray.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.24; /// @dev Checks that there's no duplicate addresses /// @param arr - List to check for dups -function _requireNoDuplicates(address[] memory arr) pure { +function _requireNoDuplicates(address[] calldata arr) pure { uint256 arrLength = arr.length; // only up to len - 1 (no j to check if i == len - 1) for (uint i; i < arrLength - 1;) { @@ -20,3 +20,11 @@ function _requireNoDuplicates(address[] memory arr) pure { } } } + +function _requireNoNegatives(int88[] memory vals) pure { + uint256 arrLength = vals.length; + + for (uint i; i < arrLength; i++) { + require(vals[i] >= 0, "Cannot be negative"); + } +} diff --git a/test/BribeInitiative.t.sol b/test/BribeInitiative.t.sol index a922c0e5..c4290eb2 100644 --- a/test/BribeInitiative.t.sol +++ b/test/BribeInitiative.t.sol @@ -574,59 +574,6 @@ contract BribeInitiativeTest is Test { assertEq(bribeTokenAmount, 0, "vetoer receives bribe amount"); } - // TODO: check favorability of splitting allocation between different initiative/epochs - // @audit doesn't seem like it makes it more favorable because user still withdraws full bribe amount - // forge test --match-test test_splitting_allocation -vv - function test_splitting_allocation() public { - // =========== epoch 1 ================== - // user stakes half in epoch 1 - int88 lqtyAmount = 2e18; - _stakeLQTY(user1, uint88(lqtyAmount / 2)); - - // =========== 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 4 - _depositBribe(1e18, 1e18, governance.epoch() + 1); - uint16 epochToClaimFor = governance.epoch() + 1; - - // user votes on bribeInitiative with half - _allocateLQTY(user1, lqtyAmount / 2, 0); - (, uint32 averageStakingTimestamp1) = governance.userStates(user1); - - uint16 epochDepositedHalf = governance.epoch(); - - // =========== epoch 2 (end of cutoff) ================== - vm.warp(block.timestamp + EPOCH_DURATION - EPOCH_VOTING_CUTOFF); - assertEq(2, governance.epoch(), "not in epoch 2"); - - // user stakes other half - _stakeLQTY(user1, uint88(lqtyAmount / 2)); - // user votes on bribeInitiative with other half - _allocateLQTY(user1, lqtyAmount / 2, 0); - - uint16 epochDepositedRest = governance.epoch(); - (, uint32 averageStakingTimestamp2) = governance.userStates(user1); - assertTrue( - averageStakingTimestamp1 != averageStakingTimestamp2, "averageStakingTimestamp1 == averageStakingTimestamp2" - ); - - assertEq(epochDepositedHalf, epochDepositedRest, "We are in the same epoch"); - - // =========== epoch 4 ================== - vm.warp(block.timestamp + (EPOCH_DURATION * 2)); - assertEq(4, governance.epoch(), "not in epoch 4"); - - // user should receive bribe from their allocated stake - (uint256 boldAmount, uint256 bribeTokenAmount) = - _claimBribe(user1, epochToClaimFor, epochDepositedRest, epochDepositedRest); - assertEq(boldAmount, 1e18, "boldAmount"); - assertEq(bribeTokenAmount, 1e18, "bribeTokenAmount"); - - // With non spliting the amount would be 1e18, so this is a bug due to how allocations work - } - // checks that user can receive bribes for an epoch in which they were allocated even if they're no longer allocated function test_decrement_after_claimBribes() public { // =========== epoch 1 ================== @@ -1001,11 +948,11 @@ contract BribeInitiativeTest is Test { vm.stopPrank(); } - function _depositBribe(address initiative, uint128 boldAmount, uint128 bribeAmount, uint16 epoch) public { + function _depositBribe(address _initiative, uint128 boldAmount, uint128 bribeAmount, uint16 epoch) public { vm.startPrank(lusdHolder); - lqty.approve(initiative, boldAmount); - lusd.approve(initiative, bribeAmount); - BribeInitiative(initiative).depositBribe(boldAmount, bribeAmount, epoch); + lqty.approve(_initiative, boldAmount); + lusd.approve(_initiative, bribeAmount); + BribeInitiative(_initiative).depositBribe(boldAmount, bribeAmount, epoch); vm.stopPrank(); } diff --git a/test/BribeInitiativeAllocate.t.sol b/test/BribeInitiativeAllocate.t.sol index e2053826..3653a68f 100644 --- a/test/BribeInitiativeAllocate.t.sol +++ b/test/BribeInitiativeAllocate.t.sol @@ -80,14 +80,14 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); } - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); { IGovernance.UserState memory userState2 = @@ -104,11 +104,11 @@ contract BribeInitiativeAllocateTest is Test { bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState2, allocation2, initiativeState2); } - (uint88 totalLQTYAllocated2, uint32 totalAverageTimestamp2) = + (uint88 totalLQTYAllocated2, uint120 totalAverageTimestamp2) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated2, 1001e18); assertEq(totalAverageTimestamp2, block.timestamp); - (uint88 userLQTYAllocated2, uint32 userAverageTimestamp2) = + (uint88 userLQTYAllocated2, uint120 userAverageTimestamp2) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocated2, 1000e18); assertEq(userAverageTimestamp2, block.timestamp); @@ -159,9 +159,12 @@ contract BribeInitiativeAllocateTest is Test { 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)}); @@ -174,16 +177,18 @@ contract BribeInitiativeAllocateTest is Test { lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + 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)}); @@ -196,16 +201,18 @@ contract BribeInitiativeAllocateTest is Test { lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1001e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } + // lusdHolder deposits bribes into the initiative vm.startPrank(lusdHolder); lqty.approve(address(bribeInitiative), 1000e18); lusd.approve(address(bribeInitiative), 1000e18); @@ -213,9 +220,12 @@ contract BribeInitiativeAllocateTest is Test { 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)}); @@ -228,16 +238,18 @@ contract BribeInitiativeAllocateTest is Test { lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 0); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocated, 0); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + 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)}); @@ -250,17 +262,18 @@ contract BribeInitiativeAllocateTest is Test { lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 0); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); assertEq(userLQTYAllocated, 0); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } governance.setEpoch(3); + vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to third epoch ts vm.startPrank(address(user)); @@ -269,12 +282,13 @@ contract BribeInitiativeAllocateTest is Test { claimData[0].prevLQTYAllocationEpoch = 2; claimData[0].prevTotalLQTYAllocationEpoch = 2; (uint256 boldAmount, uint256 bribeTokenAmount) = bribeInitiative.claimBribes(claimData); - assertEq(boldAmount, 0); - assertEq(bribeTokenAmount, 0); + 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)); @@ -291,14 +305,14 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); IGovernance.UserState memory userStateVeto = IGovernance.UserState({allocatedLQTY: 1000e18, averageStakingTimestamp: uint32(block.timestamp)}); @@ -315,16 +329,17 @@ contract BribeInitiativeAllocateTest is Test { governance.epoch(), user, userStateVeto, allocationVeto, initiativeStateVeto ); - (uint88 totalLQTYAllocatedAfterVeto, uint32 totalAverageTimestampAfterVeto) = + (uint88 totalLQTYAllocatedAfterVeto, uint120 totalAverageTimestampAfterVeto) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocatedAfterVeto, 1e18); - assertEq(totalAverageTimestampAfterVeto, uint32(block.timestamp)); - (uint88 userLQTYAllocatedAfterVeto, uint32 userAverageTimestampAfterVeto) = + assertEq(totalAverageTimestampAfterVeto, uint120(block.timestamp)); + (uint88 userLQTYAllocatedAfterVeto, uint120 userAverageTimestampAfterVeto) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocatedAfterVeto, 0); - assertEq(userAverageTimestampAfterVeto, uint32(block.timestamp)); + 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)}); @@ -341,14 +356,14 @@ contract BribeInitiativeAllocateTest is Test { governance.epoch(), user, userStateNewEpoch, allocationNewEpoch, initiativeStateNewEpoch ); - (uint88 totalLQTYAllocatedNewEpoch, uint32 totalAverageTimestampNewEpoch) = + (uint88 totalLQTYAllocatedNewEpoch, uint120 totalAverageTimestampNewEpoch) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocatedNewEpoch, 1e18); - assertEq(totalAverageTimestampNewEpoch, uint32(block.timestamp)); - (uint88 userLQTYAllocatedNewEpoch, uint32 userAverageTimestampNewEpoch) = + assertEq(totalAverageTimestampNewEpoch, uint120(block.timestamp)); + (uint88 userLQTYAllocatedNewEpoch, uint120 userAverageTimestampNewEpoch) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocatedNewEpoch, 0); - assertEq(userAverageTimestampNewEpoch, uint32(block.timestamp)); + assertEq(userAverageTimestampNewEpoch, uint120(block.timestamp)); vm.startPrank(lusdHolder); lqty.approve(address(bribeInitiative), 1000e18); @@ -359,6 +374,7 @@ contract BribeInitiativeAllocateTest is Test { 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)}); @@ -375,16 +391,17 @@ contract BribeInitiativeAllocateTest is Test { governance.epoch(), user, userStateNewEpoch3, allocationNewEpoch3, initiativeStateNewEpoch3 ); - (uint88 totalLQTYAllocatedNewEpoch3, uint32 totalAverageTimestampNewEpoch3) = + (uint88 totalLQTYAllocatedNewEpoch3, uint120 totalAverageTimestampNewEpoch3) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocatedNewEpoch3, 2001e18); - assertEq(totalAverageTimestampNewEpoch3, uint32(block.timestamp)); - (uint88 userLQTYAllocatedNewEpoch3, uint32 userAverageTimestampNewEpoch3) = + assertEq(totalAverageTimestampNewEpoch3, uint120(block.timestamp)); + (uint88 userLQTYAllocatedNewEpoch3, uint120 userAverageTimestampNewEpoch3) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocatedNewEpoch3, 2000e18); - assertEq(userAverageTimestampNewEpoch3, uint32(block.timestamp)); + assertEq(userAverageTimestampNewEpoch3, uint120(block.timestamp)); governance.setEpoch(4); + vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to fourth epoch ts vm.startPrank(address(user)); @@ -414,14 +431,14 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } { @@ -438,14 +455,14 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1001e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocated, 1000e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } governance.setEpoch(2); @@ -464,14 +481,14 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocated, 0); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } governance.setEpoch(3); @@ -490,14 +507,14 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocated, 0); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } } @@ -509,6 +526,7 @@ contract BribeInitiativeAllocateTest is Test { vm.stopPrank(); governance.setEpoch(1); + vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch vm.startPrank(address(governance)); @@ -526,14 +544,14 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } { @@ -550,14 +568,14 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1001e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocated, 1000e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } { @@ -574,17 +592,18 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 2001e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocated, 2000e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } governance.setEpoch(2); + vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts vm.startPrank(address(user)); @@ -603,6 +622,7 @@ contract BribeInitiativeAllocateTest is Test { vm.stopPrank(); governance.setEpoch(1); + vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch vm.startPrank(address(governance)); @@ -620,14 +640,14 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } { @@ -644,14 +664,14 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1001e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocated, 1000e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } { @@ -668,17 +688,18 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocated, 0); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } governance.setEpoch(2); + vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts vm.startPrank(address(user)); @@ -699,6 +720,7 @@ contract BribeInitiativeAllocateTest is Test { vm.stopPrank(); governance.setEpoch(1); + vm.warp(governance.EPOCH_DURATION()); // warp to end of first epoch vm.startPrank(address(governance)); @@ -716,14 +738,14 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } { @@ -740,14 +762,14 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1001e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocated, 1000e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } { @@ -764,14 +786,14 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocated, 0); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } { @@ -788,17 +810,18 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 2001e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocated, 2000e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } governance.setEpoch(2); + vm.warp(block.timestamp + governance.EPOCH_DURATION()); // warp to second epoch ts vm.startPrank(address(user)); @@ -828,14 +851,14 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user2, governance.epoch()); assertEq(userLQTYAllocated, 1e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } { @@ -852,59 +875,59 @@ contract BribeInitiativeAllocateTest is Test { }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1001e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocated, 1000e18); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } { IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 1, averageStakingTimestamp: uint32(block.timestamp)}); + 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: uint32(block.timestamp), + averageStakingTimestampVoteLQTY: uint120(block.timestamp), averageStakingTimestampVetoLQTY: 0, lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocated, 0); - assertEq(userAverageTimestamp, uint32(block.timestamp)); + assertEq(userAverageTimestamp, uint120(block.timestamp)); } { IGovernance.UserState memory userState = - IGovernance.UserState({allocatedLQTY: 2, averageStakingTimestamp: uint32(block.timestamp)}); + 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: uint32(block.timestamp), + averageStakingTimestampVoteLQTY: uint120(block.timestamp), averageStakingTimestampVetoLQTY: 0, lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); - (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = + (uint88 totalLQTYAllocated, uint120 totalAverageTimestamp) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); assertEq(totalLQTYAllocated, 1e18); - assertEq(totalAverageTimestamp, uint32(block.timestamp)); - (uint88 userLQTYAllocated, uint32 userAverageTimestamp) = + assertEq(totalAverageTimestamp, uint120(block.timestamp)); + (uint88 userLQTYAllocated, uint120 userAverageTimestamp) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()); assertEq(userLQTYAllocated, 0); assertEq(userAverageTimestamp, uint32(block.timestamp)); diff --git a/test/E2E.t.sol b/test/E2E.t.sol index 6eb98bda..fb7c8bb4 100644 --- a/test/E2E.t.sol +++ b/test/E2E.t.sol @@ -121,6 +121,54 @@ contract E2ETests is Test { _allocate(address(0x123123), 1e18, 0); } + function test_canYouVoteWith100MLNLQTY() public { + deal(address(lqty), user, 100_000_000e18); + vm.startPrank(user); + // Check that we can vote on the first epoch, right after deployment + _deposit(100_000_000e18); + + console.log("epoch", governance.epoch()); + _allocate(baseInitiative1, 100_000_000e18, 0); + } + + function test_canYouVoteWith100MLNLQTY_after_10_years() public { + deal(address(lqty), user, 100_000_000e18); + deal(address(lusd), user, 1e18); + + vm.startPrank(user); + lusd.approve(address(governance), 1e18); + + // Check that we can vote on the first epoch, right after deployment + _deposit(100_000_000e18); + + vm.warp(block.timestamp + 365 days * 10); + address newInitiative = address(0x123123); + governance.registerInitiative(newInitiative); + + vm.warp(block.timestamp + EPOCH_DURATION); + + 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 + vm.startPrank(user); + // Check that we can vote on the first epoch, right after deployment + _deposit(1000e18); + + console.log("epoch", governance.epoch()); + _allocate(baseInitiative1, 0, 1e18); // Doesn't work due to cool down I think + + vm.expectRevert(); + governance.unregisterInitiative(baseInitiative1); + + vm.warp(block.timestamp + EPOCH_DURATION); + governance.unregisterInitiative(baseInitiative1); + } + // forge test --match-test test_deregisterIsSound -vv function test_deregisterIsSound() public { // Deregistration works as follows: @@ -144,14 +192,7 @@ contract E2ETests is Test { uint256 skipCount; - address[] memory toAllocate = new address[](2); - toAllocate[0] = baseInitiative1; - toAllocate[1] = newInitiative; - - int88[] memory votes = new int88[](2); - votes[0] = 1e18; - votes[1] = 100; - int88[] memory vetos = new int88[](2); + // WARM_UP at 0 // Whereas in next week it will work vm.warp(block.timestamp + EPOCH_DURATION); // 1 @@ -167,13 +208,136 @@ contract E2ETests is Test { ++skipCount; assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + vm.warp(block.timestamp + EPOCH_DURATION); // 3 + ++skipCount; + assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + + vm.warp(block.timestamp + EPOCH_DURATION); // 4 + ++skipCount; + assertEq( + uint256(Governance.InitiativeStatus.UNREGISTERABLE), _getInitiativeStatus(newInitiative), "UNREGISTERABLE" + ); + + /// 4 + 1 ?? + assertEq(skipCount, UNREGISTRATION_AFTER_EPOCHS + 1, "Skipped exactly UNREGISTRATION_AFTER_EPOCHS"); + } + + // forge test --match-test test_unregisterWorksCorrectlyEvenAfterXEpochs -vv + function test_unregisterWorksCorrectlyEvenAfterXEpochs(uint8 epochsInFuture) public { + vm.warp(block.timestamp + epochsInFuture * EPOCH_DURATION); + vm.startPrank(user); + // Check that we can vote on the first epoch, right after deployment + _deposit(1000e18); + + // And for sanity, you cannot vote on new ones, they need to be added first + deal(address(lusd), address(user), REGISTRATION_FEE * 2); + lusd.approve(address(governance), REGISTRATION_FEE * 2); + + address newInitiative = address(0x123123); + 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"); + + uint256 skipCount; + + // SPEC: + // Initiative is at WARM_UP at registration epoch + + // The following EPOCH it can be voted on, it has status SKIP + + vm.warp(block.timestamp + EPOCH_DURATION); // 1 + ++skipCount; + assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + + _allocate(newInitiative2, 1e18, 0); + + // 2nd Week of SKIP + + // Cooldown on epoch Staert + vm.warp(block.timestamp + EPOCH_DURATION); // 2 + ++skipCount; + assertEq(uint256(Governance.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"); + + // 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"); + vm.warp(block.timestamp + EPOCH_DURATION); // 4 ++skipCount; assertEq( uint256(Governance.InitiativeStatus.UNREGISTERABLE), _getInitiativeStatus(newInitiative), "UNREGISTERABLE" ); - assertEq(skipCount, UNREGISTRATION_AFTER_EPOCHS, "Skipped exactly UNREGISTRATION_AFTER_EPOCHS"); + /// 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); + vm.startPrank(user); + // Check that we can vote on the first epoch, right after deployment + _deposit(1000e18); + + // And for sanity, you cannot vote on new ones, they need to be added first + deal(address(lusd), address(user), REGISTRATION_FEE * 2); + lusd.approve(address(governance), REGISTRATION_FEE * 2); + + address newInitiative = address(0x123123); + 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"); + + uint256 skipCount; + + // SPEC: + // Initiative is at WARM_UP at registration epoch + + // The following EPOCH it can be voted on, it has status SKIP + + vm.warp(block.timestamp + EPOCH_DURATION); // 1 + ++skipCount; + assertEq(uint256(Governance.InitiativeStatus.SKIP), _getInitiativeStatus(newInitiative), "SKIP"); + + _allocate(newInitiative2, 1e18, 0); + + // 2nd Week of SKIP + + // Cooldown on epoch Staert + vm.warp(block.timestamp + EPOCH_DURATION); // 2 + ++skipCount; + assertEq(uint256(Governance.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"); + + // 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"); + + // Allocating to it, saves it + _allocate(newInitiative, 1e18, 0); + + vm.warp(block.timestamp + EPOCH_DURATION); // 4 + ++skipCount; + assertEq(uint256(Governance.InitiativeStatus.CLAIMABLE), _getInitiativeStatus(newInitiative), "UNREGISTERABLE"); } function _deposit(uint88 amt) internal { @@ -184,11 +348,12 @@ contract E2ETests is Test { } function _allocate(address initiative, int88 votes, int88 vetos) internal { - address[] memory initiativesToDeRegister = new address[](4); + address[] memory initiativesToDeRegister = new address[](5); initiativesToDeRegister[0] = baseInitiative1; initiativesToDeRegister[1] = baseInitiative2; initiativesToDeRegister[2] = baseInitiative3; initiativesToDeRegister[3] = address(0x123123); + initiativesToDeRegister[4] = address(0x1231234); address[] memory initiatives = new address[](1); initiatives[0] = initiative; @@ -201,11 +366,12 @@ contract E2ETests is Test { } function _allocate(address[] memory initiatives, int88[] memory votes, int88[] memory vetos) internal { - address[] memory initiativesToDeRegister = new address[](4); + address[] memory initiativesToDeRegister = new address[](5); initiativesToDeRegister[0] = baseInitiative1; initiativesToDeRegister[1] = baseInitiative2; initiativesToDeRegister[2] = baseInitiative3; initiativesToDeRegister[3] = address(0x123123); + initiativesToDeRegister[4] = address(0x1231234); governance.allocateLQTY(initiativesToDeRegister, initiatives, votes, vetos); } diff --git a/test/EncodingDecoding.t.sol b/test/EncodingDecoding.t.sol index d7571ff3..49e205e0 100644 --- a/test/EncodingDecoding.t.sol +++ b/test/EncodingDecoding.t.sol @@ -7,16 +7,16 @@ import {EncodingDecodingLib} from "src/utils/EncodingDecodingLib.sol"; contract EncodingDecodingTest is Test { // value -> encoding -> decoding -> value - function test_encoding_and_decoding_symmetrical(uint88 lqty, uint32 averageTimestamp) public { + function test_encoding_and_decoding_symmetrical(uint88 lqty, uint120 averageTimestamp) public { uint224 encodedValue = EncodingDecodingLib.encodeLQTYAllocation(lqty, averageTimestamp); - (uint88 decodedLqty, uint32 decodedAverageTimestamp) = EncodingDecodingLib.decodeLQTYAllocation(encodedValue); + (uint88 decodedLqty, uint120 decodedAverageTimestamp) = EncodingDecodingLib.decodeLQTYAllocation(encodedValue); assertEq(lqty, decodedLqty); assertEq(averageTimestamp, decodedAverageTimestamp); // Redo uint224 reEncoded = EncodingDecodingLib.encodeLQTYAllocation(decodedLqty, decodedAverageTimestamp); - (uint88 reDecodedLqty, uint32 reDecodedAverageTimestamp) = + (uint88 reDecodedLqty, uint120 reDecodedAverageTimestamp) = EncodingDecodingLib.decodeLQTYAllocation(encodedValue); assertEq(reEncoded, encodedValue); @@ -24,11 +24,6 @@ contract EncodingDecodingTest is Test { assertEq(reDecodedAverageTimestamp, decodedAverageTimestamp); } - /// We expect this test to fail as the encoding is ambigous past u120 - function testFail_encoding_not_equal_reproducer() public { - _receive_undo_compare(18371677541005923091065047412368542483005086202); - } - // receive -> undo -> check -> redo -> compare function test_receive_undo_compare(uint120 encodedValue) public { _receive_undo_compare(encodedValue); @@ -37,10 +32,11 @@ contract EncodingDecodingTest is Test { // 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, uint32 decodedAverageTimestamp) = EncodingDecodingLib.decodeLQTYAllocation(encodedValue); + (uint88 decodedLqty, uint120 decodedAverageTimestamp) = EncodingDecodingLib.decodeLQTYAllocation(encodedValue); uint224 encodedValue2 = EncodingDecodingLib.encodeLQTYAllocation(decodedLqty, decodedAverageTimestamp); - (uint88 decodedLqty2, uint32 decodedAverageTimestamp2) = EncodingDecodingLib.decodeLQTYAllocation(encodedValue2); + (uint88 decodedLqty2, uint120 decodedAverageTimestamp2) = + EncodingDecodingLib.decodeLQTYAllocation(encodedValue2); assertEq(encodedValue, encodedValue2, "encoded values not equal"); assertEq(decodedLqty, decodedLqty2, "decoded lqty not equal"); diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 458b31d1..18f232a0 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -28,16 +28,16 @@ contract GovernanceInternal is Governance { address[] memory _initiatives ) Governance(_lqty, _lusd, _stakingV1, _bold, _config, _initiatives) {} - function averageAge(uint32 _currentTimestamp, uint32 _averageTimestamp) external pure returns (uint32) { + function averageAge(uint120 _currentTimestamp, uint120 _averageTimestamp) external pure returns (uint120) { return _averageAge(_currentTimestamp, _averageTimestamp); } function calculateAverageTimestamp( - uint32 _prevOuterAverageTimestamp, - uint32 _newInnerAverageTimestamp, + uint120 _prevOuterAverageTimestamp, + uint120 _newInnerAverageTimestamp, uint88 _prevLQTYBalance, uint88 _newLQTYBalance - ) external view returns (uint32) { + ) external view returns (uint208) { return _calculateAverageTimestamp( _prevOuterAverageTimestamp, _newInnerAverageTimestamp, _prevLQTYBalance, _newLQTYBalance ); @@ -145,8 +145,8 @@ contract GovernanceTest is Test { } // should not revert under any input - function test_averageAge(uint32 _currentTimestamp, uint32 _timestamp) public { - uint32 averageAge = governanceInternal.averageAge(_currentTimestamp, _timestamp); + function test_averageAge(uint120 _currentTimestamp, uint120 _timestamp) public { + uint120 averageAge = governanceInternal.averageAge(_currentTimestamp, _timestamp); if (_timestamp == 0 || _currentTimestamp < _timestamp) { assertEq(averageAge, 0); } else { @@ -170,6 +170,7 @@ contract GovernanceTest is Test { ); } + // forge test --match-test test_depositLQTY_withdrawLQTY -vv function test_depositLQTY_withdrawLQTY() public { uint256 timeIncrease = 86400 * 30; vm.warp(block.timestamp + timeIncrease); @@ -195,10 +196,10 @@ contract GovernanceTest is Test { // deploy and deposit 1 LQTY governance.depositLQTY(1e18); assertEq(UserProxy(payable(userProxy)).staked(), 1e18); - (uint88 allocatedLQTY, uint32 averageStakingTimestamp) = governance.userStates(user); + (uint88 allocatedLQTY, uint120 averageStakingTimestamp) = governance.userStates(user); assertEq(allocatedLQTY, 0); // first deposit should have an averageStakingTimestamp if block.timestamp - assertEq(averageStakingTimestamp, block.timestamp); + assertEq(averageStakingTimestamp, block.timestamp * 1e26); vm.warp(block.timestamp + timeIncrease); @@ -208,7 +209,7 @@ contract GovernanceTest is Test { (allocatedLQTY, averageStakingTimestamp) = governance.userStates(user); assertEq(allocatedLQTY, 0); // subsequent deposits should have a stake weighted average - assertEq(averageStakingTimestamp, block.timestamp - timeIncrease / 2); + assertEq(averageStakingTimestamp, (block.timestamp - timeIncrease / 2) * 1e26, "Avg ts"); // withdraw 0.5 half of LQTY vm.warp(block.timestamp + timeIncrease); @@ -220,21 +221,18 @@ contract GovernanceTest is Test { vm.startPrank(user); - vm.expectRevert("Governance: insufficient-unallocated-lqty"); - governance.withdrawLQTY(type(uint88).max); - governance.withdrawLQTY(1e18); assertEq(UserProxy(payable(userProxy)).staked(), 1e18); (allocatedLQTY, averageStakingTimestamp) = governance.userStates(user); assertEq(allocatedLQTY, 0); - assertEq(averageStakingTimestamp, (block.timestamp - timeIncrease) - timeIncrease / 2); + assertEq(averageStakingTimestamp, ((block.timestamp - timeIncrease) - timeIncrease / 2) * 1e26, "avg ts2"); // withdraw remaining LQTY governance.withdrawLQTY(1e18); assertEq(UserProxy(payable(userProxy)).staked(), 0); (allocatedLQTY, averageStakingTimestamp) = governance.userStates(user); assertEq(allocatedLQTY, 0); - assertEq(averageStakingTimestamp, (block.timestamp - timeIncrease) - timeIncrease / 2); + assertEq(averageStakingTimestamp, ((block.timestamp - timeIncrease) - timeIncrease / 2) * 1e26, "avg ts3"); vm.stopPrank(); } @@ -303,9 +301,9 @@ contract GovernanceTest is Test { // deploy and deposit 1 LQTY governance.depositLQTYViaPermit(1e18, permitParams); assertEq(UserProxy(payable(userProxy)).staked(), 1e18); - (uint88 allocatedLQTY, uint32 averageStakingTimestamp) = governance.userStates(wallet.addr); + (uint88 allocatedLQTY, uint120 averageStakingTimestamp) = governance.userStates(wallet.addr); assertEq(allocatedLQTY, 0); - assertEq(averageStakingTimestamp, block.timestamp); + assertEq(averageStakingTimestamp, block.timestamp * 1e26); } function test_claimFromStakingV1() public { @@ -384,7 +382,7 @@ contract GovernanceTest is Test { } // should not revert under any input - function test_lqtyToVotes(uint88 _lqtyAmount, uint256 _currentTimestamp, uint32 _averageTimestamp) public { + function test_lqtyToVotes(uint88 _lqtyAmount, uint120 _currentTimestamp, uint120 _averageTimestamp) public { governance.lqtyToVotes(_lqtyAmount, _currentTimestamp, _averageTimestamp); } @@ -615,8 +613,6 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + governance.EPOCH_DURATION()); // should revert if the initiative is still active or the vetos don't meet the threshold - /// @audit TO REVIEW, this never got any votes, so it seems correct to remove - // No votes = can be kicked vm.expectRevert("Governance: cannot-unregister-initiative"); governance.unregisterInitiative(baseInitiative3); @@ -630,7 +626,7 @@ contract GovernanceTest is Test { assertEq(votes, 1e18); assertEq(forEpoch, governance.epoch() - 1); - vm.warp(block.timestamp + governance.EPOCH_DURATION() * 3); // 3 more epochs + vm.warp(block.timestamp + governance.EPOCH_DURATION() * UNREGISTRATION_AFTER_EPOCHS); governance.unregisterInitiative(baseInitiative3); @@ -647,7 +643,7 @@ contract GovernanceTest is Test { governance.registerInitiative(baseInitiative3); } - // Test: You can always remove allocation + /// 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 @@ -673,12 +669,14 @@ contract GovernanceTest is Test { (uint256 allocatedLQTY,) = governance.userStates(user); assertEq(allocatedLQTY, 1_000e18); - (uint88 voteLQTY1,, uint32 averageStakingTimestampVoteLQTY1,,) = governance.initiativeStates(baseInitiative1); + (uint88 voteLQTY1,, uint120 averageStakingTimestampVoteLQTY1,,) = governance.initiativeStates(baseInitiative1); (uint88 voteLQTY2,,,,) = governance.initiativeStates(baseInitiative2); // Get power at time of vote - uint256 votingPower = governance.lqtyToVotes(voteLQTY1, block.timestamp, averageStakingTimestampVoteLQTY1); + uint256 votingPower = governance.lqtyToVotes( + voteLQTY1, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY1 + ); assertGt(votingPower, 0, "Non zero power"); /// @audit TODO Fully digest and explain the bug @@ -698,7 +696,9 @@ contract GovernanceTest is Test { assertLt(initiativeVoteSnapshot1.votes, threshold, "it didn't get rewards"); uint256 votingPowerWithProjection = governance.lqtyToVotes( - voteLQTY1, governance.epochStart() + governance.EPOCH_DURATION(), averageStakingTimestampVoteLQTY1 + voteLQTY1, + uint120(governance.epochStart() + governance.EPOCH_DURATION()), + averageStakingTimestampVoteLQTY1 ); assertLt(votingPower, threshold, "Current Power is not enough - Desynch A"); assertLt(votingPowerWithProjection, threshold, "Future Power is also not enough - Desynch B"); @@ -750,10 +750,8 @@ contract GovernanceTest is Test { // @audit Warmup is not necessary // Warmup would only work for urgent veto // But urgent veto is not relevant here - // TODO: Check and prob separate - // CRIT - I want to remove my allocation - // I cannot + // I want to remove my allocation address[] memory removeInitiatives = new address[](2); removeInitiatives[0] = baseInitiative1; removeInitiatives[1] = baseInitiative2; @@ -764,11 +762,9 @@ contract GovernanceTest is Test { governance.allocateLQTY(removeInitiatives, removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); - // Security Check | TODO: MORE INVARIANTS - // trying to explicitly remove allocation fails because allocation gets reset removeDeltaLQTYVotes[0] = -1e18; - vm.expectRevert(); // TODO: This is a panic + vm.expectRevert("Cannot be negative"); governance.allocateLQTY(removeInitiatives, removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); address[] memory reAddInitiatives = new address[](1); @@ -782,6 +778,74 @@ contract GovernanceTest is Test { governance.allocateLQTY(reAddInitiatives, 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(); + + lqty.approve(address(userProxy_2), 1_000e18); + governance.depositLQTY(1_000e18); + + // User setup + vm.startPrank(user); + address userProxy = governance.deployUserProxy(); + + lqty.approve(address(userProxy), 1_000e18); + governance.depositLQTY(1_000e18); + + vm.warp(block.timestamp + governance.EPOCH_DURATION()); + + /// Setup and vote for 2 initiatives, 0.1% vs 99.9% + address[] memory initiatives = new address[](2); + initiatives[0] = baseInitiative1; + initiatives[1] = baseInitiative2; + int88[] memory deltaLQTYVotes = new int88[](2); + deltaLQTYVotes[0] = 1e18; + deltaLQTYVotes[1] = 999e18; + int88[] memory deltaLQTYVetos = new int88[](2); + + governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + + vm.startPrank(user2); + governance.allocateLQTY(initiatives, initiatives, deltaLQTYVotes, deltaLQTYVetos); + + vm.startPrank(user); + + // Roll for the rest of the epochs so we can unregister + vm.warp(block.timestamp + (governance.UNREGISTRATION_AFTER_EPOCHS()) * governance.EPOCH_DURATION()); + governance.unregisterInitiative(baseInitiative1); + + // Get state here + // Get initiative state + (uint88 b4_countedVoteLQTY, uint120 b4_countedVoteLQTYAverageTimestamp) = 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); + // don't need to explicitly remove allocation because it already gets reset + removeDeltaLQTYVotes[0] = 0; + removeDeltaLQTYVotes[1] = 999e18; + + int88[] memory removeDeltaLQTYVetos = new int88[](2); + + governance.allocateLQTY(removeInitiatives, removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); + + { + // Get state here + // TODO Get initiative state + (uint88 after_countedVoteLQTY, uint120 after_countedVoteLQTYAverageTimestamp) = governance.globalState(); + + assertEq(after_countedVoteLQTY, b4_countedVoteLQTY, "LQTY should not change"); + assertEq( + b4_countedVoteLQTYAverageTimestamp, after_countedVoteLQTYAverageTimestamp, "Avg TS should not change" + ); + } + } + // Remove allocation but check accounting // Need to find bug in accounting code // forge test --match-test test_addRemoveAllocation_accounting -vv @@ -826,8 +890,8 @@ contract GovernanceTest is Test { // Grab values b4 unregistering and b4 removing user allocation - (uint88 b4_countedVoteLQTY, uint32 b4_countedVoteLQTYAverageTimestamp) = governance.globalState(); - (uint88 b4_allocatedLQTY, uint32 b4_averageStakingTimestamp) = governance.userStates(user); + (uint88 b4_countedVoteLQTY, uint120 b4_countedVoteLQTYAverageTimestamp) = governance.globalState(); + (uint88 b4_allocatedLQTY, uint120 b4_averageStakingTimestamp) = governance.userStates(user); (uint88 b4_voteLQTY,,,,) = governance.initiativeStates(baseInitiative1); // Unregistering @@ -843,14 +907,14 @@ contract GovernanceTest is Test { assertEq(after_countedVoteLQTY, b4_countedVoteLQTY - b4_voteLQTY, "Global Lqty change after unregister"); assertEq(1e18, b4_voteLQTY, "sanity check"); - (uint88 after_allocatedLQTY, uint32 after_averageStakingTimestamp) = governance.userStates(user); + (uint88 after_allocatedLQTY, uint120 after_averageStakingTimestamp) = governance.userStates(user); // We expect no changes here ( uint88 after_voteLQTY, uint88 after_vetoLQTY, - uint32 after_averageStakingTimestampVoteLQTY, - uint32 after_averageStakingTimestampVetoLQTY, + uint120 after_averageStakingTimestampVoteLQTY, + uint120 after_averageStakingTimestampVetoLQTY, uint16 after_lastEpochClaim ) = governance.initiativeStates(baseInitiative1); assertEq(b4_voteLQTY, after_voteLQTY, "Initiative votes are the same"); @@ -874,7 +938,7 @@ contract GovernanceTest is Test { // After user counts LQTY the { - (uint88 after_user_countedVoteLQTY, uint32 after_user_countedVoteLQTYAverageTimestamp) = + (uint88 after_user_countedVoteLQTY, uint120 after_user_countedVoteLQTYAverageTimestamp) = governance.globalState(); // The LQTY was already removed assertEq(after_user_countedVoteLQTY, 0, "Removal 1"); @@ -930,7 +994,6 @@ contract GovernanceTest is Test { removeInitiatives[0] = baseInitiative1; removeInitiatives[1] = baseInitiative2; int88[] memory removeDeltaLQTYVotes = new int88[](2); - // removeDeltaLQTYVotes[0] = int88(-1e18); // @audit deallocating is no longer possible removeDeltaLQTYVotes[0] = 0; int88[] memory removeDeltaLQTYVetos = new int88[](2); @@ -963,7 +1026,7 @@ contract GovernanceTest is Test { lqty.approve(address(userProxy), 1e18); governance.depositLQTY(1e18); - (uint88 allocatedLQTY, uint32 averageStakingTimestampUser) = governance.userStates(user); + (uint88 allocatedLQTY, uint120 averageStakingTimestampUser) = governance.userStates(user); assertEq(allocatedLQTY, 0); (uint88 countedVoteLQTY,) = governance.globalState(); assertEq(countedVoteLQTY, 0); @@ -987,15 +1050,15 @@ contract GovernanceTest is Test { ( uint88 voteLQTY, uint88 vetoLQTY, - uint32 averageStakingTimestampVoteLQTY, - uint32 averageStakingTimestampVetoLQTY, + uint120 averageStakingTimestampVoteLQTY, + uint120 averageStakingTimestampVetoLQTY, ) = 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()); + assertEq(averageStakingTimestampVoteLQTY, (block.timestamp - governance.EPOCH_DURATION()) * 1e26); assertEq(averageStakingTimestampVoteLQTY, averageStakingTimestampUser); assertEq(averageStakingTimestampVetoLQTY, 0); // should remove or add the initiatives voting LQTY from the counter @@ -1028,8 +1091,8 @@ contract GovernanceTest is Test { lqty.approve(address(user2Proxy), 1e18); governance.depositLQTY(1e18); - (, uint32 averageAge) = governance.userStates(user2); - assertEq(governance.lqtyToVotes(1e18, block.timestamp, averageAge), 0); + (, uint120 averageAge) = governance.userStates(user2); + assertEq(governance.lqtyToVotes(1e18, uint120(block.timestamp) * uint120(1e26), averageAge), 0); deltaLQTYVetos[0] = 1e18; @@ -1048,12 +1111,12 @@ contract GovernanceTest is Test { governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 2e18); assertEq(vetoLQTY, 0); - assertEq(averageStakingTimestampVoteLQTY, block.timestamp - governance.EPOCH_DURATION()); + assertEq(averageStakingTimestampVoteLQTY, (block.timestamp - governance.EPOCH_DURATION()) * 1e26); assertGt(averageStakingTimestampVoteLQTY, averageStakingTimestampUser); assertEq(averageStakingTimestampVetoLQTY, 0); // should revert if the user doesn't have enough unallocated LQTY available - vm.expectRevert("Governance: insufficient-unallocated-lqty"); + vm.expectRevert("Governance: must-allocate-zero"); governance.withdrawLQTY(1e18); vm.warp(block.timestamp + EPOCH_DURATION - governance.secondsWithinEpoch() - 1); @@ -1087,7 +1150,7 @@ contract GovernanceTest is Test { lqty.approve(address(userProxy), 1e18); governance.depositLQTY(1e18); - (uint88 allocatedLQTY, uint32 averageStakingTimestampUser) = governance.userStates(user); + (uint88 allocatedLQTY, uint120 averageStakingTimestampUser) = governance.userStates(user); assertEq(allocatedLQTY, 0); (uint88 countedVoteLQTY,) = governance.globalState(); assertEq(countedVoteLQTY, 0); @@ -1111,15 +1174,15 @@ contract GovernanceTest is Test { ( uint88 voteLQTY, uint88 vetoLQTY, - uint32 averageStakingTimestampVoteLQTY, - uint32 averageStakingTimestampVetoLQTY, + uint120 averageStakingTimestampVoteLQTY, + uint120 averageStakingTimestampVetoLQTY, ) = 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(), "TS"); + assertEq(averageStakingTimestampVoteLQTY, (block.timestamp - governance.EPOCH_DURATION()) * 1e26, "TS"); assertEq(averageStakingTimestampVoteLQTY, averageStakingTimestampUser); assertEq(averageStakingTimestampVetoLQTY, 0); // should remove or add the initiatives voting LQTY from the counter @@ -1152,8 +1215,8 @@ contract GovernanceTest is Test { lqty.approve(address(user2Proxy), 1e18); governance.depositLQTY(1e18); - (, uint32 averageAge) = governance.userStates(user2); - assertEq(governance.lqtyToVotes(1e18, block.timestamp, averageAge), 0); + (, uint120 averageAge) = governance.userStates(user2); + assertEq(governance.lqtyToVotes(1e18, uint120(block.timestamp) * uint120(1e26), averageAge), 0); deltaLQTYVetos[0] = 1e18; @@ -1172,12 +1235,12 @@ contract GovernanceTest is Test { governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 2e18); assertEq(vetoLQTY, 0); - assertEq(averageStakingTimestampVoteLQTY, block.timestamp - governance.EPOCH_DURATION(), "TS 2"); + assertEq(averageStakingTimestampVoteLQTY, (block.timestamp - governance.EPOCH_DURATION()) * 1e26, "TS 2"); assertGt(averageStakingTimestampVoteLQTY, averageStakingTimestampUser); assertEq(averageStakingTimestampVetoLQTY, 0); // should revert if the user doesn't have enough unallocated LQTY available - vm.expectRevert("Governance: insufficient-unallocated-lqty"); + vm.expectRevert("Governance: must-allocate-zero"); governance.withdrawLQTY(1e18); vm.warp(block.timestamp + EPOCH_DURATION - governance.secondsWithinEpoch() - 1); @@ -1229,8 +1292,8 @@ contract GovernanceTest is Test { ( uint88 voteLQTY, uint88 vetoLQTY, - uint32 averageStakingTimestampVoteLQTY, - uint32 averageStakingTimestampVetoLQTY, + uint120 averageStakingTimestampVoteLQTY, + uint120 averageStakingTimestampVetoLQTY, ) = governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); @@ -1482,7 +1545,7 @@ contract GovernanceTest is Test { data[6] = abi.encodeWithSignature("withdrawLQTY(uint88)", lqtyAmount); bytes[] memory response = governance.multicall(data); - (uint88 allocatedLQTY,) = abi.decode(response[3], (uint88, uint32)); + (uint88 allocatedLQTY,) = abi.decode(response[3], (uint88, uint120)); assertEq(allocatedLQTY, lqtyAmount); (IGovernance.VoteSnapshot memory votes, IGovernance.InitiativeVoteSnapshot memory votesForInitiative) = abi.decode(response[4], (IGovernance.VoteSnapshot, IGovernance.InitiativeVoteSnapshot)); @@ -1646,12 +1709,14 @@ contract GovernanceTest is Test { uint88 lqtyAmount = 1e18; _stakeLQTY(user, lqtyAmount); - (uint88 allocatedLQTY0, uint32 averageStakingTimestamp0) = governance.userStates(user); - uint240 currentUserPower0 = governance.lqtyToVotes(allocatedLQTY0, block.timestamp, averageStakingTimestamp0); + (uint88 allocatedLQTY0, uint120 averageStakingTimestamp0) = governance.userStates(user); + uint240 currentUserPower0 = + governance.lqtyToVotes(allocatedLQTY0, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp0); - (uint88 voteLQTY0,, uint32 averageStakingTimestampVoteLQTY0,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower0 = - governance.lqtyToVotes(voteLQTY0, block.timestamp, averageStakingTimestampVoteLQTY0); + (uint88 voteLQTY0,, uint120 averageStakingTimestampVoteLQTY0,,) = governance.initiativeStates(baseInitiative1); + uint240 currentInitiativePower0 = governance.lqtyToVotes( + voteLQTY0, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY0 + ); // (uint224 votes, uint16 forEpoch,,) = governance.votesForInitiativeSnapshot(baseInitiative1); // console2.log("votes0: ", votes); @@ -1663,15 +1728,17 @@ contract GovernanceTest is Test { _allocateLQTY(user, lqtyAmount); // check user voting power for the current epoch - (uint88 allocatedLQTY1, uint32 averageStakingTimestamp1) = governance.userStates(user); - uint240 currentUserPower1 = governance.lqtyToVotes(allocatedLQTY1, block.timestamp, averageStakingTimestamp1); + (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 assertGt(currentUserPower1, 0, "current user voting power is 0"); // check initiative voting power for the current epoch - (uint88 voteLQTY1,, uint32 averageStakingTimestampVoteLQTY1,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower1 = - governance.lqtyToVotes(voteLQTY1, block.timestamp, averageStakingTimestampVoteLQTY1); + (uint88 voteLQTY1,, uint120 averageStakingTimestampVoteLQTY1,,) = governance.initiativeStates(baseInitiative1); + uint240 currentInitiativePower1 = governance.lqtyToVotes( + voteLQTY1, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY1 + ); assertGt(currentInitiativePower1, 0, "current initiative voting power is 0"); assertEq(currentUserPower1, currentInitiativePower1, "initiative and user voting power should be equal"); @@ -1684,14 +1751,16 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // user voting power should increase over a given chunk of time - (uint88 allocatedLQTY2, uint32 averageStakingTimestamp2) = governance.userStates(user); - uint240 currentUserPower2 = governance.lqtyToVotes(allocatedLQTY2, block.timestamp, averageStakingTimestamp2); + (uint88 allocatedLQTY2, uint120 averageStakingTimestamp2) = governance.userStates(user); + uint240 currentUserPower2 = + governance.lqtyToVotes(allocatedLQTY2, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp2); assertGt(currentUserPower2, currentUserPower1); // initiative voting power should increase over a given chunk of time - (uint88 voteLQTY2,, uint32 averageStakingTimestampVoteLQTY2,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower2 = - governance.lqtyToVotes(voteLQTY2, block.timestamp, averageStakingTimestampVoteLQTY2); + (uint88 voteLQTY2,, uint120 averageStakingTimestampVoteLQTY2,,) = governance.initiativeStates(baseInitiative1); + uint240 currentInitiativePower2 = governance.lqtyToVotes( + voteLQTY2, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY2 + ); assertEq( currentUserPower2, currentInitiativePower2, "user power and initiative power should increase by same amount" ); @@ -1706,13 +1775,15 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // user voting power should increase - (uint88 allocatedLQTY3, uint32 averageStakingTimestamp3) = governance.userStates(user); - uint240 currentUserPower3 = governance.lqtyToVotes(allocatedLQTY3, block.timestamp, averageStakingTimestamp3); + (uint88 allocatedLQTY3, uint120 averageStakingTimestamp3) = governance.userStates(user); + uint240 currentUserPower3 = + governance.lqtyToVotes(allocatedLQTY3, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp3); // votes should match the voting power for the initiative and subsequently the user since they're the only one allocated - (uint88 voteLQTY3,, uint32 averageStakingTimestampVoteLQTY3,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower3 = - governance.lqtyToVotes(voteLQTY3, block.timestamp, averageStakingTimestampVoteLQTY3); + (uint88 voteLQTY3,, uint120 averageStakingTimestampVoteLQTY3,,) = governance.initiativeStates(baseInitiative1); + uint240 currentInitiativePower3 = governance.lqtyToVotes( + voteLQTY3, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY3 + ); // votes should be counted in this epoch (votes, forEpoch,,) = governance.votesForInitiativeSnapshot(baseInitiative1); @@ -1723,12 +1794,14 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + EPOCH_DURATION - 1); governance.snapshotVotesForInitiative(baseInitiative1); - (uint88 allocatedLQTY4, uint32 averageStakingTimestamp4) = governance.userStates(user); - uint240 currentUserPower4 = governance.lqtyToVotes(allocatedLQTY4, block.timestamp, averageStakingTimestamp4); + (uint88 allocatedLQTY4, uint120 averageStakingTimestamp4) = governance.userStates(user); + uint240 currentUserPower4 = + governance.lqtyToVotes(allocatedLQTY4, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp4); - (uint88 voteLQTY4,, uint32 averageStakingTimestampVoteLQTY4,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower4 = - governance.lqtyToVotes(voteLQTY4, block.timestamp, averageStakingTimestampVoteLQTY4); + (uint88 voteLQTY4,, uint120 averageStakingTimestampVoteLQTY4,,) = governance.initiativeStates(baseInitiative1); + uint240 currentInitiativePower4 = governance.lqtyToVotes( + voteLQTY4, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY4 + ); // checking if snapshotting at the end of an epoch increases the voting power (uint224 votes2,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); @@ -1771,14 +1844,16 @@ contract GovernanceTest is Test { assertEq(2, governance.epoch(), "not in epoch 2"); // check user voting power before allocation at epoch start - (uint88 allocatedLQTY0, uint32 averageStakingTimestamp0) = governance.userStates(user); - uint240 currentUserPower0 = governance.lqtyToVotes(allocatedLQTY0, block.timestamp, averageStakingTimestamp0); + (uint88 allocatedLQTY0, uint120 averageStakingTimestamp0) = governance.userStates(user); + uint240 currentUserPower0 = + governance.lqtyToVotes(allocatedLQTY0, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp0); assertEq(currentUserPower0, 0, "user has voting power > 0"); // check initiative voting power before allocation at epoch start - (uint88 voteLQTY0,, uint32 averageStakingTimestampVoteLQTY0,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower0 = - governance.lqtyToVotes(voteLQTY0, block.timestamp, averageStakingTimestampVoteLQTY0); + (uint88 voteLQTY0,, uint120 averageStakingTimestampVoteLQTY0,,) = governance.initiativeStates(baseInitiative1); + uint240 currentInitiativePower0 = governance.lqtyToVotes( + voteLQTY0, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY0 + ); assertEq(currentInitiativePower0, 0, "current initiative voting power is > 0"); _allocateLQTY(user, lqtyAmount); @@ -1787,14 +1862,16 @@ contract GovernanceTest is Test { assertEq(2, governance.epoch(), "not in epoch 2"); // check user voting power after allocation at epoch end - (uint88 allocatedLQTY1, uint32 averageStakingTimestamp1) = governance.userStates(user); - uint240 currentUserPower1 = governance.lqtyToVotes(allocatedLQTY1, block.timestamp, averageStakingTimestamp1); + (uint88 allocatedLQTY1, uint120 averageStakingTimestamp1) = governance.userStates(user); + uint240 currentUserPower1 = + governance.lqtyToVotes(allocatedLQTY1, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp1); assertGt(currentUserPower1, 0, "user has no voting power after allocation"); // check initiative voting power after allocation at epoch end - (uint88 voteLQTY1,, uint32 averageStakingTimestampVoteLQTY1,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower1 = - governance.lqtyToVotes(voteLQTY1, block.timestamp, averageStakingTimestampVoteLQTY1); + (uint88 voteLQTY1,, uint120 averageStakingTimestampVoteLQTY1,,) = governance.initiativeStates(baseInitiative1); + uint240 currentInitiativePower1 = governance.lqtyToVotes( + voteLQTY1, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY1 + ); assertGt(currentInitiativePower1, 0, "initiative has no voting power after allocation"); // check that user and initiative voting power is equivalent at epoch end @@ -1804,14 +1881,16 @@ contract GovernanceTest is Test { assertEq(42, governance.epoch(), "not in epoch 42"); // get user voting power after multiple epochs - (uint88 allocatedLQTY2, uint32 averageStakingTimestamp2) = governance.userStates(user); - uint240 currentUserPower2 = governance.lqtyToVotes(allocatedLQTY2, block.timestamp, averageStakingTimestamp2); + (uint88 allocatedLQTY2, uint120 averageStakingTimestamp2) = governance.userStates(user); + uint240 currentUserPower2 = + governance.lqtyToVotes(allocatedLQTY2, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp2); assertGt(currentUserPower2, currentUserPower1, "user voting power doesn't increase"); // get initiative voting power after multiple epochs - (uint88 voteLQTY2,, uint32 averageStakingTimestampVoteLQTY2,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower2 = - governance.lqtyToVotes(voteLQTY2, block.timestamp, averageStakingTimestampVoteLQTY2); + (uint88 voteLQTY2,, uint120 averageStakingTimestampVoteLQTY2,,) = governance.initiativeStates(baseInitiative1); + uint240 currentInitiativePower2 = governance.lqtyToVotes( + voteLQTY2, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY2 + ); assertGt(currentInitiativePower2, currentInitiativePower1, "initiative voting power doesn't increase"); // check that initiative and user voting always track each other @@ -1853,9 +1932,10 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + EPOCH_DURATION); // warp to second epoch // get initiative voting power at start of epoch - (uint88 voteLQTY0,, uint32 averageStakingTimestampVoteLQTY0,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower0 = - governance.lqtyToVotes(voteLQTY0, block.timestamp, averageStakingTimestampVoteLQTY0); + (uint88 voteLQTY0,, uint120 averageStakingTimestampVoteLQTY0,,) = governance.initiativeStates(baseInitiative1); + uint240 currentInitiativePower0 = governance.lqtyToVotes( + voteLQTY0, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY0 + ); assertEq(currentInitiativePower0, 0, "initiative voting power is > 0"); _allocateLQTY(user, lqtyAmount); @@ -1866,9 +1946,10 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // get initiative voting power at time of snapshot - (uint88 voteLQTY1,, uint32 averageStakingTimestampVoteLQTY1,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower1 = - governance.lqtyToVotes(voteLQTY1, block.timestamp, averageStakingTimestampVoteLQTY1); + (uint88 voteLQTY1,, uint120 averageStakingTimestampVoteLQTY1,,) = governance.initiativeStates(baseInitiative1); + uint240 currentInitiativePower1 = governance.lqtyToVotes( + voteLQTY1, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY1 + ); assertGt(currentInitiativePower1, 0, "initiative voting power is 0"); uint240 deltaInitiativeVotingPower = currentInitiativePower1 - currentInitiativePower0; @@ -1914,11 +1995,11 @@ contract GovernanceTest is Test { // get user voting power at start of epoch from lqtyAllocatedByUserToInitiative (uint88 voteLQTY0,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); - (uint88 allocatedLQTY, uint32 averageStakingTimestamp) = governance.userStates(user); + (uint88 allocatedLQTY, uint120 averageStakingTimestamp) = governance.userStates(user); uint240 currentInitiativePowerFrom1 = - governance.lqtyToVotes(voteLQTY0, block.timestamp, averageStakingTimestamp); + governance.lqtyToVotes(voteLQTY0, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp); uint240 currentInitiativePowerFrom2 = - governance.lqtyToVotes(allocatedLQTY, block.timestamp, averageStakingTimestamp); + governance.lqtyToVotes(allocatedLQTY, uint120(block.timestamp) * uint120(1e26), averageStakingTimestamp); assertEq( currentInitiativePowerFrom1, @@ -1963,7 +2044,7 @@ contract GovernanceTest is Test { _allocateLQTY(user, 1e18); // get user voting power at start of epoch 2 from lqtyAllocatedByUserToInitiative - (, uint32 averageStakingTimestamp1) = governance.userStates(user); + (, uint120 averageStakingTimestamp1) = governance.userStates(user); // =========== epoch 3 (start) ================== // 3. user allocates to baseInitiative2 in epoch 3 @@ -1972,7 +2053,7 @@ contract GovernanceTest is Test { _allocateLQTYToInitiative(user, baseInitiative2, 1e18); // get user voting power at start of epoch 3 from lqtyAllocatedByUserToInitiative - (, uint32 averageStakingTimestamp2) = governance.userStates(user); + (, uint120 averageStakingTimestamp2) = governance.userStates(user); assertEq(averageStakingTimestamp1, averageStakingTimestamp2); } @@ -2013,7 +2094,7 @@ contract GovernanceTest is Test { _allocateLQTY(user, 1e18); // get user voting power at start of epoch 2 from lqtyAllocatedByUserToInitiative - (, uint32 averageStakingTimestamp1) = governance.userStates(user); + (, uint120 averageStakingTimestamp1) = governance.userStates(user); console2.log("averageStakingTimestamp1: ", averageStakingTimestamp1); // =========== epoch 3 (start) ================== @@ -2023,7 +2104,7 @@ contract GovernanceTest is Test { _allocateLQTY(user, 1e18); // get user voting power at start of epoch 3 from lqtyAllocatedByUserToInitiative - (, uint32 averageStakingTimestamp2) = governance.userStates(user); + (, uint120 averageStakingTimestamp2) = governance.userStates(user); assertEq(averageStakingTimestamp1, averageStakingTimestamp2, "average timestamps differ"); } @@ -2065,7 +2146,7 @@ contract GovernanceTest is Test { _allocateLQTY(user, lqtyAmount2); // get user voting power at start of epoch 2 from lqtyAllocatedByUserToInitiative - (, uint32 averageStakingTimestamp1) = governance.userStates(user); + (, uint120 averageStakingTimestamp1) = governance.userStates(user); // =========== epoch 3 (start) ================== // 3. user allocates to baseInitiative1 in epoch 3 @@ -2078,7 +2159,7 @@ contract GovernanceTest is Test { _allocateLQTY(user, lqtyAmount3); // get user voting power at start of epoch 3 from lqtyAllocatedByUserToInitiative - (, uint32 averageStakingTimestamp2) = governance.userStates(user); + (, uint120 averageStakingTimestamp2) = governance.userStates(user); assertEq( averageStakingTimestamp1, averageStakingTimestamp2, "averageStakingTimestamp1 != averageStakingTimestamp2" ); @@ -2116,9 +2197,10 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + EPOCH_DURATION); // warp to second epoch // get initiative voting power at start of epoch - (uint88 voteLQTY0,, uint32 averageStakingTimestampVoteLQTY0,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower0 = - governance.lqtyToVotes(voteLQTY0, block.timestamp, averageStakingTimestampVoteLQTY0); + (uint88 voteLQTY0,, uint120 averageStakingTimestampVoteLQTY0,,) = governance.initiativeStates(baseInitiative1); + uint240 currentInitiativePower0 = governance.lqtyToVotes( + voteLQTY0, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY0 + ); assertEq(currentInitiativePower0, 0, "initiative voting power is > 0"); _allocateLQTY(user, lqtyAmount); @@ -2132,9 +2214,10 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // get initiative voting power at start of epoch - (uint88 voteLQTY1,, uint32 averageStakingTimestampVoteLQTY1,,) = governance.initiativeStates(baseInitiative1); - uint240 currentInitiativePower1 = - governance.lqtyToVotes(voteLQTY1, block.timestamp, averageStakingTimestampVoteLQTY1); + (uint88 voteLQTY1,, uint120 averageStakingTimestampVoteLQTY1,,) = governance.initiativeStates(baseInitiative1); + uint240 currentInitiativePower1 = governance.lqtyToVotes( + voteLQTY1, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY1 + ); // 4a. votes from snapshotting at begging of epoch (uint224 votes,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); @@ -2329,11 +2412,11 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + EPOCH_DURATION); governance.snapshotVotesForInitiative(baseInitiative1); - (, uint32 averageStakingTimestampBefore) = governance.userStates(user); + (, uint120 averageStakingTimestampBefore) = governance.userStates(user); _deAllocateLQTY(user, 0); - (, uint32 averageStakingTimestampAfter) = governance.userStates(user); + (, uint120 averageStakingTimestampAfter) = governance.userStates(user); assertEq(averageStakingTimestampBefore, averageStakingTimestampAfter); } @@ -2384,9 +2467,9 @@ contract GovernanceTest is Test { governance.snapshotVotesForInitiative(baseInitiative1); // voting power for initiative should be the same as votes from snapshot - (uint88 voteLQTY,, uint32 averageStakingTimestampVoteLQTY,,) = governance.initiativeStates(baseInitiative1); + (uint88 voteLQTY,, uint120 averageStakingTimestampVoteLQTY,,) = governance.initiativeStates(baseInitiative1); uint240 currentInitiativePower = - governance.lqtyToVotes(voteLQTY, block.timestamp, averageStakingTimestampVoteLQTY); + governance.lqtyToVotes(voteLQTY, uint120(block.timestamp) * uint120(1e26), averageStakingTimestampVoteLQTY); // 4. votes should not affect accounting for votes (uint224 votes,,,) = governance.votesForInitiativeSnapshot(baseInitiative1); diff --git a/test/GovernanceAttacks.t.sol b/test/GovernanceAttacks.t.sol index eb572667..2b54807e 100644 --- a/test/GovernanceAttacks.t.sol +++ b/test/GovernanceAttacks.t.sol @@ -83,10 +83,10 @@ contract GovernanceTest is Test { // deploy and deposit 1 LQTY governance.depositLQTY(1e18); assertEq(UserProxy(payable(userProxy)).staked(), 1e18); - (uint88 allocatedLQTY, uint32 averageStakingTimestamp) = governance.userStates(user); + (uint88 allocatedLQTY, uint120 averageStakingTimestamp) = governance.userStates(user); assertEq(allocatedLQTY, 0); // first deposit should have an averageStakingTimestamp if block.timestamp - assertEq(averageStakingTimestamp, block.timestamp); + assertEq(averageStakingTimestamp, block.timestamp * 1e26); // TODO: Normalize vm.stopPrank(); vm.startPrank(lusdHolder); diff --git a/test/VotingPower.t.sol b/test/VotingPower.t.sol index 861ce24a..29860585 100644 --- a/test/VotingPower.t.sol +++ b/test/VotingPower.t.sol @@ -114,7 +114,7 @@ contract VotingPowerTest is Test { assertEq(powerInTheFuture, powerFromMoreDeposits, "Same result"); } - function test_math_soundness_fuzz(uint32 multiplier) public { + function test_math_soundness_fuzz(uint32 multiplier) public view { vm.assume(multiplier < type(uint32).max - 1); uint88 lqtyAmount = 1e10; @@ -165,89 +165,90 @@ contract VotingPowerTest is Test { // 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 { - 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); + // 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"); + // 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)); + // // 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"); + // 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 + // // If we can get it back, then we can also perform other operations like addition and subtraction + // // Easy when same TS - // // 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; + // // // 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; - uint240 power_2 = governance.lqtyToVotes(votes_2, current_time, time_2); + // uint240 power_2 = governance.lqtyToVotes(votes_2, current_time, time_2); - uint240 total_power = power + power_2; + // uint240 total_power = power + power_2; - assertLe(total_power, uint240(type(uint88).max), "LT"); + // assertLe(total_power, uint240(type(uint88).max), "LT"); - uint88 total_liquity = votes + votes_2; + // uint88 total_liquity = votes + votes_2; - uint32 avgTs = _calculateAverageTimestamp(time, time_2, votes, total_liquity); + // uint32 avgTs = _calculateAverageTimestamp(time, time_2, votes, total_liquity); - console.log("votes", votes); - console.log("time", current_time - time); - console.log("power", power); + // console.log("votes", votes); + // console.log("time", current_time - time); + // console.log("power", power); - console.log("votes_2", votes_2); - console.log("time_2", current_time - time_2); - console.log("power_2", power_2); + // console.log("votes_2", votes_2); + // console.log("time_2", current_time - time_2); + // console.log("power_2", power_2); - uint256 total_power_from_avg = governance.lqtyToVotes(total_liquity, current_time, avgTs); + // uint256 total_power_from_avg = governance.lqtyToVotes(total_liquity, current_time, avgTs); - console.log("total_liquity", total_liquity); - console.log("avgTs", current_time - avgTs); - console.log("total_power_from_avg", total_power_from_avg); + // console.log("total_liquity", total_liquity); + // console.log("avgTs", current_time - avgTs); + // console.log("total_power_from_avg", total_power_from_avg); - // Now remove the same math so we show that the rounding can be weaponized, let's see + // // Now remove the same math so we show that the rounding can be weaponized, let's see - // WTF + // // WTF - // 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 - ); + // // 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 + // ); - // NOTE: != time due to rounding error - console.log("attacked_avg_ts", current_time - attacked_avg_ts); + // // NOTE: != time due to rounding error + // console.log("attacked_avg_ts", current_time - attacked_avg_ts); - // BASIC VOTING TEST - // AFTER VOTING POWER IS X - // AFTER REMOVING VOTING IS 0 + // // BASIC VOTING TEST + // // AFTER VOTING POWER IS X + // // AFTER REMOVING VOTING IS 0 - // Add a middle of random shit - // Show that the math remains sound + // // Add a middle of random shit + // // Show that the math remains sound - // Off by 40 BPS????? WAYY TOO MUCH | SOMETHING IS WRONG + // // Off by 40 BPS????? WAYY TOO MUCH | SOMETHING IS WRONG - // 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"); + // // 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"); - // 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"); - } + // // 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"); + // } // forge test --match-test test_crit_user_can_dilute_total_votes -vv function test_crit_user_can_dilute_total_votes() public { @@ -269,7 +270,6 @@ contract VotingPowerTest is Test { vm.startPrank(user2); _allocate(address(baseInitiative1), 15, 0); - uint256 both_avg = _getAverageTS(baseInitiative1); _allocate(address(baseInitiative1), 0, 0); uint256 griefed_avg = _getAverageTS(baseInitiative1); @@ -307,38 +307,44 @@ contract VotingPowerTest is Test { vm.startPrank(user); _allocate(address(baseInitiative1), 124, 0); - uint256 user1_avg = _getAverageTS(baseInitiative1); vm.startPrank(user2); _allocate(address(baseInitiative1), 15, 0); - uint256 both_avg = _getAverageTS(baseInitiative1); _allocate(address(baseInitiative1), 0, 0); uint256 griefed_avg = _getAverageTS(baseInitiative1); console.log("griefed_avg", griefed_avg); console.log("block.timestamp", block.timestamp); + console.log("0?"); + + uint256 currentMagnifiedTs = uint120(block.timestamp) * uint120(1e26); + vm.startPrank(user2); _allocate(address(baseInitiative1), 15, 0); _allocate(address(baseInitiative1), 0, 0); uint256 ts = _getAverageTS(baseInitiative1); - uint256 delta = block.timestamp - ts; + uint256 delta = currentMagnifiedTs - ts; console.log("griefed_avg", ts); console.log("delta", delta); - console.log("block.timestamp", block.timestamp); + console.log("currentMagnifiedTs", currentMagnifiedTs); + console.log("0?"); uint256 i; while (i++ < 122) { + console.log("i", i); _allocate(address(baseInitiative1), 15, 0); _allocate(address(baseInitiative1), 0, 0); } + console.log("1?"); + ts = _getAverageTS(baseInitiative1); - delta = block.timestamp - ts; + delta = currentMagnifiedTs - ts; console.log("griefed_avg", ts); console.log("delta", delta); - console.log("block.timestamp", block.timestamp); + console.log("currentMagnifiedTs", currentMagnifiedTs); // One more time _allocate(address(baseInitiative1), 15, 0); @@ -365,10 +371,6 @@ contract VotingPowerTest is Test { // forge test --match-test test_basic_reset_flow -vv function test_basic_reset_flow() public { - uint256 snapshot0 = vm.snapshot(); - - uint256 snapshotBefore = vm.snapshot(); - vm.startPrank(user); // =========== epoch 1 ================== // 1. user stakes lqty @@ -377,7 +379,7 @@ contract VotingPowerTest is Test { // user allocates to baseInitiative1 _allocate(address(baseInitiative1), lqtyAmount / 2, 0); // 50% to it - (uint88 allocatedLQTY, uint32 averageStakingTimestamp1) = governance.userStates(user); + (uint88 allocatedLQTY,) = governance.userStates(user); assertEq(allocatedLQTY, uint88(lqtyAmount / 2), "half"); _allocate(address(baseInitiative1), lqtyAmount / 2, 0); // 50% to it @@ -386,10 +388,6 @@ contract VotingPowerTest is Test { // forge test --match-test test_cutoff_logic -vv function test_cutoff_logic() public { - uint256 snapshot0 = vm.snapshot(); - - uint256 snapshotBefore = vm.snapshot(); - vm.startPrank(user); // =========== epoch 1 ================== // 1. user stakes lqty @@ -398,7 +396,7 @@ contract VotingPowerTest is Test { // user allocates to baseInitiative1 _allocate(address(baseInitiative1), lqtyAmount / 2, 0); // 50% to it - (uint88 allocatedLQTY, uint32 averageStakingTimestamp1) = governance.userStates(user); + (uint88 allocatedLQTY,) = governance.userStates(user); assertEq(allocatedLQTY, uint88(lqtyAmount / 2), "Half"); // Go to Cutoff @@ -424,78 +422,6 @@ contract VotingPowerTest is Test { _allocate(address(baseInitiative1), 0, lqtyAmount); } - //// Compare the relative power per epoch - /// As in, one epoch should reliably increase the power by X amt - // forge test --match-test test_allocation_avg_ts_mismatch -vv - function test_allocation_avg_ts_mismatch() public { - uint256 snapshot0 = vm.snapshot(); - - uint256 snapshotBefore = vm.snapshot(); - - 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 - (, uint32 averageStakingTimestamp1) = governance.userStates(user); - - // =========== epoch 2 (start) ================== - // 2. user allocates in epoch 2 - vm.warp(block.timestamp + EPOCH_DURATION); // warp to second epoch - - // Remainer - _stakeLQTY(user, uint88(lqtyAmount / 2)); - _allocate(address(baseInitiative2), lqtyAmount / 2, 0); // 50% to it - - (, uint32 averageStakingTimestamp2) = governance.userStates(user); - - assertGt(averageStakingTimestamp2, averageStakingTimestamp1, "Time increase"); - - // Get TS for "exploit" - uint256 avgTs1 = _getAverageTS(baseInitiative1); - uint256 avgTs2 = _getAverageTS(baseInitiative2); - assertGt(avgTs2, avgTs1, "TS in initiative is increased"); - - // Check if Resetting will fix the issue - - _allocate(address(baseInitiative1), 0, 0); - _allocate(address(baseInitiative2), 0, 0); - - _allocate(address(baseInitiative1), 0, 0); - _allocate(address(baseInitiative2), 0, 0); - - uint256 avgTs_reset_1 = _getAverageTS(baseInitiative1); - uint256 avgTs_reset_2 = _getAverageTS(baseInitiative2); - - // Intuition, Delta time * LQTY = POWER - vm.revertTo(snapshotBefore); - - // Compare against - // Deposit 1 on epoch 1 - // Deposit 2 on epoch 2 - // Vote on epoch 2 exclusively - _stakeLQTY(user, uint88(lqtyAmount / 2)); - - vm.warp(block.timestamp + EPOCH_DURATION); // warp to second epoch - _stakeLQTY(user, uint88(lqtyAmount / 2)); - _allocate(address(baseInitiative2), lqtyAmount / 2, 0); // 50% to it - _allocate(address(baseInitiative1), lqtyAmount / 2, 0); // 50% to it - - uint256 avgTs1_diff = _getAverageTS(baseInitiative1); - uint256 avgTs2_diff = _getAverageTS(baseInitiative2); - // assertEq(avgTs2_diff, avgTs1_diff, "TS in initiative is increased"); - assertGt(avgTs1_diff, avgTs2_diff, "TS in initiative is increased"); - - assertLt(avgTs2_diff, avgTs2, "Ts2 is same"); - assertGt(avgTs1_diff, avgTs1, "Ts1 lost the power"); - - assertLt(avgTs_reset_1, avgTs1_diff, "Same as diff means it does reset"); - assertEq(avgTs_reset_2, avgTs2_diff, "Same as diff means it does reset"); - } - // 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 @@ -506,8 +432,8 @@ contract VotingPowerTest is Test { // Removing just updates that + the weights // The weights are the avg time * the number - function _getAverageTS(address initiative) internal returns (uint256) { - (,, uint32 averageStakingTimestampVoteLQTY,,) = governance.initiativeStates(initiative); + function _getAverageTS(address initiative) internal view returns (uint256) { + (,, uint120 averageStakingTimestampVoteLQTY,,) = governance.initiativeStates(initiative); return averageStakingTimestampVoteLQTY; } @@ -533,4 +459,13 @@ contract VotingPowerTest is Test { 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); + } } diff --git a/test/mocks/MockGovernance.sol b/test/mocks/MockGovernance.sol index 3ed3e752..ee94c8c7 100644 --- a/test/mocks/MockGovernance.sol +++ b/test/mocks/MockGovernance.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.24; contract MockGovernance { uint16 private __epoch; + uint32 public constant EPOCH_START = 0; + uint32 public constant EPOCH_DURATION = 7 days; + function claimForInitiative(address) external pure returns (uint256) { return 1000e18; } @@ -16,16 +19,16 @@ contract MockGovernance { return __epoch; } - function _averageAge(uint32 _currentTimestamp, uint32 _averageTimestamp) internal pure returns (uint32) { + function _averageAge(uint120 _currentTimestamp, uint120 _averageTimestamp) internal pure returns (uint120) { if (_averageTimestamp == 0 || _currentTimestamp < _averageTimestamp) return 0; return _currentTimestamp - _averageTimestamp; } - function lqtyToVotes(uint88 _lqtyAmount, uint256 _currentTimestamp, uint32 _averageTimestamp) + function lqtyToVotes(uint88 _lqtyAmount, uint120 _currentTimestamp, uint120 _averageTimestamp) public pure - returns (uint240) + returns (uint208) { - return uint240(_lqtyAmount) * _averageAge(uint32(_currentTimestamp), _averageTimestamp); + return uint208(_lqtyAmount) * uint208(_averageAge(_currentTimestamp, _averageTimestamp)); } } diff --git a/test/recon/CryticTester.sol b/test/recon/CryticTester.sol index 332659e0..95d29eb1 100644 --- a/test/recon/CryticTester.sol +++ b/test/recon/CryticTester.sol @@ -5,6 +5,7 @@ import {TargetFunctions} from "./TargetFunctions.sol"; import {CryticAsserts} from "@chimera/CryticAsserts.sol"; // echidna . --contract CryticTester --config echidna.yaml +// echidna . --contract CryticTester --config echidna.yaml --format text --test-limit 1000000 --test-mode assertion --workers 10 // medusa fuzz contract CryticTester is TargetFunctions, CryticAsserts { constructor() payable { diff --git a/test/recon/CryticToFoundry.sol b/test/recon/CryticToFoundry.sol index ebf54136..4e1a2d2e 100644 --- a/test/recon/CryticToFoundry.sol +++ b/test/recon/CryticToFoundry.sol @@ -5,6 +5,8 @@ import {Test} from "forge-std/Test.sol"; import {TargetFunctions} from "./TargetFunctions.sol"; import {FoundryAsserts} from "@chimera/FoundryAsserts.sol"; import {IBribeInitiative} from "src/interfaces/IBribeInitiative.sol"; +import {IGovernance} from "src/interfaces/IGovernance.sol"; +import {Governance} from "src/Governance.sol"; import {console} from "forge-std/console.sol"; @@ -13,70 +15,32 @@ contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts { setup(); } - /// Example fixed bribes properties - // Use `https://getrecon.xyz/tools/echidna` to scrape properties into this format - // forge test --match-test test_property_BI03_1 -vv - function test_property_BI03_1() public { - vm.roll(block.number + 1); - vm.warp(block.timestamp + 239415); - governance_depositLQTY(2); - vm.roll(block.number + 1); - vm.warp(block.timestamp + 366071); - governance_allocateLQTY_clamped_single_initiative(0, 1, 0); - check_skip_consistecy(0); - property_BI03(); - } - - // forge test --match-test test_property_BI04_4 -vv - function test_property_BI04_4() public { - governance_depositLQTY(2); - vm.roll(block.number + 1); - vm.warp(block.timestamp + 606998); - governance_allocateLQTY_clamped_single_initiative(0, 0, 1); - property_BI04(); - } + // forge test --match-test test_optimize_property_sum_of_initatives_matches_total_votes_insolvency_0 -vv + function test_optimize_property_sum_of_initatives_matches_total_votes_insolvency_0() public { + vm.warp(block.timestamp + 574062); - // forge test --match-test test_property_resetting_never_reverts_0 -vv - function test_property_resetting_never_reverts_0() public { - vm.roll(block.number + 1); - vm.warp(block.timestamp + 193521); - governance_depositLQTY(155989603725201422915398867); + vm.roll(block.number + 280); - vm.roll(block.number + 1); - vm.warp(block.timestamp + 411452); - governance_allocateLQTY_clamped_single_initiative(0, 0, 154742504910672534362390527); + governance_depositLQTY_2(106439091954186822399173735); - property_resetting_never_reverts(); - } - - // forge test --match-test test_property_BI11_3 -vv - function test_property_BI11_3() public { - vm.roll(block.number + 1); - vm.warp(block.timestamp + 461046); - governance_depositLQTY(2); + vm.roll(block.number + 748); + vm.warp(block.timestamp + 75040); + governance_depositLQTY(2116436955066717227177); - vm.roll(block.number + 1); - vm.warp(block.timestamp + 301396); - governance_allocateLQTY_clamped_single_initiative(0, 1, 0); + governance_allocateLQTY_clamped_single_initiative(1, 1, 0); - vm.roll(block.number + 1); - vm.warp(block.timestamp + 450733); - initiative_claimBribes(0, 3, 0, 0); - property_BI11(); - } + helper_deployInitiative(); - // forge test --match-test test_property_BI04_1 -vv - function test_property_BI04_1() public { - governance_depositLQTY(2); + governance_registerInitiative(1); - vm.roll(block.number + 1); - vm.warp(block.timestamp + 654326); - governance_allocateLQTY_clamped_single_initiative(0, 1, 0); + vm.warp(block.timestamp + 566552); - vm.roll(block.number + 1); - vm.warp(block.timestamp + 559510); - property_resetting_never_reverts(); + vm.roll(block.number + 23889); - property_BI04(); + governance_allocateLQTY_clamped_single_initiative_2nd_user(31, 1314104679369829143691540410, 0); + (,, uint256 votedPowerSum, uint256 govPower) = _getInitiativeStateAndGlobalState(); + console.log("votedPowerSum", votedPowerSum); + console.log("govPower", govPower); + assert(optimize_property_sum_of_initatives_matches_total_votes_insolvency()); } } diff --git a/test/recon/Properties.sol b/test/recon/Properties.sol index f9ffc54a..b639746a 100644 --- a/test/recon/Properties.sol +++ b/test/recon/Properties.sol @@ -2,7 +2,18 @@ pragma solidity ^0.8.0; import {BeforeAfter} from "./BeforeAfter.sol"; -import {GovernanceProperties} from "./properties/GovernanceProperties.sol"; + +// NOTE: OptimizationProperties imports Governance properties, to reuse a few fetchers +import {OptimizationProperties} from "./properties/OptimizationProperties.sol"; import {BribeInitiativeProperties} from "./properties/BribeInitiativeProperties.sol"; +import {SynchProperties} from "./properties/SynchProperties.sol"; +import {RevertProperties} from "./properties/RevertProperties.sol"; +import {TsProperties} from "./properties/TsProperties.sol"; -abstract contract Properties is GovernanceProperties, BribeInitiativeProperties {} +abstract contract Properties is + OptimizationProperties, + BribeInitiativeProperties, + SynchProperties, + RevertProperties, + TsProperties +{} diff --git a/test/recon/Setup.sol b/test/recon/Setup.sol index 5924352f..2cd45582 100644 --- a/test/recon/Setup.sol +++ b/test/recon/Setup.sol @@ -22,17 +22,12 @@ abstract contract Setup is BaseSetup { address internal user2 = address(0x537C8f3d3E18dF5517a58B3fB9D9143697996802); // derived using makeAddrAndKey address internal stakingV1; address internal userProxy; - address[] internal users = new address[](2); + address[] internal users; address[] internal deployedInitiatives; uint256 internal user2Pk = 23868421370328131711506074113045611601786642648093516849953535378706721142721; // derived using makeAddrAndKey bool internal claimedTwice; bool internal unableToClaim; - mapping(uint16 => uint88) internal ghostTotalAllocationAtEpoch; - mapping(address => uint88) internal ghostLqtyAllocationByUserAtEpoch; - // initiative => epoch => bribe - mapping(address => mapping(uint16 => IBribeInitiative.Bribe)) internal ghostBribeByEpoch; - uint128 internal constant REGISTRATION_FEE = 1e18; uint128 internal constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; uint128 internal constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; @@ -44,6 +39,8 @@ abstract contract Setup is BaseSetup { uint32 internal constant EPOCH_DURATION = 604800; uint32 internal constant EPOCH_VOTING_CUTOFF = 518400; + uint120 magnifiedStartTS; + function setup() internal virtual override { vm.warp(block.timestamp + EPOCH_DURATION * 4); // Somehow Medusa goes back after the constructor // Random TS that is realistic @@ -92,6 +89,8 @@ abstract contract Setup is BaseSetup { deployedInitiatives.push(address(initiative1)); governance.registerInitiative(address(initiative1)); + + magnifiedStartTS = uint120(block.timestamp) * uint120(1e18); } function _getDeployedInitiative(uint8 index) internal view returns (address initiative) { diff --git a/test/recon/properties/BribeInitiativeProperties.sol b/test/recon/properties/BribeInitiativeProperties.sol index 5eea47fa..6c041115 100644 --- a/test/recon/properties/BribeInitiativeProperties.sol +++ b/test/recon/properties/BribeInitiativeProperties.sol @@ -66,7 +66,7 @@ abstract contract BribeInitiativeProperties is BeforeAfter { (uint88 voteLQTY,, uint16 epoch) = governance.lqtyAllocatedByUserToInitiative(user, deployedInitiatives[i]); - try initiative.lqtyAllocatedByUserAtEpoch(user, epoch) returns (uint88 amt, uint32) { + try initiative.lqtyAllocatedByUserAtEpoch(user, epoch) returns (uint88 amt, uint120) { eq(voteLQTY, amt, "Allocation must match"); } catch { t(false, "Allocation doesn't match governance"); @@ -102,89 +102,76 @@ abstract contract BribeInitiativeProperties is BeforeAfter { return totalLQTYAllocatedAtEpoch; } - function property_BI05() public { - // users can't claim for current epoch so checking for previous - uint16 checkEpoch = governance.epoch() - 1; - - for (uint8 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++) { - // if the bool switches, the user has claimed their bribe for the epoch - if ( - _before.claimedBribeForInitiativeAtEpoch[initiative][user][checkEpoch] - != _after.claimedBribeForInitiativeAtEpoch[initiative][user][checkEpoch] - ) { - // add user claimed balance delta to the accumulator - lqtyClaimedAccumulator += _after.userLqtyBalance[users[j]] - _before.userLqtyBalance[users[j]]; - lusdClaimedAccumulator += _after.userLqtyBalance[users[j]] - _before.userLqtyBalance[users[j]]; - } - } - - (uint128 boldAmount, uint128 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); - - // find delta between bribe and claimed amount (how much should be remaining in contract) - uint128 lusdDelta = boldAmount - lusdClaimedAccumulator128; - uint128 lqtyDelta = bribeTokenAmount - lqtyClaimedAccumulator128; - - uint128 initiativeLusdBalance = uint128(lusd.balanceOf(initiative) >> 128); - uint128 initiativeLqtyBalance = uint128(lqty.balanceOf(initiative) >> 128); - - lte( - lusdDelta - initiativeLusdBalance, - 1e8, - "BI-05: Bold token dust amount remaining after claiming should be less than 100 million wei" - ); - lte( - lqtyDelta - initiativeLqtyBalance, - 1e8, - "BI-05: Bribe token dust amount remaining after claiming should be less than 100 million wei" - ); - } - } - - function property_BI06() public { - // using ghost tracking for successful bribe deposits - uint16 currentEpoch = governance.epoch(); - - for (uint8 i; i < deployedInitiatives.length; i++) { - address initiative = deployedInitiatives[i]; - IBribeInitiative.Bribe memory bribe = ghostBribeByEpoch[initiative][currentEpoch]; - (uint128 boldAmount, uint128 bribeTokenAmount) = IBribeInitiative(initiative).bribeByEpoch(currentEpoch); - eq( - bribe.boldAmount, - boldAmount, - "BI-06: Accounting for bold amount in bribe for an epoch is always correct" - ); - eq( - bribe.bribeTokenAmount, - bribeTokenAmount, - "BI-06: Accounting for bold amount in bribe for an epoch is always correct" - ); - } - } + // TODO: Looks pretty wrong and inaccurate + // Loop over the initiative + // Have all users claim all + // See what the result is + // See the dust + // Dust cap check + // function property_BI05() public { + // // users can't claim for current epoch so checking for previous + // uint16 checkEpoch = governance.epoch() - 1; + + // for (uint8 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++) { + // // if the bool switches, the user has claimed their bribe for the epoch + // if ( + // _before.claimedBribeForInitiativeAtEpoch[initiative][user][checkEpoch] + // != _after.claimedBribeForInitiativeAtEpoch[initiative][user][checkEpoch] + // ) { + // // add user claimed balance delta to the accumulator + // lqtyClaimedAccumulator += _after.userLqtyBalance[users[j]] - _before.userLqtyBalance[users[j]]; + // lusdClaimedAccumulator += _after.userLqtyBalance[users[j]] - _before.userLqtyBalance[users[j]]; + // } + // } + + // (uint128 boldAmount, uint128 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); + + // // find delta between bribe and claimed amount (how much should be remaining in contract) + // uint128 lusdDelta = boldAmount - lusdClaimedAccumulator128; + // uint128 lqtyDelta = bribeTokenAmount - lqtyClaimedAccumulator128; + + // uint128 initiativeLusdBalance = uint128(lusd.balanceOf(initiative) >> 128); + // uint128 initiativeLqtyBalance = uint128(lqty.balanceOf(initiative) >> 128); + + // lte( + // lusdDelta - initiativeLusdBalance, + // 1e8, + // "BI-05: Bold token dust amount remaining after claiming should be less than 100 million wei" + // ); + // lte( + // lqtyDelta - initiativeLqtyBalance, + // 1e8, + // "BI-05: Bribe token dust amount remaining after claiming should be less than 100 million wei" + // ); + // } + // } function property_BI07() public { - uint16 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++) { IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); + uint16 currentEpoch = initiative.getMostRecentTotalEpoch(); + uint88 sumLqtyAllocated; for (uint8 j; j < users.length; j++) { - address user = users[j]; - (uint88 lqtyAllocated,) = initiative.lqtyAllocatedByUserAtEpoch(user, currentEpoch); + // NOTE: We need to grab user latest + uint16 userEpoch = initiative.getMostRecentUserEpoch(users[j]); + (uint88 lqtyAllocated,) = initiative.lqtyAllocatedByUserAtEpoch(users[j], userEpoch); sumLqtyAllocated += lqtyAllocated; } + (uint88 totalLQTYAllocated,) = initiative.totalLQTYAllocatedByEpoch(currentEpoch); eq( sumLqtyAllocated, @@ -203,10 +190,10 @@ abstract contract BribeInitiativeProperties is BeforeAfter { IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); uint256 sumOfPower; for (uint8 j; j < users.length; j++) { - (uint88 lqtyAllocated, uint32 userTS) = initiative.lqtyAllocatedByUserAtEpoch(users[j], currentEpoch); + (uint88 lqtyAllocated, uint120 userTS) = initiative.lqtyAllocatedByUserAtEpoch(users[j], currentEpoch); sumOfPower += governance.lqtyToVotes(lqtyAllocated, userTS, uint32(block.timestamp)); } - (uint88 totalLQTYAllocated, uint32 totalTS) = initiative.totalLQTYAllocatedByEpoch(currentEpoch); + (uint88 totalLQTYAllocated, uint120 totalTS) = initiative.totalLQTYAllocatedByEpoch(currentEpoch); uint256 totalRecordedPower = governance.lqtyToVotes(totalLQTYAllocated, totalTS, uint32(block.timestamp)); @@ -271,10 +258,4 @@ abstract contract BribeInitiativeProperties is BeforeAfter { } } } - - // BI-11: User can always claim a bribe amount for which they are entitled - function property_BI11() public { - // unableToClaim gets set in the call to claimBribes and checks if user had a claimable allocation that wasn't yet claimed and tried to claim it unsuccessfully - t(!unableToClaim, "BI-11: User can always claim a bribe amount for which they are entitled "); - } } diff --git a/test/recon/properties/GovernanceProperties.sol b/test/recon/properties/GovernanceProperties.sol index 88468607..99e40b4f 100644 --- a/test/recon/properties/GovernanceProperties.sol +++ b/test/recon/properties/GovernanceProperties.sol @@ -6,8 +6,12 @@ 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"; abstract contract GovernanceProperties is BeforeAfter { + uint256 constant TOLLERANCE = 1e19; // NOTE: 1e18 is 1 second due to upscaling + /// So we accept at most 10 seconds of errors + /// A Initiative cannot change in status /// Except for being unregistered /// Or claiming rewards @@ -92,16 +96,23 @@ abstract contract GovernanceProperties is BeforeAfter { // Function sound total math - // NOTE: Global vs USer vs Initiative requires changes + // NOTE: Global vs Uer vs Initiative requires changes // User is tracking votes and vetos together // Whereas Votes and Initiatives only track Votes /// The Sum of LQTY allocated by Users matches the global state // NOTE: Sum of positive votes + // Remove the initiative from Unregistered Initiatives function property_sum_of_lqty_global_user_matches() public { // Get state // Get all users // Sum up all voted users // Total must match + (uint256 totalUserCountedLQTY, uint256 totalCountedLQTY) = _getGlobalLQTYAndUserSum(); + + eq(totalUserCountedLQTY, totalCountedLQTY, "Global vs SUM(Users_lqty) must match"); + } + + function _getGlobalLQTYAndUserSum() internal returns (uint256, uint256) { ( uint88 totalCountedLQTY, // uint32 after_user_countedVoteLQTYAverageTimestamp // TODO: How do we do this? @@ -110,18 +121,18 @@ abstract contract GovernanceProperties is BeforeAfter { uint256 totalUserCountedLQTY; for (uint256 i; i < users.length; i++) { // Only sum up user votes - (uint88 user_voteLQTY,) = _getAllUserAllocations(users[i]); + (uint88 user_voteLQTY,) = _getAllUserAllocations(users[i], true); totalUserCountedLQTY += user_voteLQTY; } - eq(totalCountedLQTY, totalUserCountedLQTY, "Global vs SUM(Users_lqty) must match"); + return (totalUserCountedLQTY, totalCountedLQTY); } // NOTE: In principle this will work since this is a easier to reach property vs checking each initiative 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]); + (uint88 user_voteLQTY,) = _getAllUserAllocations(users[i], false); lte(user_voteLQTY, uint88(type(int88).max), "User can never allocate more than int88"); } @@ -177,33 +188,74 @@ abstract contract GovernanceProperties is BeforeAfter { // sum of voting power for users that allocated to an initiative == the voting power of the initiative /// TODO ?? - function property_sum_of_user_voting_weights() public { + function property_sum_of_user_voting_weights_strict() public { // loop through all users // - calculate user voting weight for the given timestamp // - sum user voting weights for the given epoch // - compare with the voting weight of the initiative for the epoch for the same timestamp + VotesSumAndInitiativeSum[] memory votesSumAndInitiativeValues = _getUserVotesSumAndInitiativesVotes(); + + for (uint256 i; i < votesSumAndInitiativeValues.length; i++) { + eq( + votesSumAndInitiativeValues[i].userSum, + votesSumAndInitiativeValues[i].initiativeWeight, + "initiative voting weights and user's allocated weight differs for initiative" + ); + } + } + function property_sum_of_user_voting_weights_bounded() public { + // loop through all users + // - calculate user voting weight for the given timestamp + // - sum user voting weights for the given epoch + // - compare with the voting weight of the initiative for the epoch for the same timestamp + VotesSumAndInitiativeSum[] memory votesSumAndInitiativeValues = _getUserVotesSumAndInitiativesVotes(); + + for (uint256 i; i < votesSumAndInitiativeValues.length; i++) { + eq(votesSumAndInitiativeValues[i].userSum, votesSumAndInitiativeValues[i].initiativeWeight, "Matching"); + t( + votesSumAndInitiativeValues[i].userSum == votesSumAndInitiativeValues[i].initiativeWeight + || ( + votesSumAndInitiativeValues[i].userSum + >= votesSumAndInitiativeValues[i].initiativeWeight - TOLLERANCE + && votesSumAndInitiativeValues[i].userSum + <= votesSumAndInitiativeValues[i].initiativeWeight + TOLLERANCE + ), + "initiative voting weights and user's allocated weight match within tollerance" + ); + } + } + + struct VotesSumAndInitiativeSum { + uint256 userSum; + uint256 initiativeWeight; + } + + function _getUserVotesSumAndInitiativesVotes() internal returns (VotesSumAndInitiativeSum[] memory) { + VotesSumAndInitiativeSum[] memory acc = new VotesSumAndInitiativeSum[](deployedInitiatives.length); for (uint256 i; i < deployedInitiatives.length; i++) { uint240 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 - (, uint32 averageStakingTimestamp) = governance.userStates(users[j]); + (, uint120 averageStakingTimestamp) = governance.userStates(users[j]); // add the weight calculated for each user's allocation to the accumulator - userWeightAccumulatorForInitiative += - governance.lqtyToVotes(userVoteLQTY, block.timestamp, averageStakingTimestamp); + userWeightAccumulatorForInitiative += governance.lqtyToVotes( + userVoteLQTY, uint120(block.timestamp) * uint120(1e18), averageStakingTimestamp + ); } - (uint88 initiativeVoteLQTY,, uint32 initiativeAverageStakingTimestampVoteLQTY,,) = + (uint88 initiativeVoteLQTY,, uint120 initiativeAverageStakingTimestampVoteLQTY,,) = governance.initiativeStates(deployedInitiatives[i]); - uint240 initiativeWeight = - governance.lqtyToVotes(initiativeVoteLQTY, block.timestamp, initiativeAverageStakingTimestampVoteLQTY); - eq( - initiativeWeight, - userWeightAccumulatorForInitiative, - "initiative voting weights and user's allocated weight differs for initiative" + uint240 initiativeWeight = governance.lqtyToVotes( + initiativeVoteLQTY, uint120(block.timestamp) * uint120(1e18), initiativeAverageStakingTimestampVoteLQTY ); + + acc[i].userSum = userWeightAccumulatorForInitiative; + acc[i].initiativeWeight = initiativeWeight; } + + return acc; } function property_allocations_are_never_dangerously_high() public { @@ -216,24 +268,73 @@ abstract contract GovernanceProperties is BeforeAfter { } } - function property_sum_of_initatives_matches_total_votes() public { + function property_sum_of_initatives_matches_total_votes_strict() public { // Sum up all initiatives // Compare to total votes - (IGovernance.VoteSnapshot memory snapshot,,) = governance.getTotalVotesAndState(); + (uint256 allocatedLQTYSum, uint256 totalCountedLQTY, uint256 votedPowerSum, uint256 govPower) = + _getInitiativeStateAndGlobalState(); + + eq(allocatedLQTYSum, totalCountedLQTY, "LQTY Sum of Initiative State matches Global State at all times"); + eq(votedPowerSum, govPower, "Voting Power Sum of Initiative State matches Global State at all times"); + } + + function property_sum_of_initatives_matches_total_votes_bounded() public { + // Sum up all initiatives + // Compare to total votes + (uint256 allocatedLQTYSum, uint256 totalCountedLQTY, uint256 votedPowerSum, uint256 govPower) = + _getInitiativeStateAndGlobalState(); + + t( + allocatedLQTYSum == totalCountedLQTY + || (allocatedLQTYSum >= totalCountedLQTY - TOLLERANCE && allocatedLQTYSum <= totalCountedLQTY + TOLLERANCE), + "Sum of Initiative LQTY And State matches within absolute tollerance" + ); - uint256 initiativeVotesSum; + t( + votedPowerSum == govPower + || (votedPowerSum >= govPower - TOLLERANCE && votedPowerSum <= govPower + TOLLERANCE), + "Sum of Initiative LQTY And State matches within absolute tollerance" + ); + } + + function _getInitiativeStateAndGlobalState() internal returns (uint256, uint256, uint256, uint256) { + (uint88 totalCountedLQTY, uint120 global_countedVoteLQTYAverageTimestamp) = governance.globalState(); + + // Can sum via projection I guess + + // Global Acc + // Initiative Acc + uint256 allocatedLQTYSum; + uint256 votedPowerSum; for (uint256 i; i < deployedInitiatives.length; i++) { - (IGovernance.InitiativeVoteSnapshot memory initiativeSnapshot,,) = - governance.getInitiativeSnapshotAndState(deployedInitiatives[i]); + ( + uint88 voteLQTY, + uint88 vetoLQTY, + uint120 averageStakingTimestampVoteLQTY, + uint120 averageStakingTimestampVetoLQTY, + ) = governance.initiativeStates(deployedInitiatives[i]); + + // Conditional, only if not DISABLED (Governance.InitiativeStatus status,,) = governance.getInitiativeState(deployedInitiatives[i]); - + // Conditionally add based on state if (status != Governance.InitiativeStatus.DISABLED) { - // FIX: Only count total if initiative is not disabled - initiativeVotesSum += initiativeSnapshot.votes; + allocatedLQTYSum += voteLQTY; + // Sum via projection + votedPowerSum += governance.lqtyToVotes( + voteLQTY, + uint120(block.timestamp) * uint120(governance.TIMESTAMP_PRECISION()), + averageStakingTimestampVoteLQTY + ); } } - eq(snapshot.votes, initiativeVotesSum, "Sum of votes matches"); + uint256 govPower = governance.lqtyToVotes( + totalCountedLQTY, + uint120(block.timestamp) * uint120(governance.TIMESTAMP_PRECISION()), + global_countedVoteLQTYAverageTimestamp + ); + + return (allocatedLQTYSum, totalCountedLQTY, votedPowerSum, govPower); } /// NOTE: This property can break in some specific combinations of: @@ -258,6 +359,22 @@ 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); + + if (status == Governance.InitiativeStatus.WARM_UP) { + vm.warp(block.timestamp + governance.EPOCH_DURATION()); + (Governance.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"); + } + } + /// NOTE: This property can break in some specific combinations of: /// Becomes unregisterable due to high treshold /// Is not unregistered @@ -276,6 +393,8 @@ abstract contract GovernanceProperties is BeforeAfter { } } + // TODO: Maybe check snapshot of states and ensure it can never be less than 4 epochs b4 unregisterable + function check_claim_soundness() public { // Check if initiative is claimable // If it is assert the check @@ -293,6 +412,47 @@ abstract contract GovernanceProperties is BeforeAfter { } } + // TODO: Optimization property to show max loss + // TODO: Same identical optimization property for Bribes claiming + /// Should prob change the math to view it in bribes for easier debug + function check_claimable_solvency() public { + // Accrue all initiatives + // Get bold amount + // Sum up the initiatives claimable vs the bold + + // Check if initiative is claimable + // If it is assert the check + uint256 claimableSum; + for (uint256 i; i < deployedInitiatives.length; i++) { + // NOTE: Non view so it accrues state + (Governance.InitiativeStatus status,, uint256 claimableAmount) = + governance.getInitiativeState(deployedInitiatives[i]); + + claimableSum += claimableAmount; + } + + // Grab accrued + uint256 boldAccrued = governance.boldAccrued(); + + lte(claimableSum, boldAccrued, "Total Claims are always LT all bold"); + } + + function check_realized_claiming_solvency() public { + uint256 claimableSum; + for (uint256 i; i < deployedInitiatives.length; i++) { + uint256 claimed = governance.claimForInitiative(deployedInitiatives[i]); + + claimableSum += claimed; + } + + // Grab accrued + uint256 boldAccrued = governance.boldAccrued(); + + lte(claimableSum, boldAccrued, "Total Claims are always LT all bold"); + } + + // TODO: Optimization of this to determine max damage, and max insolvency + function _getUserAllocation(address theUser, address initiative) internal view @@ -301,12 +461,101 @@ abstract contract GovernanceProperties is BeforeAfter { (votes, vetos,) = governance.lqtyAllocatedByUserToInitiative(theUser, initiative); } - function _getAllUserAllocations(address theUser) internal view returns (uint88 votes, uint88 vetos) { + function _getAllUserAllocations(address theUser, bool skipDisabled) internal returns (uint88 votes, uint88 vetos) { for (uint256 i; i < deployedInitiatives.length; i++) { (uint88 allocVotes, uint88 allocVetos,) = governance.lqtyAllocatedByUserToInitiative(theUser, deployedInitiatives[i]); - votes += allocVotes; - vetos += allocVetos; + if (skipDisabled) { + (Governance.InitiativeStatus status,,) = governance.getInitiativeState(deployedInitiatives[i]); + + // Conditionally add based on state + if (status != Governance.InitiativeStatus.DISABLED) { + votes += allocVotes; + vetos += allocVetos; + } + } else { + // Always add + votes += allocVotes; + vetos += allocVetos; + } + } + } + + function property_alloc_deposit_reset_is_idempotent( + uint8 initiativesIndex, + uint96 deltaLQTYVotes, + uint96 deltaLQTYVetos, + uint88 lqtyAmount + ) public withChecks { + address targetInitiative = _getDeployedInitiative(initiativesIndex); + + // 0. Reset first to ensure we start fresh, else the totals can be out of whack + // TODO: prob unnecessary + // Cause we always reset anyway + { + int88[] memory zeroes = new int88[](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); + + // Allocate + { + uint96 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)); + + governance.allocateLQTY(deployedInitiatives, initiatives, deltaLQTYVotesArray, deltaLQTYVetosArray); + } + + // Deposit (Changes total LQTY an hopefully also changes ts) + { + (, uint120 averageStakingTimestamp1) = governance.userStates(user); + + lqtyAmount = uint88(lqtyAmount % lqty.balanceOf(user)); + governance.depositLQTY(lqtyAmount); + (, uint120 averageStakingTimestamp2) = governance.userStates(user); + + require(averageStakingTimestamp2 > averageStakingTimestamp1, "Must have changed"); + } + + // REMOVE STUFF to remove the user data + { + int88[] memory zeroes = new int88[](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); + + 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(totalCountedLQTY, after_totalCountedLQTY, "Same total LQTY"); + eq(user_countedVoteLQTYAverageTimestamp, after_user_countedVoteLQTYAverageTimestamp, "Same total ts"); } } } diff --git a/test/recon/properties/OptimizationProperties.sol b/test/recon/properties/OptimizationProperties.sol new file mode 100644 index 00000000..6c6c8d2f --- /dev/null +++ b/test/recon/properties/OptimizationProperties.sol @@ -0,0 +1,155 @@ +// 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"; +import {console} from "forge-std/console.sol"; + +// NOTE: These run only if you use `optimization` mode and set the correct prefix +// See echidna.yaml +abstract contract OptimizationProperties is GovernanceProperties { + function optimize_max_sum_of_user_voting_weights_insolvent() public returns (int256) { + VotesSumAndInitiativeSum[] memory results = _getUserVotesSumAndInitiativesVotes(); + + int256 max = 0; + + // User have more than initiative, we are insolvent + for (uint256 i; i < results.length; i++) { + if (results[i].userSum > results[i].initiativeWeight) { + max = int256(results[i].userSum) - int256(results[i].initiativeWeight); + } + } + + return max; + } + + function optimize_max_sum_of_user_voting_weights_underpaying() public returns (int256) { + VotesSumAndInitiativeSum[] memory results = _getUserVotesSumAndInitiativesVotes(); + + int256 max = 0; + + for (uint256 i; i < results.length; i++) { + // Initiative has more than users, we are underpaying + if (results[i].initiativeWeight > results[i].userSum) { + max = int256(results[i].initiativeWeight) - int256(results[i].userSum); + } + } + + return max; + } + + function optimize_max_claim_insolvent() public returns (int256) { + uint256 claimableSum; + for (uint256 i; i < deployedInitiatives.length; i++) { + // NOTE: Non view so it accrues state + (Governance.InitiativeStatus status,, uint256 claimableAmount) = + governance.getInitiativeState(deployedInitiatives[i]); + + claimableSum += claimableAmount; + } + + // Grab accrued + uint256 boldAccrued = governance.boldAccrued(); + + int256 max; + if (claimableSum > boldAccrued) { + max = int256(claimableSum) - int256(boldAccrued); + } + + return max; + } + + // NOTE: This property is not particularly good as you can just do a donation and not vote + // This douesn't really highlight a loss + function optimize_max_claim_underpay() public returns (int256) { + uint256 claimableSum; + for (uint256 i; i < deployedInitiatives.length; i++) { + // NOTE: Non view so it accrues state + (Governance.InitiativeStatus status,, uint256 claimableAmount) = + governance.getInitiativeState(deployedInitiatives[i]); + + claimableSum += claimableAmount; + } + + // Grab accrued + uint256 boldAccrued = governance.boldAccrued(); + + int256 max; + if (boldAccrued > claimableSum) { + max = int256(boldAccrued) - int256(claimableSum); + } + + return max; + } + + function property_sum_of_initatives_matches_total_votes_insolvency_assertion() public { + uint256 delta = 0; + + (,, uint256 votedPowerSum, uint256 govPower) = _getInitiativeStateAndGlobalState(); + + if (votedPowerSum > govPower) { + delta = votedPowerSum - govPower; + + console.log("votedPowerSum * 1e18 / govPower", votedPowerSum * 1e18 / govPower); + } + + console.log("votedPowerSum", votedPowerSum); + console.log("govPower", govPower); + console.log("delta", delta); + + t(delta < 4e25, "Delta is too big"); // 3e25 was found via optimization, no value past that was found + } + + function optimize_property_sum_of_lqty_global_user_matches_insolvency() public returns (int256) { + int256 max = 0; + + (uint256 totalUserCountedLQTY, uint256 totalCountedLQTY) = _getGlobalLQTYAndUserSum(); + + if (totalUserCountedLQTY > totalCountedLQTY) { + max = int256(totalUserCountedLQTY) - int256(totalCountedLQTY); + } + + return max; + } + + function optimize_property_sum_of_lqty_global_user_matches_underpaying() public returns (int256) { + int256 max = 0; + + (uint256 totalUserCountedLQTY, uint256 totalCountedLQTY) = _getGlobalLQTYAndUserSum(); + + if (totalCountedLQTY > totalUserCountedLQTY) { + max = int256(totalCountedLQTY) - int256(totalUserCountedLQTY); + } + + return max; + } + + function optimize_property_sum_of_initatives_matches_total_votes_insolvency() public returns (bool) { + int256 max = 0; + + (,, uint256 votedPowerSum, uint256 govPower) = _getInitiativeStateAndGlobalState(); + + if (votedPowerSum > govPower) { + max = int256(votedPowerSum) - int256(govPower); + } + + return max < 3e25; + } + + function optimize_property_sum_of_initatives_matches_total_votes_underpaying() public returns (int256) { + int256 max = 0; + + (,, uint256 votedPowerSum, uint256 govPower) = _getInitiativeStateAndGlobalState(); + + if (govPower > votedPowerSum) { + max = int256(govPower) - int256(votedPowerSum); + } + + return max; // 177155848800000000000000000000000000 (2^117) + } +} diff --git a/test/recon/properties/RevertProperties.sol b/test/recon/properties/RevertProperties.sol new file mode 100644 index 00000000..6d73d5e6 --- /dev/null +++ b/test/recon/properties/RevertProperties.sol @@ -0,0 +1,167 @@ +// 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 {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(); + + try governance.lqtyToVotes( + totalCountedLQTY, + uint120(block.timestamp) * uint120(governance.TIMESTAMP_PRECISION()), + global_countedVoteLQTYAverageTimestamp + ) {} catch { + t(false, "Should never revert"); + } + } + + 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]); + + // Sum via projection + uint256 prevSum = votedPowerSum; + unchecked { + try governance.lqtyToVotes( + voteLQTY, + uint120(block.timestamp) * uint120(governance.TIMESTAMP_PRECISION()), + averageStakingTimestampVoteLQTY + ) returns (uint208 res) { + votedPowerSum += res; + } catch { + t(false, "Should never revert"); + } + } + gte(votedPowerSum, prevSum, "overflow detected"); + } + } + + function property_shouldNeverRevertSnapshotAndState(uint8 initiativeIndex) public { + address initiative = _getDeployedInitiative(initiativeIndex); + + try governance.getInitiativeSnapshotAndState(initiative) {} + catch { + t(false, "should never revert"); + } + } + + function property_shouldGetTotalVotesAndState() public { + try governance.getTotalVotesAndState() {} + catch { + t(false, "should never revert"); + } + } + + function property_shouldNeverRevertepoch() public { + try governance.epoch() {} + catch { + t(false, "should never revert"); + } + } + + function property_shouldNeverRevertepochStart(uint8 initiativeIndex) public { + address initiative = _getDeployedInitiative(initiativeIndex); + + try governance.getInitiativeSnapshotAndState(initiative) {} + catch { + t(false, "should never revert"); + } + } + + function property_shouldNeverRevertsecondsWithinEpoch() public { + try governance.secondsWithinEpoch() {} + catch { + t(false, "should never revert"); + } + } + + function property_shouldNeverRevertlqtyToVotes() public { + // TODO GRAB THE STATE VALUES + // governance.lqtyToVotes(); + } + + function property_shouldNeverRevertgetLatestVotingThreshold() public { + try governance.getLatestVotingThreshold() {} + catch { + t(false, "should never revert"); + } + } + + function property_shouldNeverRevertcalculateVotingThreshold() public { + try governance.calculateVotingThreshold() {} + catch { + t(false, "should never revert"); + } + } + + function property_shouldNeverRevertgetTotalVotesAndState() public { + try governance.getTotalVotesAndState() {} + catch { + t(false, "should never revert"); + } + } + + function property_shouldNeverRevertgetInitiativeSnapshotAndState(uint8 initiativeIndex) public { + address initiative = _getDeployedInitiative(initiativeIndex); + + try governance.getInitiativeSnapshotAndState(initiative) {} + catch { + t(false, "should never revert"); + } + } + + function property_shouldNeverRevertsnapshotVotesForInitiative(uint8 initiativeIndex) public { + address initiative = _getDeployedInitiative(initiativeIndex); + + try governance.snapshotVotesForInitiative(initiative) {} + catch { + t(false, "should never revert"); + } + } + + function property_shouldNeverRevertgetInitiativeState(uint8 initiativeIndex) public { + address initiative = _getDeployedInitiative(initiativeIndex); + + try governance.getInitiativeState(initiative) {} + catch { + t(false, "should never revert"); + } + } + + function property_shouldNeverRevertgetInitiativeState_arbitrary(address initiative) public { + try governance.getInitiativeState(initiative) {} + catch { + t(false, "should never revert"); + } + } + + /// TODO: Consider creating this with somewhat realistic value + /// Arbitrary values can too easily overflow + // function property_shouldNeverRevertgetInitiativeState_arbitrary( + // address _initiative, + // IGovernance.VoteSnapshot memory _votesSnapshot, + // IGovernance.InitiativeVoteSnapshot memory _votesForInitiativeSnapshot, + // IGovernance.InitiativeState memory _initiativeState + // ) public { + // // NOTE: Maybe this can revert due to specific max values + // try governance.getInitiativeState( + // _initiative, + // _votesSnapshot, + // _votesForInitiativeSnapshot, + // _initiativeState + // ) {} catch { + // t(false, "should never revert"); + // } + // } +} diff --git a/test/recon/properties/SynchProperties.sol b/test/recon/properties/SynchProperties.sol new file mode 100644 index 00000000..1414c64c --- /dev/null +++ b/test/recon/properties/SynchProperties.sol @@ -0,0 +1,43 @@ +// 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 {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 + // This ensures that the math is correct in removal and addition + function property_initiative_ts_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) = + governance.lqtyAllocatedByUserToInitiative(users[j], deployedInitiatives[i]); + + // Grab epoch from initiative + (uint88 lqtyAllocatedByUserAtEpoch, uint120 ts) = + IBribeInitiative(deployedInitiatives[i]).lqtyAllocatedByUserAtEpoch(users[j], epoch); + + // Check that TS matches (only for votes) + 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]); + + eq(averageStakingTimestamp, ts, "Timestamp must be most recent when it's non zero"); + } 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 + } + } + } + } +} diff --git a/test/recon/properties/TsProperties.sol b/test/recon/properties/TsProperties.sol new file mode 100644 index 00000000..1fa84773 --- /dev/null +++ b/test/recon/properties/TsProperties.sol @@ -0,0 +1,30 @@ +// 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 {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 { + for (uint256 i; i < users.length; i++) { + (uint88 user_allocatedLQTY, uint120 userTs) = governance.userStates(users[i]); + if (user_allocatedLQTY > 0) { + gte(userTs, 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(); + + if (totalCountedLQTY > 0) { + gte(globalTs, magnifiedStartTS, "Global ts must always be GTE than start"); + } + } + + // TODO: Waiting 1 second should give 1 an extra second * WAD power +} diff --git a/test/recon/targets/BribeInitiativeTargets.sol b/test/recon/targets/BribeInitiativeTargets.sol index 883988c5..694c7e0e 100644 --- a/test/recon/targets/BribeInitiativeTargets.sol +++ b/test/recon/targets/BribeInitiativeTargets.sol @@ -5,9 +5,9 @@ import {Test} from "forge-std/Test.sol"; import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; import {vm} from "@chimera/Hevm.sol"; -import {IInitiative} from "../../../src/interfaces/IInitiative.sol"; -import {IBribeInitiative} from "../../../src/interfaces/IBribeInitiative.sol"; -import {DoubleLinkedList} from "../../../src/utils/DoubleLinkedList.sol"; +import {IInitiative} from "src/interfaces/IInitiative.sol"; +import {IBribeInitiative} from "src/interfaces/IBribeInitiative.sol"; +import {DoubleLinkedList} from "src/utils/DoubleLinkedList.sol"; import {Properties} from "../Properties.sol"; abstract contract BribeInitiativeTargets is Test, BaseTargetFunctions, Properties { @@ -25,12 +25,39 @@ abstract contract BribeInitiativeTargets is Test, BaseTargetFunctions, Propertie boldAmount = uint128(boldAmount % lusd.balanceOf(user)); bribeTokenAmount = uint128(bribeTokenAmount % lqty.balanceOf(user)); + lusd.approve(address(initiative), boldAmount); + lqty.approve(address(initiative), bribeTokenAmount); + + (uint128 boldAmountB4, uint128 bribeTokenAmountB4) = IBribeInitiative(initiative).bribeByEpoch(epoch); + initiative.depositBribe(boldAmount, bribeTokenAmount, epoch); - // tracking to check that bribe accounting is always correct - uint16 currentEpoch = governance.epoch(); - ghostBribeByEpoch[address(initiative)][currentEpoch].boldAmount += boldAmount; - ghostBribeByEpoch[address(initiative)][currentEpoch].bribeTokenAmount += bribeTokenAmount; + (uint128 boldAmountAfter, uint128 bribeTokenAmountAfter) = IBribeInitiative(initiative).bribeByEpoch(epoch); + + eq(boldAmountB4 + boldAmount, boldAmountAfter, "Bold amount tracking is sound"); + eq(bribeTokenAmountB4 + bribeTokenAmount, bribeTokenAmountAfter, "Bribe amount tracking is sound"); + } + + // Canaries are no longer necessary + // function canary_bribeWasThere(uint8 initiativeIndex) public { + // uint16 epoch = governance.epoch(); + // IBribeInitiative initiative = IBribeInitiative(_getDeployedInitiative(initiativeIndex)); + + // (uint128 boldAmount, uint128 bribeTokenAmount) = initiative.bribeByEpoch(epoch); + // t(boldAmount == 0 && bribeTokenAmount == 0, "A bribe was found"); + // } + + // bool hasClaimedBribes; + // function canary_has_claimed() public { + // t(!hasClaimedBribes, "has claimed"); + // } + + function clamped_claimBribes(uint8 initiativeIndex) public { + IBribeInitiative initiative = IBribeInitiative(_getDeployedInitiative(initiativeIndex)); + + uint16 userEpoch = initiative.getMostRecentUserEpoch(user); + uint16 stateEpoch = initiative.getMostRecentTotalEpoch(); + initiative_claimBribes(governance.epoch() - 1, userEpoch, stateEpoch, initiativeIndex); } function initiative_claimBribes( @@ -55,11 +82,21 @@ abstract contract BribeInitiativeTargets is Test, BaseTargetFunctions, Propertie bool alreadyClaimed = initiative.claimedBribeAtEpoch(user, epoch); - try initiative.claimBribes(claimData) {} - catch { - // check if user had a claimable allocation - (uint88 lqtyAllocated,) = initiative.lqtyAllocatedByUserAtEpoch(user, prevAllocationEpoch); - bool claimedBribe = initiative.claimedBribeAtEpoch(user, prevAllocationEpoch); + try initiative.claimBribes(claimData) { + // Claiming at the same epoch is an issue + if (alreadyClaimed) { + // toggle canary that breaks the BI-02 property + claimedTwice = true; + } + } catch { + // 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); + 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); @@ -71,13 +108,8 @@ abstract contract BribeInitiativeTargets is Test, BaseTargetFunctions, Propertie if (lqtyAllocated > 0 && !claimedBribe && bribeWasThere) { // user wasn't able to claim a bribe they were entitled to unableToClaim = true; + /// @audit Consider adding this as a test once claiming is simplified } } - - // check if the bribe was already claimed at the given epoch - if (alreadyClaimed) { - // toggle canary that breaks the BI-02 property - claimedTwice = true; - } } } diff --git a/test/recon/targets/GovernanceTargets.sol b/test/recon/targets/GovernanceTargets.sol index e9af8f2e..d8ef2244 100644 --- a/test/recon/targets/GovernanceTargets.sol +++ b/test/recon/targets/GovernanceTargets.sol @@ -8,12 +8,13 @@ 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 {ILQTYStaking} from "../../../src/interfaces/ILQTYStaking.sol"; -import {IInitiative} from "../../../src/interfaces/IInitiative.sol"; -import {IUserProxy} from "../../../src/interfaces/IUserProxy.sol"; -import {PermitParams} from "../../../src/utils/Types.sol"; -import {add} from "../../../src/utils/Math.sol"; +import {BribeInitiative} from "src/BribeInitiative.sol"; +import {Governance} from "src/Governance.sol"; +import {ILQTYStaking} from "src/interfaces/ILQTYStaking.sol"; +import {IInitiative} from "src/interfaces/IInitiative.sol"; +import {IUserProxy} from "src/interfaces/IUserProxy.sol"; +import {PermitParams} from "src/utils/Types.sol"; +import {add} from "src/utils/Math.sol"; abstract contract GovernanceTargets is BaseTargetFunctions, Properties { // clamps to a single initiative to ensure coverage in case both haven't been registered yet @@ -22,9 +23,52 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { uint96 deltaLQTYVotes, uint96 deltaLQTYVetos ) public withChecks { - uint16 currentEpoch = governance.epoch(); uint96 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user)).staked(); // clamp using the user's staked balance + 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))); + + // User B4 + // (uint88 b4_user_allocatedLQTY,) = governance.userStates(user); // TODO + // StateB4 + (uint88 b4_global_allocatedLQTY,) = governance.globalState(); + + (Governance.InitiativeStatus status,,) = governance.getInitiativeState(initiatives[0]); + + try governance.allocateLQTY(deployedInitiatives, 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 + } + + // The test here should be: + // If initiative was DISABLED + // No Global State accounting should change + // User State accounting should change + + // 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(); + + if (status == Governance.InitiativeStatus.DISABLED) { + // NOTE: It could be 0 + lte(after_global_allocatedLQTY, b4_global_allocatedLQTY, "Alloc can only be strictly decreasing"); + } + } + + function governance_allocateLQTY_clamped_single_initiative_2nd_user( + uint8 initiativesIndex, + uint96 deltaLQTYVotes, + uint96 deltaLQTYVetos + ) public withChecks { + uint96 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user2)).staked(); // clamp using the user's staked balance + address[] memory initiatives = new address[](1); initiatives[0] = _getDeployedInitiative(initiativesIndex); int88[] memory deltaLQTYVotesArray = new int88[](1); @@ -32,21 +76,23 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { int88[] memory deltaLQTYVetosArray = new int88[](1); deltaLQTYVetosArray[0] = int88(uint88(deltaLQTYVetos % stakedAmount)); + require(stakedAmount > 0, "0 stake"); + + vm.prank(user2); governance.allocateLQTY(deployedInitiatives, initiatives, deltaLQTYVotesArray, deltaLQTYVetosArray); + } - // if call was successful update the ghost tracking variables - // allocation only allows voting OR vetoing at a time so need to check which was executed - if (deltaLQTYVotesArray[0] > 0) { - ghostLqtyAllocationByUserAtEpoch[user] = add(ghostLqtyAllocationByUserAtEpoch[user], deltaLQTYVotesArray[0]); - ghostTotalAllocationAtEpoch[currentEpoch] = - add(ghostTotalAllocationAtEpoch[currentEpoch], deltaLQTYVotesArray[0]); - } else { - ghostLqtyAllocationByUserAtEpoch[user] = add(ghostLqtyAllocationByUserAtEpoch[user], deltaLQTYVetosArray[0]); - ghostTotalAllocationAtEpoch[currentEpoch] = - add(ghostTotalAllocationAtEpoch[currentEpoch], deltaLQTYVetosArray[0]); - } + function governance_resetAllocations() public { + governance.resetAllocations(deployedInitiatives, true); + } + + function governance_resetAllocations_user_2() public { + vm.prank(user2); + governance.resetAllocations(deployedInitiatives, true); } + // 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); @@ -61,12 +107,55 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { 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 + + // Deposit on zero + if (stakedAmount == 0) { + lqtyAmount = uint88(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"); + } else { + // Make sure the TS can never bo before itself + (, uint120 ts_b4) = governance.userStates(user); + lqtyAmount = uint88(lqtyAmount % lqty.balanceOf(user)); + governance.depositLQTY(lqtyAmount); + + (, uint120 ts_after) = governance.userStates(user); + + gte(ts_after, ts_b4, "User TS must always increase"); + } + } + + function depositMustFailOnNonZeroAlloc(uint88 lqtyAmount) public withChecks { + (uint88 user_allocatedLQTY,) = governance.userStates(user); + + require(user_allocatedLQTY != 0, "0 alloc"); + + lqtyAmount = uint88(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); + + require(user_allocatedLQTY != 0); + + try governance.withdrawLQTY(_lqtyAmount) { + t(false, "Withdraw Must always revert when user is not reset"); + } catch {} + } + // For every previous epoch go grab ghost values and ensure they match snapshot // For every initiative, make ghost values and ensure they match // For all operations, you also need to add the VESTED AMT? - /// TODO: This is not really working - function governance_allocateLQTY(int88[] calldata _deltaLQTYVotes, int88[] calldata _deltaLQTYVetos) + function governance_allocateLQTY(int88[] memory _deltaLQTYVotes, int88[] memory _deltaLQTYVetos) public withChecks { @@ -99,8 +188,8 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { require(governance.epoch() > 2); // Prevent reverts due to timewarp address initiative = _getDeployedInitiative(initiativeIndex); - try governance.claimForInitiative(initiative) { - } catch { + try governance.claimForInitiative(initiative) {} + catch { t(false, "claimForInitiative should never revert"); } } @@ -119,6 +208,19 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { governance.depositLQTY(lqtyAmount); } + function governance_depositLQTY_2(uint88 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); + } catch {} + + lqtyAmount = uint88(lqtyAmount % lqty.balanceOf(user2)); + vm.prank(user2); + governance.depositLQTY(lqtyAmount); + } + function governance_depositLQTYViaPermit(uint88 _lqtyAmount) public withChecks { // Get the current block timestamp for the deadline uint256 deadline = block.timestamp + 1 hours; @@ -139,7 +241,7 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { PermitParams memory permitParams = PermitParams({owner: user2, spender: user, value: _lqtyAmount, deadline: deadline, v: v, r: r, s: s}); - + // TODO: BROKEN governance.depositLQTYViaPermit(_lqtyAmount, permitParams); } @@ -160,4 +262,20 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { function governance_withdrawLQTY(uint88 _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 + + // Ensure we have 0 votes + try governance.resetAllocations(deployedInitiatives, true) {} + catch { + t(false, "Should not revert cause OOG is unlikely"); + } + + _lqtyAmount %= stakedAmount + 1; + try governance.withdrawLQTY(_lqtyAmount) {} + catch { + t(false, "Clamped withdraw should not revert"); + } + } } diff --git a/test/recon/trophies/SecondTrophiesToFoundry.sol b/test/recon/trophies/SecondTrophiesToFoundry.sol new file mode 100644 index 00000000..f46bd61b --- /dev/null +++ b/test/recon/trophies/SecondTrophiesToFoundry.sol @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {TargetFunctions} from "../TargetFunctions.sol"; +import {FoundryAsserts} from "@chimera/FoundryAsserts.sol"; +import {IBribeInitiative} from "src/interfaces/IBribeInitiative.sol"; +import {IGovernance} from "src/interfaces/IGovernance.sol"; +import {Governance} from "src/Governance.sol"; + +import {console} from "forge-std/console.sol"; + +contract SecondTrophiesToFoundry is Test, TargetFunctions, FoundryAsserts { + function setUp() public { + setup(); + } + + // forge test --match-test test_property_sum_of_initatives_matches_total_votes_strict_2 -vv + function test_property_sum_of_initatives_matches_total_votes_strict_2() public { + governance_depositLQTY_2(2); + + vm.warp(block.timestamp + 434544); + + vm.roll(block.number + 1); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 171499); + governance_allocateLQTY_clamped_single_initiative_2nd_user(0, 1, 0); + + helper_deployInitiative(); + + governance_depositLQTY(2); + + vm.warp(block.timestamp + 322216); + + vm.roll(block.number + 1); + + governance_registerInitiative(1); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 449572); + governance_allocateLQTY_clamped_single_initiative(1, 75095343, 0); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 436994); + property_sum_of_initatives_matches_total_votes_strict(); + // Of by 1 + // I think this should be off by a bit more than 1 + // But ultimately always less + } + + // forge test --match-test test_property_sum_of_user_voting_weights_0 -vv + function test_property_sum_of_user_voting_weights_0() public { + vm.warp(block.timestamp + 365090); + + vm.roll(block.number + 1); + + governance_depositLQTY_2(3); + + vm.warp(block.timestamp + 164968); + + vm.roll(block.number + 1); + + governance_depositLQTY(2); + + vm.warp(block.timestamp + 74949); + + vm.roll(block.number + 1); + + governance_allocateLQTY_clamped_single_initiative_2nd_user(0, 2, 0); + + governance_allocateLQTY_clamped_single_initiative(0, 1, 0); + + property_sum_of_user_voting_weights_bounded(); + + /// Of by 2 + } + + // forge test --match-test test_property_sum_of_lqty_global_user_matches_3 -vv + function test_property_sum_of_lqty_global_user_matches_3() public { + vm.roll(block.number + 2); + vm.warp(block.timestamp + 45381); + governance_depositLQTY_2(161673733563); + + vm.roll(block.number + 92); + vm.warp(block.timestamp + 156075); + property_BI03(); + + vm.roll(block.number + 305); + vm.warp(block.timestamp + 124202); + property_BI04(); + + vm.roll(block.number + 2); + vm.warp(block.timestamp + 296079); + governance_allocateLQTY_clamped_single_initiative_2nd_user(0, 1, 0); + + vm.roll(block.number + 4); + vm.warp(block.timestamp + 179667); + helper_deployInitiative(); + + governance_depositLQTY(2718660550802480907); + + vm.roll(block.number + 6); + vm.warp(block.timestamp + 383590); + property_BI07(); + + vm.warp(block.timestamp + 246073); + + vm.roll(block.number + 79); + + vm.roll(block.number + 4); + vm.warp(block.timestamp + 322216); + governance_depositLQTY(1); + + vm.warp(block.timestamp + 472018); + + vm.roll(block.number + 215); + + governance_registerInitiative(1); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 419805); + governance_allocateLQTY_clamped_single_initiative(1, 3700338125821584341973, 0); + + vm.warp(block.timestamp + 379004); + + vm.roll(block.number + 112); + + governance_unregisterInitiative(0); + + property_sum_of_lqty_global_user_matches(); + } + + // forge test --match-test test_governance_claimForInitiativeDoesntRevert_5 -vv + function test_governance_claimForInitiativeDoesntRevert_5() public { + governance_depositLQTY_2(96505858); + _loginitiative_and_state(); // 0 + + vm.roll(block.number + 3); + vm.warp(block.timestamp + 191303); + property_BI03(); + _loginitiative_and_state(); // 1 + + vm.warp(block.timestamp + 100782); + + vm.roll(block.number + 1); + + vm.roll(block.number + 1); + vm.warp(block.timestamp + 344203); + governance_allocateLQTY_clamped_single_initiative_2nd_user(0, 1, 0); + _loginitiative_and_state(); // 2 + + vm.warp(block.timestamp + 348184); + + vm.roll(block.number + 177); + + helper_deployInitiative(); + _loginitiative_and_state(); // 3 + + helper_accrueBold(1000135831883853852074); + _loginitiative_and_state(); // 4 + + governance_depositLQTY(2293362807359); + _loginitiative_and_state(); // 5 + + vm.roll(block.number + 2); + vm.warp(block.timestamp + 151689); + property_BI04(); + _loginitiative_and_state(); // 6 + + governance_registerInitiative(1); + _loginitiative_and_state(); // 7 + property_sum_of_initatives_matches_total_votes_strict(); + + 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(); + + governance_resetAllocations(); // NOTE: This leaves 1 vote from user2, and removes the votes from user1 + _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(); + console.log("time 0", block.timestamp); + + vm.warp(block.timestamp + 231771); + vm.roll(block.number + 5); + _loginitiative_and_state(); + console.log("time 0", block.timestamp); + + // Both of these are fine + // Meaning all LQTY allocation is fine here + // Same for user voting weights + 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 + (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]); + + // if (status != Governance.InitiativeStatus.DISABLED) { + // FIX: Only count total if initiative is not disabled + initiativeVotesSum += initiativeSnapshot.votes; + // } + } + console.log("snapshot.votes", snapshot.votes); + console.log("initiativeVotesSum", initiativeVotesSum); + console.log("bold.balance", lusd.balanceOf(address(governance))); + governance_claimForInitiativeDoesntRevert(0); // Because of the quickfix this will not revert anymore + } + + uint256 loggerCount; + + function _loginitiative_and_state() internal { + (IGovernance.VoteSnapshot memory snapshot, IGovernance.GlobalState memory state,) = + governance.getTotalVotesAndState(); + console.log(""); + console.log("loggerCount", loggerCount++); + 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++) { + ( + IGovernance.InitiativeVoteSnapshot memory initiativeSnapshot, + IGovernance.InitiativeState memory initiativeState, + ) = 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); + + vm.roll(block.number + 1); + + governance_depositLQTY_2(2); + + vm.warp(block.timestamp + 243877); + + vm.roll(block.number + 1); + + governance_allocateLQTY_clamped_single_initiative_2nd_user(0, 1, 0); + + vm.warp(block.timestamp + 403427); + + vm.roll(block.number + 1); + + // SHIFTS the week + // 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(); + + property_BI07(); + } + + // forge test --match-test test_property_sum_of_user_voting_weights_0 -vv + function test_property_sum_of_user_voting_weights_1() public { + vm.warp(block.timestamp + 365090); + + vm.roll(block.number + 1); + + governance_depositLQTY_2(3); + + vm.warp(block.timestamp + 164968); + + vm.roll(block.number + 1); + + governance_depositLQTY(2); + + vm.warp(block.timestamp + 74949); + + vm.roll(block.number + 1); + + governance_allocateLQTY_clamped_single_initiative_2nd_user(0, 2, 0); + + governance_allocateLQTY_clamped_single_initiative(0, 1, 0); + + property_sum_of_user_voting_weights_bounded(); + } +} diff --git a/test/recon/trophies/TrophiesToFoundry.sol b/test/recon/trophies/TrophiesToFoundry.sol index 5a03b2ce..c26a8632 100644 --- a/test/recon/trophies/TrophiesToFoundry.sol +++ b/test/recon/trophies/TrophiesToFoundry.sol @@ -13,46 +13,46 @@ contract TrophiesToFoundry is Test, TargetFunctions, FoundryAsserts { setup(); } - // forge test --match-test test_check_unregisterable_consistecy_0 -vv /// This shows another issue tied to snapshot vs voting /// This state transition will not be possible if you always unregister an initiative /// But can happen if unregistering is skipped - function test_check_unregisterable_consistecy_0() public { - vm.roll(block.number + 1); - vm.warp(block.timestamp + 385918); - governance_depositLQTY(2); + // function test_check_unregisterable_consistecy_0() public { + /// TODO AUDIT Known bug + // vm.roll(block.number + 1); + // vm.warp(block.timestamp + 385918); + // governance_depositLQTY(2); - vm.roll(block.number + 1); - vm.warp(block.timestamp + 300358); - governance_allocateLQTY_clamped_single_initiative(0, 0, 1); + // vm.roll(block.number + 1); + // vm.warp(block.timestamp + 300358); + // governance_allocateLQTY_clamped_single_initiative(0, 0, 1); - vm.roll(block.number + 1); - vm.warp(block.timestamp + 525955); - property_resetting_never_reverts(); + // vm.roll(block.number + 1); + // vm.warp(block.timestamp + 525955); + // property_resetting_never_reverts(); - uint256 state = _getInitiativeStatus(_getDeployedInitiative(0)); - assertEq(state, 5, "Should not be this tbh"); - // check_unregisterable_consistecy(0); - uint16 epoch = _getLastEpochClaim(_getDeployedInitiative(0)); + // uint256 state = _getInitiativeStatus(_getDeployedInitiative(0)); + // assertEq(state, 5, "Should not be this tbh"); + // // check_unregisterable_consistecy(0); + // uint16 epoch = _getLastEpochClaim(_getDeployedInitiative(0)); - console.log(epoch + governance.UNREGISTRATION_AFTER_EPOCHS() < governance.epoch() - 1); + // console.log(epoch + governance.UNREGISTRATION_AFTER_EPOCHS() < governance.epoch() - 1); - vm.warp(block.timestamp + governance.EPOCH_DURATION()); - uint256 newState = _getInitiativeStatus(_getDeployedInitiative(0)); + // vm.warp(block.timestamp + governance.EPOCH_DURATION()); + // uint256 newState = _getInitiativeStatus(_getDeployedInitiative(0)); - uint16 lastEpochClaim = _getLastEpochClaim(_getDeployedInitiative(0)); + // uint16 lastEpochClaim = _getLastEpochClaim(_getDeployedInitiative(0)); - console.log("governance.UNREGISTRATION_AFTER_EPOCHS()", governance.UNREGISTRATION_AFTER_EPOCHS()); - console.log("governance.epoch()", governance.epoch()); + // console.log("governance.UNREGISTRATION_AFTER_EPOCHS()", governance.UNREGISTRATION_AFTER_EPOCHS()); + // console.log("governance.epoch()", governance.epoch()); - console.log(lastEpochClaim + governance.UNREGISTRATION_AFTER_EPOCHS() < governance.epoch() - 1); + // console.log(lastEpochClaim + governance.UNREGISTRATION_AFTER_EPOCHS() < governance.epoch() - 1); - console.log("lastEpochClaim", lastEpochClaim); + // console.log("lastEpochClaim", lastEpochClaim); - assertEq(epoch, lastEpochClaim, "epochs"); - assertEq(newState, state, "??"); - } + // assertEq(epoch, lastEpochClaim, "epochs"); + // assertEq(newState, state, "??"); + // } function _getLastEpochClaim(address _initiative) internal returns (uint16) { (, uint16 epoch,) = governance.getInitiativeState(_initiative);