From 958749fe2689a69e799a219dec7d96fed1b92001 Mon Sep 17 00:00:00 2001 From: Colin Platt Date: Fri, 4 Oct 2024 11:59:05 +0200 Subject: [PATCH 01/93] test for vote amount --- .gitmodules | 3 ++ lib/solmate | 1 + src/Governance.sol | 36 +++++++++++++ test/Governance.t.sol | 116 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 154 insertions(+), 2 deletions(-) create mode 160000 lib/solmate diff --git a/.gitmodules b/.gitmodules index 5e007a6..c54835d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/v4-core"] path = lib/v4-core url = https://github.com/Uniswap/v4-core +[submodule "lib/solmate"] + path = lib/solmate + url = https://github.com/Transmissions11/solmate diff --git a/lib/solmate b/lib/solmate new file mode 160000 index 0000000..97bdb20 --- /dev/null +++ b/lib/solmate @@ -0,0 +1 @@ +Subproject commit 97bdb2003b70382996a79a406813f76417b1cf90 diff --git a/src/Governance.sol b/src/Governance.sol index 2f361b3..94409be 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -16,6 +16,8 @@ import {add, max} from "./utils/Math.sol"; import {Multicall} from "./utils/Multicall.sol"; import {WAD, PermitParams} from "./utils/Types.sol"; +import {SafeCastLib} from "lib/solmate/src/utils/SafeCastLib.sol"; + /// @title Governance: Modular Initiative based Governance contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance { using SafeERC20 for IERC20; @@ -68,6 +70,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @inheritdoc IGovernance mapping(address => uint16) public override registeredInitiatives; + error RegistrationFailed(address initiative); + constructor( address _lqty, address _lusd, @@ -372,6 +376,33 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance try IInitiative(_initiative).onUnregisterInitiative(currentEpoch) {} catch {} } + event log_votes(uint votes); + + function _checkSufficientVotes( + UserState memory userState, + address[] calldata _initiatives, + int176[] calldata _deltaLQTYVotes, + int176[] calldata _deltaLQTYVetos + ) internal returns (bool) { + uint userVotes = lqtyToVotes(userState.allocatedLQTY, block.timestamp, userState.averageStakingTimestamp); + // an allocation can only be made if the user has more voting power (LQTY * age) + + emit log_votes(userVotes); + + uint176 absVote; + uint176 absVeto; + + for (uint256 i = 0; i < _initiatives.length; i++) { + absVote = _deltaLQTYVotes[i] < 0 ? uint176(-_deltaLQTYVotes[i]) : uint176(_deltaLQTYVotes[i]); + absVeto = _deltaLQTYVetos[i] < 0 ? uint176(-_deltaLQTYVetos[i]) : uint176(_deltaLQTYVetos[i]); + if (absVote > userVotes || absVeto > userVotes) { + return false; + } + } + + return true; + } + /// @inheritdoc IGovernance function allocateLQTY( address[] calldata _initiatives, @@ -390,6 +421,11 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance UserState memory userState = userStates[msg.sender]; + require( + _checkSufficientVotes(userState, _initiatives, _deltaLQTYVotes, _deltaLQTYVetos), + "Governance: invalid-votes" + ); + for (uint256 i = 0; i < _initiatives.length; i++) { address initiative = _initiatives[i]; int176 deltaLQTYVotes = _deltaLQTYVotes[i]; diff --git a/test/Governance.t.sol b/test/Governance.t.sol index eb2a555..22af825 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -100,6 +100,7 @@ contract GovernanceTest is Test { initialInitiatives.push(baseInitiative1); initialInitiatives.push(baseInitiative2); + initialInitiatives.push(baseInitiative3); governance = new Governance( address(lqty), @@ -738,7 +739,7 @@ contract GovernanceTest is Test { vm.stopPrank(); } - function test_allocateLQTY() public { + function test_allocateLQTY_single() public { vm.startPrank(user); address userProxy = governance.deployUserProxy(); @@ -754,7 +755,7 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; int176[] memory deltaLQTYVotes = new int176[](1); - deltaLQTYVotes[0] = 1e18; + deltaLQTYVotes[0] = 1e18; //this should be 0 int176[] memory deltaLQTYVetos = new int176[](1); // should revert if the initiative has been registered in the current epoch @@ -1036,6 +1037,82 @@ contract GovernanceTest is Test { vm.stopPrank(); } + // this shouldn't happen + function off_claimForInitiativeEOA() public { + address EOAInitiative = address(0xbeef); + + vm.startPrank(user); + + // deploy + address userProxy = governance.deployUserProxy(); + + lqty.approve(address(userProxy), 1000e18); + governance.depositLQTY(1000e18); + + vm.warp(block.timestamp + 365 days); + + vm.stopPrank(); + + vm.startPrank(lusdHolder); + lusd.transfer(address(governance), 10000e18); + vm.stopPrank(); + + vm.startPrank(user); + + address[] memory initiatives = new address[](2); + initiatives[0] = EOAInitiative; // attempt for an EOA + initiatives[1] = baseInitiative2; + int176[] memory deltaVoteLQTY = new int176[](2); + deltaVoteLQTY[0] = 500e18; + deltaVoteLQTY[1] = 500e18; + int176[] memory deltaVetoLQTY = new int176[](2); + governance.allocateLQTY(initiatives, deltaVoteLQTY, deltaVetoLQTY); + (uint88 allocatedLQTY,) = governance.userStates(user); + assertEq(allocatedLQTY, 1000e18); + + vm.warp(block.timestamp + governance.EPOCH_DURATION() + 1); + + // should compute the claim and transfer it to the initiative + assertEq(governance.claimForInitiative(EOAInitiative), 5000e18); + governance.claimForInitiative(EOAInitiative); + assertEq(governance.claimForInitiative(EOAInitiative), 0); + assertEq(lusd.balanceOf(EOAInitiative), 5000e18); + + assertEq(governance.claimForInitiative(baseInitiative2), 5000e18); + assertEq(governance.claimForInitiative(baseInitiative2), 0); + + assertEq(lusd.balanceOf(baseInitiative2), 5000e18); + + vm.stopPrank(); + + vm.startPrank(lusdHolder); + lusd.transfer(address(governance), 10000e18); + vm.stopPrank(); + + vm.startPrank(user); + + initiatives[0] = EOAInitiative; + initiatives[1] = baseInitiative2; + deltaVoteLQTY[0] = 495e18; + deltaVoteLQTY[1] = -495e18; + governance.allocateLQTY(initiatives, deltaVoteLQTY, deltaVetoLQTY); + + vm.warp(block.timestamp + governance.EPOCH_DURATION() + 1); + + assertEq(governance.claimForInitiative(EOAInitiative), 10000e18); + // should not allow double claiming + assertEq(governance.claimForInitiative(EOAInitiative), 0); + + assertEq(lusd.balanceOf(EOAInitiative), 15000e18); + + assertEq(governance.claimForInitiative(baseInitiative2), 0); + assertEq(governance.claimForInitiative(baseInitiative2), 0); + + assertEq(lusd.balanceOf(baseInitiative2), 5000e18); + + vm.stopPrank(); + } + function test_multicall() public { vm.startPrank(user); @@ -1171,4 +1248,39 @@ contract GovernanceTest is Test { governance.unregisterInitiative(address(mockInitiative)); } + + // CS exploit PoC + function test_allocateLQTY_overflow() public { + vm.startPrank(user); + + address[] memory initiatives = new address[](3); + initiatives[0] = baseInitiative1; + initiatives[1] = baseInitiative3; + initiatives[2] = baseInitiative2; + int176[] memory deltaLQTYVotes = new int176[](3); + deltaLQTYVotes[0] = 154742504910672534362390528; // 2**87 + deltaLQTYVotes[1] = 0; + deltaLQTYVotes[2] = 154742504910672534362390527; // 2**87 - 1 + int176[] memory deltaLQTYVetos = new int176[](3); + deltaLQTYVetos[0] = 0; + deltaLQTYVetos[1] = 1; + deltaLQTYVetos[2] = -309485009821345068724781056; // - 2**88 + + vm.warp(block.timestamp + 365 days); + governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); + + (uint allocatedLQTY,) = governance.userStates(user); + assertEq(allocatedLQTY, 0); + console.log(allocatedLQTY); + + (uint88 voteLQTY1,,,,) = governance.initiativeStates(baseInitiative1); + assertGt(voteLQTY1, 0); + console.log(voteLQTY1); + + (uint88 voteLQTY2,,,,) = governance.initiativeStates(baseInitiative2); + assertGt(voteLQTY2, 0); + console.log(voteLQTY2); + + vm.stopPrank(); + } } From f66f972c0c2912e59dc162b201b06446ed310823 Mon Sep 17 00:00:00 2001 From: jlqty <172397380+jltqy@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:29:55 +0100 Subject: [PATCH 02/93] Restrict deltas to int96 and assert <= 2**88 --- src/Governance.sol | 49 +++++---------------- src/interfaces/IGovernance.sol | 7 +-- src/utils/Math.sol | 10 +++-- test/BribeInitiative.t.sol | 4 +- test/Governance.t.sol | 80 ++++++++++++++-------------------- test/mocks/MockInitiative.sol | 4 +- 6 files changed, 58 insertions(+), 96 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 94409be..408ee33 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; +import {console} from "forge-std/console.sol"; + import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; @@ -12,7 +14,7 @@ import {ILQTYStaking} from "./interfaces/ILQTYStaking.sol"; import {UserProxy} from "./UserProxy.sol"; import {UserProxyFactory} from "./UserProxyFactory.sol"; -import {add, max} from "./utils/Math.sol"; +import {add, max, abs} from "./utils/Math.sol"; import {Multicall} from "./utils/Multicall.sol"; import {WAD, PermitParams} from "./utils/Types.sol"; @@ -376,38 +378,11 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance try IInitiative(_initiative).onUnregisterInitiative(currentEpoch) {} catch {} } - event log_votes(uint votes); - - function _checkSufficientVotes( - UserState memory userState, - address[] calldata _initiatives, - int176[] calldata _deltaLQTYVotes, - int176[] calldata _deltaLQTYVetos - ) internal returns (bool) { - uint userVotes = lqtyToVotes(userState.allocatedLQTY, block.timestamp, userState.averageStakingTimestamp); - // an allocation can only be made if the user has more voting power (LQTY * age) - - emit log_votes(userVotes); - - uint176 absVote; - uint176 absVeto; - - for (uint256 i = 0; i < _initiatives.length; i++) { - absVote = _deltaLQTYVotes[i] < 0 ? uint176(-_deltaLQTYVotes[i]) : uint176(_deltaLQTYVotes[i]); - absVeto = _deltaLQTYVetos[i] < 0 ? uint176(-_deltaLQTYVetos[i]) : uint176(_deltaLQTYVetos[i]); - if (absVote > userVotes || absVeto > userVotes) { - return false; - } - } - - return true; - } - /// @inheritdoc IGovernance function allocateLQTY( address[] calldata _initiatives, - int176[] calldata _deltaLQTYVotes, - int176[] calldata _deltaLQTYVetos + int96[] calldata _deltaLQTYVotes, + int96[] calldata _deltaLQTYVetos ) external nonReentrant { require( _initiatives.length == _deltaLQTYVotes.length && _initiatives.length == _deltaLQTYVetos.length, @@ -421,15 +396,15 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance UserState memory userState = userStates[msg.sender]; - require( - _checkSufficientVotes(userState, _initiatives, _deltaLQTYVotes, _deltaLQTYVetos), - "Governance: invalid-votes" - ); - for (uint256 i = 0; i < _initiatives.length; i++) { address initiative = _initiatives[i]; - int176 deltaLQTYVotes = _deltaLQTYVotes[i]; - int176 deltaLQTYVetos = _deltaLQTYVetos[i]; + int96 deltaLQTYVotes = _deltaLQTYVotes[i]; + int96 deltaLQTYVetos = _deltaLQTYVetos[i]; + + require( + abs(deltaLQTYVotes) <= type(uint88).max && abs(deltaLQTYVetos) <= type(uint88).max, + "Governance: deltas-too-large" + ); // only allow vetoing post the voting cutoff require( diff --git a/src/interfaces/IGovernance.sol b/src/interfaces/IGovernance.sol index 1a4cb51..1453ffd 100644 --- a/src/interfaces/IGovernance.sol +++ b/src/interfaces/IGovernance.sol @@ -246,11 +246,8 @@ interface IGovernance { /// @param _initiatives Addresses of the initiatives to allocate to /// @param _deltaLQTYVotes Delta LQTY to allocate to the initiatives as votes /// @param _deltaLQTYVetos Delta LQTY to allocate to the initiatives as vetos - function allocateLQTY( - address[] memory _initiatives, - int176[] memory _deltaLQTYVotes, - int176[] memory _deltaLQTYVetos - ) external; + function allocateLQTY(address[] memory _initiatives, int96[] memory _deltaLQTYVotes, int96[] memory _deltaLQTYVetos) + external; /// @notice Splits accrued funds according to votes received between all initiatives /// @param _initiative Addresse of the initiative diff --git a/src/utils/Math.sol b/src/utils/Math.sol index 2e1d624..07cb9ba 100644 --- a/src/utils/Math.sol +++ b/src/utils/Math.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -function add(uint88 a, int192 b) pure returns (uint88) { +function add(uint88 a, int96 b) pure returns (uint88) { if (b < 0) { - return uint88(a - uint88(uint192(-b))); + return uint88(a - uint88(uint96(-b))); } - return uint88(a + uint88(uint192(b))); + return uint88(a + uint88(uint96(b))); } function sub(uint256 a, int256 b) pure returns (uint128) { @@ -18,3 +18,7 @@ function sub(uint256 a, int256 b) pure returns (uint128) { function max(uint256 a, uint256 b) pure returns (uint256) { return a > b ? a : b; } + +function abs(int96 a) pure returns (uint96) { + return a < 0 ? uint96(-a) : uint96(a); +} diff --git a/test/BribeInitiative.t.sol b/test/BribeInitiative.t.sol index 6f8df63..703fd40 100644 --- a/test/BribeInitiative.t.sol +++ b/test/BribeInitiative.t.sol @@ -102,9 +102,9 @@ contract BribeInitiativeTest is Test { address[] memory initiatives = new address[](1); initiatives[0] = address(bribeInitiative); - int176[] memory deltaVoteLQTY = new int176[](1); + int96[] memory deltaVoteLQTY = new int96[](1); deltaVoteLQTY[0] = 1e18; - int176[] memory deltaVetoLQTY = new int176[](1); + int96[] memory deltaVetoLQTY = new int96[](1); governance.allocateLQTY(initiatives, deltaVoteLQTY, deltaVetoLQTY); assertEq(bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()), 1e18); assertEq(bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()), 1e18); diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 22af825..800411f 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -100,7 +100,6 @@ contract GovernanceTest is Test { initialInitiatives.push(baseInitiative1); initialInitiatives.push(baseInitiative2); - initialInitiatives.push(baseInitiative3); governance = new Governance( address(lqty), @@ -754,9 +753,9 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int176[] memory deltaLQTYVotes = new int176[](1); + int96[] memory deltaLQTYVotes = new int96[](1); deltaLQTYVotes[0] = 1e18; //this should be 0 - int176[] memory deltaLQTYVetos = new int176[](1); + int96[] memory deltaLQTYVetos = new int96[](1); // should revert if the initiative has been registered in the current epoch vm.expectRevert("Governance: initiative-not-active"); @@ -886,10 +885,10 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int176[] memory deltaLQTYVotes = new int176[](2); + int96[] memory deltaLQTYVotes = new int96[](2); deltaLQTYVotes[0] = 1e18; deltaLQTYVotes[1] = 1e18; - int176[] memory deltaLQTYVetos = new int176[](2); + int96[] memory deltaLQTYVetos = new int96[](2); vm.warp(block.timestamp + 365 days); @@ -929,9 +928,9 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int176[] memory deltaLQTYVotes = new int176[](1); - deltaLQTYVotes[0] = int176(uint176(_deltaLQTYVotes)); - int176[] memory deltaLQTYVetos = new int176[](1); + int96[] memory deltaLQTYVotes = new int96[](1); + deltaLQTYVotes[0] = int96(uint96(_deltaLQTYVotes)); + int96[] memory deltaLQTYVetos = new int96[](1); vm.warp(block.timestamp + 365 days); @@ -953,9 +952,9 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int176[] memory deltaLQTYVotes = new int176[](1); - int176[] memory deltaLQTYVetos = new int176[](1); - deltaLQTYVetos[0] = int176(uint176(_deltaLQTYVetos)); + int96[] memory deltaLQTYVotes = new int96[](1); + int96[] memory deltaLQTYVetos = new int96[](1); + deltaLQTYVetos[0] = int96(uint96(_deltaLQTYVetos)); vm.warp(block.timestamp + 365 days); @@ -986,10 +985,10 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int176[] memory deltaVoteLQTY = new int176[](2); + int96[] memory deltaVoteLQTY = new int96[](2); deltaVoteLQTY[0] = 500e18; deltaVoteLQTY[1] = 500e18; - int176[] memory deltaVetoLQTY = new int176[](2); + int96[] memory deltaVetoLQTY = new int96[](2); governance.allocateLQTY(initiatives, deltaVoteLQTY, deltaVetoLQTY); (uint88 allocatedLQTY,) = governance.userStates(user); assertEq(allocatedLQTY, 1000e18); @@ -1062,10 +1061,10 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](2); initiatives[0] = EOAInitiative; // attempt for an EOA initiatives[1] = baseInitiative2; - int176[] memory deltaVoteLQTY = new int176[](2); + int96[] memory deltaVoteLQTY = new int96[](2); deltaVoteLQTY[0] = 500e18; deltaVoteLQTY[1] = 500e18; - int176[] memory deltaVetoLQTY = new int176[](2); + int96[] memory deltaVetoLQTY = new int96[](2); governance.allocateLQTY(initiatives, deltaVoteLQTY, deltaVetoLQTY); (uint88 allocatedLQTY,) = governance.userStates(user); assertEq(allocatedLQTY, 1000e18); @@ -1126,22 +1125,22 @@ contract GovernanceTest is Test { bytes[] memory data = new bytes[](7); address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int176[] memory deltaVoteLQTY = new int176[](1); - deltaVoteLQTY[0] = int176(uint176(lqtyAmount)); - int176[] memory deltaVetoLQTY = new int176[](1); + int96[] memory deltaVoteLQTY = new int96[](1); + deltaVoteLQTY[0] = int96(uint96(lqtyAmount)); + int96[] memory deltaVetoLQTY = new int96[](1); - int176[] memory deltaVoteLQTY_ = new int176[](1); - deltaVoteLQTY_[0] = -int176(uint176(lqtyAmount)); + int96[] memory deltaVoteLQTY_ = new int96[](1); + deltaVoteLQTY_[0] = -int96(uint96(lqtyAmount)); data[0] = abi.encodeWithSignature("deployUserProxy()"); data[1] = abi.encodeWithSignature("depositLQTY(uint88)", lqtyAmount); data[2] = abi.encodeWithSignature( - "allocateLQTY(address[],int176[],int176[])", initiatives, deltaVoteLQTY, deltaVetoLQTY + "allocateLQTY(address[],int96[],int96[])", initiatives, deltaVoteLQTY, deltaVetoLQTY ); data[3] = abi.encodeWithSignature("userStates(address)", user); data[4] = abi.encodeWithSignature("snapshotVotesForInitiative(address)", baseInitiative1); data[5] = abi.encodeWithSignature( - "allocateLQTY(address[],int176[],int176[])", initiatives, deltaVoteLQTY_, deltaVetoLQTY + "allocateLQTY(address[],int96[],int96[])", initiatives, deltaVoteLQTY_, deltaVetoLQTY ); data[6] = abi.encodeWithSignature("withdrawLQTY(uint88)", lqtyAmount); bytes[] memory response = governance.multicall(data); @@ -1193,8 +1192,8 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](1); initiatives[0] = address(mockInitiative); - int176[] memory deltaLQTYVotes = new int176[](1); - int176[] memory deltaLQTYVetos = new int176[](1); + int96[] memory deltaLQTYVotes = new int96[](1); + int96[] memory deltaLQTYVetos = new int96[](1); governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); // check that votingThreshold is is high enough such that MIN_CLAIM is met @@ -1253,34 +1252,21 @@ contract GovernanceTest is Test { function test_allocateLQTY_overflow() public { vm.startPrank(user); - address[] memory initiatives = new address[](3); + address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; - initiatives[1] = baseInitiative3; - initiatives[2] = baseInitiative2; - int176[] memory deltaLQTYVotes = new int176[](3); - deltaLQTYVotes[0] = 154742504910672534362390528; // 2**87 - deltaLQTYVotes[1] = 0; - deltaLQTYVotes[2] = 154742504910672534362390527; // 2**87 - 1 - int176[] memory deltaLQTYVetos = new int176[](3); - deltaLQTYVetos[0] = 0; - deltaLQTYVetos[1] = 1; - deltaLQTYVetos[2] = -309485009821345068724781056; // - 2**88 + initiatives[1] = baseInitiative2; + + int96[] memory deltaLQTYVotes = new int96[](2); + deltaLQTYVotes[0] = 0; + deltaLQTYVotes[1] = 2 ** 88 - 1; + int96[] memory deltaLQTYVetos = new int96[](2); + deltaLQTYVetos[0] = 1; + deltaLQTYVetos[1] = -(2 ** 88); vm.warp(block.timestamp + 365 days); + vm.expectRevert("Governance: deltas-too-large"); governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); - (uint allocatedLQTY,) = governance.userStates(user); - assertEq(allocatedLQTY, 0); - console.log(allocatedLQTY); - - (uint88 voteLQTY1,,,,) = governance.initiativeStates(baseInitiative1); - assertGt(voteLQTY1, 0); - console.log(voteLQTY1); - - (uint88 voteLQTY2,,,,) = governance.initiativeStates(baseInitiative2); - assertGt(voteLQTY2, 0); - console.log(voteLQTY2); - vm.stopPrank(); } } diff --git a/test/mocks/MockInitiative.sol b/test/mocks/MockInitiative.sol index 8861f42..88d05e7 100644 --- a/test/mocks/MockInitiative.sol +++ b/test/mocks/MockInitiative.sol @@ -24,8 +24,8 @@ contract MockInitiative is IInitiative { /// @inheritdoc IInitiative function onAfterAllocateLQTY(uint16, address, uint88, uint88) external virtual { address[] memory initiatives = new address[](0); - int176[] memory deltaLQTYVotes = new int176[](0); - int176[] memory deltaLQTYVetos = new int176[](0); + int96[] memory deltaLQTYVotes = new int96[](0); + int96[] memory deltaLQTYVetos = new int96[](0); governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); } From f1f741eec6fd4669494c465492390ad3351a59ab Mon Sep 17 00:00:00 2001 From: gallo Date: Thu, 10 Oct 2024 17:51:45 +0200 Subject: [PATCH 03/93] fix: compilation - safeTransfer --- src/ForwardBribe.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ForwardBribe.sol b/src/ForwardBribe.sol index 234c740..8b2be81 100644 --- a/src/ForwardBribe.sol +++ b/src/ForwardBribe.sol @@ -18,7 +18,7 @@ contract ForwardBribe is BribeInitiative { uint boldAmount = bold.balanceOf(address(this)); uint bribeTokenAmount = bribeToken.balanceOf(address(this)); - if (boldAmount != 0) bold.safeTransfer(receiver, boldAmount); - if (bribeTokenAmount != 0) bribeToken.safeTransfer(receiver, bribeTokenAmount); + if (boldAmount != 0) bold.transfer(receiver, boldAmount); + if (bribeTokenAmount != 0) bribeToken.transfer(receiver, bribeTokenAmount); } } From d56007f45cacc9f48ff202a875f9abbee8b23ed9 Mon Sep 17 00:00:00 2001 From: gallo Date: Thu, 10 Oct 2024 17:51:59 +0200 Subject: [PATCH 04/93] feat: introduce `lastEpochClaim` --- src/Governance.sol | 38 ++++++++++++++++++++++++++++++++-- src/interfaces/IGovernance.sol | 4 +++- test/Governance.t.sol | 16 +++++++------- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 2f361b3..98eac0e 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -94,7 +94,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance require(_config.epochVotingCutoff < _config.epochDuration, "Gov: epoch-voting-cutoff-gt-epoch-duration"); EPOCH_VOTING_CUTOFF = _config.epochVotingCutoff; for (uint256 i = 0; i < _initiatives.length; i++) { - initiativeStates[_initiatives[i]] = InitiativeState(0, 0, 0, 0, 0); + initiativeStates[_initiatives[i]] = InitiativeState(0, 0, 0, 0, 0, 0); registeredInitiatives[_initiatives[i]] = 1; } } @@ -298,6 +298,39 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance (initiativeVoteSnapshot,) = _snapshotVotesForInitiative(_initiative); } + + /// @notice Given an initiative, return whether the initiative will be unregisted, whether it can claim and which epoch it last claimed at + function getInitiativeState(address _initiative) public returns (bool mustUnregister, bool canClaimRewards, uint16 lastEpochClaim){ + (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); + (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(_initiative); + + // TODO: Should this be start - 1? + uint256 vetosForInitiative = + lqtyToVotes(initiativeState.vetoLQTY, epochStart(), initiativeState.averageStakingTimestampVetoLQTY); + + // Unregister Condition + // TODO: Figure out `UNREGISTRATION_AFTER_EPOCHS` + /// @audit epoch() - 1 because we can have Now - 1 and that's not a removal case + if((votesForInitiativeSnapshot_.lastCountedEpoch + UNREGISTRATION_AFTER_EPOCHS < epoch() - 1) + || vetosForInitiative > votesForInitiativeSnapshot_.votes + && vetosForInitiative > calculateVotingThreshold() * UNREGISTRATION_THRESHOLD_FACTOR / WAD + ) { + mustUnregister = true; + } + + // How do we know that they have canClaimRewards? + // They must have votes / totalVotes AND meet the Requirement AND not be vetoed + /// @audit if we already are above, then why are we re-computing this? + // Ultimately the checkpoint logic for initiative is fine, so we can skip this + if(votesForInitiativeSnapshot_.votes > 0) { + canClaimRewards = true; + } + + lastEpochClaim = initiativeStates[_initiative].lastEpochClaim; + + // implicit return (mustUnregister, canClaimRewards, lastEpochClaim) + } + /// @inheritdoc IGovernance function registerInitiative(address _initiative) external nonReentrant { bold.safeTransferFrom(msg.sender, address(this), REGISTRATION_FEE); @@ -416,7 +449,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance initiativeState.vetoLQTY, initiativeState.averageStakingTimestampVoteLQTY, initiativeState.averageStakingTimestampVetoLQTY, - initiativeState.counted + initiativeState.counted, + initiativeState.lastEpochClaim ); // update the average staking timestamp for the initiative based on the user's average staking timestamp diff --git a/src/interfaces/IGovernance.sol b/src/interfaces/IGovernance.sol index 1a4cb51..adef06c 100644 --- a/src/interfaces/IGovernance.sol +++ b/src/interfaces/IGovernance.sol @@ -125,6 +125,7 @@ interface IGovernance { uint32 averageStakingTimestampVoteLQTY; // Average staking timestamp of the voting LQTY for the initiative uint32 averageStakingTimestampVetoLQTY; // Average staking timestamp of the vetoing LQTY for the initiative uint16 counted; // Whether votes should be counted in the next snapshot (in 'globalAllocation.countedLQTY') + uint16 lastEpochClaim; } struct GlobalState { @@ -152,7 +153,8 @@ interface IGovernance { uint88 vetoLQTY, uint32 averageStakingTimestampVoteLQTY, uint32 averageStakingTimestampVetoLQTY, - uint16 counted + uint16 counted, + uint16 lastEpochClaim ); /// @notice Returns the global state /// @return countedVoteLQTY Total LQTY that is included in vote counting diff --git a/test/Governance.t.sol b/test/Governance.t.sol index eb2a555..6ff547c 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -692,7 +692,7 @@ contract GovernanceTest is Test { assertEq(countedVoteLQTYAverageTimestamp, block.timestamp); IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState( - 1, 10e18, uint32(block.timestamp - 365 days), uint32(block.timestamp - 365 days), 1 + 1, 10e18, uint32(block.timestamp - 365 days), uint32(block.timestamp - 365 days), 1, 0 ); vm.store( address(governance), @@ -714,7 +714,7 @@ contract GovernanceTest is Test { uint88 vetoLQTY, uint32 averageStakingTimestampVoteLQTY, uint32 averageStakingTimestampVetoLQTY, - uint16 counted + uint16 counted, ) = governance.initiativeStates(baseInitiative3); assertEq(voteLQTY, 1); assertEq(vetoLQTY, 10e18); @@ -727,7 +727,7 @@ contract GovernanceTest is Test { assertEq(governance.registeredInitiatives(baseInitiative3), 0); // should delete the initiative state and the registration timestamp - (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, counted) = + (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, counted, ) = governance.initiativeStates(baseInitiative3); assertEq(voteLQTY, 0); assertEq(vetoLQTY, 0); @@ -772,7 +772,7 @@ contract GovernanceTest is Test { uint88 vetoLQTY, uint32 averageStakingTimestampVoteLQTY, uint32 averageStakingTimestampVetoLQTY, - uint16 counted + uint16 counted, ) = governance.initiativeStates(baseInitiative1); // should update the `voteLQTY` and `vetoLQTY` variables assertEq(voteLQTY, 1e18); @@ -828,7 +828,7 @@ contract GovernanceTest is Test { (allocatedLQTY,) = governance.userStates(user2); assertEq(allocatedLQTY, 1e18); - (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, counted) = + (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, counted, ) = governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 2e18); assertEq(vetoLQTY, 0); @@ -858,7 +858,7 @@ contract GovernanceTest is Test { (countedVoteLQTY,) = governance.globalState(); assertEq(countedVoteLQTY, 1e18); - (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, counted) = + (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, counted, ) = governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); @@ -904,12 +904,12 @@ contract GovernanceTest is Test { uint88 vetoLQTY, uint32 averageStakingTimestampVoteLQTY, uint32 averageStakingTimestampVetoLQTY, - uint16 counted + uint16 counted, ) = governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); - (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, counted) = + (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, counted, ) = governance.initiativeStates(baseInitiative2); assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); From 66f081c6c92173a3dd6bed67272aced5fca11919 Mon Sep 17 00:00:00 2001 From: gallo Date: Thu, 10 Oct 2024 17:59:42 +0200 Subject: [PATCH 05/93] feat: invariant - can only claim or unregister per epoch --- src/Governance.sol | 10 ++++++++-- zzz_TEMP_TO_FIX.MD | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 zzz_TEMP_TO_FIX.MD diff --git a/src/Governance.sol b/src/Governance.sol index 98eac0e..37de68d 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -370,6 +370,9 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(_initiative); + /// Invariant: Must only claim once or unregister + require(initiativeState.lastEpochClaim < epoch() - 1); + uint256 vetosForInitiative = lqtyToVotes(initiativeState.vetoLQTY, block.timestamp, initiativeState.averageStakingTimestampVetoLQTY); @@ -529,14 +532,17 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @inheritdoc IGovernance function claimForInitiative(address _initiative) external nonReentrant returns (uint256) { (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); - (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_,) = _snapshotVotesForInitiative(_initiative); + (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState_) = _snapshotVotesForInitiative(_initiative); + + /// Invariant: Must only claim once or unregister + require(initiativeState_.lastEpochClaim < epoch() - 1); // return 0 if the initiative has no votes if (votesSnapshot_.votes == 0 || votesForInitiativeSnapshot_.votes == 0) return 0; uint256 claim = votesForInitiativeSnapshot_.votes * boldAccrued / votesSnapshot_.votes; - votesForInitiativeSnapshot_.votes = 0; + initiativeStates[_initiative].lastEpochClaim = epoch() - 1; votesForInitiativeSnapshot[_initiative] = votesForInitiativeSnapshot_; // implicitly prevents double claiming bold.safeTransfer(_initiative, claim); diff --git a/zzz_TEMP_TO_FIX.MD b/zzz_TEMP_TO_FIX.MD new file mode 100644 index 0000000..aa28643 --- /dev/null +++ b/zzz_TEMP_TO_FIX.MD @@ -0,0 +1,4 @@ +[FAIL. Reason: EvmError: Revert] test_claimForInitiative() (gas: 835404) + +This test tries to claim more than once per epoch, so it correctly fails +We need to enforce that you can only claim once \ No newline at end of file From bd34a8323387d4a6ce945a0310ab440e9afaecca Mon Sep 17 00:00:00 2001 From: gallo Date: Thu, 10 Oct 2024 17:59:48 +0200 Subject: [PATCH 06/93] fix: test that is broken --- test/Governance.t.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 6ff547c..9e17571 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -1152,6 +1152,8 @@ contract GovernanceTest is Test { governance.claimForInitiative(address(mockInitiative)); + vm.warp(block.timestamp + governance.EPOCH_DURATION()); + initiativeSnapshot = IGovernance.InitiativeVoteSnapshot(0, governance.epoch() - 1, 0); vm.store( address(governance), From f63a5414cf1ef48ceaefe6c11edbaf807e59c5e8 Mon Sep 17 00:00:00 2001 From: gallo Date: Thu, 10 Oct 2024 18:07:13 +0200 Subject: [PATCH 07/93] feat: `getInitiativeState` --- src/Governance.sol | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 37de68d..5ffdae8 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -300,6 +300,13 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @notice Given an initiative, return whether the initiative will be unregisted, whether it can claim and which epoch it last claimed at + /** + FSM: + - Can claim (false, true, epoch - 1 - X) + - Has claimed (false, false, epoch - 1) + - Cannot claim and should not be kicked (false, false, epoch - 1 - [0, X]) + - Should be kicked (true, false, epoch - 1 - [UNREGISTRATION_AFTER_EPOCHS, UNREGISTRATION_AFTER_EPOCHS + X]) + */ function getInitiativeState(address _initiative) public returns (bool mustUnregister, bool canClaimRewards, uint16 lastEpochClaim){ (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(_initiative); @@ -307,6 +314,14 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // TODO: Should this be start - 1? uint256 vetosForInitiative = lqtyToVotes(initiativeState.vetoLQTY, epochStart(), initiativeState.averageStakingTimestampVetoLQTY); + + // TODO: If we the call was already done, we must return false + lastEpochClaim = initiativeStates[_initiative].lastEpochClaim; + + if(lastEpochClaim >= epoch() - 1) { + // early return, we have already claimed + return (false, false, lastEpochClaim); + } // Unregister Condition // TODO: Figure out `UNREGISTRATION_AFTER_EPOCHS` @@ -326,7 +341,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance canClaimRewards = true; } - lastEpochClaim = initiativeStates[_initiative].lastEpochClaim; + // implicit return (mustUnregister, canClaimRewards, lastEpochClaim) } @@ -373,6 +388,10 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// Invariant: Must only claim once or unregister require(initiativeState.lastEpochClaim < epoch() - 1); + (bool mustUnregister, , ) = getInitiativeState(_initiative); + require(mustUnregister, "Governance: cannot-unregister-initiative"); + + uint256 vetosForInitiative = lqtyToVotes(initiativeState.vetoLQTY, block.timestamp, initiativeState.averageStakingTimestampVetoLQTY); @@ -386,7 +405,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance && vetosForInitiative > calculateVotingThreshold() * UNREGISTRATION_THRESHOLD_FACTOR / WAD ), "Governance: cannot-unregister-initiative" - ); + ); /// @audit TODO: Differential review of this vs `mustUnregister` // recalculate the average staking timestamp for all counted voting LQTY if the initiative was counted in if (initiativeState.counted == 1) { @@ -537,6 +556,9 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// Invariant: Must only claim once or unregister require(initiativeState_.lastEpochClaim < epoch() - 1); + (, bool canClaimRewards, ) = getInitiativeState(_initiative); + require(canClaimRewards, "Governance: claim-not-met"); + // return 0 if the initiative has no votes if (votesSnapshot_.votes == 0 || votesForInitiativeSnapshot_.votes == 0) return 0; From a06b94b9316b58fc25ab472af088cb29aee8a9c5 Mon Sep 17 00:00:00 2001 From: gallo Date: Thu, 10 Oct 2024 18:17:16 +0200 Subject: [PATCH 08/93] feat: initial fixes around maintaining storage for disabled proposals --- src/Governance.sol | 7 +++++-- test/Governance.t.sol | 2 +- zzz_TEMP_TO_FIX.MD | 7 ++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 5ffdae8..f49e316 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -420,7 +420,10 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance } delete initiativeStates[_initiative]; - delete registeredInitiatives[_initiative]; + + /// @audit Should not delete this + /// weeks * 2^16 > u32 so the contract will stop working before this is an issue + registeredInitiatives[_initiative] = type(uint16).max; emit UnregisterInitiative(_initiative, currentEpoch); @@ -461,7 +464,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance { uint16 registeredAtEpoch = registeredInitiatives[initiative]; require(currentEpoch > registeredAtEpoch && registeredAtEpoch != 0, "Governance: initiative-not-active"); - } + } /// @audit TODO: We must allow removals for Proposals that are disabled | Should use the flag u16 (, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(initiative); diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 9e17571..44d67ed 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -598,7 +598,7 @@ contract GovernanceTest is Test { // should revert if the initiative isn't registered vm.expectRevert("Governance: initiative-not-registered"); governance.unregisterInitiative(baseInitiative3); - + governance.registerInitiative(baseInitiative3); uint16 atEpoch = governance.registeredInitiatives(baseInitiative3); assertEq(atEpoch, governance.epoch()); diff --git a/zzz_TEMP_TO_FIX.MD b/zzz_TEMP_TO_FIX.MD index aa28643..f0805d8 100644 --- a/zzz_TEMP_TO_FIX.MD +++ b/zzz_TEMP_TO_FIX.MD @@ -1,4 +1,9 @@ [FAIL. Reason: EvmError: Revert] test_claimForInitiative() (gas: 835404) This test tries to claim more than once per epoch, so it correctly fails -We need to enforce that you can only claim once \ No newline at end of file +We need to enforce that you can only claim once + + +[FAIL. Reason: revert: Governance: initiative-already-registered] test_unregisterInitiative() (gas: 559412) + +This rightfully fails because we do not want to re-enable a disabled initiative \ No newline at end of file From 84fc1e83030511e09548bed21df3c8ed52e156a8 Mon Sep 17 00:00:00 2001 From: gallo Date: Fri, 11 Oct 2024 10:27:23 +0200 Subject: [PATCH 09/93] feat: `test_crit_accounting_mismatch`and `test_canAlwaysRemoveAllocation` --- test/Governance.t.sol | 149 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 44d67ed..3fc229d 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -567,6 +567,7 @@ contract GovernanceTest is Test { vm.stopPrank(); } + // TODO: Broken: Fix it by simplifying most likely function test_unregisterInitiative() public { vm.startPrank(user); @@ -738,6 +739,152 @@ contract GovernanceTest is Test { vm.stopPrank(); } + // Test: You can always remove allocation + // forge test --match-test test_crit_accounting_mismatch -vv + function test_crit_accounting_mismatch() public { + // 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; + int176[] memory deltaLQTYVotes = new int176[](2); + deltaLQTYVotes[0] = 1e18; + deltaLQTYVotes[1] = 999e18; + int176[] memory deltaLQTYVetos = new int176[](2); + + governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); + + (uint256 allocatedLQTY,) = governance.userStates(user); + assertEq(allocatedLQTY, 1_000e18); + + ( + uint88 voteLQTY1, + , + uint32 averageStakingTimestampVoteLQTY1, + , + uint16 counted1, + ) = governance.initiativeStates(baseInitiative1); + + ( + uint88 voteLQTY2, + , + , + , + uint16 counted2, + ) = governance.initiativeStates(baseInitiative2); + + // Get power at time of vote + uint256 votingPower = governance.lqtyToVotes(voteLQTY1, block.timestamp, averageStakingTimestampVoteLQTY1); + assertGt(votingPower, 0, "Non zero power"); + + /// @audit TODO Fully digest and explain the bug + // Warp to end so we check the threshold against future threshold + + { + vm.warp(block.timestamp + governance.EPOCH_DURATION()); + + (IGovernance.VoteSnapshot memory snapshot, IGovernance.InitiativeVoteSnapshot memory initiativeVoteSnapshot1) = governance.snapshotVotesForInitiative(baseInitiative1); + (, IGovernance.InitiativeVoteSnapshot memory initiativeVoteSnapshot2) = governance.snapshotVotesForInitiative(baseInitiative2); + + + ( + , + , + , + , + uint16 counted1again, + ) = governance.initiativeStates(baseInitiative1); + assertEq(counted1, 1, "1 is counted inspite below voting"); + assertEq(counted1again, 1, "Counted is true"); + uint256 threshold = governance.calculateVotingThreshold(); + assertEq(initiativeVoteSnapshot1.votes, 0, "it didn't get votes"); + + uint256 votingPowerWithProjection = governance.lqtyToVotes(voteLQTY1, 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"); + + assertEq(counted1, counted2, "both counted"); + } + } + + // TODO: test_canAlwaysRemoveAllocation + // Same setup as above (but no need for bug) + // Show that you cannot withdraw + // forge test --match-test test_canAlwaysRemoveAllocation -vv + function test_canAlwaysRemoveAllocation() public { + // User setup + vm.startPrank(user); + 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; + int176[] memory deltaLQTYVotes = new int176[](2); + deltaLQTYVotes[0] = 1e18; + deltaLQTYVotes[1] = 999e18; + int176[] memory deltaLQTYVetos = new int176[](2); + + governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); + + + // Warp to end so we check the threshold against future threshold + + { + vm.warp(block.timestamp + governance.EPOCH_DURATION()); + + (IGovernance.VoteSnapshot memory snapshot, IGovernance.InitiativeVoteSnapshot memory initiativeVoteSnapshot1) = governance.snapshotVotesForInitiative(baseInitiative1); + + uint256 threshold = governance.calculateVotingThreshold(); + assertEq(initiativeVoteSnapshot1.votes, 0, "it didn't get votes"); + } + + // Roll for + vm.warp(block.timestamp + governance.UNREGISTRATION_AFTER_EPOCHS() * governance.EPOCH_DURATION()); + governance.unregisterInitiative(baseInitiative1); + + // @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 + address[] memory removeInitiatives = new address[](1); + initiatives[0] = baseInitiative1; + int176[] memory removeDeltaLQTYVotes = new int176[](1); + deltaLQTYVotes[0] = -1e18; + int176[] memory removeDeltaLQTYVetos = new int176[](1); + + /// @audit the next call MUST not revert - this is a critical bug + // vm.expectRevert("Governance: initiative-not-active"); + governance.allocateLQTY(removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); + + + address[] memory reAddInitiatives = new address[](1); + initiatives[0] = baseInitiative1; + int176[] memory reAddDeltaLQTYVotes = new int176[](1); + deltaLQTYVotes[0] = -1e18; + int176[] memory reAddDeltaLQTYVetos = new int176[](1); + + /// @audit This MUST revert, an initiative should not be re-votable once disabled + vm.expectRevert("Governance: initiative-not-active"); + governance.allocateLQTY(reAddInitiatives, reAddDeltaLQTYVotes, reAddDeltaLQTYVetos); + } + function test_allocateLQTY() public { vm.startPrank(user); @@ -959,7 +1106,7 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + 365 days); governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); - + /// @audit needs overflow tests!! vm.stopPrank(); } From dcc3dd06d3621b29114ca15e381ecc9aadea5404 Mon Sep 17 00:00:00 2001 From: gallo Date: Fri, 11 Oct 2024 10:37:16 +0200 Subject: [PATCH 10/93] feat: use constant for disabled initiatives --- src/Governance.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Governance.sol b/src/Governance.sol index f49e316..588c625 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -68,6 +68,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @inheritdoc IGovernance mapping(address => uint16) public override registeredInitiatives; + uint16 constant DISABLED_INITIATIVE = type(uint16).max; + constructor( address _lqty, address _lusd, @@ -423,7 +425,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @audit Should not delete this /// weeks * 2^16 > u32 so the contract will stop working before this is an issue - registeredInitiatives[_initiative] = type(uint16).max; + registeredInitiatives[_initiative] = DISABLED_INITIATIVE; emit UnregisterInitiative(_initiative, currentEpoch); From efab2c6929c94b51d9fe17875a1e549814886c90 Mon Sep 17 00:00:00 2001 From: gallo Date: Fri, 11 Oct 2024 11:44:27 +0200 Subject: [PATCH 11/93] chore: cleanup + isolate overflow checks --- test/Governance.t.sol | 73 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 3fc229d..32821eb 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -815,7 +815,6 @@ contract GovernanceTest is Test { } } - // TODO: test_canAlwaysRemoveAllocation // Same setup as above (but no need for bug) // Show that you cannot withdraw // forge test --match-test test_canAlwaysRemoveAllocation -vv @@ -864,20 +863,24 @@ contract GovernanceTest is Test { // CRIT - I want to remove my allocation // I cannot address[] memory removeInitiatives = new address[](1); - initiatives[0] = baseInitiative1; + removeInitiatives[0] = baseInitiative1; int176[] memory removeDeltaLQTYVotes = new int176[](1); - deltaLQTYVotes[0] = -1e18; + removeDeltaLQTYVotes[0] = -1e18; int176[] memory removeDeltaLQTYVetos = new int176[](1); /// @audit the next call MUST not revert - this is a critical bug - // vm.expectRevert("Governance: initiative-not-active"); + governance.allocateLQTY(removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); + + // Security Check | TODO: MORE INVARIANTS + // I should not be able to remove votes again + vm.expectRevert(); // TODO: This is a panic governance.allocateLQTY(removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); address[] memory reAddInitiatives = new address[](1); - initiatives[0] = baseInitiative1; + reAddInitiatives[0] = baseInitiative1; int176[] memory reAddDeltaLQTYVotes = new int176[](1); - deltaLQTYVotes[0] = -1e18; + reAddDeltaLQTYVotes[0] = 1e18; int176[] memory reAddDeltaLQTYVetos = new int176[](1); /// @audit This MUST revert, an initiative should not be re-votable once disabled @@ -885,6 +888,64 @@ contract GovernanceTest is Test { governance.allocateLQTY(reAddInitiatives, reAddDeltaLQTYVotes, reAddDeltaLQTYVetos); } + // Just pass a negative value and see what happens + // forge test --match-test test_overflow_crit -vv + function test_overflow_crit() public { + // 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; + int176[] memory deltaLQTYVotes = new int176[](2); + deltaLQTYVotes[0] = 1e18; + deltaLQTYVotes[1] = 999e18; + int176[] memory deltaLQTYVetos = new int176[](2); + + governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); + (uint88 allocatedB4Test,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + console.log("allocatedB4Test", allocatedB4Test); + + vm.warp(block.timestamp + governance.EPOCH_DURATION()); + vm.warp(block.timestamp + governance.EPOCH_DURATION()); + vm.warp(block.timestamp + governance.EPOCH_DURATION()); + vm.warp(block.timestamp + governance.EPOCH_DURATION()); + + address[] memory removeInitiatives = new address[](1); + removeInitiatives[0] = baseInitiative1; + int176[] memory removeDeltaLQTYVotes = new int176[](1); + removeDeltaLQTYVotes[0] = int176(-1e18); + int176[] memory removeDeltaLQTYVetos = new int176[](1); + + (uint88 allocatedB4Removal,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + console.log("allocatedB4Removal", allocatedB4Removal); + + governance.allocateLQTY(removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); + (uint88 allocatedAfterRemoval,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + console.log("allocatedAfterRemoval", allocatedAfterRemoval); + + vm.expectRevert(); + governance.allocateLQTY(removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); + (uint88 allocatedAfter,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + console.log("allocatedAfter", allocatedAfter); + } + + /// Find some random amount + /// Divide into chunks + /// Ensure chunks above 1 wei + /// Go ahead and remove + /// Ensure that at the end you remove 100% + function test_fuzz_canRemoveExtact() public { + + } + function test_allocateLQTY() public { vm.startPrank(user); From e9d117df6f87e7aceda8d573463ee2fcaaff7509 Mon Sep 17 00:00:00 2001 From: jlqty <172397380+jltqy@users.noreply.github.com> Date: Fri, 11 Oct 2024 13:46:16 +0100 Subject: [PATCH 12/93] Restrict deltas to int88 --- src/Governance.sol | 13 ++----- src/UserProxy.sol | 4 +- src/interfaces/IGovernance.sol | 2 +- src/interfaces/IUserProxy.sol | 2 +- src/utils/Math.sol | 17 +++------ test/BribeInitiative.t.sol | 4 +- test/Governance.t.sol | 70 +++++++++++++++++++--------------- test/mocks/MockInitiative.sol | 4 +- 8 files changed, 56 insertions(+), 60 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 408ee33..e550d71 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -381,8 +381,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @inheritdoc IGovernance function allocateLQTY( address[] calldata _initiatives, - int96[] calldata _deltaLQTYVotes, - int96[] calldata _deltaLQTYVetos + int88[] calldata _deltaLQTYVotes, + int88[] calldata _deltaLQTYVetos ) external nonReentrant { require( _initiatives.length == _deltaLQTYVotes.length && _initiatives.length == _deltaLQTYVetos.length, @@ -398,13 +398,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance for (uint256 i = 0; i < _initiatives.length; i++) { address initiative = _initiatives[i]; - int96 deltaLQTYVotes = _deltaLQTYVotes[i]; - int96 deltaLQTYVetos = _deltaLQTYVetos[i]; - - require( - abs(deltaLQTYVotes) <= type(uint88).max && abs(deltaLQTYVetos) <= type(uint88).max, - "Governance: deltas-too-large" - ); + int88 deltaLQTYVotes = _deltaLQTYVotes[i]; + int88 deltaLQTYVetos = _deltaLQTYVetos[i]; // only allow vetoing post the voting cutoff require( diff --git a/src/UserProxy.sol b/src/UserProxy.sol index 08ae5fb..fee4107 100644 --- a/src/UserProxy.sol +++ b/src/UserProxy.sol @@ -82,8 +82,8 @@ contract UserProxy is IUserProxy { } /// @inheritdoc IUserProxy - function staked() external view returns (uint96) { - return uint96(stakingV1.stakes(address(this))); + function staked() external view returns (uint88) { + return uint88(stakingV1.stakes(address(this))); } receive() external payable {} diff --git a/src/interfaces/IGovernance.sol b/src/interfaces/IGovernance.sol index 1453ffd..6a80dfd 100644 --- a/src/interfaces/IGovernance.sol +++ b/src/interfaces/IGovernance.sol @@ -246,7 +246,7 @@ interface IGovernance { /// @param _initiatives Addresses of the initiatives to allocate to /// @param _deltaLQTYVotes Delta LQTY to allocate to the initiatives as votes /// @param _deltaLQTYVetos Delta LQTY to allocate to the initiatives as vetos - function allocateLQTY(address[] memory _initiatives, int96[] memory _deltaLQTYVotes, int96[] memory _deltaLQTYVetos) + function allocateLQTY(address[] memory _initiatives, int88[] memory _deltaLQTYVotes, int88[] memory _deltaLQTYVetos) external; /// @notice Splits accrued funds according to votes received between all initiatives diff --git a/src/interfaces/IUserProxy.sol b/src/interfaces/IUserProxy.sol index 126812b..bd8c041 100644 --- a/src/interfaces/IUserProxy.sol +++ b/src/interfaces/IUserProxy.sol @@ -47,5 +47,5 @@ interface IUserProxy { returns (uint256 lusdAmount, uint256 ethAmount); /// @notice Returns the current amount LQTY staked by a user in the V1 staking contract /// @return staked Amount of LQTY tokens staked - function staked() external view returns (uint96); + function staked() external view returns (uint88); } diff --git a/src/utils/Math.sol b/src/utils/Math.sol index 07cb9ba..dd60873 100644 --- a/src/utils/Math.sol +++ b/src/utils/Math.sol @@ -1,24 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -function add(uint88 a, int96 b) pure returns (uint88) { +function add(uint88 a, int88 b) pure returns (uint88) { if (b < 0) { - return uint88(a - uint88(uint96(-b))); + return a - abs(b); } - return uint88(a + uint88(uint96(b))); -} - -function sub(uint256 a, int256 b) pure returns (uint128) { - if (b < 0) { - return uint128(a + uint256(-b)); - } - return uint128(a - uint256(b)); + return a + abs(b); } function max(uint256 a, uint256 b) pure returns (uint256) { return a > b ? a : b; } -function abs(int96 a) pure returns (uint96) { - return a < 0 ? uint96(-a) : uint96(a); +function abs(int88 a) pure returns (uint88) { + return a < 0 ? uint88(uint256(-int256(a))) : uint88(a); } diff --git a/test/BribeInitiative.t.sol b/test/BribeInitiative.t.sol index 703fd40..b4bb4e4 100644 --- a/test/BribeInitiative.t.sol +++ b/test/BribeInitiative.t.sol @@ -102,9 +102,9 @@ contract BribeInitiativeTest is Test { address[] memory initiatives = new address[](1); initiatives[0] = address(bribeInitiative); - int96[] memory deltaVoteLQTY = new int96[](1); + int88[] memory deltaVoteLQTY = new int88[](1); deltaVoteLQTY[0] = 1e18; - int96[] memory deltaVetoLQTY = new int96[](1); + int88[] memory deltaVetoLQTY = new int88[](1); governance.allocateLQTY(initiatives, deltaVoteLQTY, deltaVetoLQTY); assertEq(bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()), 1e18); assertEq(bribeInitiative.lqtyAllocatedByUserAtEpoch(user, governance.epoch()), 1e18); diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 800411f..7ac8d88 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -753,9 +753,9 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int96[] memory deltaLQTYVotes = new int96[](1); + int88[] memory deltaLQTYVotes = new int88[](1); deltaLQTYVotes[0] = 1e18; //this should be 0 - int96[] memory deltaLQTYVetos = new int96[](1); + int88[] memory deltaLQTYVetos = new int88[](1); // should revert if the initiative has been registered in the current epoch vm.expectRevert("Governance: initiative-not-active"); @@ -885,10 +885,10 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int96[] memory deltaLQTYVotes = new int96[](2); + int88[] memory deltaLQTYVotes = new int88[](2); deltaLQTYVotes[0] = 1e18; deltaLQTYVotes[1] = 1e18; - int96[] memory deltaLQTYVetos = new int96[](2); + int88[] memory deltaLQTYVetos = new int88[](2); vm.warp(block.timestamp + 365 days); @@ -916,7 +916,7 @@ contract GovernanceTest is Test { } function test_allocateLQTY_fuzz_deltaLQTYVotes(uint88 _deltaLQTYVotes) public { - vm.assume(_deltaLQTYVotes > 0); + vm.assume(_deltaLQTYVotes > 0 && _deltaLQTYVotes < uint88(type(int88).max)); vm.startPrank(user); @@ -928,9 +928,9 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int96[] memory deltaLQTYVotes = new int96[](1); - deltaLQTYVotes[0] = int96(uint96(_deltaLQTYVotes)); - int96[] memory deltaLQTYVetos = new int96[](1); + int88[] memory deltaLQTYVotes = new int88[](1); + deltaLQTYVotes[0] = int88(uint88(_deltaLQTYVotes)); + int88[] memory deltaLQTYVetos = new int88[](1); vm.warp(block.timestamp + 365 days); @@ -940,7 +940,7 @@ contract GovernanceTest is Test { } function test_allocateLQTY_fuzz_deltaLQTYVetos(uint88 _deltaLQTYVetos) public { - vm.assume(_deltaLQTYVetos > 0); + vm.assume(_deltaLQTYVetos > 0 && _deltaLQTYVetos < uint88(type(int88).max)); vm.startPrank(user); @@ -952,9 +952,9 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int96[] memory deltaLQTYVotes = new int96[](1); - int96[] memory deltaLQTYVetos = new int96[](1); - deltaLQTYVetos[0] = int96(uint96(_deltaLQTYVetos)); + int88[] memory deltaLQTYVotes = new int88[](1); + int88[] memory deltaLQTYVetos = new int88[](1); + deltaLQTYVetos[0] = int88(uint88(_deltaLQTYVetos)); vm.warp(block.timestamp + 365 days); @@ -985,10 +985,10 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int96[] memory deltaVoteLQTY = new int96[](2); + int88[] memory deltaVoteLQTY = new int88[](2); deltaVoteLQTY[0] = 500e18; deltaVoteLQTY[1] = 500e18; - int96[] memory deltaVetoLQTY = new int96[](2); + int88[] memory deltaVetoLQTY = new int88[](2); governance.allocateLQTY(initiatives, deltaVoteLQTY, deltaVetoLQTY); (uint88 allocatedLQTY,) = governance.userStates(user); assertEq(allocatedLQTY, 1000e18); @@ -1061,10 +1061,10 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](2); initiatives[0] = EOAInitiative; // attempt for an EOA initiatives[1] = baseInitiative2; - int96[] memory deltaVoteLQTY = new int96[](2); + int88[] memory deltaVoteLQTY = new int88[](2); deltaVoteLQTY[0] = 500e18; deltaVoteLQTY[1] = 500e18; - int96[] memory deltaVetoLQTY = new int96[](2); + int88[] memory deltaVetoLQTY = new int88[](2); governance.allocateLQTY(initiatives, deltaVoteLQTY, deltaVetoLQTY); (uint88 allocatedLQTY,) = governance.userStates(user); assertEq(allocatedLQTY, 1000e18); @@ -1125,22 +1125,22 @@ contract GovernanceTest is Test { bytes[] memory data = new bytes[](7); address[] memory initiatives = new address[](1); initiatives[0] = baseInitiative1; - int96[] memory deltaVoteLQTY = new int96[](1); - deltaVoteLQTY[0] = int96(uint96(lqtyAmount)); - int96[] memory deltaVetoLQTY = new int96[](1); + int88[] memory deltaVoteLQTY = new int88[](1); + deltaVoteLQTY[0] = int88(uint88(lqtyAmount)); + int88[] memory deltaVetoLQTY = new int88[](1); - int96[] memory deltaVoteLQTY_ = new int96[](1); - deltaVoteLQTY_[0] = -int96(uint96(lqtyAmount)); + int88[] memory deltaVoteLQTY_ = new int88[](1); + deltaVoteLQTY_[0] = -int88(uint88(lqtyAmount)); data[0] = abi.encodeWithSignature("deployUserProxy()"); data[1] = abi.encodeWithSignature("depositLQTY(uint88)", lqtyAmount); data[2] = abi.encodeWithSignature( - "allocateLQTY(address[],int96[],int96[])", initiatives, deltaVoteLQTY, deltaVetoLQTY + "allocateLQTY(address[],int88[],int88[])", initiatives, deltaVoteLQTY, deltaVetoLQTY ); data[3] = abi.encodeWithSignature("userStates(address)", user); data[4] = abi.encodeWithSignature("snapshotVotesForInitiative(address)", baseInitiative1); data[5] = abi.encodeWithSignature( - "allocateLQTY(address[],int96[],int96[])", initiatives, deltaVoteLQTY_, deltaVetoLQTY + "allocateLQTY(address[],int88[],int88[])", initiatives, deltaVoteLQTY_, deltaVetoLQTY ); data[6] = abi.encodeWithSignature("withdrawLQTY(uint88)", lqtyAmount); bytes[] memory response = governance.multicall(data); @@ -1192,8 +1192,8 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](1); initiatives[0] = address(mockInitiative); - int96[] memory deltaLQTYVotes = new int96[](1); - int96[] memory deltaLQTYVetos = new int96[](1); + int88[] memory deltaLQTYVotes = new int88[](1); + int88[] memory deltaLQTYVetos = new int88[](1); governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); // check that votingThreshold is is high enough such that MIN_CLAIM is met @@ -1256,15 +1256,23 @@ contract GovernanceTest is Test { initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int96[] memory deltaLQTYVotes = new int96[](2); + int88[] memory deltaLQTYVotes = new int88[](2); deltaLQTYVotes[0] = 0; - deltaLQTYVotes[1] = 2 ** 88 - 1; - int96[] memory deltaLQTYVetos = new int96[](2); - deltaLQTYVetos[0] = 1; - deltaLQTYVetos[1] = -(2 ** 88); + deltaLQTYVotes[1] = type(int88).max; + int88[] memory deltaLQTYVetos = new int88[](2); + deltaLQTYVetos[0] = 0; + deltaLQTYVetos[1] = 0; vm.warp(block.timestamp + 365 days); - vm.expectRevert("Governance: deltas-too-large"); + vm.expectRevert("Governance: insufficient-or-unallocated-lqty"); + governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); + + deltaLQTYVotes[0] = 0; + deltaLQTYVotes[1] = 0; + deltaLQTYVetos[0] = 0; + deltaLQTYVetos[1] = type(int88).max; + + vm.expectRevert("Governance: insufficient-or-unallocated-lqty"); governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); vm.stopPrank(); diff --git a/test/mocks/MockInitiative.sol b/test/mocks/MockInitiative.sol index 88d05e7..e0f5260 100644 --- a/test/mocks/MockInitiative.sol +++ b/test/mocks/MockInitiative.sol @@ -24,8 +24,8 @@ contract MockInitiative is IInitiative { /// @inheritdoc IInitiative function onAfterAllocateLQTY(uint16, address, uint88, uint88) external virtual { address[] memory initiatives = new address[](0); - int96[] memory deltaLQTYVotes = new int96[](0); - int96[] memory deltaLQTYVetos = new int96[](0); + int88[] memory deltaLQTYVotes = new int88[](0); + int88[] memory deltaLQTYVetos = new int88[](0); governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); } From a816b679ae4cd46120e45adbedf45a2c558b890f Mon Sep 17 00:00:00 2001 From: gallo Date: Fri, 11 Oct 2024 16:08:04 +0200 Subject: [PATCH 13/93] feat: FSM is enforced --- src/Governance.sol | 80 +++++++++++++++++++++++++------ test/Governance.t.sol | 106 +++++++----------------------------------- zzz_TEMP_TO_FIX.MD | 10 ++-- 3 files changed, 87 insertions(+), 109 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 588c625..51542a3 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -16,6 +16,9 @@ import {add, max} from "./utils/Math.sol"; import {Multicall} from "./utils/Multicall.sol"; import {WAD, PermitParams} from "./utils/Types.sol"; +// TODO: REMOVE +import {console} from "forge-std/console.sol"; + /// @title Governance: Modular Initiative based Governance contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance { using SafeERC20 for IERC20; @@ -68,7 +71,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @inheritdoc IGovernance mapping(address => uint16) public override registeredInitiatives; - uint16 constant DISABLED_INITIATIVE = type(uint16).max; + uint16 constant UNREGISTERED_INITIATIVE = type(uint16).max; constructor( address _lqty, @@ -313,11 +316,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(_initiative); - // TODO: Should this be start - 1? - uint256 vetosForInitiative = - lqtyToVotes(initiativeState.vetoLQTY, epochStart(), initiativeState.averageStakingTimestampVetoLQTY); - - // TODO: If we the call was already done, we must return false lastEpochClaim = initiativeStates[_initiative].lastEpochClaim; if(lastEpochClaim >= epoch() - 1) { @@ -325,6 +323,18 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance return (false, false, lastEpochClaim); } + // TODO: If a initiative is disabled, we return false and the last epoch claim + if(registeredInitiatives[_initiative] == UNREGISTERED_INITIATIVE) { + return (false, false, lastEpochClaim); + } + + + // TODO: Should this be start - 1? + uint256 vetosForInitiative = + lqtyToVotes(initiativeState.vetoLQTY, epochStart(), initiativeState.averageStakingTimestampVetoLQTY); + + + // Unregister Condition // TODO: Figure out `UNREGISTRATION_AFTER_EPOCHS` /// @audit epoch() - 1 because we can have Now - 1 and that's not a removal case @@ -421,11 +431,12 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance globalState = state; } - delete initiativeStates[_initiative]; + /// @audit removal math causes issues + // delete initiativeStates[_initiative]; /// @audit Should not delete this /// weeks * 2^16 > u32 so the contract will stop working before this is an issue - registeredInitiatives[_initiative] = DISABLED_INITIATIVE; + registeredInitiatives[_initiative] = UNREGISTERED_INITIATIVE; emit UnregisterInitiative(_initiative, currentEpoch); @@ -443,6 +454,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance "Governance: array-length-mismatch" ); + console.log("0"); + (, GlobalState memory state) = _snapshotVotes(); uint256 votingThreshold = calculateVotingThreshold(); @@ -455,20 +468,37 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance int176 deltaLQTYVotes = _deltaLQTYVotes[i]; int176 deltaLQTYVetos = _deltaLQTYVetos[i]; + // TODO: Better assertion + /// Can remove or add + /// But cannot add or remove both + // only allow vetoing post the voting cutoff + console.log("1"); require( deltaLQTYVotes <= 0 || deltaLQTYVotes >= 0 && secondsWithinEpoch() <= EPOCH_VOTING_CUTOFF, "Governance: epoch-voting-cutoff" ); - - // only allow allocations to initiatives that are active - // an initiative becomes active in the epoch after it is registered + { uint16 registeredAtEpoch = registeredInitiatives[initiative]; - require(currentEpoch > registeredAtEpoch && registeredAtEpoch != 0, "Governance: initiative-not-active"); - } /// @audit TODO: We must allow removals for Proposals that are disabled | Should use the flag u16 + if(deltaLQTYVotes > 0 || deltaLQTYVetos > 0) { + require(currentEpoch > registeredAtEpoch && registeredAtEpoch != 0, "Governance: initiative-not-active"); + } /// @audit TODO: We must allow removals for Proposals that are disabled | Should use the flag u16 + + if(registeredAtEpoch == UNREGISTERED_INITIATIVE) { + require(deltaLQTYVotes <= 0 && deltaLQTYVetos <= 0, "Must be a withdrawal"); + } + } + console.log("3"); + // TODO: CHANGE + // Can add if active + // Can remove if inactive + // only allow allocations to initiatives that are active + // an initiative becomes active in the epoch after it is registered + (, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(initiative); + console.log("4"); // deep copy of the initiative's state before the allocation InitiativeState memory prevInitiativeState = InitiativeState( @@ -480,6 +510,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance initiativeState.lastEpochClaim ); + console.log("add(initiativeState.voteLQTY, deltaLQTYVotes)", add(initiativeState.voteLQTY, deltaLQTYVotes)); // update the average staking timestamp for the initiative based on the user's average staking timestamp initiativeState.averageStakingTimestampVoteLQTY = _calculateAverageTimestamp( initiativeState.averageStakingTimestampVoteLQTY, @@ -487,22 +518,27 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance initiativeState.voteLQTY, add(initiativeState.voteLQTY, deltaLQTYVotes) ); + console.log("5"); initiativeState.averageStakingTimestampVetoLQTY = _calculateAverageTimestamp( initiativeState.averageStakingTimestampVetoLQTY, userState.averageStakingTimestamp, initiativeState.vetoLQTY, add(initiativeState.vetoLQTY, deltaLQTYVetos) ); + console.log("6"); // allocate the voting and vetoing LQTY to the initiative initiativeState.voteLQTY = add(initiativeState.voteLQTY, deltaLQTYVotes); initiativeState.vetoLQTY = add(initiativeState.vetoLQTY, deltaLQTYVetos); + console.log("7"); + // determine if the initiative's allocated voting LQTY should be included in the vote count uint240 votesForInitiative = lqtyToVotes(initiativeState.voteLQTY, block.timestamp, initiativeState.averageStakingTimestampVoteLQTY); initiativeState.counted = (votesForInitiative >= votingThreshold) ? 1 : 0; + console.log("8"); // update the initiative's state initiativeStates[initiative] = initiativeState; @@ -526,13 +562,24 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance state.countedVoteLQTY += initiativeState.voteLQTY; } + console.log("9"); + // allocate the voting and vetoing LQTY to the initiative + console.log("B4 lqtyAllocatedByUserToInitiative[msg.sender][initiative]", lqtyAllocatedByUserToInitiative[msg.sender][initiative].voteLQTY); Allocation memory allocation = lqtyAllocatedByUserToInitiative[msg.sender][initiative]; + console.log("allocation.voteLQTY", allocation.voteLQTY); + console.log("deltaLQTYVotes", uint256(int256(deltaLQTYVotes))); allocation.voteLQTY = add(allocation.voteLQTY, deltaLQTYVotes); + console.log("allocation.voteLQTY", allocation.voteLQTY); allocation.vetoLQTY = add(allocation.vetoLQTY, deltaLQTYVetos); + console.log("allocation.vetoLQTY", allocation.vetoLQTY); allocation.atEpoch = currentEpoch; + console.log("allocation.atEpoch", allocation.atEpoch); require(!(allocation.voteLQTY != 0 && allocation.vetoLQTY != 0), "Governance: vote-and-veto"); lqtyAllocatedByUserToInitiative[msg.sender][initiative] = allocation; + console.log("After lqtyAllocatedByUserToInitiative[msg.sender][initiative]", lqtyAllocatedByUserToInitiative[msg.sender][initiative].voteLQTY); + + console.log("10"); userState.allocatedLQTY = add(userState.allocatedLQTY, deltaLQTYVotes + deltaLQTYVetos); @@ -559,10 +606,15 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState_) = _snapshotVotesForInitiative(_initiative); /// Invariant: Must only claim once or unregister - require(initiativeState_.lastEpochClaim < epoch() - 1); + require(initiativeState_.lastEpochClaim < epoch() - 1, "Governance: already-claimed"); /// TODO: Merge into rest + // TODO: We can do a early return instead + // TODO: Return type from state FSM can be standardized (, bool canClaimRewards, ) = getInitiativeState(_initiative); require(canClaimRewards, "Governance: claim-not-met"); + // if(!canClaimRewards) { + // return 0; + // } // return 0 if the initiative has no votes if (votesSnapshot_.votes == 0 || votesForInitiativeSnapshot_.votes == 0) return 0; diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 32821eb..c549444 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -654,91 +654,11 @@ contract GovernanceTest is Test { vm.startPrank(user); lusd.approve(address(governance), 1e18); - + vm.expectRevert("Governance: initiative-already-registered"); governance.registerInitiative(baseInitiative3); - atEpoch = governance.registeredInitiatives(baseInitiative3); - assertEq(atEpoch, governance.epoch()); - - vm.warp(block.timestamp + 365 days); - - initiativeSnapshot = IGovernance.InitiativeVoteSnapshot(1, governance.epoch() - 1, governance.epoch() - 1); - vm.store( - address(governance), - keccak256(abi.encode(baseInitiative3, uint256(3))), - bytes32( - abi.encodePacked( - uint16(initiativeSnapshot.lastCountedEpoch), - uint16(initiativeSnapshot.forEpoch), - uint224(initiativeSnapshot.votes) - ) - ) - ); - (votes_, forEpoch_, lastCountedEpoch) = governance.votesForInitiativeSnapshot(baseInitiative3); - assertEq(votes_, 1); - assertEq(forEpoch_, governance.epoch() - 1); - assertEq(lastCountedEpoch, governance.epoch() - 1); - - IGovernance.GlobalState memory globalState = IGovernance.GlobalState(type(uint88).max, uint32(block.timestamp)); - vm.store( - address(governance), - bytes32(uint256(4)), - bytes32( - abi.encodePacked( - uint136(0), uint32(globalState.countedVoteLQTYAverageTimestamp), uint88(globalState.countedVoteLQTY) - ) - ) - ); - (uint88 countedVoteLQTY, uint32 countedVoteLQTYAverageTimestamp) = governance.globalState(); - assertEq(countedVoteLQTY, type(uint88).max); - assertEq(countedVoteLQTYAverageTimestamp, block.timestamp); - - IGovernance.InitiativeState memory initiativeState = IGovernance.InitiativeState( - 1, 10e18, uint32(block.timestamp - 365 days), uint32(block.timestamp - 365 days), 1, 0 - ); - vm.store( - address(governance), - keccak256(abi.encode(baseInitiative3, uint256(6))), - bytes32( - abi.encodePacked( - uint16(initiativeState.counted), - uint32(initiativeState.averageStakingTimestampVetoLQTY), - uint32(initiativeState.averageStakingTimestampVoteLQTY), - uint88(initiativeState.vetoLQTY), - uint88(initiativeState.voteLQTY) - ) - ) - ); - - // should update the average timestamp for counted lqty if the initiative has been counted in - ( - uint88 voteLQTY, - uint88 vetoLQTY, - uint32 averageStakingTimestampVoteLQTY, - uint32 averageStakingTimestampVetoLQTY, - uint16 counted, - ) = governance.initiativeStates(baseInitiative3); - assertEq(voteLQTY, 1); - assertEq(vetoLQTY, 10e18); - assertEq(averageStakingTimestampVoteLQTY, block.timestamp - 365 days); - assertEq(averageStakingTimestampVetoLQTY, block.timestamp - 365 days); - assertEq(counted, 1); - - governance.unregisterInitiative(baseInitiative3); - - assertEq(governance.registeredInitiatives(baseInitiative3), 0); - - // should delete the initiative state and the registration timestamp - (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, counted, ) = - governance.initiativeStates(baseInitiative3); - assertEq(voteLQTY, 0); - assertEq(vetoLQTY, 0); - assertEq(averageStakingTimestampVoteLQTY, 0); - assertEq(averageStakingTimestampVetoLQTY, 0); - assertEq(counted, 0); - - vm.stopPrank(); } + // Test: You can always remove allocation // forge test --match-test test_crit_accounting_mismatch -vv function test_crit_accounting_mismatch() public { @@ -1205,12 +1125,14 @@ contract GovernanceTest is Test { // should compute the claim and transfer it to the initiative assertEq(governance.claimForInitiative(baseInitiative1), 5000e18); + + vm.expectRevert("Governance: already-claimed"); /// @audit TODO: Decide if we should not revert governance.claimForInitiative(baseInitiative1); - assertEq(governance.claimForInitiative(baseInitiative1), 0); - assertEq(lusd.balanceOf(baseInitiative1), 5000e18); assertEq(governance.claimForInitiative(baseInitiative2), 5000e18); - assertEq(governance.claimForInitiative(baseInitiative2), 0); + + vm.expectRevert("Governance: already-claimed"); + governance.claimForInitiative(baseInitiative2); assertEq(lusd.balanceOf(baseInitiative2), 5000e18); @@ -1231,13 +1153,21 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + governance.EPOCH_DURATION() + 1); assertEq(governance.claimForInitiative(baseInitiative1), 10000e18); - // should not allow double claiming - assertEq(governance.claimForInitiative(baseInitiative1), 0); + + vm.expectRevert("Governance: already-claimed"); + governance.claimForInitiative(baseInitiative1); assertEq(lusd.balanceOf(baseInitiative1), 15000e18); + // TODO: This should most likely either return 0 or we accept the claim not met + /// Claim not met is kind of a weird thing to return tbh + /// @audit THIS FAILS ON PURPOSE + /// TODO: Let's fix this by fixing single claiming + /// Let's decide how to handle 0 rewards case and then decide assertEq(governance.claimForInitiative(baseInitiative2), 0); - assertEq(governance.claimForInitiative(baseInitiative2), 0); + + vm.expectRevert("Governance: already-claimed"); + governance.claimForInitiative(baseInitiative2); assertEq(lusd.balanceOf(baseInitiative2), 5000e18); diff --git a/zzz_TEMP_TO_FIX.MD b/zzz_TEMP_TO_FIX.MD index f0805d8..ab90ca9 100644 --- a/zzz_TEMP_TO_FIX.MD +++ b/zzz_TEMP_TO_FIX.MD @@ -1,9 +1,5 @@ -[FAIL. Reason: EvmError: Revert] test_claimForInitiative() (gas: 835404) +[FAIL. Reason: revert: Governance: claim-not-met] test_claimForInitiative() (gas: 1198986) -This test tries to claim more than once per epoch, so it correctly fails -We need to enforce that you can only claim once +Fails because of Governance: claim-not-met - -[FAIL. Reason: revert: Governance: initiative-already-registered] test_unregisterInitiative() (gas: 559412) - -This rightfully fails because we do not want to re-enable a disabled initiative \ No newline at end of file +TODO: Discuss if we should return 0 in those scenarios \ No newline at end of file From 2c52bc3dfeed20279fc829db43855fde86e3ad54 Mon Sep 17 00:00:00 2001 From: gallo Date: Fri, 11 Oct 2024 16:20:35 +0200 Subject: [PATCH 14/93] chore: remove logs --- src/Governance.sol | 24 ------------------------ test/Governance.t.sol | 1 + 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 51542a3..b2a4b52 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -16,9 +16,6 @@ import {add, max} from "./utils/Math.sol"; import {Multicall} from "./utils/Multicall.sol"; import {WAD, PermitParams} from "./utils/Types.sol"; -// TODO: REMOVE -import {console} from "forge-std/console.sol"; - /// @title Governance: Modular Initiative based Governance contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance { using SafeERC20 for IERC20; @@ -454,8 +451,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance "Governance: array-length-mismatch" ); - console.log("0"); - (, GlobalState memory state) = _snapshotVotes(); uint256 votingThreshold = calculateVotingThreshold(); @@ -473,7 +468,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// But cannot add or remove both // only allow vetoing post the voting cutoff - console.log("1"); require( deltaLQTYVotes <= 0 || deltaLQTYVotes >= 0 && secondsWithinEpoch() <= EPOCH_VOTING_CUTOFF, "Governance: epoch-voting-cutoff" @@ -489,7 +483,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance require(deltaLQTYVotes <= 0 && deltaLQTYVetos <= 0, "Must be a withdrawal"); } } - console.log("3"); // TODO: CHANGE // Can add if active // Can remove if inactive @@ -498,7 +491,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance (, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(initiative); - console.log("4"); // deep copy of the initiative's state before the allocation InitiativeState memory prevInitiativeState = InitiativeState( @@ -510,7 +502,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance initiativeState.lastEpochClaim ); - console.log("add(initiativeState.voteLQTY, deltaLQTYVotes)", add(initiativeState.voteLQTY, deltaLQTYVotes)); // update the average staking timestamp for the initiative based on the user's average staking timestamp initiativeState.averageStakingTimestampVoteLQTY = _calculateAverageTimestamp( initiativeState.averageStakingTimestampVoteLQTY, @@ -518,27 +509,22 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance initiativeState.voteLQTY, add(initiativeState.voteLQTY, deltaLQTYVotes) ); - console.log("5"); initiativeState.averageStakingTimestampVetoLQTY = _calculateAverageTimestamp( initiativeState.averageStakingTimestampVetoLQTY, userState.averageStakingTimestamp, initiativeState.vetoLQTY, add(initiativeState.vetoLQTY, deltaLQTYVetos) ); - console.log("6"); // allocate the voting and vetoing LQTY to the initiative initiativeState.voteLQTY = add(initiativeState.voteLQTY, deltaLQTYVotes); initiativeState.vetoLQTY = add(initiativeState.vetoLQTY, deltaLQTYVetos); - console.log("7"); - // determine if the initiative's allocated voting LQTY should be included in the vote count uint240 votesForInitiative = lqtyToVotes(initiativeState.voteLQTY, block.timestamp, initiativeState.averageStakingTimestampVoteLQTY); initiativeState.counted = (votesForInitiative >= votingThreshold) ? 1 : 0; - console.log("8"); // update the initiative's state initiativeStates[initiative] = initiativeState; @@ -562,24 +548,14 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance state.countedVoteLQTY += initiativeState.voteLQTY; } - console.log("9"); // allocate the voting and vetoing LQTY to the initiative - console.log("B4 lqtyAllocatedByUserToInitiative[msg.sender][initiative]", lqtyAllocatedByUserToInitiative[msg.sender][initiative].voteLQTY); Allocation memory allocation = lqtyAllocatedByUserToInitiative[msg.sender][initiative]; - console.log("allocation.voteLQTY", allocation.voteLQTY); - console.log("deltaLQTYVotes", uint256(int256(deltaLQTYVotes))); allocation.voteLQTY = add(allocation.voteLQTY, deltaLQTYVotes); - console.log("allocation.voteLQTY", allocation.voteLQTY); allocation.vetoLQTY = add(allocation.vetoLQTY, deltaLQTYVetos); - console.log("allocation.vetoLQTY", allocation.vetoLQTY); allocation.atEpoch = currentEpoch; - console.log("allocation.atEpoch", allocation.atEpoch); require(!(allocation.voteLQTY != 0 && allocation.vetoLQTY != 0), "Governance: vote-and-veto"); lqtyAllocatedByUserToInitiative[msg.sender][initiative] = allocation; - console.log("After lqtyAllocatedByUserToInitiative[msg.sender][initiative]", lqtyAllocatedByUserToInitiative[msg.sender][initiative].voteLQTY); - - console.log("10"); userState.allocatedLQTY = add(userState.allocatedLQTY, deltaLQTYVotes + deltaLQTYVetos); diff --git a/test/Governance.t.sol b/test/Governance.t.sol index c549444..9e70832 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -1091,6 +1091,7 @@ contract GovernanceTest is Test { vm.stopPrank(); } + // forge test --match-test test_claimForInitiative -vv function test_claimForInitiative() public { vm.startPrank(user); From 1fee011b734fa1b2b50bf8c30b9dc1cdf4d9917b Mon Sep 17 00:00:00 2001 From: gallo Date: Fri, 11 Oct 2024 16:27:48 +0200 Subject: [PATCH 15/93] feat: return 0 on duplicate claim --- INTEGRATION.MD | 8 ++++++++ src/Governance.sol | 15 ++++++++++----- test/Governance.t.sol | 21 +++++---------------- 3 files changed, 23 insertions(+), 21 deletions(-) create mode 100644 INTEGRATION.MD diff --git a/INTEGRATION.MD b/INTEGRATION.MD new file mode 100644 index 0000000..f2ce9d2 --- /dev/null +++ b/INTEGRATION.MD @@ -0,0 +1,8 @@ +# Risks to integrators + +Somebody could claim on your behalf + +Votes not meeting the threshold may result in 0 rewards + +Claiming more than once will return 0 + diff --git a/src/Governance.sol b/src/Governance.sol index b2a4b52..dd4da50 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -581,13 +581,18 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState_) = _snapshotVotesForInitiative(_initiative); - /// Invariant: Must only claim once or unregister - require(initiativeState_.lastEpochClaim < epoch() - 1, "Governance: already-claimed"); /// TODO: Merge into rest - // TODO: We can do a early return instead - // TODO: Return type from state FSM can be standardized (, bool canClaimRewards, ) = getInitiativeState(_initiative); - require(canClaimRewards, "Governance: claim-not-met"); + + /// @audit Return 0 if we cannot claim + /// INVARIANT: + /// We cannot claim only for 2 reasons: + /// We have already claimed + /// We do not meet the threshold + /// TODO: Enforce this with assertions + if(!canClaimRewards) { + return 0; + } // if(!canClaimRewards) { // return 0; // } diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 9e70832..b224569 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -1126,14 +1126,11 @@ contract GovernanceTest is Test { // should compute the claim and transfer it to the initiative assertEq(governance.claimForInitiative(baseInitiative1), 5000e18); - - vm.expectRevert("Governance: already-claimed"); /// @audit TODO: Decide if we should not revert - governance.claimForInitiative(baseInitiative1); + // 2nd claim = 0 + assertEq(governance.claimForInitiative(baseInitiative1), 0); assertEq(governance.claimForInitiative(baseInitiative2), 5000e18); - - vm.expectRevert("Governance: already-claimed"); - governance.claimForInitiative(baseInitiative2); + assertEq(governance.claimForInitiative(baseInitiative2), 0); assertEq(lusd.balanceOf(baseInitiative2), 5000e18); @@ -1154,21 +1151,13 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + governance.EPOCH_DURATION() + 1); assertEq(governance.claimForInitiative(baseInitiative1), 10000e18); + assertEq(governance.claimForInitiative(baseInitiative1), 0); - vm.expectRevert("Governance: already-claimed"); - governance.claimForInitiative(baseInitiative1); assertEq(lusd.balanceOf(baseInitiative1), 15000e18); - // TODO: This should most likely either return 0 or we accept the claim not met - /// Claim not met is kind of a weird thing to return tbh - /// @audit THIS FAILS ON PURPOSE - /// TODO: Let's fix this by fixing single claiming - /// Let's decide how to handle 0 rewards case and then decide assertEq(governance.claimForInitiative(baseInitiative2), 0); - - vm.expectRevert("Governance: already-claimed"); - governance.claimForInitiative(baseInitiative2); + assertEq(governance.claimForInitiative(baseInitiative2), 0); assertEq(lusd.balanceOf(baseInitiative2), 5000e18); From 838b48c36b0398ef398967af729647b48a775fc0 Mon Sep 17 00:00:00 2001 From: gallo Date: Fri, 11 Oct 2024 16:28:52 +0200 Subject: [PATCH 16/93] chore: re-order functions --- src/Governance.sol | 114 ++++++++++++++++++++++----------------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index dd4da50..6ff1f9d 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -383,63 +383,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance try IInitiative(_initiative).onRegisterInitiative(currentEpoch) {} catch {} } - /// @inheritdoc IGovernance - function unregisterInitiative(address _initiative) external nonReentrant { - uint16 registrationEpoch = registeredInitiatives[_initiative]; - require(registrationEpoch != 0, "Governance: initiative-not-registered"); - uint16 currentEpoch = epoch(); - require(registrationEpoch + REGISTRATION_WARM_UP_PERIOD < currentEpoch, "Governance: initiative-in-warm-up"); - - (, GlobalState memory state) = _snapshotVotes(); - (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = - _snapshotVotesForInitiative(_initiative); - - /// Invariant: Must only claim once or unregister - require(initiativeState.lastEpochClaim < epoch() - 1); - - (bool mustUnregister, , ) = getInitiativeState(_initiative); - require(mustUnregister, "Governance: cannot-unregister-initiative"); - - - uint256 vetosForInitiative = - lqtyToVotes(initiativeState.vetoLQTY, block.timestamp, initiativeState.averageStakingTimestampVetoLQTY); - - // an initiative can be unregistered if it has no votes and has been inactive for 'UNREGISTRATION_AFTER_EPOCHS' - // epochs or if it has received more vetos than votes and the vetos are more than - // 'UNREGISTRATION_THRESHOLD_FACTOR' times the voting threshold - require( - (votesForInitiativeSnapshot_.lastCountedEpoch + UNREGISTRATION_AFTER_EPOCHS < currentEpoch) - || ( - vetosForInitiative > votesForInitiativeSnapshot_.votes - && vetosForInitiative > calculateVotingThreshold() * UNREGISTRATION_THRESHOLD_FACTOR / WAD - ), - "Governance: cannot-unregister-initiative" - ); /// @audit TODO: Differential review of this vs `mustUnregister` - - // recalculate the average staking timestamp for all counted voting LQTY if the initiative was counted in - if (initiativeState.counted == 1) { - state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( - state.countedVoteLQTYAverageTimestamp, - initiativeState.averageStakingTimestampVoteLQTY, - state.countedVoteLQTY, - state.countedVoteLQTY - initiativeState.voteLQTY - ); - state.countedVoteLQTY -= initiativeState.voteLQTY; - globalState = state; - } - - /// @audit removal math causes issues - // delete initiativeStates[_initiative]; - - /// @audit Should not delete this - /// weeks * 2^16 > u32 so the contract will stop working before this is an issue - registeredInitiatives[_initiative] = UNREGISTERED_INITIATIVE; - - emit UnregisterInitiative(_initiative, currentEpoch); - - try IInitiative(_initiative).onUnregisterInitiative(currentEpoch) {} catch {} - } - /// @inheritdoc IGovernance function allocateLQTY( address[] calldata _initiatives, @@ -576,6 +519,63 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance userStates[msg.sender] = userState; } + /// @inheritdoc IGovernance + function unregisterInitiative(address _initiative) external nonReentrant { + uint16 registrationEpoch = registeredInitiatives[_initiative]; + require(registrationEpoch != 0, "Governance: initiative-not-registered"); + uint16 currentEpoch = epoch(); + require(registrationEpoch + REGISTRATION_WARM_UP_PERIOD < currentEpoch, "Governance: initiative-in-warm-up"); + + (, GlobalState memory state) = _snapshotVotes(); + (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = + _snapshotVotesForInitiative(_initiative); + + /// Invariant: Must only claim once or unregister + require(initiativeState.lastEpochClaim < epoch() - 1); + + (bool mustUnregister, , ) = getInitiativeState(_initiative); + require(mustUnregister, "Governance: cannot-unregister-initiative"); + + + uint256 vetosForInitiative = + lqtyToVotes(initiativeState.vetoLQTY, block.timestamp, initiativeState.averageStakingTimestampVetoLQTY); + + // an initiative can be unregistered if it has no votes and has been inactive for 'UNREGISTRATION_AFTER_EPOCHS' + // epochs or if it has received more vetos than votes and the vetos are more than + // 'UNREGISTRATION_THRESHOLD_FACTOR' times the voting threshold + require( + (votesForInitiativeSnapshot_.lastCountedEpoch + UNREGISTRATION_AFTER_EPOCHS < currentEpoch) + || ( + vetosForInitiative > votesForInitiativeSnapshot_.votes + && vetosForInitiative > calculateVotingThreshold() * UNREGISTRATION_THRESHOLD_FACTOR / WAD + ), + "Governance: cannot-unregister-initiative" + ); /// @audit TODO: Differential review of this vs `mustUnregister` + + // recalculate the average staking timestamp for all counted voting LQTY if the initiative was counted in + if (initiativeState.counted == 1) { + state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( + state.countedVoteLQTYAverageTimestamp, + initiativeState.averageStakingTimestampVoteLQTY, + state.countedVoteLQTY, + state.countedVoteLQTY - initiativeState.voteLQTY + ); + state.countedVoteLQTY -= initiativeState.voteLQTY; + globalState = state; + } + + /// @audit removal math causes issues + // delete initiativeStates[_initiative]; + + /// @audit Should not delete this + /// weeks * 2^16 > u32 so the contract will stop working before this is an issue + registeredInitiatives[_initiative] = UNREGISTERED_INITIATIVE; + + emit UnregisterInitiative(_initiative, currentEpoch); + + try IInitiative(_initiative).onUnregisterInitiative(currentEpoch) {} catch {} + } + /// @inheritdoc IGovernance function claimForInitiative(address _initiative) external nonReentrant returns (uint256) { (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); From 7f66112e3c8a170d62d8987509f450e0c4be1607 Mon Sep 17 00:00:00 2001 From: gallo Date: Fri, 11 Oct 2024 16:47:06 +0200 Subject: [PATCH 17/93] feat: enum initiative status and improvement to `getInitiativeState` --- src/Governance.sol | 65 ++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 6ff1f9d..8d31bc5 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -302,6 +302,13 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @notice Given an initiative, return whether the initiative will be unregisted, whether it can claim and which epoch it last claimed at + enum InitiativeStatus { + SKIP, /// This epoch will result in no rewards and no unregistering + CLAIMABLE, /// This epoch will result in claiming rewards + CLAIMED, /// The rewards for this epoch have been claimed + UNREGISTERABLE, /// Can be unregistered + DISABLED // It was already Unregistered + } /** FSM: - Can claim (false, true, epoch - 1 - X) @@ -309,7 +316,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance - Cannot claim and should not be kicked (false, false, epoch - 1 - [0, X]) - Should be kicked (true, false, epoch - 1 - [UNREGISTRATION_AFTER_EPOCHS, UNREGISTRATION_AFTER_EPOCHS + X]) */ - function getInitiativeState(address _initiative) public returns (bool mustUnregister, bool canClaimRewards, uint16 lastEpochClaim){ + + function getInitiativeState(address _initiative) public returns (InitiativeStatus status, uint16 lastEpochClaim, uint256 claimableAmount) { (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(_initiative); @@ -317,21 +325,20 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance if(lastEpochClaim >= epoch() - 1) { // early return, we have already claimed - return (false, false, lastEpochClaim); + return (InitiativeStatus.CLAIMED, lastEpochClaim, claimableAmount); } // TODO: If a initiative is disabled, we return false and the last epoch claim if(registeredInitiatives[_initiative] == UNREGISTERED_INITIATIVE) { - return (false, false, lastEpochClaim); + return (InitiativeStatus.DISABLED, lastEpochClaim, 0); /// @audit By definition it must have zero rewards } - // TODO: Should this be start - 1? + // TODO: Should this be start - 1? | QA at most uint256 vetosForInitiative = lqtyToVotes(initiativeState.vetoLQTY, epochStart(), initiativeState.averageStakingTimestampVetoLQTY); - // Unregister Condition // TODO: Figure out `UNREGISTRATION_AFTER_EPOCHS` /// @audit epoch() - 1 because we can have Now - 1 and that's not a removal case @@ -339,20 +346,29 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance || vetosForInitiative > votesForInitiativeSnapshot_.votes && vetosForInitiative > calculateVotingThreshold() * UNREGISTRATION_THRESHOLD_FACTOR / WAD ) { - mustUnregister = true; + return (InitiativeStatus.UNREGISTERABLE, lastEpochClaim, 0); } // How do we know that they have canClaimRewards? // They must have votes / totalVotes AND meet the Requirement AND not be vetoed /// @audit if we already are above, then why are we re-computing this? // Ultimately the checkpoint logic for initiative is fine, so we can skip this - if(votesForInitiativeSnapshot_.votes > 0) { - canClaimRewards = true; - } - + /// @audit TODO: Add Votes vs Vetos + // For now the code always returns Votes iif votes > vetos, so we can trust it + + uint256 claim; + + if (votesSnapshot_.votes == 0 || votesForInitiativeSnapshot_.votes == 0) { + claim = 0; + return (InitiativeStatus.SKIP, lastEpochClaim, 0); + } else { + claim = votesForInitiativeSnapshot_.votes * boldAccrued / votesSnapshot_.votes; + return (InitiativeStatus.CLAIMABLE, lastEpochClaim, claim); + } - // implicit return (mustUnregister, canClaimRewards, lastEpochClaim) + /// Unrecheable state, we should be covering all possible states + assert(false); } /// @inheritdoc IGovernance @@ -532,9 +548,9 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// Invariant: Must only claim once or unregister require(initiativeState.lastEpochClaim < epoch() - 1); - - (bool mustUnregister, , ) = getInitiativeState(_initiative); - require(mustUnregister, "Governance: cannot-unregister-initiative"); + + (InitiativeStatus status, , )= getInitiativeState(_initiative); + require(status == InitiativeStatus.UNREGISTERABLE, "Governance: cannot-unregister-initiative"); uint256 vetosForInitiative = @@ -582,7 +598,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState_) = _snapshotVotesForInitiative(_initiative); // TODO: Return type from state FSM can be standardized - (, bool canClaimRewards, ) = getInitiativeState(_initiative); + (InitiativeStatus status, , uint256 claimableAmount )= getInitiativeState(_initiative); /// @audit Return 0 if we cannot claim /// INVARIANT: @@ -590,27 +606,20 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// We have already claimed /// We do not meet the threshold /// TODO: Enforce this with assertions - if(!canClaimRewards) { + if(status != InitiativeStatus.CLAIMABLE) { return 0; } - // if(!canClaimRewards) { - // return 0; - // } - - // return 0 if the initiative has no votes - if (votesSnapshot_.votes == 0 || votesForInitiativeSnapshot_.votes == 0) return 0; - - uint256 claim = votesForInitiativeSnapshot_.votes * boldAccrued / votesSnapshot_.votes; + initiativeStates[_initiative].lastEpochClaim = epoch() - 1; votesForInitiativeSnapshot[_initiative] = votesForInitiativeSnapshot_; // implicitly prevents double claiming - bold.safeTransfer(_initiative, claim); + bold.safeTransfer(_initiative, claimableAmount); - emit ClaimForInitiative(_initiative, claim, votesSnapshot_.forEpoch); + emit ClaimForInitiative(_initiative, claimableAmount, votesSnapshot_.forEpoch); - try IInitiative(_initiative).onClaimForInitiative(votesSnapshot_.forEpoch, claim) {} catch {} + try IInitiative(_initiative).onClaimForInitiative(votesSnapshot_.forEpoch, claimableAmount) {} catch {} - return claim; + return claimableAmount; } } From 9e8fd9e5493d2c72aeaa9754c656add31376de98 Mon Sep 17 00:00:00 2001 From: gallo Date: Fri, 11 Oct 2024 16:48:35 +0200 Subject: [PATCH 18/93] feat: snapshot asserts --- INTEGRATION.MD | 4 ++++ src/Governance.sol | 2 ++ 2 files changed, 6 insertions(+) diff --git a/INTEGRATION.MD b/INTEGRATION.MD index f2ce9d2..f582012 100644 --- a/INTEGRATION.MD +++ b/INTEGRATION.MD @@ -6,3 +6,7 @@ Votes not meeting the threshold may result in 0 rewards Claiming more than once will return 0 +## INVARIANT: You can only claim for previous epoch + +assert(votesSnapshot_.forEpoch == epoch() - 1); /// @audit INVARIANT: You can only claim for previous epoch +/// All unclaimed rewards are always recycled \ No newline at end of file diff --git a/src/Governance.sol b/src/Governance.sol index 8d31bc5..5303a8d 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -610,6 +610,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance return 0; } + assert(votesSnapshot_.forEpoch == epoch() - 1); /// @audit INVARIANT: You can only claim for previous epoch + /// All unclaimed rewards are always recycled initiativeStates[_initiative].lastEpochClaim = epoch() - 1; votesForInitiativeSnapshot[_initiative] = votesForInitiativeSnapshot_; // implicitly prevents double claiming From e4ccbc010cf271bc3d64257f7466b21e2b569a83 Mon Sep 17 00:00:00 2001 From: gallo Date: Fri, 11 Oct 2024 17:16:04 +0200 Subject: [PATCH 19/93] chore: future notes --- src/Governance.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Governance.sol b/src/Governance.sol index 5303a8d..00c718b 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -279,7 +279,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance lqtyToVotes(initiativeState.vetoLQTY, start, initiativeState.averageStakingTimestampVetoLQTY); // if the votes didn't meet the voting threshold then no votes qualify if (votes >= votingThreshold && votes >= vetos) { - initiativeSnapshot.votes = uint224(votes); + initiativeSnapshot.votes = uint224(votes); /// @audit TODO: We should change this to check the treshold, we should instead use the snapshot to just report all the valid data initiativeSnapshot.lastCountedEpoch = currentEpoch - 1; } else { initiativeSnapshot.votes = 0; From 480c0772a9ee267139bcb4d331796206304548f6 Mon Sep 17 00:00:00 2001 From: gallo Date: Fri, 11 Oct 2024 17:24:31 +0200 Subject: [PATCH 20/93] feat: counted is gone --- src/Governance.sol | 54 ++++++++++++++++++++----------------------- test/Governance.t.sol | 7 ++++-- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 00c718b..64a9224 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -482,30 +482,28 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // determine if the initiative's allocated voting LQTY should be included in the vote count uint240 votesForInitiative = lqtyToVotes(initiativeState.voteLQTY, block.timestamp, initiativeState.averageStakingTimestampVoteLQTY); - initiativeState.counted = (votesForInitiative >= votingThreshold) ? 1 : 0; + initiativeState.counted = 1; /// TODO: Remove counted and change tests // update the initiative's state initiativeStates[initiative] = initiativeState; // update the average staking timestamp for all counted voting LQTY - if (prevInitiativeState.counted == 1) { - state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( - state.countedVoteLQTYAverageTimestamp, - initiativeState.averageStakingTimestampVoteLQTY, - state.countedVoteLQTY, - state.countedVoteLQTY - prevInitiativeState.voteLQTY - ); - state.countedVoteLQTY -= prevInitiativeState.voteLQTY; - } - if (initiativeState.counted == 1) { - 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 - prevInitiativeState.voteLQTY + ); + state.countedVoteLQTY -= prevInitiativeState.voteLQTY; + + state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( + state.countedVoteLQTYAverageTimestamp, + initiativeState.averageStakingTimestampVoteLQTY, + state.countedVoteLQTY, + state.countedVoteLQTY + initiativeState.voteLQTY + ); + state.countedVoteLQTY += initiativeState.voteLQTY; + // allocate the voting and vetoing LQTY to the initiative @@ -569,16 +567,14 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance ); /// @audit TODO: Differential review of this vs `mustUnregister` // recalculate the average staking timestamp for all counted voting LQTY if the initiative was counted in - if (initiativeState.counted == 1) { - state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( - state.countedVoteLQTYAverageTimestamp, - initiativeState.averageStakingTimestampVoteLQTY, - state.countedVoteLQTY, - state.countedVoteLQTY - initiativeState.voteLQTY - ); - state.countedVoteLQTY -= initiativeState.voteLQTY; - globalState = state; - } + state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( + state.countedVoteLQTYAverageTimestamp, + initiativeState.averageStakingTimestampVoteLQTY, + state.countedVoteLQTY, + state.countedVoteLQTY - initiativeState.voteLQTY + ); + state.countedVoteLQTY -= initiativeState.voteLQTY; + globalState = state; /// @audit removal math causes issues // delete initiativeStates[_initiative]; diff --git a/test/Governance.t.sol b/test/Governance.t.sol index b224569..c326b30 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -1150,11 +1150,14 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + governance.EPOCH_DURATION() + 1); - assertEq(governance.claimForInitiative(baseInitiative1), 10000e18); + /// @audit this fails, because by counting 100% of votes, the ones that don't make it steal the yield + /// This is MED at most, in this test a 50 BPS loss + /// Due to this, we'll acknowledge it for now + assertEq(governance.claimForInitiative(baseInitiative1), 9950e18); assertEq(governance.claimForInitiative(baseInitiative1), 0); - assertEq(lusd.balanceOf(baseInitiative1), 15000e18); + assertEq(lusd.balanceOf(baseInitiative1), 14950e18); assertEq(governance.claimForInitiative(baseInitiative2), 0); assertEq(governance.claimForInitiative(baseInitiative2), 0); From ac4d1eb8773223a0d3cb13bd97992509326039d0 Mon Sep 17 00:00:00 2001 From: gallo Date: Fri, 11 Oct 2024 17:27:41 +0200 Subject: [PATCH 21/93] feat: counted is gone --- src/Governance.sol | 4 +--- src/interfaces/IGovernance.sol | 4 +--- test/Governance.t.sol | 33 ++++++++++++--------------------- 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 64a9224..b716ce7 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -96,7 +96,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance require(_config.epochVotingCutoff < _config.epochDuration, "Gov: epoch-voting-cutoff-gt-epoch-duration"); EPOCH_VOTING_CUTOFF = _config.epochVotingCutoff; for (uint256 i = 0; i < _initiatives.length; i++) { - initiativeStates[_initiatives[i]] = InitiativeState(0, 0, 0, 0, 0, 0); + initiativeStates[_initiatives[i]] = InitiativeState(0, 0, 0, 0, 0); registeredInitiatives[_initiatives[i]] = 1; } } @@ -457,7 +457,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance initiativeState.vetoLQTY, initiativeState.averageStakingTimestampVoteLQTY, initiativeState.averageStakingTimestampVetoLQTY, - initiativeState.counted, initiativeState.lastEpochClaim ); @@ -482,7 +481,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // determine if the initiative's allocated voting LQTY should be included in the vote count uint240 votesForInitiative = lqtyToVotes(initiativeState.voteLQTY, block.timestamp, initiativeState.averageStakingTimestampVoteLQTY); - initiativeState.counted = 1; /// TODO: Remove counted and change tests // update the initiative's state initiativeStates[initiative] = initiativeState; diff --git a/src/interfaces/IGovernance.sol b/src/interfaces/IGovernance.sol index adef06c..37f5bbb 100644 --- a/src/interfaces/IGovernance.sol +++ b/src/interfaces/IGovernance.sol @@ -124,7 +124,6 @@ interface IGovernance { 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 - uint16 counted; // Whether votes should be counted in the next snapshot (in 'globalAllocation.countedLQTY') uint16 lastEpochClaim; } @@ -144,7 +143,7 @@ interface IGovernance { /// @return vetoLQTY LQTY allocated vetoing the initiative /// @return averageStakingTimestampVoteLQTY // Average staking timestamp of the voting LQTY for the initiative /// @return averageStakingTimestampVetoLQTY // Average staking timestamp of the vetoing LQTY for the initiative - /// @return counted // Whether votes should be counted in the next snapshot (in 'globalAllocation.countedLQTY') + /// @return lastEpochClaim // Last epoch at which rewards were claimed function initiativeStates(address _initiative) external view @@ -153,7 +152,6 @@ interface IGovernance { uint88 vetoLQTY, uint32 averageStakingTimestampVoteLQTY, uint32 averageStakingTimestampVetoLQTY, - uint16 counted, uint16 lastEpochClaim ); /// @notice Returns the global state diff --git a/test/Governance.t.sol b/test/Governance.t.sol index c326b30..49235a8 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -690,7 +690,6 @@ contract GovernanceTest is Test { , uint32 averageStakingTimestampVoteLQTY1, , - uint16 counted1, ) = governance.initiativeStates(baseInitiative1); ( @@ -698,7 +697,6 @@ contract GovernanceTest is Test { , , , - uint16 counted2, ) = governance.initiativeStates(baseInitiative2); // Get power at time of vote @@ -715,15 +713,9 @@ contract GovernanceTest is Test { (, IGovernance.InitiativeVoteSnapshot memory initiativeVoteSnapshot2) = governance.snapshotVotesForInitiative(baseInitiative2); - ( - , - , - , - , - uint16 counted1again, - ) = governance.initiativeStates(baseInitiative1); - assertEq(counted1, 1, "1 is counted inspite below voting"); - assertEq(counted1again, 1, "Counted is true"); + /// @audit TODO: No longer counted + // assertEq(counted1, 1, "1 is counted inspite below voting"); + // assertEq(counted1again, 1, "Counted is true"); uint256 threshold = governance.calculateVotingThreshold(); assertEq(initiativeVoteSnapshot1.votes, 0, "it didn't get votes"); @@ -731,7 +723,7 @@ contract GovernanceTest is Test { assertLt(votingPower, threshold, "Current Power is not enough - Desynch A"); assertLt(votingPowerWithProjection, threshold, "Future Power is also not enough - Desynch B"); - assertEq(counted1, counted2, "both counted"); + // assertEq(counted1, counted2, "both counted"); } } @@ -899,8 +891,8 @@ contract GovernanceTest is Test { uint88 voteLQTY, uint88 vetoLQTY, uint32 averageStakingTimestampVoteLQTY, - uint32 averageStakingTimestampVetoLQTY, - uint16 counted, + uint32 averageStakingTimestampVetoLQTY + , ) = governance.initiativeStates(baseInitiative1); // should update the `voteLQTY` and `vetoLQTY` variables assertEq(voteLQTY, 1e18); @@ -911,7 +903,7 @@ contract GovernanceTest is Test { assertEq(averageStakingTimestampVoteLQTY, averageStakingTimestampUser); assertEq(averageStakingTimestampVetoLQTY, 0); // should remove or add the initiatives voting LQTY from the counter - assertEq(counted, 1); + (countedVoteLQTY,) = governance.globalState(); assertEq(countedVoteLQTY, 1e18); @@ -956,14 +948,14 @@ contract GovernanceTest is Test { (allocatedLQTY,) = governance.userStates(user2); assertEq(allocatedLQTY, 1e18); - (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, counted, ) = + (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, ) = governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 2e18); assertEq(vetoLQTY, 0); assertEq(averageStakingTimestampVoteLQTY, block.timestamp - 365 days); assertGt(averageStakingTimestampVoteLQTY, averageStakingTimestampUser); assertEq(averageStakingTimestampVetoLQTY, 0); - assertEq(counted, 1); + // should revert if the user doesn't have enough unallocated LQTY available vm.expectRevert("Governance: insufficient-unallocated-lqty"); @@ -986,13 +978,13 @@ contract GovernanceTest is Test { (countedVoteLQTY,) = governance.globalState(); assertEq(countedVoteLQTY, 1e18); - (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, counted, ) = + (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, ) = governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); assertEq(averageStakingTimestampVoteLQTY, averageStakingTimestampUser); assertEq(averageStakingTimestampVetoLQTY, 0); - assertEq(counted, 1); + vm.stopPrank(); } @@ -1032,12 +1024,11 @@ contract GovernanceTest is Test { uint88 vetoLQTY, uint32 averageStakingTimestampVoteLQTY, uint32 averageStakingTimestampVetoLQTY, - uint16 counted, ) = governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); - (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, counted, ) = + (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, ) = governance.initiativeStates(baseInitiative2); assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); From 9c9f77c43185590690b7f9bbe9a2dfb638416ddf Mon Sep 17 00:00:00 2001 From: gallo Date: Sat, 12 Oct 2024 14:33:49 +0200 Subject: [PATCH 22/93] feat: introduce vetos as part of snapshot storage --- src/Governance.sol | 3 ++- src/interfaces/IGovernance.sol | 3 ++- test/Governance.t.sol | 14 +++++++------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index b716ce7..3770dea 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -335,7 +335,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // TODO: Should this be start - 1? | QA at most - uint256 vetosForInitiative = + /// @audit this is always wrong unless we allow an urgent veto to also exist + uint256 vetosForInitiative = /// @audit this needs to be the snapshot man else we can't do this lqtyToVotes(initiativeState.vetoLQTY, epochStart(), initiativeState.averageStakingTimestampVetoLQTY); diff --git a/src/interfaces/IGovernance.sol b/src/interfaces/IGovernance.sol index 37f5bbb..5c0e605 100644 --- a/src/interfaces/IGovernance.sol +++ b/src/interfaces/IGovernance.sol @@ -92,6 +92,7 @@ interface IGovernance { uint224 votes; // Votes at epoch transition uint16 forEpoch; // Epoch for which the votes are counted uint16 lastCountedEpoch; // Epoch at which which the votes where counted last in the global snapshot + uint224 vetos; // Vetos at epoch transition } /// @notice Returns the vote count snapshot of the previous epoch @@ -106,7 +107,7 @@ interface IGovernance { function votesForInitiativeSnapshot(address _initiative) external view - returns (uint224 votes, uint16 forEpoch, uint16 lastCountedEpoch); + returns (uint224 votes, uint16 forEpoch, uint16 lastCountedEpoch, uint224 vetos); struct Allocation { uint88 voteLQTY; // LQTY allocated vouching for the initiative diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 49235a8..470c888 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -625,7 +625,7 @@ contract GovernanceTest is Test { assertEq(forEpoch, governance.epoch() - 1); IGovernance.InitiativeVoteSnapshot memory initiativeSnapshot = - IGovernance.InitiativeVoteSnapshot(0, governance.epoch() - 1, 0); + IGovernance.InitiativeVoteSnapshot(0, governance.epoch() - 1, 0, 0); vm.store( address(governance), keccak256(abi.encode(baseInitiative3, uint256(3))), @@ -637,7 +637,7 @@ contract GovernanceTest is Test { ) ) ); - (uint224 votes_, uint16 forEpoch_, uint16 lastCountedEpoch) = + (uint224 votes_, uint16 forEpoch_, uint16 lastCountedEpoch, ) = governance.votesForInitiativeSnapshot(baseInitiative3); assertEq(votes_, 0); assertEq(forEpoch_, governance.epoch() - 1); @@ -918,7 +918,7 @@ contract GovernanceTest is Test { // should snapshot the global and initiatives votes if there hasn't been a snapshot in the current epoch yet (, uint16 forEpoch) = governance.votesSnapshot(); assertEq(forEpoch, governance.epoch() - 1); - (, forEpoch,) = governance.votesForInitiativeSnapshot(baseInitiative1); + (, forEpoch, ,) = governance.votesForInitiativeSnapshot(baseInitiative1); assertEq(forEpoch, governance.epoch() - 1); vm.stopPrank(); @@ -1254,7 +1254,7 @@ contract GovernanceTest is Test { assertEq(forEpoch, governance.epoch() - 1); IGovernance.InitiativeVoteSnapshot memory initiativeSnapshot = - IGovernance.InitiativeVoteSnapshot(1, governance.epoch() - 1, governance.epoch() - 1); + IGovernance.InitiativeVoteSnapshot(1, governance.epoch() - 1, governance.epoch() - 1, 0); vm.store( address(governance), keccak256(abi.encode(address(mockInitiative), uint256(3))), @@ -1266,7 +1266,7 @@ contract GovernanceTest is Test { ) ) ); - (uint224 votes_, uint16 forEpoch_, uint16 lastCountedEpoch) = + (uint224 votes_, uint16 forEpoch_, uint16 lastCountedEpoch, ) = governance.votesForInitiativeSnapshot(address(mockInitiative)); assertEq(votes_, 1); assertEq(forEpoch_, governance.epoch() - 1); @@ -1276,7 +1276,7 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + governance.EPOCH_DURATION()); - initiativeSnapshot = IGovernance.InitiativeVoteSnapshot(0, governance.epoch() - 1, 0); + initiativeSnapshot = IGovernance.InitiativeVoteSnapshot(0, governance.epoch() - 1, 0, 0); vm.store( address(governance), keccak256(abi.encode(address(mockInitiative), uint256(3))), @@ -1288,7 +1288,7 @@ contract GovernanceTest is Test { ) ) ); - (votes_, forEpoch_, lastCountedEpoch) = governance.votesForInitiativeSnapshot(address(mockInitiative)); + (votes_, forEpoch_, lastCountedEpoch, ) = governance.votesForInitiativeSnapshot(address(mockInitiative)); assertEq(votes_, 0); assertEq(forEpoch_, governance.epoch() - 1); assertEq(lastCountedEpoch, 0); From 10e3659809002cf85380bac8b8252d1fe60a9d7c Mon Sep 17 00:00:00 2001 From: gallo Date: Sat, 12 Oct 2024 14:46:34 +0200 Subject: [PATCH 23/93] feat: use vetos from snapshot to determine FSM --- src/Governance.sol | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 3770dea..f7daedf 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -284,6 +284,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance } else { initiativeSnapshot.votes = 0; } + initiativeSnapshot.vetos = uint224(vetos); /// @audit TODO: Overflow + order of operations initiativeSnapshot.forEpoch = currentEpoch - 1; votesForInitiativeSnapshot[_initiative] = initiativeSnapshot; emit SnapshotVotesForInitiative(_initiative, initiativeSnapshot.votes, initiativeSnapshot.forEpoch); @@ -323,29 +324,25 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance lastEpochClaim = initiativeStates[_initiative].lastEpochClaim; + // == Already Claimed Condition == // if(lastEpochClaim >= epoch() - 1) { // early return, we have already claimed return (InitiativeStatus.CLAIMED, lastEpochClaim, claimableAmount); } + // == Disabled Condition == // // TODO: If a initiative is disabled, we return false and the last epoch claim if(registeredInitiatives[_initiative] == UNREGISTERED_INITIATIVE) { return (InitiativeStatus.DISABLED, lastEpochClaim, 0); /// @audit By definition it must have zero rewards } - - // TODO: Should this be start - 1? | QA at most - /// @audit this is always wrong unless we allow an urgent veto to also exist - uint256 vetosForInitiative = /// @audit this needs to be the snapshot man else we can't do this - lqtyToVotes(initiativeState.vetoLQTY, epochStart(), initiativeState.averageStakingTimestampVetoLQTY); - // Unregister Condition - // TODO: Figure out `UNREGISTRATION_AFTER_EPOCHS` - /// @audit epoch() - 1 because we can have Now - 1 and that's not a removal case + // == Unregister Condition == // + /// @audit epoch() - 1 because we can have Now - 1 and that's not a removal case | TODO: Double check | Worst case QA, off by one epoch if((votesForInitiativeSnapshot_.lastCountedEpoch + UNREGISTRATION_AFTER_EPOCHS < epoch() - 1) - || vetosForInitiative > votesForInitiativeSnapshot_.votes - && vetosForInitiative > calculateVotingThreshold() * UNREGISTRATION_THRESHOLD_FACTOR / WAD + || votesForInitiativeSnapshot_.vetos > votesForInitiativeSnapshot_.votes + && votesForInitiativeSnapshot_.vetos > calculateVotingThreshold() * UNREGISTRATION_THRESHOLD_FACTOR / WAD ) { return (InitiativeStatus.UNREGISTERABLE, lastEpochClaim, 0); } @@ -355,11 +352,15 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @audit if we already are above, then why are we re-computing this? // Ultimately the checkpoint logic for initiative is fine, so we can skip this - /// @audit TODO: Add Votes vs Vetos - // For now the code always returns Votes iif votes > vetos, so we can trust it + + // == Vetoed this Epoch Condition == // + if(votesForInitiativeSnapshot_.vetos > votesForInitiativeSnapshot_.votes) { + return (InitiativeStatus.SKIP, lastEpochClaim, 0); + } uint256 claim; + // == Should have Rewards Conditions == // if (votesSnapshot_.votes == 0 || votesForInitiativeSnapshot_.votes == 0) { claim = 0; return (InitiativeStatus.SKIP, lastEpochClaim, 0); From bbc9dc81977cb8b29cb8dd40fe423c6f2cc18636 Mon Sep 17 00:00:00 2001 From: gallo Date: Sat, 12 Oct 2024 14:46:49 +0200 Subject: [PATCH 24/93] chore: remove unused variable --- src/Governance.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index f7daedf..645972e 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -480,10 +480,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance initiativeState.voteLQTY = add(initiativeState.voteLQTY, deltaLQTYVotes); initiativeState.vetoLQTY = add(initiativeState.vetoLQTY, deltaLQTYVetos); - // determine if the initiative's allocated voting LQTY should be included in the vote count - uint240 votesForInitiative = - lqtyToVotes(initiativeState.voteLQTY, block.timestamp, initiativeState.averageStakingTimestampVoteLQTY); - // update the initiative's state initiativeStates[initiative] = initiativeState; From 27b33db2dd5b2ffa311e34a2f49d741f9314036c Mon Sep 17 00:00:00 2001 From: gallo Date: Sat, 12 Oct 2024 14:48:13 +0200 Subject: [PATCH 25/93] feat: use `getInitiativeState` for `unregisterInitiative` --- src/Governance.sol | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 645972e..5e945b1 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -546,21 +546,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance (InitiativeStatus status, , )= getInitiativeState(_initiative); require(status == InitiativeStatus.UNREGISTERABLE, "Governance: cannot-unregister-initiative"); - - uint256 vetosForInitiative = - lqtyToVotes(initiativeState.vetoLQTY, block.timestamp, initiativeState.averageStakingTimestampVetoLQTY); - - // an initiative can be unregistered if it has no votes and has been inactive for 'UNREGISTRATION_AFTER_EPOCHS' - // epochs or if it has received more vetos than votes and the vetos are more than - // 'UNREGISTRATION_THRESHOLD_FACTOR' times the voting threshold - require( - (votesForInitiativeSnapshot_.lastCountedEpoch + UNREGISTRATION_AFTER_EPOCHS < currentEpoch) - || ( - vetosForInitiative > votesForInitiativeSnapshot_.votes - && vetosForInitiative > calculateVotingThreshold() * UNREGISTRATION_THRESHOLD_FACTOR / WAD - ), - "Governance: cannot-unregister-initiative" - ); /// @audit TODO: Differential review of this vs `mustUnregister` + /// @audit TODO: Verify that the FSM here is correct // recalculate the average staking timestamp for all counted voting LQTY if the initiative was counted in state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( From 0584ac0ec89d9e60f924893b19f4deadf88db96b Mon Sep 17 00:00:00 2001 From: gallo Date: Sat, 12 Oct 2024 14:49:12 +0200 Subject: [PATCH 26/93] chore: comments --- src/Governance.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Governance.sol b/src/Governance.sol index 5e945b1..fcb11d6 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -355,7 +355,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // == Vetoed this Epoch Condition == // if(votesForInitiativeSnapshot_.vetos > votesForInitiativeSnapshot_.votes) { - return (InitiativeStatus.SKIP, lastEpochClaim, 0); + return (InitiativeStatus.SKIP, lastEpochClaim, 0); /// @audit Technically VETOED } uint256 claim; From c1945bf7d93b1e8bb34f091cb7e3210b9e781276 Mon Sep 17 00:00:00 2001 From: gallo Date: Sat, 12 Oct 2024 15:30:53 +0200 Subject: [PATCH 27/93] fix: vetos, `lastCountedEpoch` and votes FSM follow the spec --- src/Governance.sol | 53 +++++++++++++++++++++++++++---------------- test/Governance.t.sol | 33 ++++++++++++++++----------- 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index fcb11d6..0812a71 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -278,14 +278,24 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance uint240 vetos = lqtyToVotes(initiativeState.vetoLQTY, start, initiativeState.averageStakingTimestampVetoLQTY); // if the votes didn't meet the voting threshold then no votes qualify - if (votes >= votingThreshold && votes >= vetos) { - initiativeSnapshot.votes = uint224(votes); /// @audit TODO: We should change this to check the treshold, we should instead use the snapshot to just report all the valid data - initiativeSnapshot.lastCountedEpoch = currentEpoch - 1; - } else { - initiativeSnapshot.votes = 0; - } + /// @audit TODO TEST THIS + /// The change means that all logic for votes and rewards must be done in `getInitiativeState` + initiativeSnapshot.votes = uint224(votes); /// @audit TODO: We should change this to check the treshold, we should instead use the snapshot to just report all the valid data + initiativeSnapshot.vetos = uint224(vetos); /// @audit TODO: Overflow + order of operations - initiativeSnapshot.forEpoch = currentEpoch - 1; + + initiativeSnapshot.forEpoch = currentEpoch - 1; + + /// @audit Conditional + /// If we meet the threshold then we increase this + /// TODO: Either simplify, or use this for the state machine as well + if( + initiativeSnapshot.votes > initiativeSnapshot.vetos && + initiativeSnapshot.votes >= votingThreshold + ) { + initiativeSnapshot.lastCountedEpoch = currentEpoch - 1; /// @audit This updating makes it so that we lose track | TODO: Find a better way + } + votesForInitiativeSnapshot[_initiative] = initiativeSnapshot; emit SnapshotVotesForInitiative(_initiative, initiativeSnapshot.votes, initiativeSnapshot.forEpoch); } @@ -336,11 +346,12 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance return (InitiativeStatus.DISABLED, lastEpochClaim, 0); /// @audit By definition it must have zero rewards } - // == Unregister Condition == // /// @audit epoch() - 1 because we can have Now - 1 and that's not a removal case | TODO: Double check | Worst case QA, off by one epoch - if((votesForInitiativeSnapshot_.lastCountedEpoch + UNREGISTRATION_AFTER_EPOCHS < epoch() - 1) + // TODO: IMO we can use the claimed variable here + /// This shifts the logic by 1 epoch + if((votesForInitiativeSnapshot_.lastCountedEpoch + UNREGISTRATION_AFTER_EPOCHS < epoch() - 1) || votesForInitiativeSnapshot_.vetos > votesForInitiativeSnapshot_.votes && votesForInitiativeSnapshot_.vetos > calculateVotingThreshold() * UNREGISTRATION_THRESHOLD_FACTOR / WAD ) { @@ -351,26 +362,28 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // They must have votes / totalVotes AND meet the Requirement AND not be vetoed /// @audit if we already are above, then why are we re-computing this? // Ultimately the checkpoint logic for initiative is fine, so we can skip this + + // TODO: Where does this fit exactly? + // Edge case of 0 votes + if(votesSnapshot_.votes == 0) { + return (InitiativeStatus.SKIP, lastEpochClaim, 0); + } // == Vetoed this Epoch Condition == // - if(votesForInitiativeSnapshot_.vetos > votesForInitiativeSnapshot_.votes) { + if(votesForInitiativeSnapshot_.vetos >= votesForInitiativeSnapshot_.votes) { return (InitiativeStatus.SKIP, lastEpochClaim, 0); /// @audit Technically VETOED } - uint256 claim; + // == Not meeting threshold Condition == // - // == Should have Rewards Conditions == // - if (votesSnapshot_.votes == 0 || votesForInitiativeSnapshot_.votes == 0) { - claim = 0; + if(calculateVotingThreshold() > votesForInitiativeSnapshot_.votes) { return (InitiativeStatus.SKIP, lastEpochClaim, 0); - } else { - claim = votesForInitiativeSnapshot_.votes * boldAccrued / votesSnapshot_.votes; - return (InitiativeStatus.CLAIMABLE, lastEpochClaim, claim); } - /// Unrecheable state, we should be covering all possible states - assert(false); + // == Rewards Conditions (votes can be zero, logic is the same) == // + uint256 claim = votesForInitiativeSnapshot_.votes * boldAccrued / votesSnapshot_.votes; + return (InitiativeStatus.CLAIMABLE, lastEpochClaim, claim); } /// @inheritdoc IGovernance @@ -576,7 +589,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState_) = _snapshotVotesForInitiative(_initiative); // TODO: Return type from state FSM can be standardized - (InitiativeStatus status, , uint256 claimableAmount )= getInitiativeState(_initiative); + (InitiativeStatus status, , uint256 claimableAmount ) = getInitiativeState(_initiative); /// @audit Return 0 if we cannot claim /// INVARIANT: diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 470c888..b675157 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -611,8 +611,10 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + 365 days); // should revert if the initiative is still active or the vetos don't meet the threshold - vm.expectRevert("Governance: cannot-unregister-initiative"); - governance.unregisterInitiative(baseInitiative3); + /// @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); snapshot = IGovernance.VoteSnapshot(1e18, governance.epoch() - 1); vm.store( @@ -712,12 +714,8 @@ contract GovernanceTest is Test { (IGovernance.VoteSnapshot memory snapshot, IGovernance.InitiativeVoteSnapshot memory initiativeVoteSnapshot1) = governance.snapshotVotesForInitiative(baseInitiative1); (, IGovernance.InitiativeVoteSnapshot memory initiativeVoteSnapshot2) = governance.snapshotVotesForInitiative(baseInitiative2); - - /// @audit TODO: No longer counted - // assertEq(counted1, 1, "1 is counted inspite below voting"); - // assertEq(counted1again, 1, "Counted is true"); uint256 threshold = governance.calculateVotingThreshold(); - assertEq(initiativeVoteSnapshot1.votes, 0, "it didn't get votes"); + assertLt(initiativeVoteSnapshot1.votes, threshold, "it didn't get rewards"); uint256 votingPowerWithProjection = governance.lqtyToVotes(voteLQTY1, governance.epochStart() + governance.EPOCH_DURATION(), averageStakingTimestampVoteLQTY1); assertLt(votingPower, threshold, "Current Power is not enough - Desynch A"); @@ -760,7 +758,7 @@ contract GovernanceTest is Test { (IGovernance.VoteSnapshot memory snapshot, IGovernance.InitiativeVoteSnapshot memory initiativeVoteSnapshot1) = governance.snapshotVotesForInitiative(baseInitiative1); uint256 threshold = governance.calculateVotingThreshold(); - assertEq(initiativeVoteSnapshot1.votes, 0, "it didn't get votes"); + assertLt(initiativeVoteSnapshot1.votes, threshold, "it didn't get rewards"); } // Roll for @@ -1116,11 +1114,12 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + governance.EPOCH_DURATION() + 1); // should compute the claim and transfer it to the initiative - assertEq(governance.claimForInitiative(baseInitiative1), 5000e18); + + assertEq(governance.claimForInitiative(baseInitiative1), 5000e18, "first claim"); // 2nd claim = 0 assertEq(governance.claimForInitiative(baseInitiative1), 0); - assertEq(governance.claimForInitiative(baseInitiative2), 5000e18); + assertEq(governance.claimForInitiative(baseInitiative2), 5000e18, "first claim 2"); assertEq(governance.claimForInitiative(baseInitiative2), 0); assertEq(lusd.balanceOf(baseInitiative2), 5000e18); @@ -1150,10 +1149,18 @@ contract GovernanceTest is Test { assertEq(lusd.balanceOf(baseInitiative1), 14950e18); - assertEq(governance.claimForInitiative(baseInitiative2), 0); - assertEq(governance.claimForInitiative(baseInitiative2), 0); + (Governance.InitiativeStatus status, , uint256 claimable) = governance.getInitiativeState(baseInitiative2); + console.log("res", uint8(status)); + console.log("claimable", claimable); + (uint224 votes, , , uint224 vetos) = governance.votesForInitiativeSnapshot(baseInitiative2); + console.log("snapshot votes", votes); + console.log("snapshot vetos", vetos); - assertEq(lusd.balanceOf(baseInitiative2), 5000e18); + console.log("governance.calculateVotingThreshold()", governance.calculateVotingThreshold()); + assertEq(governance.claimForInitiative(baseInitiative2), 0, "zero 2"); + assertEq(governance.claimForInitiative(baseInitiative2), 0, "zero 3"); + + assertEq(lusd.balanceOf(baseInitiative2), 5000e18, "zero bal"); vm.stopPrank(); } From 3adf6dc7a26bbd8baad26e4634b75c77ded5192a Mon Sep 17 00:00:00 2001 From: gallo Date: Sun, 13 Oct 2024 14:58:33 +0200 Subject: [PATCH 28/93] fix: compilation --- test/Governance.t.sol | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 7ba2a15..8dc2ee9 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -677,10 +677,10 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int176[] memory deltaLQTYVotes = new int176[](2); + int88[] memory deltaLQTYVotes = new int88[](2); deltaLQTYVotes[0] = 1e18; deltaLQTYVotes[1] = 999e18; - int176[] memory deltaLQTYVetos = new int176[](2); + int88[] memory deltaLQTYVetos = new int88[](2); governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); @@ -742,10 +742,10 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int176[] memory deltaLQTYVotes = new int176[](2); + int88[] memory deltaLQTYVotes = new int88[](2); deltaLQTYVotes[0] = 1e18; deltaLQTYVotes[1] = 999e18; - int176[] memory deltaLQTYVetos = new int176[](2); + int88[] memory deltaLQTYVetos = new int88[](2); governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); @@ -774,9 +774,9 @@ contract GovernanceTest is Test { // I cannot address[] memory removeInitiatives = new address[](1); removeInitiatives[0] = baseInitiative1; - int176[] memory removeDeltaLQTYVotes = new int176[](1); + int88[] memory removeDeltaLQTYVotes = new int88[](1); removeDeltaLQTYVotes[0] = -1e18; - int176[] memory removeDeltaLQTYVetos = new int176[](1); + int88[] memory removeDeltaLQTYVetos = new int88[](1); /// @audit the next call MUST not revert - this is a critical bug governance.allocateLQTY(removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); @@ -789,9 +789,9 @@ contract GovernanceTest is Test { address[] memory reAddInitiatives = new address[](1); reAddInitiatives[0] = baseInitiative1; - int176[] memory reAddDeltaLQTYVotes = new int176[](1); + int88[] memory reAddDeltaLQTYVotes = new int88[](1); reAddDeltaLQTYVotes[0] = 1e18; - int176[] memory reAddDeltaLQTYVetos = new int176[](1); + int88[] memory reAddDeltaLQTYVetos = new int88[](1); /// @audit This MUST revert, an initiative should not be re-votable once disabled vm.expectRevert("Governance: initiative-not-active"); @@ -814,10 +814,10 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](2); initiatives[0] = baseInitiative1; initiatives[1] = baseInitiative2; - int176[] memory deltaLQTYVotes = new int176[](2); + int88[] memory deltaLQTYVotes = new int88[](2); deltaLQTYVotes[0] = 1e18; deltaLQTYVotes[1] = 999e18; - int176[] memory deltaLQTYVetos = new int176[](2); + int88[] memory deltaLQTYVetos = new int88[](2); governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); (uint88 allocatedB4Test,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); @@ -830,9 +830,9 @@ contract GovernanceTest is Test { address[] memory removeInitiatives = new address[](1); removeInitiatives[0] = baseInitiative1; - int176[] memory removeDeltaLQTYVotes = new int176[](1); - removeDeltaLQTYVotes[0] = int176(-1e18); - int176[] memory removeDeltaLQTYVetos = new int176[](1); + int88[] memory removeDeltaLQTYVotes = new int88[](1); + removeDeltaLQTYVotes[0] = int88(-1e18); + int88[] memory removeDeltaLQTYVetos = new int88[](1); (uint88 allocatedB4Removal,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); console.log("allocatedB4Removal", allocatedB4Removal); From 77fe12b6e3f18b2c41cd28885ceaefa063288f22 Mon Sep 17 00:00:00 2001 From: gallo Date: Sun, 13 Oct 2024 15:03:43 +0200 Subject: [PATCH 29/93] feat: broken | math tests --- test/Math.t.sol | 116 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 test/Math.t.sol diff --git a/test/Math.t.sol b/test/Math.t.sol new file mode 100644 index 0000000..1003fc0 --- /dev/null +++ b/test/Math.t.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; + +import {add, abs} from "src/utils/Math.sol"; + + +contract AddComparer { + function libraryAdd(uint88 a, int88 b) public pure returns (uint88) { + return add(a, b); + } + // Differential test + // Verify that it will revert any time it overflows + // Verify we can never get a weird value + function referenceAdd(int88 a, int88 b) public pure returns (int88) { + return a + b; + } +} +contract AbsComparer { + function libraryAbs(int88 a) public pure returns (int88) { + return int88(abs(a)); // by definition should fit, since input was int88 -> uint88 -> int88 + } + + function referenceAbs(int88 a) public pure returns (int88) { + return a < 0 ? -a : a; + } +} +contract MathTests is Test { + + + // forge test --match-test test_math_fuzz_comparison -vv + function test_math_fuzz_comparison(uint88 a, int88 b) public { + AddComparer tester = new AddComparer(); + + bool revertLib; + bool revertRef; + int88 resultLib; + int88 resultRef; + + try tester.libraryAdd(a, b) returns (uint88 x) { + resultLib = int88(uint88(x)); + } catch { + revertLib = true; + } + + try tester.referenceAdd(int88(uint88(a)), b) returns (int88 x) { + resultRef = int88(uint88(x)); + } catch { + revertRef = true; + } + + // Negative overflow + if(revertLib == true && revertRef == false) { + // Check if we had a negative value + if(resultRef < 0) { + revertRef = true; + resultRef = int88(0); + } + + // Check if we overflow on the positive + if(resultRef > int88(uint88(type(uint88).max))) { + // Overflow due to above limit + revertRef = true; + resultRef = int88(0); + } + } + + assertEq(revertLib, revertRef, "Reverts"); // This breaks + assertEq(resultLib, resultRef, "Results"); // This should match excluding overflows + } + + + + /// @dev test that abs never incorrectly overflows + // forge test --match-test test_fuzz_abs_comparison -vv + /** + [FAIL. Reason: reverts: false != true; counterexample: calldata=0x2c945365ffffffffffffffffffffffffffffffffffffffffff8000000000000000000000 args=[-154742504910672534362390528 [-1.547e26]]] + */ + function test_fuzz_abs_comparison(int88 a) public { + AbsComparer tester = new AbsComparer(); + + bool revertLib; + bool revertRef; + int88 resultLib; + int88 resultRef; + + try tester.libraryAbs(a) returns (int88 x) { + resultLib = x; + } catch { + revertLib = true; + } + + try tester.referenceAbs(a) returns (int88 x) { + resultRef = x; + } catch { + revertRef = true; + } + + assertEq(revertLib, revertRef, "reverts"); + assertEq(resultLib, resultRef, "results"); + } + + /// @dev Test that Abs never revert + /// It reverts on the smaller possible number + function test_fuzz_abs(int88 a) public { + /** + Encountered 1 failing test in test/Math.t.sol:MathTests + [FAIL. Reason: panic: arithmetic underflow or overflow (0x11); counterexample: calldata=0x804d552cffffffffffffffffffffffffffffffffffffffff800000000000000000000000 args=[-39614081257132168796771975168 [-3.961e28]]] test_fuzz_abs(int88) (runs: 0, μ: 0, ~: 0) + */ + vm.assume(a > type(int88).min); + // vm.assume(a < type(int88).max); + /// @audit Reverts at the absolute minimum due to overflow as it will remain negative + abs(a); + } +} \ No newline at end of file From a4af7572d9aa93415b63bd68c70d94a257b6564d Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 14 Oct 2024 09:33:39 +0200 Subject: [PATCH 30/93] feat: ref vs lib tests --- test/Math.t.sol | 66 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/test/Math.t.sol b/test/Math.t.sol index 1003fc0..cae32c6 100644 --- a/test/Math.t.sol +++ b/test/Math.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; import {add, abs} from "src/utils/Math.sol"; +import {console} from "forge-std/console.sol"; contract AddComparer { @@ -13,39 +14,66 @@ contract AddComparer { // Differential test // Verify that it will revert any time it overflows // Verify we can never get a weird value - function referenceAdd(int88 a, int88 b) public pure returns (int88) { - return a + b; + function referenceAdd(uint88 a, int88 b) public pure returns (uint88) { + // Upscale both + int96 scaledA = int96(int256(uint256(a))); + int96 tempB = int96(b); + + int96 res = scaledA + tempB; + if(res < 0) { + revert("underflow"); + } + + if(res > int96(int256(uint256(type(uint88).max)))) { + revert("Too big"); + } + + return uint88(uint96(res)); } } contract AbsComparer { - function libraryAbs(int88 a) public pure returns (int88) { - return int88(abs(a)); // by definition should fit, since input was int88 -> uint88 -> int88 + function libraryAbs(int88 a) public pure returns (uint88) { + return abs(a); // by definition should fit, since input was int88 -> uint88 -> int88 } - function referenceAbs(int88 a) public pure returns (int88) { - return a < 0 ? -a : a; + event DebugEvent2(int256); + event DebugEvent(uint256); + function referenceAbs(int88 a) public returns (uint88) { + int256 bigger = a; + uint256 ref = bigger < 0 ? uint256(-bigger) : uint256(bigger); + emit DebugEvent2(bigger); + emit DebugEvent(ref); + if(ref > type(uint88).max) { + revert("Too big"); + } + if(ref < type(uint88).min) { + revert("Too small"); + } + return uint88(ref); } } + contract MathTests is Test { // forge test --match-test test_math_fuzz_comparison -vv function test_math_fuzz_comparison(uint88 a, int88 b) public { + vm.assume(a < uint88(type(int88).max)); AddComparer tester = new AddComparer(); bool revertLib; bool revertRef; - int88 resultLib; - int88 resultRef; + uint88 resultLib; + uint88 resultRef; try tester.libraryAdd(a, b) returns (uint88 x) { - resultLib = int88(uint88(x)); + resultLib = x; } catch { revertLib = true; } - try tester.referenceAdd(int88(uint88(a)), b) returns (int88 x) { - resultRef = int88(uint88(x)); + try tester.referenceAdd(a, b) returns (uint88 x) { + resultRef = x; } catch { revertRef = true; } @@ -55,14 +83,14 @@ contract MathTests is Test { // Check if we had a negative value if(resultRef < 0) { revertRef = true; - resultRef = int88(0); + resultRef = uint88(0); } // Check if we overflow on the positive - if(resultRef > int88(uint88(type(uint88).max))) { + if(resultRef > uint88(type(int88).max)) { // Overflow due to above limit revertRef = true; - resultRef = int88(0); + resultRef = uint88(0); } } @@ -82,16 +110,16 @@ contract MathTests is Test { bool revertLib; bool revertRef; - int88 resultLib; - int88 resultRef; + uint88 resultLib; + uint88 resultRef; - try tester.libraryAbs(a) returns (int88 x) { + try tester.libraryAbs(a) returns (uint88 x) { resultLib = x; } catch { revertLib = true; } - try tester.referenceAbs(a) returns (int88 x) { + try tester.referenceAbs(a) returns (uint88 x) { resultRef = x; } catch { revertRef = true; @@ -108,8 +136,6 @@ contract MathTests is Test { Encountered 1 failing test in test/Math.t.sol:MathTests [FAIL. Reason: panic: arithmetic underflow or overflow (0x11); counterexample: calldata=0x804d552cffffffffffffffffffffffffffffffffffffffff800000000000000000000000 args=[-39614081257132168796771975168 [-3.961e28]]] test_fuzz_abs(int88) (runs: 0, μ: 0, ~: 0) */ - vm.assume(a > type(int88).min); - // vm.assume(a < type(int88).max); /// @audit Reverts at the absolute minimum due to overflow as it will remain negative abs(a); } From 03c5315969228a3da86366185c88be877e1a6f5c Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 14 Oct 2024 09:55:14 +0200 Subject: [PATCH 31/93] fix: compilation --- src/BribeInitiative.sol | 2 -- src/Governance.sol | 2 -- test/BribeInitiativeAllocate.t.sol | 58 +++++++++++++++--------------- test/Governance.t.sol | 4 +-- test/GovernanceAttacks.t.sol | 12 +++---- 5 files changed, 37 insertions(+), 41 deletions(-) diff --git a/src/BribeInitiative.sol b/src/BribeInitiative.sol index 8da4d51..85b99dd 100644 --- a/src/BribeInitiative.sol +++ b/src/BribeInitiative.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {console} from "forge-std/console.sol"; - import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; diff --git a/src/Governance.sol b/src/Governance.sol index 3ba8494..5edac1b 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {console} from "forge-std/console.sol"; - import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; diff --git a/test/BribeInitiativeAllocate.t.sol b/test/BribeInitiativeAllocate.t.sol index 3bfa44f..e205382 100644 --- a/test/BribeInitiativeAllocate.t.sol +++ b/test/BribeInitiativeAllocate.t.sol @@ -76,7 +76,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 0 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); } @@ -99,7 +99,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 0 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState2, allocation2, initiativeState2); } @@ -132,7 +132,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(1), averageStakingTimestampVetoLQTY: 0, - counted: 0 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); } @@ -171,7 +171,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 0 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = @@ -193,7 +193,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 0 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = @@ -225,7 +225,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 1, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 0 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = @@ -247,7 +247,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 1, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 0 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); (uint88 totalLQTYAllocated, uint32 totalAverageTimestamp) = @@ -287,7 +287,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); @@ -309,7 +309,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 1000e18, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: uint32(block.timestamp), - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY( governance.epoch(), user, userStateVeto, allocationVeto, initiativeStateVeto @@ -335,7 +335,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 1, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: uint32(block.timestamp), - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY( governance.epoch(), user, userStateNewEpoch, allocationNewEpoch, initiativeStateNewEpoch @@ -369,7 +369,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY( governance.epoch(), user, userStateNewEpoch3, allocationNewEpoch3, initiativeStateNewEpoch3 @@ -410,7 +410,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); @@ -434,7 +434,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); @@ -460,7 +460,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); @@ -486,7 +486,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); @@ -522,7 +522,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); @@ -546,7 +546,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); @@ -570,7 +570,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); @@ -616,7 +616,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); @@ -640,7 +640,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); @@ -664,7 +664,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); @@ -712,7 +712,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); @@ -736,7 +736,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); @@ -760,7 +760,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); @@ -784,7 +784,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); @@ -824,7 +824,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user2, userState, allocation, initiativeState); @@ -848,7 +848,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); @@ -872,7 +872,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); @@ -896,7 +896,7 @@ contract BribeInitiativeAllocateTest is Test { vetoLQTY: 0, averageStakingTimestampVoteLQTY: uint32(block.timestamp), averageStakingTimestampVetoLQTY: 0, - counted: 1 + lastEpochClaim: 0 }); bribeInitiative.onAfterAllocateLQTY(governance.epoch(), user, userState, allocation, initiativeState); diff --git a/test/Governance.t.sol b/test/Governance.t.sol index f546fcc..1993b1d 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -1395,7 +1395,7 @@ contract GovernanceTest is Test { deltaLQTYVetos[1] = 0; vm.warp(block.timestamp + 365 days); - vm.expectRevert("Governance: insufficient-or-unallocated-lqty"); + vm.expectRevert("Governance: insufficient-or-allocated-lqty"); governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); deltaLQTYVotes[0] = 0; @@ -1403,7 +1403,7 @@ contract GovernanceTest is Test { deltaLQTYVetos[0] = 0; deltaLQTYVetos[1] = type(int88).max; - vm.expectRevert("Governance: insufficient-or-unallocated-lqty"); + vm.expectRevert("Governance: insufficient-or-allocated-lqty"); governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); vm.stopPrank(); diff --git a/test/GovernanceAttacks.t.sol b/test/GovernanceAttacks.t.sol index 73a4b0a..071c22b 100644 --- a/test/GovernanceAttacks.t.sol +++ b/test/GovernanceAttacks.t.sol @@ -77,7 +77,7 @@ contract GovernanceTest is Test { } - // forge test --match-test test_deposit_attack -vv + // forge test --match-test test_all_revert_attacks_hardcoded -vv // All calls should never revert due to malicious initiative function test_all_revert_attacks_hardcoded() public { uint256 zeroSnapshot = vm.snapshot(); @@ -145,10 +145,10 @@ contract GovernanceTest is Test { address[] memory initiatives = new address[](2); initiatives[0] = address(maliciousInitiative2); initiatives[1] = address(eoaInitiative); - int176[] memory deltaVoteLQTY = new int176[](2); + int88[] memory deltaVoteLQTY = new int88[](2); deltaVoteLQTY[0] = 5e17; deltaVoteLQTY[1] = 5e17; - int176[] memory deltaVetoLQTY = new int176[](2); + int88[] memory deltaVetoLQTY = new int88[](2); /// === Allocate LQTY REVERTS === /// uint256 allocateSnapshot = vm.snapshot(); @@ -208,11 +208,11 @@ contract GovernanceTest is Test { initiatives[0] = address(maliciousInitiative2); initiatives[1] = address(eoaInitiative); initiatives[2] = address(maliciousInitiative1); - deltaVoteLQTY = new int176[](3); + deltaVoteLQTY = new int88[](3); deltaVoteLQTY[0] = -5e17; deltaVoteLQTY[1] = -5e17; deltaVoteLQTY[2] = 5e17; - deltaVetoLQTY = new int176[](3); + deltaVetoLQTY = new int88[](3); governance.allocateLQTY(initiatives, deltaVoteLQTY, deltaVetoLQTY); (Governance.VoteSnapshot memory v, Governance.InitiativeVoteSnapshot memory initData) = governance.snapshotVotesForInitiative(address(maliciousInitiative2)); @@ -222,7 +222,7 @@ contract GovernanceTest is Test { // Inactive for 4 epochs // Add another proposal - vm.warp(block.timestamp + governance.EPOCH_DURATION() * 4); + vm.warp(block.timestamp + governance.EPOCH_DURATION() * 5); /// @audit needs 5? (v, initData) = governance.snapshotVotesForInitiative(address(maliciousInitiative2)); assertEq(initData.lastCountedEpoch, currentEpoch - 1, "Epoch Matches"); /// @audit This fails if you have 0 votes, see QA From a932aa0c4ccddada7a1cfc2a1083914ebf53793e Mon Sep 17 00:00:00 2001 From: nelson-pereira8 <94120714+nican0r@users.noreply.github.com> Date: Mon, 14 Oct 2024 08:15:16 -0300 Subject: [PATCH 32/93] feat: fuzz tests for encoding + decoding allocation --- test/EncodingDecoding.t.sol | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 test/EncodingDecoding.t.sol diff --git a/test/EncodingDecoding.t.sol b/test/EncodingDecoding.t.sol new file mode 100644 index 0000000..2b06e22 --- /dev/null +++ b/test/EncodingDecoding.t.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {Test, console2} from "forge-std/Test.sol"; + +library EncodingDecoding { + function encodeLQTYAllocation(uint88 _lqty, uint32 _averageTimestamp) public pure returns (uint224) { + uint224 _value = (uint224(_lqty) << 32) | _averageTimestamp; + return _value; + } + + function decodeLQTYAllocation(uint224 _value) public pure returns (uint88, uint32) { + return (uint88(_value >> 32), uint32(_value)); + } +} + +contract EncodingDecodingTest is Test { + // value -> encoding -> decoding -> value + function test_encoding_and_decoding_symmetrical(uint88 lqty, uint32 averageTimestamp) public { + uint224 encodedValue = EncodingDecoding.encodeLQTYAllocation(lqty, averageTimestamp); + (uint88 decodedLqty, uint32 decodedAverageTimestamp) = EncodingDecoding.decodeLQTYAllocation(encodedValue); + + assertEq(lqty, decodedLqty); + assertEq(averageTimestamp, decodedAverageTimestamp); + } + + // receive -> undo -> check -> redo -> compare + function test_receive_undo_compare(uint224 encodedValue) public { + (uint88 decodedLqty, uint32 decodedAverageTimestamp) = EncodingDecoding.decodeLQTYAllocation(encodedValue); + + uint224 encodedValue2 = EncodingDecoding.encodeLQTYAllocation(decodedLqty, decodedAverageTimestamp); + (uint88 decodedLqty2, uint32 decodedAverageTimestamp2) = EncodingDecoding.decodeLQTYAllocation(encodedValue); + + assertEq(encodedValue, encodedValue2, "encoded values not equal"); + assertEq(decodedLqty, decodedLqty2, "decoded lqty not equal"); + assertEq(decodedAverageTimestamp, decodedAverageTimestamp2, "decoded timestamps not equal"); + } +} \ No newline at end of file From 65f466b1afa97b44bf31610ca3d3fcb3c5e55249 Mon Sep 17 00:00:00 2001 From: nelson-pereira8 <94120714+nican0r@users.noreply.github.com> Date: Mon, 14 Oct 2024 08:19:42 -0300 Subject: [PATCH 33/93] test: unit test for reproducing issue with encoding --- test/EncodingDecoding.t.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/EncodingDecoding.t.sol b/test/EncodingDecoding.t.sol index 2b06e22..7aadcd1 100644 --- a/test/EncodingDecoding.t.sol +++ b/test/EncodingDecoding.t.sol @@ -35,4 +35,8 @@ contract EncodingDecodingTest is Test { assertEq(decodedLqty, decodedLqty2, "decoded lqty not equal"); assertEq(decodedAverageTimestamp, decodedAverageTimestamp2, "decoded timestamps not equal"); } + + function test_encoding_not_equal_reproducer() public { + test_receive_undo_compare(18371677541005923091065047412368542483005086202); + } } \ No newline at end of file From 9e10b09e13a7476651e9d596656d33cf72bd7db0 Mon Sep 17 00:00:00 2001 From: nelson-pereira8 <94120714+nican0r@users.noreply.github.com> Date: Mon, 14 Oct 2024 08:22:15 -0300 Subject: [PATCH 34/93] fix: encoded value passed in for decoding --- test/EncodingDecoding.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/EncodingDecoding.t.sol b/test/EncodingDecoding.t.sol index 7aadcd1..8e03f4d 100644 --- a/test/EncodingDecoding.t.sol +++ b/test/EncodingDecoding.t.sol @@ -29,7 +29,7 @@ contract EncodingDecodingTest is Test { (uint88 decodedLqty, uint32 decodedAverageTimestamp) = EncodingDecoding.decodeLQTYAllocation(encodedValue); uint224 encodedValue2 = EncodingDecoding.encodeLQTYAllocation(decodedLqty, decodedAverageTimestamp); - (uint88 decodedLqty2, uint32 decodedAverageTimestamp2) = EncodingDecoding.decodeLQTYAllocation(encodedValue); + (uint88 decodedLqty2, uint32 decodedAverageTimestamp2) = EncodingDecoding.decodeLQTYAllocation(encodedValue2); assertEq(encodedValue, encodedValue2, "encoded values not equal"); assertEq(decodedLqty, decodedLqty2, "decoded lqty not equal"); From 629799f16dbf27328d09033881dac03f177f9895 Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 14 Oct 2024 14:06:36 +0200 Subject: [PATCH 35/93] fix: random use of solmate --- src/Governance.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 5edac1b..0e5a4af 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -17,8 +17,6 @@ import {Multicall} from "./utils/Multicall.sol"; import {WAD, PermitParams} from "./utils/Types.sol"; import {safeCallWithMinGas} from "./utils/SafeCallMinGas.sol"; -import {SafeCastLib} from "lib/solmate/src/utils/SafeCastLib.sol"; - /// @title Governance: Modular Initiative based Governance contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance { using SafeERC20 for IERC20; From e662cb7ab76ff5794c2b955e25217fac1efbb1e4 Mon Sep 17 00:00:00 2001 From: nelson-pereira8 <94120714+nican0r@users.noreply.github.com> Date: Mon, 14 Oct 2024 09:08:55 -0300 Subject: [PATCH 36/93] test: rational flow for allocating/claiming bribe --- test/BribeInitiative.t.sol | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/BribeInitiative.t.sol b/test/BribeInitiative.t.sol index 5333182..b40aedb 100644 --- a/test/BribeInitiative.t.sol +++ b/test/BribeInitiative.t.sol @@ -346,6 +346,51 @@ contract BribeInitiativeTest is Test { assertEq(totalLQTYAllocated, 0); } + // forge test --match-test test_rationalFlow -vv + function test_rationalFlow() public { + vm.warp(block.timestamp + (EPOCH_DURATION)); // Initiative not active + + // We are now at epoch + + // Deposit + _stakeLQTY(user1, 1e18); + + // Deposit Bribe for now + _allocateLQTY(user1, 5e17, 0); /// @audit Allocate b4 or after bribe should be irrelevant + + /// @audit WTF + _depositBribe(1e18, 1e18, governance.epoch() + 1); /// @audit IMO this should also work + + _allocateLQTY(user1, 5e17, 0); /// @audit Allocate b4 or after bribe should be irrelevant + + // deposit bribe for Epoch + 2 + _depositBribe(1e18, 1e18, governance.epoch() + 2); + + + (uint88 totalLQTYAllocated,) = + bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + (uint88 userLQTYAllocated,) = + bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + assertEq(totalLQTYAllocated, 1e18, "total allocation"); + assertEq(userLQTYAllocated, 1e18, "user allocation"); + + vm.warp(block.timestamp + (EPOCH_DURATION)); + // We are now at epoch + 1 // Should be able to claim epoch - 1 + + // user should receive bribe from their allocated stake + (uint256 boldAmount, uint256 bribeTokenAmount) = _claimBribe(user1, governance.epoch(), governance.epoch() - 1, governance.epoch() - 1); + assertEq(boldAmount, 1e18, "bold amount"); + assertEq(bribeTokenAmount, 1e18, "bribe amount"); + + // decrease user allocation for the initiative + _allocateLQTY(user1, -1e18, 0); + + (userLQTYAllocated,) = bribeInitiative.lqtyAllocatedByUserAtEpoch(user1, governance.epoch()); + (totalLQTYAllocated,) = bribeInitiative.totalLQTYAllocatedByEpoch(governance.epoch()); + assertEq(userLQTYAllocated, 0, "total allocation"); + assertEq(totalLQTYAllocated, 0, "user allocation"); + } + /** Revert Cases */ From f4ca88f30cc8c92991f2ec54129e5deed4ebe658 Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 14 Oct 2024 14:11:01 +0200 Subject: [PATCH 37/93] feat: encode / deocde --- test/EncodingDecoding.t.sol | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/test/EncodingDecoding.t.sol b/test/EncodingDecoding.t.sol index 8e03f4d..612e964 100644 --- a/test/EncodingDecoding.t.sol +++ b/test/EncodingDecoding.t.sol @@ -22,10 +22,30 @@ contract EncodingDecodingTest is Test { assertEq(lqty, decodedLqty); assertEq(averageTimestamp, decodedAverageTimestamp); + + // Redo + uint224 reEncoded = EncodingDecoding.encodeLQTYAllocation(decodedLqty, decodedAverageTimestamp); + (uint88 reDecodedLqty, uint32 reDecodedAverageTimestamp) = EncodingDecoding.decodeLQTYAllocation(encodedValue); + + assertEq(reEncoded, encodedValue); + assertEq(reDecodedLqty, decodedLqty); + assertEq(reDecodedAverageTimestamp, decodedAverageTimestamp); + } + + + /// We expect this test to fail as the encoding is ambigous past u120 + function test_fail_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); } // receive -> undo -> check -> redo -> compare - function test_receive_undo_compare(uint224 encodedValue) public { + 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) = EncodingDecoding.decodeLQTYAllocation(encodedValue); uint224 encodedValue2 = EncodingDecoding.encodeLQTYAllocation(decodedLqty, decodedAverageTimestamp); @@ -36,7 +56,5 @@ contract EncodingDecodingTest is Test { assertEq(decodedAverageTimestamp, decodedAverageTimestamp2, "decoded timestamps not equal"); } - function test_encoding_not_equal_reproducer() public { - test_receive_undo_compare(18371677541005923091065047412368542483005086202); - } + } \ No newline at end of file From 4bb1a583448d83ed166b282868e680d8fa6279d9 Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 14 Oct 2024 14:11:45 +0200 Subject: [PATCH 38/93] feat: testFail --- test/EncodingDecoding.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/EncodingDecoding.t.sol b/test/EncodingDecoding.t.sol index 612e964..4598c1f 100644 --- a/test/EncodingDecoding.t.sol +++ b/test/EncodingDecoding.t.sol @@ -34,7 +34,7 @@ contract EncodingDecodingTest is Test { /// We expect this test to fail as the encoding is ambigous past u120 - function test_fail_encoding_not_equal_reproducer() public { + function testFail_encoding_not_equal_reproducer() public { _receive_undo_compare(18371677541005923091065047412368542483005086202); } From 3a6c038d253588f238d50b3815abf85973a5daf6 Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 14 Oct 2024 14:20:04 +0200 Subject: [PATCH 39/93] fix: bribe CEI --- src/BribeInitiative.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/BribeInitiative.sol b/src/BribeInitiative.sol index 8da4d51..b1d8ad0 100644 --- a/src/BribeInitiative.sol +++ b/src/BribeInitiative.sol @@ -56,8 +56,6 @@ contract BribeInitiative is IInitiative, IBribeInitiative { /// @inheritdoc IBribeInitiative function depositBribe(uint128 _boldAmount, uint128 _bribeTokenAmount, uint16 _epoch) external { - bold.safeTransferFrom(msg.sender, address(this), _boldAmount); - bribeToken.safeTransferFrom(msg.sender, address(this), _bribeTokenAmount); uint16 epoch = governance.epoch(); require(_epoch > epoch, "BribeInitiative: only-future-epochs"); @@ -68,6 +66,9 @@ contract BribeInitiative is IInitiative, IBribeInitiative { bribeByEpoch[_epoch] = bribe; emit DepositBribe(msg.sender, _boldAmount, _bribeTokenAmount, _epoch); + + bold.safeTransferFrom(msg.sender, address(this), _boldAmount); + bribeToken.safeTransferFrom(msg.sender, address(this), _bribeTokenAmount); } function _claimBribe( From af5132f20ae264a2f0eb0ffa0a0d1a7054799a70 Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 14 Oct 2024 14:20:31 +0200 Subject: [PATCH 40/93] fix: cannot claim in the future --- src/BribeInitiative.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BribeInitiative.sol b/src/BribeInitiative.sol index b1d8ad0..cd810d6 100644 --- a/src/BribeInitiative.sol +++ b/src/BribeInitiative.sol @@ -77,7 +77,7 @@ contract BribeInitiative is IInitiative, IBribeInitiative { uint16 _prevLQTYAllocationEpoch, uint16 _prevTotalLQTYAllocationEpoch ) internal returns (uint256 boldAmount, uint256 bribeTokenAmount) { - require(_epoch != governance.epoch(), "BribeInitiative: cannot-claim-for-current-epoch"); + require(_epoch < governance.epoch(), "BribeInitiative: cannot-claim-for-current-epoch"); require(!claimedBribeAtEpoch[_user][_epoch], "BribeInitiative: already-claimed"); Bribe memory bribe = bribeByEpoch[_epoch]; From f118d1f28abd44bc1b23d9c00283638fb163bee5 Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 14 Oct 2024 14:25:41 +0200 Subject: [PATCH 41/93] feat: rational flow --- src/BribeInitiative.sol | 2 +- test/BribeInitiative.t.sol | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/BribeInitiative.sol b/src/BribeInitiative.sol index cd810d6..6392c76 100644 --- a/src/BribeInitiative.sol +++ b/src/BribeInitiative.sol @@ -58,7 +58,7 @@ contract BribeInitiative is IInitiative, 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: only-future-epochs"); Bribe memory bribe = bribeByEpoch[_epoch]; bribe.boldAmount += _boldAmount; diff --git a/test/BribeInitiative.t.sol b/test/BribeInitiative.t.sol index b40aedb..6e4931c 100644 --- a/test/BribeInitiative.t.sol +++ b/test/BribeInitiative.t.sol @@ -346,7 +346,7 @@ contract BribeInitiativeTest is Test { assertEq(totalLQTYAllocated, 0); } - // forge test --match-test test_rationalFlow -vv + // forge test --match-test test_rationalFlow -vvvv function test_rationalFlow() public { vm.warp(block.timestamp + (EPOCH_DURATION)); // Initiative not active @@ -359,12 +359,12 @@ contract BribeInitiativeTest is Test { _allocateLQTY(user1, 5e17, 0); /// @audit Allocate b4 or after bribe should be irrelevant /// @audit WTF - _depositBribe(1e18, 1e18, governance.epoch() + 1); /// @audit IMO this should also work + _depositBribe(1e18, 1e18, governance.epoch()); /// @audit IMO this should also work _allocateLQTY(user1, 5e17, 0); /// @audit Allocate b4 or after bribe should be irrelevant // deposit bribe for Epoch + 2 - _depositBribe(1e18, 1e18, governance.epoch() + 2); + _depositBribe(1e18, 1e18, governance.epoch() + 1); (uint88 totalLQTYAllocated,) = @@ -378,10 +378,13 @@ contract BribeInitiativeTest is Test { // We are now at epoch + 1 // Should be able to claim epoch - 1 // user should receive bribe from their allocated stake - (uint256 boldAmount, uint256 bribeTokenAmount) = _claimBribe(user1, governance.epoch(), governance.epoch() - 1, governance.epoch() - 1); + (uint256 boldAmount, uint256 bribeTokenAmount) = _claimBribe(user1, governance.epoch() - 1, governance.epoch() - 1, governance.epoch() - 1); assertEq(boldAmount, 1e18, "bold amount"); assertEq(bribeTokenAmount, 1e18, "bribe amount"); + // And they cannot claim the one that is being added currently + _claimBribe(user1, governance.epoch(), governance.epoch() - 1, governance.epoch() - 1, true); + // decrease user allocation for the initiative _allocateLQTY(user1, -1e18, 0); @@ -643,11 +646,18 @@ contract BribeInitiativeTest is Test { } function _claimBribe(address claimer, uint16 epoch, uint16 prevLQTYAllocationEpoch, uint16 prevTotalLQTYAllocationEpoch) public returns (uint256 boldAmount, uint256 bribeTokenAmount){ + return _claimBribe(claimer, epoch, prevLQTYAllocationEpoch, prevTotalLQTYAllocationEpoch, false); + } + + function _claimBribe(address claimer, uint16 epoch, uint16 prevLQTYAllocationEpoch, uint16 prevTotalLQTYAllocationEpoch, bool expectRevert) public returns (uint256 boldAmount, uint256 bribeTokenAmount){ vm.startPrank(claimer); BribeInitiative.ClaimData[] memory epochs = new BribeInitiative.ClaimData[](1); epochs[0].epoch = epoch; epochs[0].prevLQTYAllocationEpoch = prevLQTYAllocationEpoch; epochs[0].prevTotalLQTYAllocationEpoch = prevTotalLQTYAllocationEpoch; + if(expectRevert) { + vm.expectRevert(); + } (boldAmount, bribeTokenAmount) = bribeInitiative.claimBribes(epochs); vm.stopPrank(); } From 12f94ed957d8e0af340ffdda732380ad8dd3f892 Mon Sep 17 00:00:00 2001 From: gallo Date: Mon, 14 Oct 2024 14:34:57 +0200 Subject: [PATCH 42/93] fix: use lib for encoding / decoding --- src/BribeInitiative.sol | 9 ++++++++- src/utils/EncodingDecodingLib.sol | 13 +++++++++++++ test/EncodingDecoding.t.sol | 25 ++++++++----------------- 3 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 src/utils/EncodingDecodingLib.sol diff --git a/src/BribeInitiative.sol b/src/BribeInitiative.sol index 85b99dd..da762b6 100644 --- a/src/BribeInitiative.sol +++ b/src/BribeInitiative.sol @@ -10,6 +10,10 @@ import {IBribeInitiative} from "./interfaces/IBribeInitiative.sol"; import {DoubleLinkedList} from "./utils/DoubleLinkedList.sol"; + +import {EncodingDecodingLib} from "src/utils/EncodingDecodingLib.sol"; + + contract BribeInitiative is IInitiative, IBribeInitiative { using SafeERC20 for IERC20; using DoubleLinkedList for DoubleLinkedList.List; @@ -162,8 +166,11 @@ contract BribeInitiative is IInitiative, IBribeInitiative { emit ModifyLQTYAllocation(_user, _epoch, _lqty, _averageTimestamp); } + function _encodeLQTYAllocation(uint88 _lqty, uint32 _averageTimestamp) private pure returns (uint224) { + return EncodingDecodingLib.encodeLQTYAllocation(_lqty, _averageTimestamp); + } function _decodeLQTYAllocation(uint224 _value) private pure returns (uint88, uint32) { - return (uint88(_value >> 32), uint32(_value)); + return EncodingDecodingLib.decodeLQTYAllocation(_value); } function _loadTotalLQTYAllocation(uint16 _epoch) private view returns (uint88, uint32) { diff --git a/src/utils/EncodingDecodingLib.sol b/src/utils/EncodingDecodingLib.sol new file mode 100644 index 0000000..1f4d05f --- /dev/null +++ b/src/utils/EncodingDecodingLib.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +library EncodingDecodingLib { + function encodeLQTYAllocation(uint88 _lqty, uint32 _averageTimestamp) public pure returns (uint224) { + uint224 _value = (uint224(_lqty) << 32) | _averageTimestamp; + return _value; + } + + function decodeLQTYAllocation(uint224 _value) public pure returns (uint88, uint32) { + return (uint88(_value >> 32), uint32(_value)); + } +} \ No newline at end of file diff --git a/test/EncodingDecoding.t.sol b/test/EncodingDecoding.t.sol index 4598c1f..f940722 100644 --- a/test/EncodingDecoding.t.sol +++ b/test/EncodingDecoding.t.sol @@ -3,29 +3,20 @@ pragma solidity ^0.8.24; import {Test, console2} from "forge-std/Test.sol"; -library EncodingDecoding { - function encodeLQTYAllocation(uint88 _lqty, uint32 _averageTimestamp) public pure returns (uint224) { - uint224 _value = (uint224(_lqty) << 32) | _averageTimestamp; - return _value; - } - - function decodeLQTYAllocation(uint224 _value) public pure returns (uint88, uint32) { - return (uint88(_value >> 32), uint32(_value)); - } -} +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 { - uint224 encodedValue = EncodingDecoding.encodeLQTYAllocation(lqty, averageTimestamp); - (uint88 decodedLqty, uint32 decodedAverageTimestamp) = EncodingDecoding.decodeLQTYAllocation(encodedValue); + uint224 encodedValue = EncodingDecodingLib.encodeLQTYAllocation(lqty, averageTimestamp); + (uint88 decodedLqty, uint32 decodedAverageTimestamp) = EncodingDecodingLib.decodeLQTYAllocation(encodedValue); assertEq(lqty, decodedLqty); assertEq(averageTimestamp, decodedAverageTimestamp); // Redo - uint224 reEncoded = EncodingDecoding.encodeLQTYAllocation(decodedLqty, decodedAverageTimestamp); - (uint88 reDecodedLqty, uint32 reDecodedAverageTimestamp) = EncodingDecoding.decodeLQTYAllocation(encodedValue); + uint224 reEncoded = EncodingDecodingLib.encodeLQTYAllocation(decodedLqty, decodedAverageTimestamp); + (uint88 reDecodedLqty, uint32 reDecodedAverageTimestamp) = EncodingDecodingLib.decodeLQTYAllocation(encodedValue); assertEq(reEncoded, encodedValue); assertEq(reDecodedLqty, decodedLqty); @@ -46,10 +37,10 @@ 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) = EncodingDecoding.decodeLQTYAllocation(encodedValue); + (uint88 decodedLqty, uint32 decodedAverageTimestamp) = EncodingDecodingLib.decodeLQTYAllocation(encodedValue); - uint224 encodedValue2 = EncodingDecoding.encodeLQTYAllocation(decodedLqty, decodedAverageTimestamp); - (uint88 decodedLqty2, uint32 decodedAverageTimestamp2) = EncodingDecoding.decodeLQTYAllocation(encodedValue2); + uint224 encodedValue2 = EncodingDecodingLib.encodeLQTYAllocation(decodedLqty, decodedAverageTimestamp); + (uint88 decodedLqty2, uint32 decodedAverageTimestamp2) = EncodingDecodingLib.decodeLQTYAllocation(encodedValue2); assertEq(encodedValue, encodedValue2, "encoded values not equal"); assertEq(decodedLqty, decodedLqty2, "decoded lqty not equal"); From 79ab94231b0fca4d6b6f14e1256ba8dce0959467 Mon Sep 17 00:00:00 2001 From: nelson-pereira8 <94120714+nican0r@users.noreply.github.com> Date: Mon, 14 Oct 2024 10:18:34 -0300 Subject: [PATCH 43/93] chore: adding governance scaffolding --- .gitignore | 5 ++ .gitmodules | 3 + echidna.yaml | 10 +++ lib/chimera | 1 + medusa.json | 88 +++++++++++++++++++ remappings.txt | 2 + test/mocks/MaliciousInitiative.sol | 77 +++++++++++++++++ test/mocks/MockERC20Tester.sol | 24 ++++++ test/recon/BeforeAfter.sol | 23 +++++ test/recon/CryticTester.sol | 13 +++ test/recon/CryticToFoundry.sol | 0 test/recon/PROPERTIES.md | 30 +++++++ test/recon/Properties.sol | 11 +++ test/recon/Setup.sol | 103 +++++++++++++++++++++++ test/recon/TargetFunctions.sol | 131 +++++++++++++++++++++++++++++ 15 files changed, 521 insertions(+) create mode 100644 echidna.yaml create mode 160000 lib/chimera create mode 100644 medusa.json create mode 100644 test/mocks/MaliciousInitiative.sol create mode 100644 test/mocks/MockERC20Tester.sol create mode 100644 test/recon/BeforeAfter.sol create mode 100644 test/recon/CryticTester.sol create mode 100644 test/recon/CryticToFoundry.sol create mode 100644 test/recon/PROPERTIES.md create mode 100644 test/recon/Properties.sol create mode 100644 test/recon/Setup.sol create mode 100644 test/recon/TargetFunctions.sol diff --git a/.gitignore b/.gitignore index 9d91b78..e116194 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ docs/ # Dotenv file .env + +# Fuzzing +crytic-export/ +echidna/ +medusa/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index c54835d..589e92c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/solmate"] path = lib/solmate url = https://github.com/Transmissions11/solmate +[submodule "lib/chimera"] + path = lib/chimera + url = https://github.com/Recon-Fuzz/chimera diff --git a/echidna.yaml b/echidna.yaml new file mode 100644 index 0000000..710dec1 --- /dev/null +++ b/echidna.yaml @@ -0,0 +1,10 @@ +testMode: "assertion" +prefix: "crytic_" +coverage: true +corpusDir: "echidna" +balanceAddr: 0x1043561a8829300000 +balanceContract: 0x1043561a8829300000 +filterFunctions: [] +cryticArgs: ["--foundry-compile-all"] +testMode: "exploration" +testLimit: 500000000 \ No newline at end of file diff --git a/lib/chimera b/lib/chimera new file mode 160000 index 0000000..d5cf52b --- /dev/null +++ b/lib/chimera @@ -0,0 +1 @@ +Subproject commit d5cf52bc5bbf75f988f8aada23fd12d0bcf7798a diff --git a/medusa.json b/medusa.json new file mode 100644 index 0000000..ea7baa0 --- /dev/null +++ b/medusa.json @@ -0,0 +1,88 @@ +{ + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 0, + "callSequenceLength": 100, + "corpusDirectory": "medusa", + "coverageEnabled": true, + "deploymentOrder": [ + "CryticTester" + ], + "targetContracts": [ + "CryticTester" + ], + "targetContractsBalances": [ + "0x27b46536c66c8e3000000" + ], + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 125000000, + "transactionGasLimit": 12500000, + "testing": { + "stopOnFailedTest": false, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": true, + "panicCodeConfig": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": [ + "crytic_" + ] + }, + "optimizationTesting": { + "enabled": false, + "testPrefixes": [ + "optimize_" + ] + } + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": false + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": ".", + "solcVersion": "", + "exportDirectory": "", + "args": [ + "--foundry-compile-all" + ] + } + }, + "logging": { + "level": "info", + "logDirectory": "" + } + } \ No newline at end of file diff --git a/remappings.txt b/remappings.txt index 41b6872..49149d1 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1 +1,3 @@ v4-core/=lib/v4-core/ +forge-std/=lib/forge-std/src/ +@chimera/=lib/chimera/src/ \ No newline at end of file diff --git a/test/mocks/MaliciousInitiative.sol b/test/mocks/MaliciousInitiative.sol new file mode 100644 index 0000000..2dbd60c --- /dev/null +++ b/test/mocks/MaliciousInitiative.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {IInitiative} from "src/interfaces/IInitiative.sol"; + +contract MaliciousInitiative is IInitiative { + + enum FunctionType { + NONE, + REGISTER, + UNREGISTER, + ALLOCATE, + CLAIM + } + + enum RevertType { + NONE, + THROW, + OOG, + RETURN_BOMB, + REVERT_BOMB + } + + mapping (FunctionType => RevertType) revertBehaviours; + + /// @dev specify the revert behaviour on each function + function setRevertBehaviour(FunctionType ft, RevertType rt) external { + revertBehaviours[ft] = rt; + } + + // Do stuff on each hook + function onRegisterInitiative(uint16 _atEpoch) external override { + _performRevertBehaviour(revertBehaviours[FunctionType.REGISTER]); + } + + function onUnregisterInitiative(uint16 _atEpoch) external override { + _performRevertBehaviour(revertBehaviours[FunctionType.UNREGISTER]); + } + + + function onAfterAllocateLQTY(uint16 _currentEpoch, address _user, uint88 _voteLQTY, uint88 _vetoLQTY) external override { + _performRevertBehaviour(revertBehaviours[FunctionType.ALLOCATE]); + } + + function onClaimForInitiative(uint16 _claimEpoch, uint256 _bold) external override { + _performRevertBehaviour(revertBehaviours[FunctionType.CLAIM]); + } + + function _performRevertBehaviour(RevertType action) internal { + if(action == RevertType.THROW) { + revert("A normal Revert"); + } + + // 3 gas per iteration, consider changing to storage changes if traces are cluttered + if(action == RevertType.OOG) { + uint256 i; + while(true) { + ++i; + } + } + + if(action == RevertType.RETURN_BOMB) { + uint256 _bytes = 2_000_000; + assembly { + return(0, _bytes) + } + } + + if(action == RevertType.REVERT_BOMB) { + uint256 _bytes = 2_000_000; + assembly { + revert(0, _bytes) + } + } + + return; // NONE + } +} \ No newline at end of file diff --git a/test/mocks/MockERC20Tester.sol b/test/mocks/MockERC20Tester.sol new file mode 100644 index 0000000..506e738 --- /dev/null +++ b/test/mocks/MockERC20Tester.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {MockERC20} from "forge-std/mocks/MockERC20.sol"; + +contract MockERC20Tester is MockERC20 { + address owner; + + modifier onlyOwner() { + require(msg.sender == owner); + _; + } + + constructor(address recipient, uint256 mintAmount, string memory name, string memory symbol, uint8 decimals) { + super.initialize(name, symbol, decimals); + _mint(recipient, mintAmount); + + owner = msg.sender; + } + + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } +} \ No newline at end of file diff --git a/test/recon/BeforeAfter.sol b/test/recon/BeforeAfter.sol new file mode 100644 index 0000000..3fc3b19 --- /dev/null +++ b/test/recon/BeforeAfter.sol @@ -0,0 +1,23 @@ + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {Setup} from "./Setup.sol"; + +abstract contract BeforeAfter is Setup { + + // struct Vars { + + // } + + // Vars internal _before; + // Vars internal _after; + + function __before() internal { + + } + + function __after() internal { + + } +} \ No newline at end of file diff --git a/test/recon/CryticTester.sol b/test/recon/CryticTester.sol new file mode 100644 index 0000000..41d603c --- /dev/null +++ b/test/recon/CryticTester.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {TargetFunctions} from "./TargetFunctions.sol"; +import {CryticAsserts} from "@chimera/CryticAsserts.sol"; + +// echidna . --contract CryticTester --config echidna.yaml +// medusa fuzz +contract CryticTester is TargetFunctions, CryticAsserts { + constructor() payable { + setup(); + } +} \ No newline at end of file diff --git a/test/recon/CryticToFoundry.sol b/test/recon/CryticToFoundry.sol new file mode 100644 index 0000000..e69de29 diff --git a/test/recon/PROPERTIES.md b/test/recon/PROPERTIES.md new file mode 100644 index 0000000..2460019 --- /dev/null +++ b/test/recon/PROPERTIES.md @@ -0,0 +1,30 @@ +## BribeInitiative + +| Property | Description | Tested | +| --- | --- | --- | +| BI-01 | User always receives their share of bribe distribution amount when claimed, never more or less | | +| BI-02 | User can always claim bribe for an epoch in which they were allocated | | + +## Governance +| Property | Description | Tested | +| --- | --- | --- | +| GV-01 | Initiative state should only return one state per epoch | | +| GV-02 | Initiative in Unregistered state reverts if a user tries to reregister it | | +| GV-03 | Initiative in Unregistered state reverts if a user tries to unregister it | | +| GV-04 | Initiative in Unregistered state reverts if a user tries to claim rewards for it | | +| GV-05 | A user can always vote if an initiative is active | | +| GV-06 | A user can always remove votes if an initiative is inactive | | +| GV-07 | A user cannot allocate to an initiative if it’s inactive | | +| GV-08 | A user cannot vote more than their voting power | | +| GV-09 | The sum of votes ≤ total votes | | +| GV-10 | Contributions are linear | | +| GV-11 | Initiatives that are removable can’t be blocked from being removed | | +| GV-12 | Removing vote allocation in multiple chunks results in 100% of requested amount being removed | | +| GV-13 | If a user has X votes and removes Y votes, they always have X - Y votes left | | +| GV-14 | If a user has X votes and removes Y votes, then withdraws X - Y votes they have 0 left | | +| GV-15 | A newly created initiative should be in `SKIP` state | | +| GV-16 | An initiative that didn't meet the threshold should be in `SKIP` | | +| GV-17 | An initiative that has sufficiently high vetoes in the next epoch should be `UNREGISTERABLE` | | +| GV-18 | An initiative that has reached sufficient votes in the previous epoch should become `CLAIMABLE` in this epoch | | +| GV-19 | A `CLAIMABLE` initiative can remain `CLAIMABLE` in the epoch, or can become `CLAIMED` once someone claims the rewards | | +| GV-20 | A `CLAIMABLE` initiative can become `CLAIMED` once someone claims the rewards | | \ No newline at end of file diff --git a/test/recon/Properties.sol b/test/recon/Properties.sol new file mode 100644 index 0000000..f9db106 --- /dev/null +++ b/test/recon/Properties.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {Asserts} from "@chimera/Asserts.sol"; +import {Setup} from "./Setup.sol"; + +abstract contract Properties is Setup, Asserts { + + function property_ + +} \ No newline at end of file diff --git a/test/recon/Setup.sol b/test/recon/Setup.sol new file mode 100644 index 0000000..4022278 --- /dev/null +++ b/test/recon/Setup.sol @@ -0,0 +1,103 @@ + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseSetup} from "@chimera/BaseSetup.sol"; +import {console2} from "forge-std/Test.sol"; +import {vm} from "@chimera/Hevm.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +import {MockERC20Tester} from "../mocks/MockERC20Tester.sol"; +import {MockStakingV1} from "../mocks/MockStakingV1.sol"; +import {MaliciousInitiative} from "../mocks/MaliciousInitiative.sol"; +import {Governance} from "src/Governance.sol"; +import {IGovernance} from "src/interfaces/IGovernance.sol"; +import {IInitiative} from "src/interfaces/IInitiative.sol"; + +abstract contract Setup is BaseSetup { + Governance governance; + MockERC20Tester internal lqty; + MockERC20Tester internal lusd; + IInitiative internal initiative1; + + address internal user = address(this); + // derived using makeAddrAndKey + address internal user2 = address(0x537C8f3d3E18dF5517a58B3fB9D9143697996802); + address[] internal users = new address[](2); + uint256 internal user2Pk = 23868421370328131711506074113045611601786642648093516849953535378706721142721; + address internal stakingV1; + address internal userProxy; + address[] internal deployedInitiatives; + + uint128 internal constant REGISTRATION_FEE = 1e18; + uint128 internal constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; + uint128 internal constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; + uint16 internal constant REGISTRATION_WARM_UP_PERIOD = 4; + uint16 internal constant UNREGISTRATION_AFTER_EPOCHS = 4; + uint128 internal constant VOTING_THRESHOLD_FACTOR = 0.04e18; + uint88 internal constant MIN_CLAIM = 500e18; + uint88 internal constant MIN_ACCRUAL = 1000e18; + uint32 internal constant EPOCH_DURATION = 604800; + uint32 internal constant EPOCH_VOTING_CUTOFF = 518400; + + + function setup() internal virtual override { + users.push(user); + users.push(user2); + + uint256 initialMintAmount = type(uint88).max; + lqty = new MockERC20Tester(user, initialMintAmount, "Liquity", "LQTY", 18); + lusd = new MockERC20Tester(user, initialMintAmount, "Liquity USD", "LUSD", 18); + lqty.mint(user2, initialMintAmount); + + stakingV1 = address(new MockStakingV1(address(lqty))); + governance = new Governance( + address(lqty), + address(lusd), + stakingV1, + address(lusd), // bold + IGovernance.Configuration({ + registrationFee: REGISTRATION_FEE, + registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, + unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, + registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, + unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, + votingThresholdFactor: VOTING_THRESHOLD_FACTOR, + minClaim: MIN_CLAIM, + minAccrual: MIN_ACCRUAL, + epochStart: uint32(block.timestamp), + epochDuration: EPOCH_DURATION, + epochVotingCutoff: EPOCH_VOTING_CUTOFF + }), + deployedInitiatives // no initial initiatives passed in because don't have cheatcodes for calculating address where gov will be deployed + ); + + // deploy proxy so user can approve it + userProxy = governance.deployUserProxy(); + lqty.approve(address(userProxy), initialMintAmount); + lusd.approve(address(userProxy), initialMintAmount); + + // approve governance for user's tokens + lqty.approve(address(governance), initialMintAmount); + lusd.approve(address(governance), initialMintAmount); + + // register one of the initiatives, leave the other for registering/unregistering via TargetFunction + initiative1 = IInitiative(address(new MaliciousInitiative())); + deployedInitiatives.push(address(initiative1)); + + governance.registerInitiative(address(initiative1)); + } + + function _getDeployedInitiative(uint8 index) internal returns (address initiative) { + return deployedInitiatives[index % deployedInitiatives.length]; + } + + function _getClampedTokenBalance(address token, address holder) internal returns (uint256 balance) { + return IERC20(token).balanceOf(holder); + } + + function _getRandomUser(uint8 index) internal returns (address randomUser) { + return users[index % users.length]; + } + +} \ No newline at end of file diff --git a/test/recon/TargetFunctions.sol b/test/recon/TargetFunctions.sol new file mode 100644 index 0000000..4ccb312 --- /dev/null +++ b/test/recon/TargetFunctions.sol @@ -0,0 +1,131 @@ + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; +import {vm} from "@chimera/Hevm.sol"; +import {IERC20Permit} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; + +import {console2} from "forge-std/Test.sol"; +import {BeforeAfter} from "./BeforeAfter.sol"; +import {Properties} from "./Properties.sol"; +import {MaliciousInitiative} from "../mocks/MaliciousInitiative.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"; + + +abstract contract TargetFunctions is BaseTargetFunctions, Properties, BeforeAfter { + + // clamps to a single initiative to ensure coverage in case both haven't been registered yet + function governance_allocateLQTY_clamped_single_initiative(uint8 initiativesIndex, uint96 deltaLQTYVotes, uint96 deltaLQTYVetos) public { + // clamp using the user's staked balance + uint96 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user)).staked(); + + address[] memory initiatives = new address[](1); + initiatives[0] = _getDeployedInitiative(initiativesIndex); + int88[] memory deltaLQTYVotesArray = new int88[](1); + deltaLQTYVotesArray[0] = int88(uint88(deltaLQTYVotes % stakedAmount)); + int88[] memory deltaLQTYVetosArray = new int88[](1); + deltaLQTYVetosArray[0] = int88(uint88(deltaLQTYVetos % stakedAmount)); + + governance.allocateLQTY(initiatives, deltaLQTYVotesArray, deltaLQTYVetosArray); + } + + function governance_allocateLQTY(int88[] calldata _deltaLQTYVotes, int88[] calldata _deltaLQTYVetos) public { + governance.allocateLQTY(deployedInitiatives, _deltaLQTYVotes, _deltaLQTYVetos); + } + + function governance_claimForInitiative(uint8 initiativeIndex) public { + address initiative = _getDeployedInitiative(initiativeIndex); + governance.claimForInitiative(initiative); + } + + function governance_claimFromStakingV1(uint8 recipientIndex) public { + address rewardRecipient = _getRandomUser(recipientIndex); + governance.claimFromStakingV1(rewardRecipient); + } + + function governance_deployUserProxy() public { + governance.deployUserProxy(); + } + + function governance_depositLQTY(uint88 lqtyAmount) public { + lqtyAmount = uint88(lqtyAmount % lqty.balanceOf(user)); + governance.depositLQTY(lqtyAmount); + } + + function governance_depositLQTYViaPermit(uint88 _lqtyAmount) public { + // Get the current block timestamp for the deadline + uint256 deadline = block.timestamp + 1 hours; + + // Create the permit message + bytes32 permitTypeHash = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 domainSeparator = IERC20Permit(address(lqty)).DOMAIN_SEPARATOR(); + + + uint256 nonce = IERC20Permit(address(lqty)).nonces(user); + + bytes32 structHash = keccak256(abi.encode( + permitTypeHash, + user, + address(governance), + _lqtyAmount, + nonce, + deadline + )); + + bytes32 digest = keccak256(abi.encodePacked( + "\x19\x01", + domainSeparator, + structHash + )); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(user2Pk, digest); + + PermitParams memory permitParams = PermitParams({ + owner: user2, + spender: user, + value: _lqtyAmount, + deadline: deadline, + v: v, + r: r, + s: s + }); + + governance.depositLQTYViaPermit(_lqtyAmount, permitParams); + } + + function governance_registerInitiative(uint8 initiativeIndex) public { + address initiative = _getDeployedInitiative(initiativeIndex); + governance.registerInitiative(initiative); + } + + function governance_snapshotVotesForInitiative(address _initiative) public { + governance.snapshotVotesForInitiative(_initiative); + } + + function governance_unregisterInitiative(uint8 initiativeIndex) public { + address initiative = _getDeployedInitiative(initiativeIndex); + governance.unregisterInitiative(initiative); + } + + function governance_withdrawLQTY(uint88 _lqtyAmount) public { + governance.withdrawLQTY(_lqtyAmount); + } + + // helper to deploy initiatives for registering that results in more bold transferred to the Governance contract + function governance_deployInitiative() public { + address initiative = address(new MaliciousInitiative()); + deployedInitiatives.push(initiative); + } + + // helper to simulate bold accrual in Governance contract + function governance_accrueBold(uint88 boldAmount) public { + boldAmount = uint88(boldAmount % lusd.balanceOf(user)); + lusd.transfer(address(governance), boldAmount); // target contract is the user so it can transfer directly + } + + +} \ No newline at end of file From d94b2cde8744b1a49e97290465c31cff5474e5cd Mon Sep 17 00:00:00 2001 From: nelson-pereira8 <94120714+nican0r@users.noreply.github.com> Date: Mon, 14 Oct 2024 10:55:34 -0300 Subject: [PATCH 44/93] feat: property_GV01 --- test/recon/BeforeAfter.sol | 34 +++++++++++++++++++++++++++------- test/recon/Properties.sol | 16 ++++++++++++---- test/recon/TargetFunctions.sol | 29 ++++++++++++++--------------- 3 files changed, 53 insertions(+), 26 deletions(-) diff --git a/test/recon/BeforeAfter.sol b/test/recon/BeforeAfter.sol index 3fc3b19..e811ced 100644 --- a/test/recon/BeforeAfter.sol +++ b/test/recon/BeforeAfter.sol @@ -2,22 +2,42 @@ // SPDX-License-Identifier: GPL-2.0 pragma solidity ^0.8.0; +import {Asserts} from "@chimera/Asserts.sol"; import {Setup} from "./Setup.sol"; +import {IGovernance} from "../../src/interfaces/IGovernance.sol"; +import {Governance} from "../../src/Governance.sol"; -abstract contract BeforeAfter is Setup { - // struct Vars { +abstract contract BeforeAfter is Setup, Asserts { + struct Vars { + uint16 epoch; + mapping(address => Governance.InitiativeStatus) initiativeStatus; + } - // } + Vars internal _before; + Vars internal _after; - // Vars internal _before; - // Vars internal _after; + modifier withChecks { + __before(); + _; + __after(); + } function __before() internal { - + _before.epoch = governance.epoch(); + for(uint8 i; i < deployedInitiatives.length; i++) { + address initiative = deployedInitiatives[i]; + (Governance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); + _before.initiativeStatus[initiative] = status; + } } function __after() internal { - + _before.epoch = governance.epoch(); + for(uint8 i; i < deployedInitiatives.length; i++) { + address initiative = deployedInitiatives[i]; + (Governance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); + _after.initiativeStatus[initiative] = status; + } } } \ No newline at end of file diff --git a/test/recon/Properties.sol b/test/recon/Properties.sol index f9db106..c8a45c7 100644 --- a/test/recon/Properties.sol +++ b/test/recon/Properties.sol @@ -1,11 +1,19 @@ // SPDX-License-Identifier: GPL-2.0 pragma solidity ^0.8.0; -import {Asserts} from "@chimera/Asserts.sol"; -import {Setup} from "./Setup.sol"; +import {BeforeAfter} from "./BeforeAfter.sol"; -abstract contract Properties is Setup, Asserts { +abstract contract Properties is BeforeAfter { - function property_ + function property_GV01() public { + // first check that epoch hasn't changed after the operation + if(_before.epoch == _after.epoch) { + // loop through the initiatives and check that their status hasn't changed + for(uint8 i; i < deployedInitiatives.length; i++) { + address initiative = deployedInitiatives[i]; + eq(uint256(_before.initiativeStatus[initiative]), uint256(_after.initiativeStatus[initiative]), "GV-01: Initiative state should only return one state per epoch"); + } + } + } } \ No newline at end of file diff --git a/test/recon/TargetFunctions.sol b/test/recon/TargetFunctions.sol index 4ccb312..5e209d7 100644 --- a/test/recon/TargetFunctions.sol +++ b/test/recon/TargetFunctions.sol @@ -7,7 +7,6 @@ import {vm} from "@chimera/Hevm.sol"; import {IERC20Permit} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; import {console2} from "forge-std/Test.sol"; -import {BeforeAfter} from "./BeforeAfter.sol"; import {Properties} from "./Properties.sol"; import {MaliciousInitiative} from "../mocks/MaliciousInitiative.sol"; import {ILQTYStaking} from "../../src/interfaces/ILQTYStaking.sol"; @@ -16,10 +15,10 @@ import {IUserProxy} from "../../src/interfaces/IUserProxy.sol"; import {PermitParams} from "../../src/utils/Types.sol"; -abstract contract TargetFunctions is BaseTargetFunctions, Properties, BeforeAfter { +abstract contract TargetFunctions is BaseTargetFunctions, Properties { // clamps to a single initiative to ensure coverage in case both haven't been registered yet - function governance_allocateLQTY_clamped_single_initiative(uint8 initiativesIndex, uint96 deltaLQTYVotes, uint96 deltaLQTYVetos) public { + function governance_allocateLQTY_clamped_single_initiative(uint8 initiativesIndex, uint96 deltaLQTYVotes, uint96 deltaLQTYVetos) withChecks public { // clamp using the user's staked balance uint96 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user)).staked(); @@ -33,30 +32,30 @@ abstract contract TargetFunctions is BaseTargetFunctions, Properties, BeforeAfte governance.allocateLQTY(initiatives, deltaLQTYVotesArray, deltaLQTYVetosArray); } - function governance_allocateLQTY(int88[] calldata _deltaLQTYVotes, int88[] calldata _deltaLQTYVetos) public { + function governance_allocateLQTY(int88[] calldata _deltaLQTYVotes, int88[] calldata _deltaLQTYVetos) withChecks public { governance.allocateLQTY(deployedInitiatives, _deltaLQTYVotes, _deltaLQTYVetos); } - function governance_claimForInitiative(uint8 initiativeIndex) public { + function governance_claimForInitiative(uint8 initiativeIndex) withChecks public { address initiative = _getDeployedInitiative(initiativeIndex); governance.claimForInitiative(initiative); } - function governance_claimFromStakingV1(uint8 recipientIndex) public { + function governance_claimFromStakingV1(uint8 recipientIndex) withChecks public { address rewardRecipient = _getRandomUser(recipientIndex); governance.claimFromStakingV1(rewardRecipient); } - function governance_deployUserProxy() public { + function governance_deployUserProxy() withChecks public { governance.deployUserProxy(); } - function governance_depositLQTY(uint88 lqtyAmount) public { + function governance_depositLQTY(uint88 lqtyAmount) withChecks public { lqtyAmount = uint88(lqtyAmount % lqty.balanceOf(user)); governance.depositLQTY(lqtyAmount); } - function governance_depositLQTYViaPermit(uint88 _lqtyAmount) public { + function governance_depositLQTYViaPermit(uint88 _lqtyAmount) withChecks public { // Get the current block timestamp for the deadline uint256 deadline = block.timestamp + 1 hours; @@ -97,32 +96,32 @@ abstract contract TargetFunctions is BaseTargetFunctions, Properties, BeforeAfte governance.depositLQTYViaPermit(_lqtyAmount, permitParams); } - function governance_registerInitiative(uint8 initiativeIndex) public { + function governance_registerInitiative(uint8 initiativeIndex) withChecks public { address initiative = _getDeployedInitiative(initiativeIndex); governance.registerInitiative(initiative); } - function governance_snapshotVotesForInitiative(address _initiative) public { + function governance_snapshotVotesForInitiative(address _initiative) withChecks public { governance.snapshotVotesForInitiative(_initiative); } - function governance_unregisterInitiative(uint8 initiativeIndex) public { + function governance_unregisterInitiative(uint8 initiativeIndex) withChecks public { address initiative = _getDeployedInitiative(initiativeIndex); governance.unregisterInitiative(initiative); } - function governance_withdrawLQTY(uint88 _lqtyAmount) public { + function governance_withdrawLQTY(uint88 _lqtyAmount) withChecks public { governance.withdrawLQTY(_lqtyAmount); } // helper to deploy initiatives for registering that results in more bold transferred to the Governance contract - function governance_deployInitiative() public { + function governance_deployInitiative() withChecks public { address initiative = address(new MaliciousInitiative()); deployedInitiatives.push(initiative); } // helper to simulate bold accrual in Governance contract - function governance_accrueBold(uint88 boldAmount) public { + function governance_accrueBold(uint88 boldAmount) withChecks public { boldAmount = uint88(boldAmount % lusd.balanceOf(user)); lusd.transfer(address(governance), boldAmount); // target contract is the user so it can transfer directly } From 2e38b40fe43078479e641038da89f0f60768dbdc Mon Sep 17 00:00:00 2001 From: nelson-pereira8 <94120714+nican0r@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:01:19 -0300 Subject: [PATCH 45/93] feat: adding properties and scaffolding from bribe-initiative-invar-tester --- test/recon/BeforeAfter.sol | 19 ++- test/recon/Properties.sol | 16 +-- test/recon/Setup.sol | 17 ++- test/recon/TargetFunctions.sol | 115 ++-------------- .../properties/BribeInitiativeProperties.sol | 79 +++++++++++ .../recon/properties/GovernanceProperties.sol | 19 +++ test/recon/targets/BribeInitiativeTargets.sol | 54 ++++++++ test/recon/targets/GovernanceTargets.sol | 129 ++++++++++++++++++ 8 files changed, 322 insertions(+), 126 deletions(-) create mode 100644 test/recon/properties/BribeInitiativeProperties.sol create mode 100644 test/recon/properties/GovernanceProperties.sol create mode 100644 test/recon/targets/BribeInitiativeTargets.sol create mode 100644 test/recon/targets/GovernanceTargets.sol diff --git a/test/recon/BeforeAfter.sol b/test/recon/BeforeAfter.sol index e811ced..192196e 100644 --- a/test/recon/BeforeAfter.sol +++ b/test/recon/BeforeAfter.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.0; import {Asserts} from "@chimera/Asserts.sol"; import {Setup} from "./Setup.sol"; import {IGovernance} from "../../src/interfaces/IGovernance.sol"; +import {IBribeInitiative} from "../../src/interfaces/IBribeInitiative.sol"; import {Governance} from "../../src/Governance.sol"; @@ -12,6 +13,10 @@ abstract contract BeforeAfter is Setup, Asserts { struct Vars { uint16 epoch; mapping(address => Governance.InitiativeStatus) initiativeStatus; + // initiative => user => epoch => claimed + mapping(address => mapping(address => mapping(uint16 => bool))) claimedBribeForInitiativeAtEpoch; + uint128 lqtyBalance; + uint128 lusdBalance; } Vars internal _before; @@ -24,20 +29,30 @@ abstract contract BeforeAfter is Setup, Asserts { } function __before() internal { - _before.epoch = governance.epoch(); + uint16 currentEpoch = governance.epoch(); + _before.epoch = currentEpoch; for(uint8 i; i < deployedInitiatives.length; i++) { address initiative = deployedInitiatives[i]; (Governance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); _before.initiativeStatus[initiative] = status; + _before.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] = IBribeInitiative(initiative).claimedBribeAtEpoch(user, currentEpoch); } + + _before.lqtyBalance = uint128(lqty.balanceOf(user)); + _before.lusdBalance = uint128(lusd.balanceOf(user)); } function __after() internal { - _before.epoch = governance.epoch(); + uint16 currentEpoch = governance.epoch(); + _after.epoch = currentEpoch; for(uint8 i; i < deployedInitiatives.length; i++) { address initiative = deployedInitiatives[i]; (Governance.InitiativeStatus status,,) = governance.getInitiativeState(initiative); _after.initiativeStatus[initiative] = status; + _after.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] = IBribeInitiative(initiative).claimedBribeAtEpoch(user, currentEpoch); } + + _after.lqtyBalance = uint128(lqty.balanceOf(user)); + _after.lusdBalance = uint128(lusd.balanceOf(user)); } } \ No newline at end of file diff --git a/test/recon/Properties.sol b/test/recon/Properties.sol index c8a45c7..8d5494f 100644 --- a/test/recon/Properties.sol +++ b/test/recon/Properties.sol @@ -2,18 +2,8 @@ pragma solidity ^0.8.0; import {BeforeAfter} from "./BeforeAfter.sol"; +import {GovernanceProperties} from "./properties/GovernanceProperties.sol"; +import {BribeInitiativeProperties} from "./properties/BribeInitiativeProperties.sol"; -abstract contract Properties is BeforeAfter { - - function property_GV01() public { - // first check that epoch hasn't changed after the operation - if(_before.epoch == _after.epoch) { - // loop through the initiatives and check that their status hasn't changed - for(uint8 i; i < deployedInitiatives.length; i++) { - address initiative = deployedInitiatives[i]; - eq(uint256(_before.initiativeStatus[initiative]), uint256(_after.initiativeStatus[initiative]), "GV-01: Initiative state should only return one state per epoch"); - } - } - } - +abstract contract Properties is GovernanceProperties { } \ No newline at end of file diff --git a/test/recon/Setup.sol b/test/recon/Setup.sol index 4022278..0738c68 100644 --- a/test/recon/Setup.sol +++ b/test/recon/Setup.sol @@ -11,6 +11,8 @@ import {MockERC20Tester} from "../mocks/MockERC20Tester.sol"; import {MockStakingV1} from "../mocks/MockStakingV1.sol"; import {MaliciousInitiative} from "../mocks/MaliciousInitiative.sol"; import {Governance} from "src/Governance.sol"; +import {BribeInitiative} from "../../src/BribeInitiative.sol"; +import {IBribeInitiative} from "../../src/interfaces/IBribeInitiative.sol"; import {IGovernance} from "src/interfaces/IGovernance.sol"; import {IInitiative} from "src/interfaces/IInitiative.sol"; @@ -18,16 +20,19 @@ abstract contract Setup is BaseSetup { Governance governance; MockERC20Tester internal lqty; MockERC20Tester internal lusd; - IInitiative internal initiative1; + IBribeInitiative internal initiative1; address internal user = address(this); - // derived using makeAddrAndKey - address internal user2 = address(0x537C8f3d3E18dF5517a58B3fB9D9143697996802); - address[] internal users = new address[](2); - uint256 internal user2Pk = 23868421370328131711506074113045611601786642648093516849953535378706721142721; + address internal user2 = address(0x537C8f3d3E18dF5517a58B3fB9D9143697996802); // derived using makeAddrAndKey address internal stakingV1; address internal userProxy; + address[] internal users = new address[](2); address[] internal deployedInitiatives; + uint256 internal user2Pk = 23868421370328131711506074113045611601786642648093516849953535378706721142721; // derived using makeAddrAndKey + bool internal claimedTwice; + + mapping(uint16 => uint88) internal ghostTotalAllocationAtEpoch; + mapping(address => uint88) internal ghostLqtyAllocationByUserAtEpoch; uint128 internal constant REGISTRATION_FEE = 1e18; uint128 internal constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; @@ -82,7 +87,7 @@ abstract contract Setup is BaseSetup { lusd.approve(address(governance), initialMintAmount); // register one of the initiatives, leave the other for registering/unregistering via TargetFunction - initiative1 = IInitiative(address(new MaliciousInitiative())); + initiative1 = IBribeInitiative(address(new BribeInitiative(address(governance), address(lusd), address(lqty)))); deployedInitiatives.push(address(initiative1)); governance.registerInitiative(address(initiative1)); diff --git a/test/recon/TargetFunctions.sol b/test/recon/TargetFunctions.sol index 5e209d7..3345ac1 100644 --- a/test/recon/TargetFunctions.sol +++ b/test/recon/TargetFunctions.sol @@ -5,126 +5,31 @@ pragma solidity ^0.8.0; import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; import {vm} from "@chimera/Hevm.sol"; import {IERC20Permit} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; - import {console2} from "forge-std/Test.sol"; + import {Properties} from "./Properties.sol"; +import {GovernanceTargets} from "./targets/GovernanceTargets.sol"; +import {BribeInitiativeTargets} from "./targets/BribeInitiativeTargets.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"; -abstract contract TargetFunctions is BaseTargetFunctions, Properties { - - // clamps to a single initiative to ensure coverage in case both haven't been registered yet - function governance_allocateLQTY_clamped_single_initiative(uint8 initiativesIndex, uint96 deltaLQTYVotes, uint96 deltaLQTYVetos) withChecks public { - // clamp using the user's staked balance - uint96 stakedAmount = IUserProxy(governance.deriveUserProxyAddress(user)).staked(); - - address[] memory initiatives = new address[](1); - initiatives[0] = _getDeployedInitiative(initiativesIndex); - int88[] memory deltaLQTYVotesArray = new int88[](1); - deltaLQTYVotesArray[0] = int88(uint88(deltaLQTYVotes % stakedAmount)); - int88[] memory deltaLQTYVetosArray = new int88[](1); - deltaLQTYVetosArray[0] = int88(uint88(deltaLQTYVetos % stakedAmount)); - - governance.allocateLQTY(initiatives, deltaLQTYVotesArray, deltaLQTYVetosArray); - } - - function governance_allocateLQTY(int88[] calldata _deltaLQTYVotes, int88[] calldata _deltaLQTYVetos) withChecks public { - governance.allocateLQTY(deployedInitiatives, _deltaLQTYVotes, _deltaLQTYVetos); - } - - function governance_claimForInitiative(uint8 initiativeIndex) withChecks public { - address initiative = _getDeployedInitiative(initiativeIndex); - governance.claimForInitiative(initiative); - } - - function governance_claimFromStakingV1(uint8 recipientIndex) withChecks public { - address rewardRecipient = _getRandomUser(recipientIndex); - governance.claimFromStakingV1(rewardRecipient); - } - - function governance_deployUserProxy() withChecks public { - governance.deployUserProxy(); - } - - function governance_depositLQTY(uint88 lqtyAmount) withChecks public { - lqtyAmount = uint88(lqtyAmount % lqty.balanceOf(user)); - governance.depositLQTY(lqtyAmount); - } - - function governance_depositLQTYViaPermit(uint88 _lqtyAmount) withChecks public { - // Get the current block timestamp for the deadline - uint256 deadline = block.timestamp + 1 hours; - - // Create the permit message - bytes32 permitTypeHash = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - bytes32 domainSeparator = IERC20Permit(address(lqty)).DOMAIN_SEPARATOR(); - - - uint256 nonce = IERC20Permit(address(lqty)).nonces(user); - - bytes32 structHash = keccak256(abi.encode( - permitTypeHash, - user, - address(governance), - _lqtyAmount, - nonce, - deadline - )); - - bytes32 digest = keccak256(abi.encodePacked( - "\x19\x01", - domainSeparator, - structHash - )); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(user2Pk, digest); - - PermitParams memory permitParams = PermitParams({ - owner: user2, - spender: user, - value: _lqtyAmount, - deadline: deadline, - v: v, - r: r, - s: s - }); - - governance.depositLQTYViaPermit(_lqtyAmount, permitParams); - } - - function governance_registerInitiative(uint8 initiativeIndex) withChecks public { - address initiative = _getDeployedInitiative(initiativeIndex); - governance.registerInitiative(initiative); - } - - function governance_snapshotVotesForInitiative(address _initiative) withChecks public { - governance.snapshotVotesForInitiative(_initiative); - } - - function governance_unregisterInitiative(uint8 initiativeIndex) withChecks public { - address initiative = _getDeployedInitiative(initiativeIndex); - governance.unregisterInitiative(initiative); - } - - function governance_withdrawLQTY(uint88 _lqtyAmount) withChecks public { - governance.withdrawLQTY(_lqtyAmount); - } +abstract contract TargetFunctions is GovernanceTargets, BribeInitiativeTargets { // helper to deploy initiatives for registering that results in more bold transferred to the Governance contract - function governance_deployInitiative() withChecks public { - address initiative = address(new MaliciousInitiative()); + function helper_deployInitiative() withChecks public { + address initiative = address(new BribeInitiative(address(governance), address(lusd), address(lqty))); deployedInitiatives.push(initiative); } // helper to simulate bold accrual in Governance contract - function governance_accrueBold(uint88 boldAmount) withChecks public { + function helper_accrueBold(uint88 boldAmount) withChecks public { boldAmount = uint88(boldAmount % lusd.balanceOf(user)); - lusd.transfer(address(governance), boldAmount); // target contract is the user so it can transfer directly + // target contract is the user so it can transfer directly + lusd.transfer(address(governance), boldAmount); } - - } \ No newline at end of file diff --git a/test/recon/properties/BribeInitiativeProperties.sol b/test/recon/properties/BribeInitiativeProperties.sol new file mode 100644 index 0000000..3d2d6a0 --- /dev/null +++ b/test/recon/properties/BribeInitiativeProperties.sol @@ -0,0 +1,79 @@ + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BeforeAfter} from "../BeforeAfter.sol"; +import {IBribeInitiative} from "../../../src/interfaces/IBribeInitiative.sol"; + +abstract contract BribeInitiativeProperties is BeforeAfter { + // function property_BI01() public { + // uint16 currentEpoch = governance.epoch(); + // for(uint8 i; i < deployedInitiatives.length; i++) { + // address initiative = deployedInitiatives[i]; + // // if the bool switches, the user has claimed their bribe for the epoch + // if(_before.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] != _after.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch]) { + // // calculate user balance delta of the bribe tokens + // uint128 lqtyBalanceDelta = _after.lqtyBalance - _before.lqtyBalance; + // uint128 lusdBalanceDelta = _after.lusdBalance - _before.lusdBalance; + + // // calculate balance delta as a percentage of the total bribe for this epoch + // (uint128 bribeBoldAmount, uint128 bribeBribeTokenAmount) = IBribeInitiative(initiative).bribeByEpoch(currentEpoch); + // uint128 lqtyPercentageOfBribe = (lqtyBalanceDelta / bribeBribeTokenAmount) * 10_000; + // uint128 lusdPercentageOfBribe = (lusdBalanceDelta / bribeBoldAmount) * 10_000; + + // // Shift right by 40 bits (128 - 88) to get the 88 most significant bits + // uint88 lqtyPercentageOfBribe88 = uint88(lqtyPercentageOfBribe >> 40); + // uint88 lusdPercentageOfBribe88 = uint88(lusdPercentageOfBribe >> 40); + + // // calculate user allocation percentage of total for this epoch + // uint88 lqtyAllocatedByUserAtEpoch = IBribeInitiative(initiative).lqtyAllocatedByUserAtEpoch(user, currentEpoch); + // uint88 totalLQTYAllocatedAtEpoch = IBribeInitiative(initiative).totalLQTYAllocatedByEpoch(currentEpoch); + // uint88 allocationPercentageOfTotal = (lqtyAllocatedByUserAtEpoch / totalLQTYAllocatedAtEpoch) * 10_000; + + // // check that allocation percentage and received bribe percentage match + // eq(lqtyPercentageOfBribe88, allocationPercentageOfTotal, "BI-01: User should receive percentage of bribes corresponding to their allocation"); + // eq(lusdPercentageOfBribe88, allocationPercentageOfTotal, "BI-01: User should receive percentage of BOLD bribes corresponding to their allocation"); + // } + // } + // } + + // function property_BI02() public { + // t(!claimedTwice, "B2-01: User can only claim bribes once in an epoch"); + // } + + // function property_BI03() public { + // uint16 currentEpoch = governance.epoch(); + // for(uint8 i; i < deployedInitiatives.length; i++) { + // IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); + // uint88 lqtyAllocatedByUserAtEpoch = initiative.lqtyAllocatedByUserAtEpoch(user, currentEpoch); + // eq(ghostLqtyAllocationByUserAtEpoch[user], lqtyAllocatedByUserAtEpoch, "BI-03: Accounting for user allocation amount is always correct"); + // } + // } + + // function property_BI04() public { + // uint16 currentEpoch = governance.epoch(); + // for(uint8 i; i < deployedInitiatives.length; i++) { + // IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); + // uint88 totalLQTYAllocatedAtEpoch = initiative.totalLQTYAllocatedByEpoch(currentEpoch); + // eq(ghostTotalAllocationAtEpoch[currentEpoch], totalLQTYAllocatedAtEpoch, "BI-04: Accounting for total allocation amount is always correct"); + // } + // } + + // // TODO: double check that this implementation is correct + // function property_BI05() public { + // uint16 currentEpoch = governance.epoch(); + // for(uint8 i; i < deployedInitiatives.length; i++) { + // address initiative = deployedInitiatives[i]; + // // if the bool switches, the user has claimed their bribe for the epoch + // if(_before.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] != _after.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch]) { + // // check that the remaining bribe amount left over is less than 100 million wei + // uint256 bribeTokenBalanceInitiative = lqty.balanceOf(initiative); + // uint256 boldTokenBalanceInitiative = lusd.balanceOf(initiative); + + // lte(bribeTokenBalanceInitiative, 1e8, "BI-05: Bribe token dust amount remaining after claiming should be less than 100 million wei"); + // lte(boldTokenBalanceInitiative, 1e8, "BI-05: Bold token dust amount remaining after claiming should be less than 100 million wei"); + // } + // } + // } + +} \ No newline at end of file diff --git a/test/recon/properties/GovernanceProperties.sol b/test/recon/properties/GovernanceProperties.sol new file mode 100644 index 0000000..df7e014 --- /dev/null +++ b/test/recon/properties/GovernanceProperties.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BeforeAfter} from "../BeforeAfter.sol"; + +abstract contract GovernanceProperties is BeforeAfter { + + // function property_GV01() public { + // // first check that epoch hasn't changed after the operation + // if(_before.epoch == _after.epoch) { + // // loop through the initiatives and check that their status hasn't changed + // for(uint8 i; i < deployedInitiatives.length; i++) { + // address initiative = deployedInitiatives[i]; + // eq(uint256(_before.initiativeStatus[initiative]), uint256(_after.initiativeStatus[initiative]), "GV-01: Initiative state should only return one state per epoch"); + // } + // } + // } + +} \ No newline at end of file diff --git a/test/recon/targets/BribeInitiativeTargets.sol b/test/recon/targets/BribeInitiativeTargets.sol new file mode 100644 index 0000000..b042cac --- /dev/null +++ b/test/recon/targets/BribeInitiativeTargets.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +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 {Properties} from "../Properties.sol"; + + +abstract contract BribeInitiativeTargets is Test, BaseTargetFunctions, Properties { + using DoubleLinkedList for DoubleLinkedList.List; + + // NOTE: initiatives that get called here are deployed but not necessarily registered + + function initiative_depositBribe(uint128 boldAmount, uint128 bribeTokenAmount, uint16 epoch, uint8 initiativeIndex) withChecks public { + IBribeInitiative initiative = IBribeInitiative(_getDeployedInitiative(initiativeIndex)); + + // clamp token amounts using user balance + boldAmount = uint128(boldAmount % lusd.balanceOf(user)); + bribeTokenAmount = uint128(bribeTokenAmount % lqty.balanceOf(user)); + + initiative.depositBribe(boldAmount, bribeTokenAmount, epoch); + } + + function initiative_claimBribes(uint16 epoch, uint16 prevAllocationEpoch, uint16 prevTotalAllocationEpoch, uint8 initiativeIndex) withChecks public { + IBribeInitiative initiative = IBribeInitiative(_getDeployedInitiative(initiativeIndex)); + + // clamp epochs by using the current governance epoch + epoch = epoch % governance.epoch(); + prevAllocationEpoch = prevAllocationEpoch % governance.epoch(); + prevTotalAllocationEpoch = prevTotalAllocationEpoch % governance.epoch(); + + IBribeInitiative.ClaimData[] memory claimData = new IBribeInitiative.ClaimData[](1); + claimData[0] = IBribeInitiative.ClaimData({ + epoch: epoch, + prevLQTYAllocationEpoch: prevAllocationEpoch, + prevTotalLQTYAllocationEpoch: prevTotalAllocationEpoch + }); + + bool alreadyClaimed = initiative.claimedBribeAtEpoch(user, epoch); + + initiative.claimBribes(claimData); + + // check if the bribe was already claimed at the given epoch + if(alreadyClaimed) { + // toggle canary that breaks the BI-02 property + claimedTwice = true; + } + } +} \ No newline at end of file diff --git a/test/recon/targets/GovernanceTargets.sol b/test/recon/targets/GovernanceTargets.sol new file mode 100644 index 0000000..59bc42c --- /dev/null +++ b/test/recon/targets/GovernanceTargets.sol @@ -0,0 +1,129 @@ + +// SPDX-License-Identifier: GPL-2.0 +pragma solidity ^0.8.0; + +import {BaseTargetFunctions} from "@chimera/BaseTargetFunctions.sol"; +import {vm} from "@chimera/Hevm.sol"; +import {IERC20Permit} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol"; +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"; + + + +abstract contract GovernanceTargets is BaseTargetFunctions, Properties { + + // clamps to a single initiative to ensure coverage in case both haven't been registered yet + function governance_allocateLQTY_clamped_single_initiative(uint8 initiativesIndex, uint96 deltaLQTYVotes, uint96 deltaLQTYVetos) withChecks public { + 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)); + int88[] memory deltaLQTYVetosArray = new int88[](1); + deltaLQTYVetosArray[0] = int88(uint88(deltaLQTYVetos % stakedAmount)); + + governance.allocateLQTY(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_allocateLQTY(int88[] calldata _deltaLQTYVotes, int88[] calldata _deltaLQTYVetos) withChecks public { + governance.allocateLQTY(deployedInitiatives, _deltaLQTYVotes, _deltaLQTYVetos); + } + + function governance_claimForInitiative(uint8 initiativeIndex) withChecks public { + address initiative = _getDeployedInitiative(initiativeIndex); + governance.claimForInitiative(initiative); + } + + function governance_claimFromStakingV1(uint8 recipientIndex) withChecks public { + address rewardRecipient = _getRandomUser(recipientIndex); + governance.claimFromStakingV1(rewardRecipient); + } + + function governance_deployUserProxy() withChecks public { + governance.deployUserProxy(); + } + + function governance_depositLQTY(uint88 lqtyAmount) withChecks public { + lqtyAmount = uint88(lqtyAmount % lqty.balanceOf(user)); + governance.depositLQTY(lqtyAmount); + } + + function governance_depositLQTYViaPermit(uint88 _lqtyAmount) withChecks public { + // Get the current block timestamp for the deadline + uint256 deadline = block.timestamp + 1 hours; + + // Create the permit message + bytes32 permitTypeHash = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 domainSeparator = IERC20Permit(address(lqty)).DOMAIN_SEPARATOR(); + + + uint256 nonce = IERC20Permit(address(lqty)).nonces(user); + + bytes32 structHash = keccak256(abi.encode( + permitTypeHash, + user, + address(governance), + _lqtyAmount, + nonce, + deadline + )); + + bytes32 digest = keccak256(abi.encodePacked( + "\x19\x01", + domainSeparator, + structHash + )); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(user2Pk, digest); + + PermitParams memory permitParams = PermitParams({ + owner: user2, + spender: user, + value: _lqtyAmount, + deadline: deadline, + v: v, + r: r, + s: s + }); + + governance.depositLQTYViaPermit(_lqtyAmount, permitParams); + } + + function governance_registerInitiative(uint8 initiativeIndex) withChecks public { + address initiative = _getDeployedInitiative(initiativeIndex); + governance.registerInitiative(initiative); + } + + function governance_snapshotVotesForInitiative(address _initiative) withChecks public { + governance.snapshotVotesForInitiative(_initiative); + } + + function governance_unregisterInitiative(uint8 initiativeIndex) withChecks public { + address initiative = _getDeployedInitiative(initiativeIndex); + governance.unregisterInitiative(initiative); + } + + function governance_withdrawLQTY(uint88 _lqtyAmount) withChecks public { + governance.withdrawLQTY(_lqtyAmount); + } +} \ No newline at end of file From eae43cd46e7753dc0edcad02e743395c19c249f7 Mon Sep 17 00:00:00 2001 From: nelson-pereira8 <94120714+nican0r@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:04:28 -0300 Subject: [PATCH 46/93] chore: uncommenting properties --- .../properties/BribeInitiativeProperties.sol | 120 +++++++++--------- .../recon/properties/GovernanceProperties.sol | 20 +-- 2 files changed, 70 insertions(+), 70 deletions(-) diff --git a/test/recon/properties/BribeInitiativeProperties.sol b/test/recon/properties/BribeInitiativeProperties.sol index 3d2d6a0..d1319a0 100644 --- a/test/recon/properties/BribeInitiativeProperties.sol +++ b/test/recon/properties/BribeInitiativeProperties.sol @@ -6,74 +6,74 @@ import {BeforeAfter} from "../BeforeAfter.sol"; import {IBribeInitiative} from "../../../src/interfaces/IBribeInitiative.sol"; abstract contract BribeInitiativeProperties is BeforeAfter { - // function property_BI01() public { - // uint16 currentEpoch = governance.epoch(); - // for(uint8 i; i < deployedInitiatives.length; i++) { - // address initiative = deployedInitiatives[i]; - // // if the bool switches, the user has claimed their bribe for the epoch - // if(_before.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] != _after.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch]) { - // // calculate user balance delta of the bribe tokens - // uint128 lqtyBalanceDelta = _after.lqtyBalance - _before.lqtyBalance; - // uint128 lusdBalanceDelta = _after.lusdBalance - _before.lusdBalance; + function property_BI01() public { + uint16 currentEpoch = governance.epoch(); + for(uint8 i; i < deployedInitiatives.length; i++) { + address initiative = deployedInitiatives[i]; + // if the bool switches, the user has claimed their bribe for the epoch + if(_before.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] != _after.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch]) { + // calculate user balance delta of the bribe tokens + uint128 lqtyBalanceDelta = _after.lqtyBalance - _before.lqtyBalance; + uint128 lusdBalanceDelta = _after.lusdBalance - _before.lusdBalance; - // // calculate balance delta as a percentage of the total bribe for this epoch - // (uint128 bribeBoldAmount, uint128 bribeBribeTokenAmount) = IBribeInitiative(initiative).bribeByEpoch(currentEpoch); - // uint128 lqtyPercentageOfBribe = (lqtyBalanceDelta / bribeBribeTokenAmount) * 10_000; - // uint128 lusdPercentageOfBribe = (lusdBalanceDelta / bribeBoldAmount) * 10_000; + // calculate balance delta as a percentage of the total bribe for this epoch + (uint128 bribeBoldAmount, uint128 bribeBribeTokenAmount) = IBribeInitiative(initiative).bribeByEpoch(currentEpoch); + uint128 lqtyPercentageOfBribe = (lqtyBalanceDelta / bribeBribeTokenAmount) * 10_000; + uint128 lusdPercentageOfBribe = (lusdBalanceDelta / bribeBoldAmount) * 10_000; - // // Shift right by 40 bits (128 - 88) to get the 88 most significant bits - // uint88 lqtyPercentageOfBribe88 = uint88(lqtyPercentageOfBribe >> 40); - // uint88 lusdPercentageOfBribe88 = uint88(lusdPercentageOfBribe >> 40); + // Shift right by 40 bits (128 - 88) to get the 88 most significant bits + uint88 lqtyPercentageOfBribe88 = uint88(lqtyPercentageOfBribe >> 40); + uint88 lusdPercentageOfBribe88 = uint88(lusdPercentageOfBribe >> 40); - // // calculate user allocation percentage of total for this epoch - // uint88 lqtyAllocatedByUserAtEpoch = IBribeInitiative(initiative).lqtyAllocatedByUserAtEpoch(user, currentEpoch); - // uint88 totalLQTYAllocatedAtEpoch = IBribeInitiative(initiative).totalLQTYAllocatedByEpoch(currentEpoch); - // uint88 allocationPercentageOfTotal = (lqtyAllocatedByUserAtEpoch / totalLQTYAllocatedAtEpoch) * 10_000; + // calculate user allocation percentage of total for this epoch + uint88 lqtyAllocatedByUserAtEpoch = IBribeInitiative(initiative).lqtyAllocatedByUserAtEpoch(user, currentEpoch); + uint88 totalLQTYAllocatedAtEpoch = IBribeInitiative(initiative).totalLQTYAllocatedByEpoch(currentEpoch); + uint88 allocationPercentageOfTotal = (lqtyAllocatedByUserAtEpoch / totalLQTYAllocatedAtEpoch) * 10_000; - // // check that allocation percentage and received bribe percentage match - // eq(lqtyPercentageOfBribe88, allocationPercentageOfTotal, "BI-01: User should receive percentage of bribes corresponding to their allocation"); - // eq(lusdPercentageOfBribe88, allocationPercentageOfTotal, "BI-01: User should receive percentage of BOLD bribes corresponding to their allocation"); - // } - // } - // } + // check that allocation percentage and received bribe percentage match + eq(lqtyPercentageOfBribe88, allocationPercentageOfTotal, "BI-01: User should receive percentage of bribes corresponding to their allocation"); + eq(lusdPercentageOfBribe88, allocationPercentageOfTotal, "BI-01: User should receive percentage of BOLD bribes corresponding to their allocation"); + } + } + } - // function property_BI02() public { - // t(!claimedTwice, "B2-01: User can only claim bribes once in an epoch"); - // } + function property_BI02() public { + t(!claimedTwice, "B2-01: User can only claim bribes once in an epoch"); + } - // function property_BI03() public { - // uint16 currentEpoch = governance.epoch(); - // for(uint8 i; i < deployedInitiatives.length; i++) { - // IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); - // uint88 lqtyAllocatedByUserAtEpoch = initiative.lqtyAllocatedByUserAtEpoch(user, currentEpoch); - // eq(ghostLqtyAllocationByUserAtEpoch[user], lqtyAllocatedByUserAtEpoch, "BI-03: Accounting for user allocation amount is always correct"); - // } - // } + function property_BI03() public { + uint16 currentEpoch = governance.epoch(); + for(uint8 i; i < deployedInitiatives.length; i++) { + IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); + uint88 lqtyAllocatedByUserAtEpoch = initiative.lqtyAllocatedByUserAtEpoch(user, currentEpoch); + eq(ghostLqtyAllocationByUserAtEpoch[user], lqtyAllocatedByUserAtEpoch, "BI-03: Accounting for user allocation amount is always correct"); + } + } - // function property_BI04() public { - // uint16 currentEpoch = governance.epoch(); - // for(uint8 i; i < deployedInitiatives.length; i++) { - // IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); - // uint88 totalLQTYAllocatedAtEpoch = initiative.totalLQTYAllocatedByEpoch(currentEpoch); - // eq(ghostTotalAllocationAtEpoch[currentEpoch], totalLQTYAllocatedAtEpoch, "BI-04: Accounting for total allocation amount is always correct"); - // } - // } + function property_BI04() public { + uint16 currentEpoch = governance.epoch(); + for(uint8 i; i < deployedInitiatives.length; i++) { + IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); + uint88 totalLQTYAllocatedAtEpoch = initiative.totalLQTYAllocatedByEpoch(currentEpoch); + eq(ghostTotalAllocationAtEpoch[currentEpoch], totalLQTYAllocatedAtEpoch, "BI-04: Accounting for total allocation amount is always correct"); + } + } - // // TODO: double check that this implementation is correct - // function property_BI05() public { - // uint16 currentEpoch = governance.epoch(); - // for(uint8 i; i < deployedInitiatives.length; i++) { - // address initiative = deployedInitiatives[i]; - // // if the bool switches, the user has claimed their bribe for the epoch - // if(_before.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] != _after.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch]) { - // // check that the remaining bribe amount left over is less than 100 million wei - // uint256 bribeTokenBalanceInitiative = lqty.balanceOf(initiative); - // uint256 boldTokenBalanceInitiative = lusd.balanceOf(initiative); + // TODO: double check that this implementation is correct + function property_BI05() public { + uint16 currentEpoch = governance.epoch(); + for(uint8 i; i < deployedInitiatives.length; i++) { + address initiative = deployedInitiatives[i]; + // if the bool switches, the user has claimed their bribe for the epoch + if(_before.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch] != _after.claimedBribeForInitiativeAtEpoch[initiative][user][currentEpoch]) { + // check that the remaining bribe amount left over is less than 100 million wei + uint256 bribeTokenBalanceInitiative = lqty.balanceOf(initiative); + uint256 boldTokenBalanceInitiative = lusd.balanceOf(initiative); - // lte(bribeTokenBalanceInitiative, 1e8, "BI-05: Bribe token dust amount remaining after claiming should be less than 100 million wei"); - // lte(boldTokenBalanceInitiative, 1e8, "BI-05: Bold token dust amount remaining after claiming should be less than 100 million wei"); - // } - // } - // } + lte(bribeTokenBalanceInitiative, 1e8, "BI-05: Bribe token dust amount remaining after claiming should be less than 100 million wei"); + lte(boldTokenBalanceInitiative, 1e8, "BI-05: Bold token dust amount remaining after claiming should be less than 100 million wei"); + } + } + } } \ No newline at end of file diff --git a/test/recon/properties/GovernanceProperties.sol b/test/recon/properties/GovernanceProperties.sol index df7e014..a3033d7 100644 --- a/test/recon/properties/GovernanceProperties.sol +++ b/test/recon/properties/GovernanceProperties.sol @@ -5,15 +5,15 @@ import {BeforeAfter} from "../BeforeAfter.sol"; abstract contract GovernanceProperties is BeforeAfter { - // function property_GV01() public { - // // first check that epoch hasn't changed after the operation - // if(_before.epoch == _after.epoch) { - // // loop through the initiatives and check that their status hasn't changed - // for(uint8 i; i < deployedInitiatives.length; i++) { - // address initiative = deployedInitiatives[i]; - // eq(uint256(_before.initiativeStatus[initiative]), uint256(_after.initiativeStatus[initiative]), "GV-01: Initiative state should only return one state per epoch"); - // } - // } - // } + function property_GV01() public { + // first check that epoch hasn't changed after the operation + if(_before.epoch == _after.epoch) { + // loop through the initiatives and check that their status hasn't changed + for(uint8 i; i < deployedInitiatives.length; i++) { + address initiative = deployedInitiatives[i]; + eq(uint256(_before.initiativeStatus[initiative]), uint256(_after.initiativeStatus[initiative]), "GV-01: Initiative state should only return one state per epoch"); + } + } + } } \ No newline at end of file From 0d2e52489077701d5b64376e32fd2f9cec550c36 Mon Sep 17 00:00:00 2001 From: nelson-pereira8 <94120714+nican0r@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:08:35 -0300 Subject: [PATCH 47/93] chore: updating properties table --- test/recon/PROPERTIES.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/recon/PROPERTIES.md b/test/recon/PROPERTIES.md index 2460019..9ccfdc3 100644 --- a/test/recon/PROPERTIES.md +++ b/test/recon/PROPERTIES.md @@ -1,9 +1,12 @@ ## BribeInitiative -| Property | Description | Tested | -| --- | --- | --- | -| BI-01 | User always receives their share of bribe distribution amount when claimed, never more or less | | -| BI-02 | User can always claim bribe for an epoch in which they were allocated | | +| Property | Description | Implemented | Tested | +| --- | --- | --- | --- | +| BI-01 | User should receive percentage of bribes corresponding to their allocation | ✅ | | +| BI-02 | User can only claim bribes once in an epoch | ✅ | | +| BI-03 | Accounting for user allocation amount is always correct | ✅ | | +| BI-04 | Accounting for total allocation amount is always correct | ✅ | | +| BI-05 | Dust amount remaining after claiming should be less than 100 million wei | | | ## Governance | Property | Description | Tested | From ceb0f7e263a5f5e767b0ab4191dee47301c07a5d Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 11:24:09 +0200 Subject: [PATCH 48/93] fix: add sanity checks to thresholds --- src/Governance.sol | 11 +++++++++++ test/Governance.t.sol | 1 + 2 files changed, 12 insertions(+) diff --git a/src/Governance.sol b/src/Governance.sol index 209c5d6..cbf06bc 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -86,11 +86,22 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance bold = IERC20(_bold); require(_config.minClaim <= _config.minAccrual, "Gov: min-claim-gt-min-accrual"); REGISTRATION_FEE = _config.registrationFee; + + // Registration threshold must be below 100% of votes + require(_config.registrationThresholdFactor < WAD, "Gov: registration-config"); REGISTRATION_THRESHOLD_FACTOR = _config.registrationThresholdFactor; + + // Unregistration must be X times above the `votingThreshold` + require(_config.unregistrationThresholdFactor > WAD, "Gov: unregistration-config"); UNREGISTRATION_THRESHOLD_FACTOR = _config.unregistrationThresholdFactor; + REGISTRATION_WARM_UP_PERIOD = _config.registrationWarmUpPeriod; UNREGISTRATION_AFTER_EPOCHS = _config.unregistrationAfterEpochs; + + // Voting threshold must be below 100% of votes + require(_config.votingThresholdFactor < WAD, "Gov: voting-config"); VOTING_THRESHOLD_FACTOR = _config.votingThresholdFactor; + MIN_CLAIM = _config.minClaim; MIN_ACCRUAL = _config.minAccrual; EPOCH_START = _config.epochStart; diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 1993b1d..03fe229 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -477,6 +477,7 @@ contract GovernanceTest is Test { uint128 _votingThresholdFactor, uint88 _minClaim ) public { + _votingThresholdFactor = _votingThresholdFactor % 1e18; /// Clamp to prevent misconfig governance = new Governance( address(lqty), address(lusd), From 8b3670ec320dddc48b672bbbdf6be367e51d6abc Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 12:24:21 +0200 Subject: [PATCH 49/93] feat: move logic to allow removing `lastCountedEpoch` --- src/Governance.sol | 42 +++++++++++++++++++----------------------- test/Governance.t.sol | 8 +++++--- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index cbf06bc..6898a8e 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -307,7 +307,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance initiativeSnapshot.votes > initiativeSnapshot.vetos && initiativeSnapshot.votes >= votingThreshold ) { - initiativeSnapshot.lastCountedEpoch = currentEpoch - 1; /// @audit This updating makes it so that we lose track | TODO: Find a better way + // initiativeSnapshot.lastCountedEpoch = currentEpoch - 1; /// @audit This updating makes it so that we lose track | TODO: Find a better way } votesForInitiativeSnapshot[_initiative] = initiativeSnapshot; @@ -342,7 +342,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance - Should be kicked (true, false, epoch - 1 - [UNREGISTRATION_AFTER_EPOCHS, UNREGISTRATION_AFTER_EPOCHS + X]) */ - function getInitiativeState(address _initiative) public returns (InitiativeStatus status, uint16 lastEpochClaim, uint256 claimableAmount) { + function getInitiativeState(address _initiative) public returns (InitiativeStatus status, uint16 lastEpochClaim, uint256 claimableAmount) { (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(_initiative); @@ -360,14 +360,26 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance return (InitiativeStatus.DISABLED, lastEpochClaim, 0); /// @audit By definition it must have zero rewards } + uint256 votingTheshold = calculateVotingThreshold(); + + // If it's voted and can get rewards + // Votes > calculateVotingThreshold + // == 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; + return (InitiativeStatus.CLAIMABLE, lastEpochClaim, claim); + } + // == Unregister Condition == // /// @audit epoch() - 1 because we can have Now - 1 and that's not a removal case | TODO: Double check | Worst case QA, off by one epoch // TODO: IMO we can use the claimed variable here /// This shifts the logic by 1 epoch - if((votesForInitiativeSnapshot_.lastCountedEpoch + UNREGISTRATION_AFTER_EPOCHS < epoch() - 1) + if((initiativeState.lastEpochClaim + UNREGISTRATION_AFTER_EPOCHS < epoch() - 1) || votesForInitiativeSnapshot_.vetos > votesForInitiativeSnapshot_.votes - && votesForInitiativeSnapshot_.vetos > calculateVotingThreshold() * UNREGISTRATION_THRESHOLD_FACTOR / WAD + && votesForInitiativeSnapshot_.vetos > votingTheshold * UNREGISTRATION_THRESHOLD_FACTOR / WAD ) { return (InitiativeStatus.UNREGISTERABLE, lastEpochClaim, 0); } @@ -377,27 +389,10 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @audit if we already are above, then why are we re-computing this? // Ultimately the checkpoint logic for initiative is fine, so we can skip this - // TODO: Where does this fit exactly? - // Edge case of 0 votes - if(votesSnapshot_.votes == 0) { - return (InitiativeStatus.SKIP, lastEpochClaim, 0); - } - - - // == Vetoed this Epoch Condition == // - if(votesForInitiativeSnapshot_.vetos >= votesForInitiativeSnapshot_.votes) { - return (InitiativeStatus.SKIP, lastEpochClaim, 0); /// @audit Technically VETOED - } - // == Not meeting threshold Condition == // - if(calculateVotingThreshold() > votesForInitiativeSnapshot_.votes) { - return (InitiativeStatus.SKIP, lastEpochClaim, 0); - } - // == Rewards Conditions (votes can be zero, logic is the same) == // - uint256 claim = votesForInitiativeSnapshot_.votes * boldAccrued / votesSnapshot_.votes; - return (InitiativeStatus.CLAIMABLE, lastEpochClaim, claim); + return (InitiativeStatus.SKIP, lastEpochClaim, 0); } /// @inheritdoc IGovernance @@ -623,7 +618,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance assert(votesSnapshot_.forEpoch == epoch() - 1); /// @audit INVARIANT: You can only claim for previous epoch /// All unclaimed rewards are always recycled - + /// Invariant `lastEpochClaim` is < epoch() - 1; | + /// If `lastEpochClaim` is older than epoch() - 1 it means the initiative couldn't claim any rewards this epoch initiativeStates[_initiative].lastEpochClaim = epoch() - 1; votesForInitiativeSnapshot[_initiative] = votesForInitiativeSnapshot_; // implicitly prevents double claiming diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 03fe229..84846c8 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -1373,9 +1373,11 @@ contract GovernanceTest is Test { ) ); (votes_, forEpoch_, lastCountedEpoch, ) = governance.votesForInitiativeSnapshot(address(mockInitiative)); - assertEq(votes_, 0); - assertEq(forEpoch_, governance.epoch() - 1); - assertEq(lastCountedEpoch, 0); + assertEq(votes_, 0, "votes"); + assertEq(forEpoch_, governance.epoch() - 1, "forEpoch_"); + assertEq(lastCountedEpoch, 0, "lastCountedEpoch"); + + vm.warp(block.timestamp + governance.EPOCH_DURATION() * 4); governance.unregisterInitiative(address(mockInitiative)); } From 9109d983e098e0db429c29d607d172f472e11bfb Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 12:27:04 +0200 Subject: [PATCH 50/93] feat: remove usage of `lastCountedEpoch` --- test/GovernanceAttacks.t.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/GovernanceAttacks.t.sol b/test/GovernanceAttacks.t.sol index 071c22b..15a2563 100644 --- a/test/GovernanceAttacks.t.sol +++ b/test/GovernanceAttacks.t.sol @@ -217,15 +217,12 @@ contract GovernanceTest is Test { (Governance.VoteSnapshot memory v, Governance.InitiativeVoteSnapshot memory initData) = governance.snapshotVotesForInitiative(address(maliciousInitiative2)); uint256 currentEpoch = governance.epoch(); - assertEq(initData.lastCountedEpoch, currentEpoch - 1, "Epoch Matches"); - + // Inactive for 4 epochs // Add another proposal vm.warp(block.timestamp + governance.EPOCH_DURATION() * 5); /// @audit needs 5? (v, initData) = governance.snapshotVotesForInitiative(address(maliciousInitiative2)); - assertEq(initData.lastCountedEpoch, currentEpoch - 1, "Epoch Matches"); /// @audit This fails if you have 0 votes, see QA - uint256 unregisterSnapshot = vm.snapshot(); maliciousInitiative2.setRevertBehaviour(MaliciousInitiative.FunctionType.UNREGISTER, MaliciousInitiative.RevertType.THROW); From 089b9681bb089c82189a051f188b09684e68087f Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 14:52:50 +0200 Subject: [PATCH 51/93] feat: for packing --- src/interfaces/IGovernance.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/IGovernance.sol b/src/interfaces/IGovernance.sol index 146f3da..5e0592e 100644 --- a/src/interfaces/IGovernance.sol +++ b/src/interfaces/IGovernance.sol @@ -131,7 +131,7 @@ interface IGovernance { struct GlobalState { uint88 countedVoteLQTY; // Total LQTY that is included in vote counting uint32 countedVoteLQTYAverageTimestamp; // Average timestamp: derived initiativeAllocation.averageTimestamp - } + } /// TODO: Bold balance? Prob cheaper /// @notice Returns the user's state /// @param _user Address of the user From b692f5b9e22f13728c347dbbb6390b8d7eddcaae Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 14:53:08 +0200 Subject: [PATCH 52/93] feat: view variants of snapshot functions --- src/Governance.sol | 61 ++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 6898a8e..c25a09a 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -262,16 +262,27 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // Snapshots votes for the previous epoch and accrues funds for the current epoch function _snapshotVotes() internal returns (VoteSnapshot memory snapshot, GlobalState memory state) { + bool shouldUpdate; + (snapshot, state, shouldUpdate) = getTotalVotesAndState(); + + if(shouldUpdate) { + votesSnapshot = snapshot; + uint256 boldBalance = bold.balanceOf(address(this)); + boldAccrued = (boldBalance < MIN_ACCRUAL) ? 0 : boldBalance; + emit SnapshotVotes(snapshot.votes, snapshot.forEpoch); + } + } + + function getTotalVotesAndState() public view returns (VoteSnapshot memory snapshot, GlobalState memory state, bool shouldUpdate) { uint16 currentEpoch = epoch(); snapshot = votesSnapshot; state = globalState; + if (snapshot.forEpoch < currentEpoch - 1) { + shouldUpdate = true; + snapshot.votes = lqtyToVotes(state.countedVoteLQTY, epochStart(), state.countedVoteLQTYAverageTimestamp); snapshot.forEpoch = currentEpoch - 1; - votesSnapshot = snapshot; - uint256 boldBalance = bold.balanceOf(address(this)); - boldAccrued = (boldBalance < MIN_ACCRUAL) ? 0 : boldBalance; - emit SnapshotVotes(snapshot.votes, snapshot.forEpoch); } } @@ -281,37 +292,39 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance internal returns (InitiativeVoteSnapshot memory initiativeSnapshot, InitiativeState memory initiativeState) { + bool shouldUpdate; + (initiativeSnapshot, initiativeState, shouldUpdate) = getInitiativeSnapshotAndState(_initiative); + + if(shouldUpdate) { + votesForInitiativeSnapshot[_initiative] = initiativeSnapshot; + emit SnapshotVotesForInitiative(_initiative, initiativeSnapshot.votes, initiativeSnapshot.forEpoch); + } + } + + function getInitiativeSnapshotAndState(address _initiative) + public + view + returns (InitiativeVoteSnapshot memory initiativeSnapshot, InitiativeState memory initiativeState, bool shouldUpdate) + { + // Get the storage data uint16 currentEpoch = epoch(); initiativeSnapshot = votesForInitiativeSnapshot[_initiative]; initiativeState = initiativeStates[_initiative]; + if (initiativeSnapshot.forEpoch < currentEpoch - 1) { - uint256 votingThreshold = calculateVotingThreshold(); + shouldUpdate = true; + + // Update in memory data + // Safe as long as: Any time a initiative state changes, we first update the snapshot uint32 start = epochStart(); uint240 votes = lqtyToVotes(initiativeState.voteLQTY, start, initiativeState.averageStakingTimestampVoteLQTY); uint240 vetos = lqtyToVotes(initiativeState.vetoLQTY, start, initiativeState.averageStakingTimestampVetoLQTY); - // if the votes didn't meet the voting threshold then no votes qualify - /// @audit TODO TEST THIS - /// The change means that all logic for votes and rewards must be done in `getInitiativeState` - initiativeSnapshot.votes = uint224(votes); /// @audit TODO: We should change this to check the treshold, we should instead use the snapshot to just report all the valid data - - initiativeSnapshot.vetos = uint224(vetos); /// @audit TODO: Overflow + order of operations + initiativeSnapshot.votes = uint224(votes); + initiativeSnapshot.vetos = uint224(vetos); initiativeSnapshot.forEpoch = currentEpoch - 1; - - /// @audit Conditional - /// If we meet the threshold then we increase this - /// TODO: Either simplify, or use this for the state machine as well - if( - initiativeSnapshot.votes > initiativeSnapshot.vetos && - initiativeSnapshot.votes >= votingThreshold - ) { - // initiativeSnapshot.lastCountedEpoch = currentEpoch - 1; /// @audit This updating makes it so that we lose track | TODO: Find a better way - } - - votesForInitiativeSnapshot[_initiative] = initiativeSnapshot; - emit SnapshotVotesForInitiative(_initiative, initiativeSnapshot.votes, initiativeSnapshot.forEpoch); } } From 8e60ecb6c1a5edd623e57c892c43508062259a6f Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 14:57:33 +0200 Subject: [PATCH 53/93] chore: cleanup --- src/Governance.sol | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index c25a09a..8826b53 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -433,7 +433,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance emit RegisterInitiative(_initiative, msg.sender, currentEpoch); - // try IInitiative(_initiative).onRegisterInitiative(currentEpoch) {} catch {} // Replaces try / catch | Enforces sufficient gas is passed safeCallWithMinGas(_initiative, MIN_GAS_TO_HOOK, 0, abi.encodeCall(IInitiative.onRegisterInitiative, (currentEpoch))); } @@ -461,10 +460,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance int88 deltaLQTYVotes = _deltaLQTYVotes[i]; int88 deltaLQTYVetos = _deltaLQTYVetos[i]; - // TODO: Better assertion - /// Can remove or add - /// But cannot add or remove both - // only allow vetoing post the voting cutoff require( deltaLQTYVotes <= 0 || deltaLQTYVotes >= 0 && secondsWithinEpoch() <= EPOCH_VOTING_CUTOFF, @@ -475,17 +470,12 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance uint16 registeredAtEpoch = registeredInitiatives[initiative]; if(deltaLQTYVotes > 0 || deltaLQTYVetos > 0) { require(currentEpoch > registeredAtEpoch && registeredAtEpoch != 0, "Governance: initiative-not-active"); - } /// @audit TODO: We must allow removals for Proposals that are disabled | Should use the flag u16 + } if(registeredAtEpoch == UNREGISTERED_INITIATIVE) { require(deltaLQTYVotes <= 0 && deltaLQTYVetos <= 0, "Must be a withdrawal"); } } - // TODO: CHANGE - // Can add if active - // Can remove if inactive - // only allow allocations to initiatives that are active - // an initiative becomes active in the epoch after it is registered (, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(initiative); @@ -537,8 +527,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance ); state.countedVoteLQTY += initiativeState.voteLQTY; - - // allocate the voting and vetoing LQTY to the initiative Allocation memory allocation = lqtyAllocatedByUserToInitiative[msg.sender][initiative]; allocation.voteLQTY = add(allocation.voteLQTY, deltaLQTYVotes); @@ -551,9 +539,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance emit AllocateLQTY(msg.sender, initiative, deltaLQTYVotes, deltaLQTYVetos, currentEpoch); - // try IInitiative(initiative).onAfterAllocateLQTY( - // currentEpoch, msg.sender, userState, allocation, initiativeState - // ) {} catch {} // Replaces try / catch | Enforces sufficient gas is passed safeCallWithMinGas(initiative, MIN_GAS_TO_HOOK, 0, abi.encodeCall(IInitiative.onAfterAllocateLQTY, (currentEpoch, msg.sender, userState, allocation, initiativeState))); } @@ -598,7 +583,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance globalState = state; /// @audit removal math causes issues - // delete initiativeStates[_initiative]; /// @audit Should not delete this /// weeks * 2^16 > u32 so the contract will stop working before this is an issue @@ -606,7 +590,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance emit UnregisterInitiative(_initiative, currentEpoch); - // try IInitiative(_initiative).onUnregisterInitiative(currentEpoch) {} catch {} // Replaces try / catch | Enforces sufficient gas is passed safeCallWithMinGas(_initiative, MIN_GAS_TO_HOOK, 0, abi.encodeCall(IInitiative.onUnregisterInitiative, (currentEpoch))); } @@ -640,7 +623,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance emit ClaimForInitiative(_initiative, claimableAmount, votesSnapshot_.forEpoch); - // try IInitiative(_initiative).onClaimForInitiative(votesSnapshot_.forEpoch, claimableAmount) {} catch {} + // Replaces try / catch | Enforces sufficient gas is passed safeCallWithMinGas(_initiative, MIN_GAS_TO_HOOK, 0, abi.encodeCall(IInitiative.onClaimForInitiative, (votesSnapshot_.forEpoch, claimableAmount))); From 3a7fd77233509594d99db6c7b6479bd461131ba2 Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 14:58:40 +0200 Subject: [PATCH 54/93] chore: more cleanuop --- src/Governance.sol | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 8826b53..2766df4 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -582,9 +582,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance state.countedVoteLQTY -= initiativeState.voteLQTY; globalState = state; - /// @audit removal math causes issues - - /// @audit Should not delete this /// weeks * 2^16 > u32 so the contract will stop working before this is an issue registeredInitiatives[_initiative] = UNREGISTERED_INITIATIVE; @@ -599,20 +596,19 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState_) = _snapshotVotesForInitiative(_initiative); - // TODO: Return type from state FSM can be standardized (InitiativeStatus status, , uint256 claimableAmount ) = getInitiativeState(_initiative); - /// @audit Return 0 if we cannot claim /// INVARIANT: /// We cannot claim only for 2 reasons: /// We have already claimed /// We do not meet the threshold - /// TODO: Enforce this with assertions if(status != InitiativeStatus.CLAIMABLE) { return 0; } - assert(votesSnapshot_.forEpoch == epoch() - 1); /// @audit INVARIANT: You can only claim for previous epoch + /// @audit INVARIANT: You can only claim for previous epoch + assert(votesSnapshot_.forEpoch == epoch() - 1); + /// All unclaimed rewards are always recycled /// Invariant `lastEpochClaim` is < epoch() - 1; | /// If `lastEpochClaim` is older than epoch() - 1 it means the initiative couldn't claim any rewards this epoch From ab20d483b177f5e490f2c3d7e825ed854115e023 Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 15:47:28 +0200 Subject: [PATCH 55/93] feat: stateless checks use memory variables --- src/Governance.sol | 28 ++++++++++++++++++++++------ src/interfaces/IGovernance.sol | 2 +- test/Governance.t.sol | 16 ++++++++-------- test/TEST.md | 2 +- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 2766df4..74caf5b 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -248,8 +248,19 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance } /// @inheritdoc IGovernance - function calculateVotingThreshold() public view returns (uint256) { - uint256 snapshotVotes = votesSnapshot.votes; + function getLatestVotingThreshold() public view returns (uint256) { + uint256 snapshotVotes = votesSnapshot.votes; /// @audit technically can be out of synch + + return calculateVotingThreshold(snapshotVotes); + } + + function calculateVotingThreshold() public returns (uint256) { + (VoteSnapshot memory snapshot, ) = _snapshotVotes(); + + return calculateVotingThreshold(snapshot.votes); + } + + function calculateVotingThreshold(uint256 snapshotVotes) public view returns (uint256) { if (snapshotVotes == 0) return 0; uint256 minVotes; // to reach MIN_CLAIM: snapshotVotes * MIN_CLAIM / boldAccrued @@ -359,6 +370,10 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(_initiative); + return getInitiativeState(_initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState); + } + + function getInitiativeState(address _initiative, VoteSnapshot memory votesSnapshot_, InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) public view returns (InitiativeStatus status, uint16 lastEpochClaim, uint256 claimableAmount) { lastEpochClaim = initiativeStates[_initiative].lastEpochClaim; // == Already Claimed Condition == // @@ -368,12 +383,13 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance } // == Disabled Condition == // - // TODO: If a initiative is disabled, we return false and the last epoch claim + // If a initiative is disabled, we return false and the last epoch claim if(registeredInitiatives[_initiative] == UNREGISTERED_INITIATIVE) { - return (InitiativeStatus.DISABLED, lastEpochClaim, 0); /// @audit By definition it must have zero rewards + return (InitiativeStatus.DISABLED, lastEpochClaim, 0); /// By definition it has zero rewards } - uint256 votingTheshold = calculateVotingThreshold(); + // NOTE: Pass the snapshot value so we get accurate result + uint256 votingTheshold = calculateVotingThreshold(votesSnapshot_.votes); // If it's voted and can get rewards // Votes > calculateVotingThreshold @@ -450,7 +466,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance (, GlobalState memory state) = _snapshotVotes(); - uint256 votingThreshold = calculateVotingThreshold(); + uint256 votingThreshold = getLatestVotingThreshold(); /// TODO: Delete uint16 currentEpoch = epoch(); UserState memory userState = userStates[msg.sender]; diff --git a/src/interfaces/IGovernance.sol b/src/interfaces/IGovernance.sol index 5e0592e..e8c3c9c 100644 --- a/src/interfaces/IGovernance.sol +++ b/src/interfaces/IGovernance.sol @@ -223,7 +223,7 @@ interface IGovernance { /// - 4% of the total voting LQTY in the previous epoch /// - or the minimum number of votes necessary to claim at least MIN_CLAIM BOLD /// @return votingThreshold Voting threshold - function calculateVotingThreshold() external view returns (uint256 votingThreshold); + function getLatestVotingThreshold() external view returns (uint256 votingThreshold); /// @notice Snapshots votes for the previous epoch and accrues funds for the current epoch /// @param _initiative Address of the initiative diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 84846c8..f3a922c 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -388,7 +388,7 @@ contract GovernanceTest is Test { governance.lqtyToVotes(_lqtyAmount, _currentTimestamp, _averageTimestamp); } - function test_calculateVotingThreshold() public { + function test_getLatestVotingThreshold() public { governance = new Governance( address(lqty), address(lusd), @@ -411,7 +411,7 @@ contract GovernanceTest is Test { ); // is 0 when the previous epochs votes are 0 - assertEq(governance.calculateVotingThreshold(), 0); + assertEq(governance.getLatestVotingThreshold(), 0); // check that votingThreshold is is high enough such that MIN_CLAIM is met IGovernance.VoteSnapshot memory snapshot = IGovernance.VoteSnapshot(1e18, 1); @@ -428,7 +428,7 @@ contract GovernanceTest is Test { vm.store(address(governance), bytes32(uint256(1)), bytes32(abi.encode(boldAccrued))); assertEq(governance.boldAccrued(), 1000e18); - assertEq(governance.calculateVotingThreshold(), MIN_CLAIM / 1000); + assertEq(governance.getLatestVotingThreshold(), MIN_CLAIM / 1000); // check that votingThreshold is 4% of votes of previous epoch governance = new Governance( @@ -466,7 +466,7 @@ contract GovernanceTest is Test { vm.store(address(governance), bytes32(uint256(1)), bytes32(abi.encode(boldAccrued))); assertEq(governance.boldAccrued(), 1000e18); - assertEq(governance.calculateVotingThreshold(), 10000e18 * 0.04); + assertEq(governance.getLatestVotingThreshold(), 10000e18 * 0.04); } // should not revert under any state @@ -512,7 +512,7 @@ contract GovernanceTest is Test { vm.store(address(governance), bytes32(uint256(1)), bytes32(abi.encode(_boldAccrued))); assertEq(governance.boldAccrued(), _boldAccrued); - governance.calculateVotingThreshold(); + governance.getLatestVotingThreshold(); } function test_registerInitiative() public { @@ -715,7 +715,7 @@ contract GovernanceTest is Test { (IGovernance.VoteSnapshot memory snapshot, IGovernance.InitiativeVoteSnapshot memory initiativeVoteSnapshot1) = governance.snapshotVotesForInitiative(baseInitiative1); (, IGovernance.InitiativeVoteSnapshot memory initiativeVoteSnapshot2) = governance.snapshotVotesForInitiative(baseInitiative2); - uint256 threshold = governance.calculateVotingThreshold(); + uint256 threshold = governance.getLatestVotingThreshold(); assertLt(initiativeVoteSnapshot1.votes, threshold, "it didn't get rewards"); uint256 votingPowerWithProjection = governance.lqtyToVotes(voteLQTY1, governance.epochStart() + governance.EPOCH_DURATION(), averageStakingTimestampVoteLQTY1); @@ -758,7 +758,7 @@ contract GovernanceTest is Test { (IGovernance.VoteSnapshot memory snapshot, IGovernance.InitiativeVoteSnapshot memory initiativeVoteSnapshot1) = governance.snapshotVotesForInitiative(baseInitiative1); - uint256 threshold = governance.calculateVotingThreshold(); + uint256 threshold = governance.getLatestVotingThreshold(); assertLt(initiativeVoteSnapshot1.votes, threshold, "it didn't get rewards"); } @@ -1157,7 +1157,7 @@ contract GovernanceTest is Test { console.log("snapshot votes", votes); console.log("snapshot vetos", vetos); - console.log("governance.calculateVotingThreshold()", governance.calculateVotingThreshold()); + console.log("governance.getLatestVotingThreshold()", governance.getLatestVotingThreshold()); assertEq(governance.claimForInitiative(baseInitiative2), 0, "zero 2"); assertEq(governance.claimForInitiative(baseInitiative2), 0, "zero 3"); diff --git a/test/TEST.md b/test/TEST.md index 05f23f2..e009555 100644 --- a/test/TEST.md +++ b/test/TEST.md @@ -40,7 +40,7 @@ Governance: - should return the correct number of seconds elapsed within an epoch for a given block.timestamp - lqtyToVotes() - should not revert under any input -- calculateVotingThreshold() +- getLatestVotingThreshold() - should return a votingThreshold that's either - high enough such that MIN_CLAIM is met - 4% of the votes from the previous epoch From d77fa120fb12d85ea9cea19d5a421c767925992f Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 15:58:15 +0200 Subject: [PATCH 56/93] fix: 5.3 --- src/Governance.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Governance.sol b/src/Governance.sol index 2766df4..b9f426f 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -513,7 +513,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // update the average staking timestamp for all counted voting LQTY state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( state.countedVoteLQTYAverageTimestamp, - initiativeState.averageStakingTimestampVoteLQTY, + prevInitiativeState.averageStakingTimestampVoteLQTY, /// @audit TODO Write tests that fail from this bug state.countedVoteLQTY, state.countedVoteLQTY - prevInitiativeState.voteLQTY ); From e39a3fd15ecb69ad6aad504568f4ee4bd12f48db Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 16:15:47 +0200 Subject: [PATCH 57/93] fix: remove unnecessary threshold --- src/Governance.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Governance.sol b/src/Governance.sol index 74caf5b..e04f7af 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -466,7 +466,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance (, GlobalState memory state) = _snapshotVotes(); - uint256 votingThreshold = getLatestVotingThreshold(); /// TODO: Delete uint16 currentEpoch = epoch(); UserState memory userState = userStates[msg.sender]; From 0b72301d4a605b99e79ab98127a0baf540a6578a Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 16:28:08 +0200 Subject: [PATCH 58/93] fix: deps --- lib/chimera | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/chimera b/lib/chimera index dc8192c..d5cf52b 160000 --- a/lib/chimera +++ b/lib/chimera @@ -1 +1 @@ -Subproject commit dc8192ccc9f5e45bf626270a228566a7485f328c +Subproject commit d5cf52bc5bbf75f988f8aada23fd12d0bcf7798a From d60e671822be061d9c9f40802457a688019f6e20 Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 16:28:15 +0200 Subject: [PATCH 59/93] fix: medusa and signature --- src/utils/EncodingDecodingLib.sol | 4 ++-- test/recon/properties/BribeInitiativeProperties.sol | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/EncodingDecodingLib.sol b/src/utils/EncodingDecodingLib.sol index 1f4d05f..c3dee35 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) public pure returns (uint224) { + function encodeLQTYAllocation(uint88 _lqty, uint32 _averageTimestamp) internal pure returns (uint224) { uint224 _value = (uint224(_lqty) << 32) | _averageTimestamp; return _value; } - function decodeLQTYAllocation(uint224 _value) public pure returns (uint88, uint32) { + function decodeLQTYAllocation(uint224 _value) internal pure returns (uint88, uint32) { return (uint88(_value >> 32), uint32(_value)); } } \ No newline at end of file diff --git a/test/recon/properties/BribeInitiativeProperties.sol b/test/recon/properties/BribeInitiativeProperties.sol index d1319a0..595ad6c 100644 --- a/test/recon/properties/BribeInitiativeProperties.sol +++ b/test/recon/properties/BribeInitiativeProperties.sol @@ -26,8 +26,8 @@ abstract contract BribeInitiativeProperties is BeforeAfter { uint88 lusdPercentageOfBribe88 = uint88(lusdPercentageOfBribe >> 40); // calculate user allocation percentage of total for this epoch - uint88 lqtyAllocatedByUserAtEpoch = IBribeInitiative(initiative).lqtyAllocatedByUserAtEpoch(user, currentEpoch); - uint88 totalLQTYAllocatedAtEpoch = IBribeInitiative(initiative).totalLQTYAllocatedByEpoch(currentEpoch); + (uint88 lqtyAllocatedByUserAtEpoch, ) = IBribeInitiative(initiative).lqtyAllocatedByUserAtEpoch(user, currentEpoch); + (uint88 totalLQTYAllocatedAtEpoch, ) = IBribeInitiative(initiative).totalLQTYAllocatedByEpoch(currentEpoch); uint88 allocationPercentageOfTotal = (lqtyAllocatedByUserAtEpoch / totalLQTYAllocatedAtEpoch) * 10_000; // check that allocation percentage and received bribe percentage match @@ -45,7 +45,7 @@ abstract contract BribeInitiativeProperties is BeforeAfter { uint16 currentEpoch = governance.epoch(); for(uint8 i; i < deployedInitiatives.length; i++) { IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); - uint88 lqtyAllocatedByUserAtEpoch = initiative.lqtyAllocatedByUserAtEpoch(user, currentEpoch); + (uint88 lqtyAllocatedByUserAtEpoch, ) = initiative.lqtyAllocatedByUserAtEpoch(user, currentEpoch); eq(ghostLqtyAllocationByUserAtEpoch[user], lqtyAllocatedByUserAtEpoch, "BI-03: Accounting for user allocation amount is always correct"); } } @@ -54,7 +54,7 @@ abstract contract BribeInitiativeProperties is BeforeAfter { uint16 currentEpoch = governance.epoch(); for(uint8 i; i < deployedInitiatives.length; i++) { IBribeInitiative initiative = IBribeInitiative(deployedInitiatives[i]); - uint88 totalLQTYAllocatedAtEpoch = initiative.totalLQTYAllocatedByEpoch(currentEpoch); + (uint88 totalLQTYAllocatedAtEpoch, ) = initiative.totalLQTYAllocatedByEpoch(currentEpoch); eq(ghostTotalAllocationAtEpoch[currentEpoch], totalLQTYAllocatedAtEpoch, "BI-04: Accounting for total allocation amount is always correct"); } } From 6c8de58dd2f153a73f88dd80db14f0ce8e401885 Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 16:31:03 +0200 Subject: [PATCH 60/93] fix: cryticToFoundry and overflow --- test/recon/CryticToFoundry.sol | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/recon/CryticToFoundry.sol b/test/recon/CryticToFoundry.sol index e69de29..450a2fb 100644 --- a/test/recon/CryticToFoundry.sol +++ b/test/recon/CryticToFoundry.sol @@ -0,0 +1,32 @@ + +// 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"; + +contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts { + function setUp() public { + setup(); + } + + // forge test --match-test test_property_GV01_0 -vv + function test_property_GV01_0() public { + + vm.roll(237680); + vm.warp(2536756); + vm.prank(0x0000000000000000000000000000000000020000); + governance_depositLQTYViaPermit(37207352249250036667298804); + + vm.roll(273168); + vm.warp(3045913); + vm.prank(0x0000000000000000000000000000000000010000); + governance_unregisterInitiative(0); + + vm.roll(301841); + vm.warp(3350068); + vm.prank(0x0000000000000000000000000000000000030000); + property_GV01(); + } +} \ No newline at end of file From cd7fba34c621393396b7609e74161301c91c1fe5 Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 18:38:30 +0200 Subject: [PATCH 61/93] fix: property --- test/recon/BeforeAfter.sol | 6 +++--- test/recon/CryticToFoundry.sol | 14 +++++++------- test/recon/properties/GovernanceProperties.sol | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/test/recon/BeforeAfter.sol b/test/recon/BeforeAfter.sol index 192196e..0719c9f 100644 --- a/test/recon/BeforeAfter.sol +++ b/test/recon/BeforeAfter.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.0; import {Asserts} from "@chimera/Asserts.sol"; import {Setup} from "./Setup.sol"; -import {IGovernance} from "../../src/interfaces/IGovernance.sol"; -import {IBribeInitiative} from "../../src/interfaces/IBribeInitiative.sol"; -import {Governance} from "../../src/Governance.sol"; +import {IGovernance} from "src/interfaces/IGovernance.sol"; +import {IBribeInitiative} from "src/interfaces/IBribeInitiative.sol"; +import {Governance} from "src/Governance.sol"; abstract contract BeforeAfter is Setup, Asserts { diff --git a/test/recon/CryticToFoundry.sol b/test/recon/CryticToFoundry.sol index 450a2fb..79a797d 100644 --- a/test/recon/CryticToFoundry.sol +++ b/test/recon/CryticToFoundry.sol @@ -14,18 +14,18 @@ contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts { // forge test --match-test test_property_GV01_0 -vv function test_property_GV01_0() public { - vm.roll(237680); - vm.warp(2536756); + vm.roll(block.timestamp + 237680); + vm.warp(block.number + 2536756); vm.prank(0x0000000000000000000000000000000000020000); - governance_depositLQTYViaPermit(37207352249250036667298804); + governance_depositLQTY(37207352249250036667298804); - vm.roll(273168); - vm.warp(3045913); + vm.roll(block.timestamp + 273168); + vm.warp(block.number + 3045913); vm.prank(0x0000000000000000000000000000000000010000); governance_unregisterInitiative(0); - vm.roll(301841); - vm.warp(3350068); + vm.roll(block.timestamp + 301841); + vm.warp(block.number + 3350068); vm.prank(0x0000000000000000000000000000000000030000); property_GV01(); } diff --git a/test/recon/properties/GovernanceProperties.sol b/test/recon/properties/GovernanceProperties.sol index a3033d7..1930b35 100644 --- a/test/recon/properties/GovernanceProperties.sol +++ b/test/recon/properties/GovernanceProperties.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import {BeforeAfter} from "../BeforeAfter.sol"; +import {Governance} from "src/Governance.sol"; abstract contract GovernanceProperties is BeforeAfter { @@ -11,6 +12,23 @@ abstract contract GovernanceProperties is BeforeAfter { // loop through the initiatives and check that their status hasn't changed for(uint8 i; i < deployedInitiatives.length; i++) { address initiative = deployedInitiatives[i]; + + // Hardcoded Allowed FSM + if(_before.initiativeStatus[initiative] == Governance.InitiativeStatus.UNREGISTERABLE) { + // ALLOW TO SET DISABLE + if(_after.initiativeStatus[initiative] == Governance.InitiativeStatus.DISABLED) { + return; + } + } + + if(_before.initiativeStatus[initiative] == Governance.InitiativeStatus.CLAIMABLE) { + // ALLOW TO CLAIM + if(_after.initiativeStatus[initiative] == Governance.InitiativeStatus.CLAIMED) { + return; + } + } + + eq(uint256(_before.initiativeStatus[initiative]), uint256(_after.initiativeStatus[initiative]), "GV-01: Initiative state should only return one state per epoch"); } } From dfefc277e0fbfaa16c87a5b51bf1eb40d29f64ed Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 19:03:30 +0200 Subject: [PATCH 62/93] fix: introduce more states to track initiative and fix invariant test --- src/Governance.sol | 14 +++++ test/recon/CryticToFoundry.sol | 59 +++++++++++++------ .../recon/properties/GovernanceProperties.sol | 11 ++++ 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 9502fa3..1f915d9 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -352,6 +352,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @notice Given an initiative, return whether the initiative will be unregisted, whether it can claim and which epoch it last claimed at enum InitiativeStatus { + NONEXISTENT, /// This Initiative Doesn't exist | This is never returned + COOLDOWN, /// This epoch was just registered SKIP, /// This epoch will result in no rewards and no unregistering CLAIMABLE, /// This epoch will result in claiming rewards CLAIMED, /// The rewards for this epoch have been claimed @@ -376,6 +378,18 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance function getInitiativeState(address _initiative, VoteSnapshot memory votesSnapshot_, InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) public view returns (InitiativeStatus status, uint16 lastEpochClaim, uint256 claimableAmount) { lastEpochClaim = initiativeStates[_initiative].lastEpochClaim; + // == Non existant Condition == // + // If a initiative is disabled, we return false and the last epoch claim + if(registeredInitiatives[_initiative] == 0) { + return (InitiativeStatus.NONEXISTENT, 0, 0); /// By definition it has zero rewards + } + + // == Non existant Condition == // + // If a initiative is disabled, we return false and the last epoch claim + if(registeredInitiatives[_initiative] == epoch()) { + return (InitiativeStatus.COOLDOWN, 0, 0); /// Was registered this week + } + // == Already Claimed Condition == // if(lastEpochClaim >= epoch() - 1) { // early return, we have already claimed diff --git a/test/recon/CryticToFoundry.sol b/test/recon/CryticToFoundry.sol index 79a797d..9074abe 100644 --- a/test/recon/CryticToFoundry.sol +++ b/test/recon/CryticToFoundry.sol @@ -11,22 +11,45 @@ contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts { setup(); } - // forge test --match-test test_property_GV01_0 -vv - function test_property_GV01_0() public { - - vm.roll(block.timestamp + 237680); - vm.warp(block.number + 2536756); - vm.prank(0x0000000000000000000000000000000000020000); - governance_depositLQTY(37207352249250036667298804); - - vm.roll(block.timestamp + 273168); - vm.warp(block.number + 3045913); - vm.prank(0x0000000000000000000000000000000000010000); - governance_unregisterInitiative(0); - - vm.roll(block.timestamp + 301841); - vm.warp(block.number + 3350068); - vm.prank(0x0000000000000000000000000000000000030000); - property_GV01(); - } +// forge test --match-test test_property_GV01_0 -vv + +function test_property_GV01_0() public { + + vm.roll(block.number + 4921); + vm.warp(block.timestamp + 277805); + vm.prank(0x0000000000000000000000000000000000020000); + helper_deployInitiative(); + + vm.roll(block.number + 17731); + vm.warp(block.timestamp + 661456); + vm.prank(0x0000000000000000000000000000000000010000); + helper_deployInitiative(); + + vm.roll(block.number + 41536); + vm.warp(block.timestamp + 1020941); + vm.prank(0x0000000000000000000000000000000000010000); + helper_deployInitiative(); + + vm.roll(block.number + 41536); + vm.warp(block.timestamp + 1020941); + vm.prank(0x0000000000000000000000000000000000010000); + helper_deployInitiative(); + + vm.roll(block.number + 41536); + vm.warp(block.timestamp + 1020941); + vm.prank(0x0000000000000000000000000000000000020000); + helper_deployInitiative(); + + vm.roll(block.number + 61507); + vm.warp(block.timestamp + 1049774); + vm.prank(0x0000000000000000000000000000000000020000); + governance_registerInitiative(22); + + vm.roll(block.number + 61507); + vm.warp(block.timestamp + 1049774); + vm.prank(0x0000000000000000000000000000000000030000); + property_GV01(); +} + + } \ No newline at end of file diff --git a/test/recon/properties/GovernanceProperties.sol b/test/recon/properties/GovernanceProperties.sol index 1930b35..2725673 100644 --- a/test/recon/properties/GovernanceProperties.sol +++ b/test/recon/properties/GovernanceProperties.sol @@ -6,6 +6,10 @@ import {Governance} from "src/Governance.sol"; abstract contract GovernanceProperties is BeforeAfter { + + /// A Initiative cannot change in status + /// Except for being unregistered + /// Or claiming rewards function property_GV01() public { // first check that epoch hasn't changed after the operation if(_before.epoch == _after.epoch) { @@ -27,6 +31,13 @@ abstract contract GovernanceProperties is BeforeAfter { return; } } + + if(_before.initiativeStatus[initiative] == Governance.InitiativeStatus.NONEXISTENT) { + // Registered -> SKIP is ok + if(_after.initiativeStatus[initiative] == Governance.InitiativeStatus.COOLDOWN) { + return; + } + } eq(uint256(_before.initiativeStatus[initiative]), uint256(_after.initiativeStatus[initiative]), "GV-01: Initiative state should only return one state per epoch"); From c4da23399766a225615e150ebfbcef2ef66d4a23 Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 19:52:48 +0200 Subject: [PATCH 63/93] chore: gas + docs --- src/Governance.sol | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 1f915d9..d4dd0bd 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -376,18 +376,24 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance } function getInitiativeState(address _initiative, VoteSnapshot memory votesSnapshot_, InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) public view returns (InitiativeStatus status, uint16 lastEpochClaim, uint256 claimableAmount) { - lastEpochClaim = initiativeStates[_initiative].lastEpochClaim; - // == Non existant Condition == // - // If a initiative is disabled, we return false and the last epoch claim + // == Non existent Condition == // if(registeredInitiatives[_initiative] == 0) { return (InitiativeStatus.NONEXISTENT, 0, 0); /// By definition it has zero rewards } - // == Non existant Condition == // + // == Just Registered Condition == // // If a initiative is disabled, we return false and the last epoch claim if(registeredInitiatives[_initiative] == epoch()) { - return (InitiativeStatus.COOLDOWN, 0, 0); /// Was registered this week + return (InitiativeStatus.COOLDOWN, 0, 0); /// Was registered this week, cannot have rewards + } + + // Fetch last epoch at which we claimed + lastEpochClaim = initiativeStates[_initiative].lastEpochClaim; + + // == Disabled Condition == // + if(registeredInitiatives[_initiative] == UNREGISTERED_INITIATIVE) { + return (InitiativeStatus.DISABLED, lastEpochClaim, 0); /// By definition it has zero rewards } // == Already Claimed Condition == // @@ -396,11 +402,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance return (InitiativeStatus.CLAIMED, lastEpochClaim, claimableAmount); } - // == Disabled Condition == // - // If a initiative is disabled, we return false and the last epoch claim - if(registeredInitiatives[_initiative] == UNREGISTERED_INITIATIVE) { - return (InitiativeStatus.DISABLED, lastEpochClaim, 0); /// By definition it has zero rewards - } // NOTE: Pass the snapshot value so we get accurate result uint256 votingTheshold = calculateVotingThreshold(votesSnapshot_.votes); From 3d192fa30895da188ce09ee6f74f3909f9d92177 Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 20:11:19 +0200 Subject: [PATCH 64/93] feat: view vs non view properties --- .../recon/properties/GovernanceProperties.sol | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/recon/properties/GovernanceProperties.sol b/test/recon/properties/GovernanceProperties.sol index 2725673..1673073 100644 --- a/test/recon/properties/GovernanceProperties.sol +++ b/test/recon/properties/GovernanceProperties.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import {BeforeAfter} from "../BeforeAfter.sol"; import {Governance} from "src/Governance.sol"; +import {IGovernance} from "src/interfaces/IGovernance.sol"; abstract contract GovernanceProperties is BeforeAfter { @@ -45,4 +46,29 @@ abstract contract GovernanceProperties is BeforeAfter { } } + // View vs non view must have same results + function property_viewTotalVotesAndStateEquivalency() public { + for(uint8 i; i < deployedInitiatives.length; i++) { + (IGovernance.InitiativeVoteSnapshot memory initiativeSnapshot_view, , bool shouldUpdate) = governance.getInitiativeSnapshotAndState(deployedInitiatives[i]); + (, IGovernance.InitiativeVoteSnapshot memory initiativeVoteSnapshot) = governance.snapshotVotesForInitiative(deployedInitiatives[i]); + + eq(initiativeSnapshot_view.votes, initiativeVoteSnapshot.votes, "votes"); + eq(initiativeSnapshot_view.forEpoch, initiativeVoteSnapshot.forEpoch, "forEpoch"); + eq(initiativeSnapshot_view.lastCountedEpoch, initiativeVoteSnapshot.lastCountedEpoch, "lastCountedEpoch"); + eq(initiativeSnapshot_view.vetos, initiativeVoteSnapshot.vetos, "vetos"); + } + } + + function property_viewCalculateVotingThreshold() public { + (, , bool shouldUpdate) = governance.getTotalVotesAndState(); + + if(!shouldUpdate) { + // If it's already synched it must match + uint256 latestKnownThreshold = governance.getLatestVotingThreshold(); + uint256 calculated = governance.calculateVotingThreshold(); + eq(latestKnownThreshold, calculated, "match"); + } + } + + } \ No newline at end of file From aed7a96de2f0dc41f90b3363b4efc98430f2df22 Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 20:26:01 +0200 Subject: [PATCH 65/93] feat: mostly using FSM, can simplify a bit --- src/Governance.sol | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index d4dd0bd..31f8642 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -444,7 +444,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance bold.safeTransferFrom(msg.sender, address(this), REGISTRATION_FEE); require(_initiative != address(0), "Governance: zero-address"); - require(registeredInitiatives[_initiative] == 0, "Governance: initiative-already-registered"); + (InitiativeStatus status, ,) = getInitiativeState(_initiative); + require(status == InitiativeStatus.NONEXISTENT, "Governance: initiative-already-registered"); address userProxyAddress = deriveUserProxyAddress(msg.sender); (VoteSnapshot memory snapshot,) = _snapshotVotes(); @@ -495,14 +496,20 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance deltaLQTYVotes <= 0 || deltaLQTYVotes >= 0 && secondsWithinEpoch() <= EPOCH_VOTING_CUTOFF, "Governance: epoch-voting-cutoff" ); + + (InitiativeStatus status, ,) = getInitiativeState(initiative); { uint16 registeredAtEpoch = registeredInitiatives[initiative]; if(deltaLQTYVotes > 0 || deltaLQTYVetos > 0) { + require(currentEpoch > registeredAtEpoch && registeredAtEpoch != 0, "Governance: initiative-not-active"); + /// @audit Experimental FSM based check | This one is slightly clearer + require(status == InitiativeStatus.SKIP || status == InitiativeStatus.CLAIMABLE || status == InitiativeStatus.CLAIMED || status == InitiativeStatus.UNREGISTERABLE, "Governance: Vote FSM"); + } - if(registeredAtEpoch == UNREGISTERED_INITIATIVE) { + if(status == InitiativeStatus.DISABLED) { require(deltaLQTYVotes <= 0 && deltaLQTYVetos <= 0, "Must be a withdrawal"); } } @@ -596,7 +603,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// Invariant: Must only claim once or unregister require(initiativeState.lastEpochClaim < epoch() - 1); - + /// @audit Can remove a bunch of stuff (InitiativeStatus status, , )= getInitiativeState(_initiative); require(status == InitiativeStatus.UNREGISTERABLE, "Governance: cannot-unregister-initiative"); From c312fcbedd7e9688ad355277f80b4781250ec8d2 Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 20:31:31 +0200 Subject: [PATCH 66/93] feat: commiting to using FSM --- src/Governance.sol | 16 +++++++++------- test/Governance.t.sol | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 31f8642..8701574 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -496,16 +496,18 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance deltaLQTYVotes <= 0 || deltaLQTYVotes >= 0 && secondsWithinEpoch() <= EPOCH_VOTING_CUTOFF, "Governance: epoch-voting-cutoff" ); - - (InitiativeStatus status, ,) = getInitiativeState(initiative); + // Check FSM + // Can vote positively in SKIP, CLAIMABLE, CLAIMED and UNREGISTERABLE states + // Force to remove votes if disabled + // Can remove votes and vetos in every stage { + (InitiativeStatus status, ,) = getInitiativeState(initiative); + uint16 registeredAtEpoch = registeredInitiatives[initiative]; if(deltaLQTYVotes > 0 || deltaLQTYVetos > 0) { - - require(currentEpoch > registeredAtEpoch && registeredAtEpoch != 0, "Governance: initiative-not-active"); - /// @audit Experimental FSM based check | This one is slightly clearer - require(status == InitiativeStatus.SKIP || status == InitiativeStatus.CLAIMABLE || status == InitiativeStatus.CLAIMED || status == InitiativeStatus.UNREGISTERABLE, "Governance: Vote FSM"); + /// @audit FSM CHECK, note that the original version allowed voting on `Unregisterable` Initiatives - Prob should fix + require(status == InitiativeStatus.SKIP || status == InitiativeStatus.CLAIMABLE || status == InitiativeStatus.CLAIMED || status == InitiativeStatus.UNREGISTERABLE, "Governance: active-vote-fsm"); } @@ -604,7 +606,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// Invariant: Must only claim once or unregister require(initiativeState.lastEpochClaim < epoch() - 1); /// @audit Can remove a bunch of stuff - (InitiativeStatus status, , )= getInitiativeState(_initiative); + (InitiativeStatus status, , ) = getInitiativeState(_initiative); require(status == InitiativeStatus.UNREGISTERABLE, "Governance: cannot-unregister-initiative"); /// @audit TODO: Verify that the FSM here is correct diff --git a/test/Governance.t.sol b/test/Governance.t.sol index f3a922c..4e3372b 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -795,7 +795,7 @@ contract GovernanceTest is Test { int88[] memory reAddDeltaLQTYVetos = new int88[](1); /// @audit This MUST revert, an initiative should not be re-votable once disabled - vm.expectRevert("Governance: initiative-not-active"); + vm.expectRevert("Governance: active-vote-fsm"); governance.allocateLQTY(reAddInitiatives, reAddDeltaLQTYVotes, reAddDeltaLQTYVetos); } @@ -877,7 +877,7 @@ contract GovernanceTest is Test { int88[] memory deltaLQTYVetos = new int88[](1); // should revert if the initiative has been registered in the current epoch - vm.expectRevert("Governance: initiative-not-active"); + vm.expectRevert("Governance: active-vote-fsm"); governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); vm.warp(block.timestamp + 365 days); From 02bf4815002b261728838747c0f25d0cbf73d33b Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 20:41:10 +0200 Subject: [PATCH 67/93] feat: unregisterInitiative FSM equivalence --- src/Governance.sol | 19 +++++++------------ .../recon/properties/GovernanceProperties.sol | 1 - 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 8701574..e64527a 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -508,7 +508,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance if(deltaLQTYVotes > 0 || deltaLQTYVetos > 0) { /// @audit FSM CHECK, note that the original version allowed voting on `Unregisterable` Initiatives - Prob should fix require(status == InitiativeStatus.SKIP || status == InitiativeStatus.CLAIMABLE || status == InitiativeStatus.CLAIMED || status == InitiativeStatus.UNREGISTERABLE, "Governance: active-vote-fsm"); - } if(status == InitiativeStatus.DISABLED) { @@ -516,7 +515,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance } } - (, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(initiative); // deep copy of the initiative's state before the allocation @@ -594,22 +592,19 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @inheritdoc IGovernance function unregisterInitiative(address _initiative) external nonReentrant { - uint16 registrationEpoch = registeredInitiatives[_initiative]; - require(registrationEpoch != 0, "Governance: initiative-not-registered"); + (InitiativeStatus status, , ) = getInitiativeState(_initiative); + require(status != InitiativeStatus.NONEXISTENT, "Governance: initiative-not-registered"); + require(status != InitiativeStatus.COOLDOWN, "Governance: initiative-in-warm-up"); + require(status == InitiativeStatus.UNREGISTERABLE, "Governance: cannot-unregister-initiative"); + uint16 currentEpoch = epoch(); - require(registrationEpoch + REGISTRATION_WARM_UP_PERIOD < currentEpoch, "Governance: initiative-in-warm-up"); (, GlobalState memory state) = _snapshotVotes(); (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(_initiative); - /// Invariant: Must only claim once or unregister - require(initiativeState.lastEpochClaim < epoch() - 1); - /// @audit Can remove a bunch of stuff - (InitiativeStatus status, , ) = getInitiativeState(_initiative); - require(status == InitiativeStatus.UNREGISTERABLE, "Governance: cannot-unregister-initiative"); - - /// @audit TODO: Verify that the FSM here is correct + /// @audit Invariant: Must only claim once or unregister + assert(initiativeState.lastEpochClaim < currentEpoch - 1); // recalculate the average staking timestamp for all counted voting LQTY if the initiative was counted in state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( diff --git a/test/recon/properties/GovernanceProperties.sol b/test/recon/properties/GovernanceProperties.sol index 1673073..2667d52 100644 --- a/test/recon/properties/GovernanceProperties.sol +++ b/test/recon/properties/GovernanceProperties.sol @@ -40,7 +40,6 @@ abstract contract GovernanceProperties is BeforeAfter { } } - eq(uint256(_before.initiativeStatus[initiative]), uint256(_after.initiativeStatus[initiative]), "GV-01: Initiative state should only return one state per epoch"); } } From 121bede413c7895b7512473bfde901e5d8a802bc Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 20:45:42 +0200 Subject: [PATCH 68/93] chore: comment --- src/Governance.sol | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index d4dd0bd..7bc6d72 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -427,15 +427,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance ) { return (InitiativeStatus.UNREGISTERABLE, lastEpochClaim, 0); } - - // How do we know that they have canClaimRewards? - // They must have votes / totalVotes AND meet the Requirement AND not be vetoed - /// @audit if we already are above, then why are we re-computing this? - // Ultimately the checkpoint logic for initiative is fine, so we can skip this // == Not meeting threshold Condition == // - - return (InitiativeStatus.SKIP, lastEpochClaim, 0); } From 5d34676e79d88cc2544b0f3d2493a088f3974dda Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 20:48:52 +0200 Subject: [PATCH 69/93] chore: comment --- src/Governance.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Governance.sol b/src/Governance.sol index 7bc6d72..b9b3167 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -419,7 +419,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // == Unregister Condition == // /// @audit epoch() - 1 because we can have Now - 1 and that's not a removal case | TODO: Double check | Worst case QA, off by one epoch - // TODO: IMO we can use the claimed variable here /// This shifts the logic by 1 epoch if((initiativeState.lastEpochClaim + UNREGISTRATION_AFTER_EPOCHS < epoch() - 1) || votesForInitiativeSnapshot_.vetos > votesForInitiativeSnapshot_.votes From e80a9ce5794265688e5a97bf3bac8b2a20376168 Mon Sep 17 00:00:00 2001 From: gallo Date: Tue, 15 Oct 2024 21:03:01 +0200 Subject: [PATCH 70/93] feat: simplify claim logic --- src/Governance.sol | 5 ----- test/recon/targets/GovernanceTargets.sol | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index b9b3167..eb1d365 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -592,8 +592,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance (InitiativeStatus status, , )= getInitiativeState(_initiative); require(status == InitiativeStatus.UNREGISTERABLE, "Governance: cannot-unregister-initiative"); - /// @audit TODO: Verify that the FSM here is correct - // recalculate the average staking timestamp for all counted voting LQTY if the initiative was counted in state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( state.countedVoteLQTYAverageTimestamp, @@ -616,8 +614,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @inheritdoc IGovernance function claimForInitiative(address _initiative) external nonReentrant returns (uint256) { (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); - (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState_) = _snapshotVotesForInitiative(_initiative); - (InitiativeStatus status, , uint256 claimableAmount ) = getInitiativeState(_initiative); /// INVARIANT: @@ -635,7 +631,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// Invariant `lastEpochClaim` is < epoch() - 1; | /// If `lastEpochClaim` is older than epoch() - 1 it means the initiative couldn't claim any rewards this epoch initiativeStates[_initiative].lastEpochClaim = epoch() - 1; - votesForInitiativeSnapshot[_initiative] = votesForInitiativeSnapshot_; // implicitly prevents double claiming bold.safeTransfer(_initiative, claimableAmount); diff --git a/test/recon/targets/GovernanceTargets.sol b/test/recon/targets/GovernanceTargets.sol index 59bc42c..794aab3 100644 --- a/test/recon/targets/GovernanceTargets.sol +++ b/test/recon/targets/GovernanceTargets.sol @@ -54,6 +54,24 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { governance.claimForInitiative(initiative); } + function governance_claimForInitiativeFuzzTest(uint8 initiativeIndex) withChecks public { + address initiative = _getDeployedInitiative(initiativeIndex); + + // TODO Use view functions to get initiative and snapshot data + // Pass those and verify the claim amt matches received + // Check if we can claim + + // TODO: Check FSM as well, the initiative can be CLAIMABLE + // And must become CLAIMED right after + + + uint256 received = governance.claimForInitiative(initiative); + uint256 secondReceived = governance.claimForInitiative(initiative); + if(received != 0) { + eq(secondReceived, 0, "Cannot claim twice"); + } + } + function governance_claimFromStakingV1(uint8 recipientIndex) withChecks public { address rewardRecipient = _getRandomUser(recipientIndex); governance.claimFromStakingV1(rewardRecipient); From 5f863c3c2f1795ee3537404217aeb473ed2fcb65 Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 09:18:22 +0200 Subject: [PATCH 71/93] feat: pretty close to done --- src/Governance.sol | 28 ++++++++++++++++++---------- test/Governance.t.sol | 2 +- test/recon/Properties.sol | 2 +- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index eb1d365..72b6934 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -247,6 +247,10 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance return uint240(_lqtyAmount) * _averageAge(uint32(_currentTimestamp), _averageTimestamp); } + /*////////////////////////////////////////////////////////////// + SNAPSHOTS + //////////////////////////////////////////////////////////////*/ + /// @inheritdoc IGovernance function getLatestVotingThreshold() public view returns (uint256) { uint256 snapshotVotes = votesSnapshot.votes; /// @audit technically can be out of synch @@ -325,8 +329,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance if (initiativeSnapshot.forEpoch < currentEpoch - 1) { shouldUpdate = true; - // Update in memory data - // Safe as long as: Any time a initiative state changes, we first update the snapshot uint32 start = epochStart(); uint240 votes = lqtyToVotes(initiativeState.voteLQTY, start, initiativeState.averageStakingTimestampVoteLQTY); @@ -349,6 +351,10 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance (initiativeVoteSnapshot,) = _snapshotVotesForInitiative(_initiative); } + /*////////////////////////////////////////////////////////////// + FSM + //////////////////////////////////////////////////////////////*/ + /// @notice Given an initiative, return whether the initiative will be unregisted, whether it can claim and which epoch it last claimed at enum InitiativeStatus { @@ -375,6 +381,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance return getInitiativeState(_initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState); } + /// @dev Given an initiative address and its snapshot, determines the current state for an initiative function getInitiativeState(address _initiative, VoteSnapshot memory votesSnapshot_, InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) public view returns (InitiativeStatus status, uint16 lastEpochClaim, uint256 claimableAmount) { // == Non existent Condition == // @@ -533,14 +540,16 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance initiativeStates[initiative] = initiativeState; // update the average staking timestamp for all counted voting LQTY + /// Discount previous state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( state.countedVoteLQTYAverageTimestamp, prevInitiativeState.averageStakingTimestampVoteLQTY, /// @audit TODO Write tests that fail from this bug state.countedVoteLQTY, state.countedVoteLQTY - prevInitiativeState.voteLQTY ); - state.countedVoteLQTY -= prevInitiativeState.voteLQTY; + state.countedVoteLQTY -= prevInitiativeState.voteLQTY; /// @audit Overflow here MUST never happen2 + /// Add current state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( state.countedVoteLQTYAverageTimestamp, initiativeState.averageStakingTimestampVoteLQTY, @@ -578,10 +587,12 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @inheritdoc IGovernance function unregisterInitiative(address _initiative) external nonReentrant { uint16 registrationEpoch = registeredInitiatives[_initiative]; - require(registrationEpoch != 0, "Governance: initiative-not-registered"); + require(registrationEpoch != 0, "Governance: initiative-not-registered"); /// @audit use FSM uint16 currentEpoch = epoch(); - require(registrationEpoch + REGISTRATION_WARM_UP_PERIOD < currentEpoch, "Governance: initiative-in-warm-up"); + /// @audit Can delete this and refactor to not be necessary + require(registrationEpoch + REGISTRATION_WARM_UP_PERIOD < currentEpoch, "Governance: initiative-in-warm-up"); /// @audit use FSM + /// @audit GAS -> Use memory vals for `getInitiativeState` (, GlobalState memory state) = _snapshotVotes(); (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(_initiative); @@ -589,7 +600,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// Invariant: Must only claim once or unregister require(initiativeState.lastEpochClaim < epoch() - 1); - (InitiativeStatus status, , )= getInitiativeState(_initiative); + (InitiativeStatus status, , ) = getInitiativeState(_initiative); /// @audit use FSM require(status == InitiativeStatus.UNREGISTERABLE, "Governance: cannot-unregister-initiative"); // recalculate the average staking timestamp for all counted voting LQTY if the initiative was counted in @@ -613,13 +624,10 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @inheritdoc IGovernance function claimForInitiative(address _initiative) external nonReentrant returns (uint256) { + /// @audit GAS - initiative state vs snapshot (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); (InitiativeStatus status, , uint256 claimableAmount ) = getInitiativeState(_initiative); - /// INVARIANT: - /// We cannot claim only for 2 reasons: - /// We have already claimed - /// We do not meet the threshold if(status != InitiativeStatus.CLAIMABLE) { return 0; } diff --git a/test/Governance.t.sol b/test/Governance.t.sol index f3a922c..4681997 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -606,7 +606,7 @@ contract GovernanceTest is Test { assertEq(atEpoch, governance.epoch()); // should revert if the initiative is still in the registration warm up period - vm.expectRevert("Governance: initiative-in-warm-up"); + vm.expectRevert("Governance: initiative-in-warm-up"); /// @audit should fail due to not waiting enough time governance.unregisterInitiative(baseInitiative3); vm.warp(block.timestamp + 365 days); diff --git a/test/recon/Properties.sol b/test/recon/Properties.sol index 8d5494f..661df0f 100644 --- a/test/recon/Properties.sol +++ b/test/recon/Properties.sol @@ -5,5 +5,5 @@ import {BeforeAfter} from "./BeforeAfter.sol"; import {GovernanceProperties} from "./properties/GovernanceProperties.sol"; import {BribeInitiativeProperties} from "./properties/BribeInitiativeProperties.sol"; -abstract contract Properties is GovernanceProperties { +abstract contract Properties is GovernanceProperties, BribeInitiativeProperties { } \ No newline at end of file From cf0f98ca207c021f6b4281540862d8c409a039a8 Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 09:42:15 +0200 Subject: [PATCH 72/93] feat: cache data for `unregisterInitiative` --- src/Governance.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index e64527a..1f0c1de 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -592,17 +592,17 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @inheritdoc IGovernance function unregisterInitiative(address _initiative) external nonReentrant { - (InitiativeStatus status, , ) = getInitiativeState(_initiative); + (VoteSnapshot memory votesSnapshot_ , GlobalState memory state) = _snapshotVotes(); + (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = + _snapshotVotesForInitiative(_initiative); + + (InitiativeStatus status, , ) = getInitiativeState(_initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState); require(status != InitiativeStatus.NONEXISTENT, "Governance: initiative-not-registered"); require(status != InitiativeStatus.COOLDOWN, "Governance: initiative-in-warm-up"); require(status == InitiativeStatus.UNREGISTERABLE, "Governance: cannot-unregister-initiative"); uint16 currentEpoch = epoch(); - (, GlobalState memory state) = _snapshotVotes(); - (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = - _snapshotVotesForInitiative(_initiative); - /// @audit Invariant: Must only claim once or unregister assert(initiativeState.lastEpochClaim < currentEpoch - 1); From 36603911776c4793a339012cc46eb615abb20716 Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 09:43:59 +0200 Subject: [PATCH 73/93] gas: `claimForInitiative` --- src/Governance.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 1f0c1de..9ed2cac 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -627,10 +627,11 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @inheritdoc IGovernance function claimForInitiative(address _initiative) external nonReentrant returns (uint256) { - (VoteSnapshot memory votesSnapshot_,) = _snapshotVotes(); - (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState_) = _snapshotVotesForInitiative(_initiative); + (VoteSnapshot memory votesSnapshot_ , GlobalState memory state) = _snapshotVotes(); + (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = + _snapshotVotesForInitiative(_initiative); - (InitiativeStatus status, , uint256 claimableAmount ) = getInitiativeState(_initiative); + (InitiativeStatus status, , uint256 claimableAmount) = getInitiativeState(_initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState); /// INVARIANT: /// We cannot claim only for 2 reasons: From 16bf1d3861233bc6be55237f5c4e8514afbfe7fe Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 09:44:09 +0200 Subject: [PATCH 74/93] fix: remove extra SSTORE --- src/Governance.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Governance.sol b/src/Governance.sol index 9ed2cac..6195d5b 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -648,7 +648,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// Invariant `lastEpochClaim` is < epoch() - 1; | /// If `lastEpochClaim` is older than epoch() - 1 it means the initiative couldn't claim any rewards this epoch initiativeStates[_initiative].lastEpochClaim = epoch() - 1; - votesForInitiativeSnapshot[_initiative] = votesForInitiativeSnapshot_; // implicitly prevents double claiming bold.safeTransfer(_initiative, claimableAmount); From 6309c9070719dcca2cca1f8ea19bb8491a675392 Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 09:53:44 +0200 Subject: [PATCH 75/93] feat: FSM + Crit notice --- src/Governance.sol | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 6195d5b..4608a84 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -480,7 +480,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance "Governance: array-length-mismatch" ); - (, GlobalState memory state) = _snapshotVotes(); + (VoteSnapshot memory votesSnapshot_ , GlobalState memory state) = _snapshotVotes(); uint16 currentEpoch = epoch(); @@ -501,10 +501,12 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // Can vote positively in SKIP, CLAIMABLE, CLAIMED and UNREGISTERABLE states // Force to remove votes if disabled // Can remove votes and vetos in every stage + (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = + _snapshotVotesForInitiative(initiative); + { - (InitiativeStatus status, ,) = getInitiativeState(initiative); + (InitiativeStatus status, , ) = getInitiativeState(initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState); - uint16 registeredAtEpoch = registeredInitiatives[initiative]; if(deltaLQTYVotes > 0 || deltaLQTYVetos > 0) { /// @audit FSM CHECK, note that the original version allowed voting on `Unregisterable` Initiatives - Prob should fix require(status == InitiativeStatus.SKIP || status == InitiativeStatus.CLAIMABLE || status == InitiativeStatus.CLAIMED || status == InitiativeStatus.UNREGISTERABLE, "Governance: active-vote-fsm"); @@ -515,7 +517,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance } } - (, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(initiative); // deep copy of the initiative's state before the allocation InitiativeState memory prevInitiativeState = InitiativeState( @@ -592,6 +593,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance /// @inheritdoc IGovernance function unregisterInitiative(address _initiative) external nonReentrant { + /// Enforce FSM (VoteSnapshot memory votesSnapshot_ , GlobalState memory state) = _snapshotVotes(); (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(_initiative); @@ -601,12 +603,15 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance require(status != InitiativeStatus.COOLDOWN, "Governance: initiative-in-warm-up"); require(status == InitiativeStatus.UNREGISTERABLE, "Governance: cannot-unregister-initiative"); + // Remove weight from current state uint16 currentEpoch = epoch(); /// @audit Invariant: Must only claim once or unregister assert(initiativeState.lastEpochClaim < currentEpoch - 1); // recalculate the average staking timestamp for all counted voting LQTY if the initiative was counted in + /// @audit CRIT HERE | The math on removing messes stuff up + /// Prob need to remove this state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( state.countedVoteLQTYAverageTimestamp, initiativeState.averageStakingTimestampVoteLQTY, From 83657b5950eff930a75f4f24165722684a24d20b Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 10:38:18 +0200 Subject: [PATCH 76/93] gas: remove extra 0 check --- src/Governance.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 3df467f..4473001 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -137,13 +137,13 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance uint240 prevVotes = uint240(_prevLQTYBalance) * uint240(prevOuterAverageAge); uint240 newVotes = uint240(deltaLQTY) * uint240(newInnerAverageAge); uint240 votes = prevVotes + newVotes; - newOuterAverageAge = (_newLQTYBalance == 0) ? 0 : uint32(votes / uint240(_newLQTYBalance)); + newOuterAverageAge = uint32(votes / uint240(_newLQTYBalance)); } else { uint88 deltaLQTY = _prevLQTYBalance - _newLQTYBalance; uint240 prevVotes = uint240(_prevLQTYBalance) * uint240(prevOuterAverageAge); uint240 newVotes = uint240(deltaLQTY) * uint240(newInnerAverageAge); uint240 votes = (prevVotes >= newVotes) ? prevVotes - newVotes : 0; - newOuterAverageAge = (_newLQTYBalance == 0) ? 0 : uint32(votes / uint240(_newLQTYBalance)); + newOuterAverageAge = uint32(votes / uint240(_newLQTYBalance)); } if (newOuterAverageAge > block.timestamp) return 0; From 6b15ed623cf36256217f66bd5b979127a424f81c Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 11:52:35 +0200 Subject: [PATCH 77/93] chore: cleanup --- test/Governance.t.sol | 140 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 138 insertions(+), 2 deletions(-) diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 7c7eb0b..dec0879 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -721,8 +721,6 @@ contract GovernanceTest is Test { uint256 votingPowerWithProjection = governance.lqtyToVotes(voteLQTY1, 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"); - - // assertEq(counted1, counted2, "both counted"); } } @@ -799,6 +797,142 @@ contract GovernanceTest is Test { governance.allocateLQTY(reAddInitiatives, reAddDeltaLQTYVotes, reAddDeltaLQTYVetos); } + + // Remove allocation but check accounting + // Need to find bug in accounting code + // forge test --match-test test_addRemoveAllocation_accounting -vv + function test_addRemoveAllocation_accounting() public { + // User setup + vm.startPrank(user); + 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, deltaLQTYVotes, deltaLQTYVetos); + + + // Warp to end so we check the threshold against future threshold + { + vm.warp(block.timestamp + governance.EPOCH_DURATION()); + + (IGovernance.VoteSnapshot memory snapshot, IGovernance.InitiativeVoteSnapshot memory initiativeVoteSnapshot1) = governance.snapshotVotesForInitiative(baseInitiative1); + + uint256 threshold = governance.getLatestVotingThreshold(); + assertLt(initiativeVoteSnapshot1.votes, threshold, "it didn't get rewards"); + } + + // Roll for + vm.warp(block.timestamp + governance.UNREGISTRATION_AFTER_EPOCHS() * governance.EPOCH_DURATION()); + + + /// === END SETUP === /// + + + // 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_voteLQTY, + , + , + , + + ) = governance.initiativeStates(baseInitiative1); + + // Unregistering + governance.unregisterInitiative(baseInitiative1); + + // We expect, the initiative to have the same values (because we track them for storage purposes) + // TODO: Could change some of the values to make them 0 in view stuff + // We expect the state to already have those removed + // We expect the user to not have any changes + + ( + uint88 after_countedVoteLQTY, + + ) = governance.globalState(); + + assertEq(after_countedVoteLQTY, b4_countedVoteLQTY - b4_voteLQTY, "Global Lqty change after unregister"); + assertEq(1e18, b4_voteLQTY, "sanity check"); + + + (uint88 after_allocatedLQTY, uint32 after_averageStakingTimestamp) = governance.userStates(user); + + // We expect no changes here + ( + uint88 after_voteLQTY, + uint88 after_vetoLQTY, + uint32 after_averageStakingTimestampVoteLQTY, + uint32 after_averageStakingTimestampVetoLQTY, + uint16 after_lastEpochClaim + ) = governance.initiativeStates(baseInitiative1); + + assertEq(b4_voteLQTY, after_voteLQTY, "Initiative votes are the same"); + + + + // Need to test: + // Total Votes + // User Votes + // Initiative Votes + + // I cannot + address[] memory removeInitiatives = new address[](1); + removeInitiatives[0] = baseInitiative1; + int88[] memory removeDeltaLQTYVotes = new int88[](1); + removeDeltaLQTYVotes[0] = -1e18; + int88[] memory removeDeltaLQTYVetos = new int88[](1); + + /// @audit the next call MUST not revert - this is a critical bug + governance.allocateLQTY(removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); + + // After user counts LQTY the + { + ( + uint88 after_user_countedVoteLQTY, + uint32 after_user_countedVoteLQTYAverageTimestamp + ) = governance.globalState(); + // The LQTY was already removed + assertEq(after_user_countedVoteLQTY, after_countedVoteLQTY, "Removal"); + } + + // User State allocated LQTY changes by 1e18 + // Timestamp should not change + { + (uint88 after_user_allocatedLQTY, ) = governance.userStates(user); + assertEq(after_user_allocatedLQTY, after_allocatedLQTY - 1e18, "Removal"); + } + + // Check user math only change is the LQTY amt + { + ( + uint88 after_user_voteLQTY, + , + , + , + + ) = governance.initiativeStates(baseInitiative1); + + assertEq(after_user_voteLQTY, after_voteLQTY - 1e18, "Removal"); + } + } + // Just pass a negative value and see what happens // forge test --match-test test_overflow_crit -vv function test_overflow_crit() public { @@ -988,6 +1122,8 @@ contract GovernanceTest is Test { vm.stopPrank(); } + function test_allocate_unregister() public {} + function test_allocateLQTY_multiple() public { vm.startPrank(user); From b45e6a66c95e5128fbfa6db9fdcb25c8516e8b0b Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 11:52:43 +0200 Subject: [PATCH 78/93] fix: Critical accounting bug --- src/Governance.sol | 54 +++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 4473001..3545fb3 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -496,26 +496,27 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance "Governance: epoch-voting-cutoff" ); - // Check FSM + /// === Check FSM === /// // Can vote positively in SKIP, CLAIMABLE, CLAIMED and UNREGISTERABLE states // Force to remove votes if disabled // Can remove votes and vetos in every stage (InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) = _snapshotVotesForInitiative(initiative); - { - (InitiativeStatus status, , ) = getInitiativeState(initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState); - - if(deltaLQTYVotes > 0 || deltaLQTYVetos > 0) { - /// @audit FSM CHECK, note that the original version allowed voting on `Unregisterable` Initiatives - Prob should fix - require(status == InitiativeStatus.SKIP || status == InitiativeStatus.CLAIMABLE || status == InitiativeStatus.CLAIMED || status == InitiativeStatus.UNREGISTERABLE, "Governance: active-vote-fsm"); - } - - if(status == InitiativeStatus.DISABLED) { - require(deltaLQTYVotes <= 0 && deltaLQTYVetos <= 0, "Must be a withdrawal"); - } + (InitiativeStatus status, , ) = getInitiativeState(initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState); + + if(deltaLQTYVotes > 0 || deltaLQTYVetos > 0) { + /// @audit FSM CHECK, note that the original version allowed voting on `Unregisterable` Initiatives - Prob should fix + require(status == InitiativeStatus.SKIP || status == InitiativeStatus.CLAIMABLE || status == InitiativeStatus.CLAIMED || status == InitiativeStatus.UNREGISTERABLE, "Governance: active-vote-fsm"); + } + + if(status == InitiativeStatus.DISABLED) { + require(deltaLQTYVotes <= 0 && deltaLQTYVetos <= 0, "Must be a withdrawal"); } + /// === UPDATE ACCOUNTING === /// + + // == INITIATIVE STATE == // // deep copy of the initiative's state before the allocation InitiativeState memory prevInitiativeState = InitiativeState( @@ -547,15 +548,24 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // update the initiative's state initiativeStates[initiative] = initiativeState; + + // == GLOBAL STATE == // + // update the average staking timestamp for all counted voting LQTY - /// Discount previous - state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( - state.countedVoteLQTYAverageTimestamp, - prevInitiativeState.averageStakingTimestampVoteLQTY, /// @audit TODO Write tests that fail from this bug - state.countedVoteLQTY, - state.countedVoteLQTY - prevInitiativeState.voteLQTY - ); - state.countedVoteLQTY -= prevInitiativeState.voteLQTY; /// @audit Overflow here MUST never happen2 + /// Discount previous only if the initiative was not unregistered + + /// @audit + if(status != InitiativeStatus.DISABLED) { + state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( + state.countedVoteLQTYAverageTimestamp, + prevInitiativeState.averageStakingTimestampVoteLQTY, /// @audit TODO Write tests that fail from this bug + state.countedVoteLQTY, + state.countedVoteLQTY - prevInitiativeState.voteLQTY + ); + state.countedVoteLQTY -= prevInitiativeState.voteLQTY; /// @audit Overflow here MUST never happen2 + } + /// @audit We cannot add on disabled so the change below is safe + // TODO More asserts? | Most likely need to assert strictly less voteLQTY here /// Add current state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( @@ -611,8 +621,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance assert(initiativeState.lastEpochClaim < currentEpoch - 1); // recalculate the average staking timestamp for all counted voting LQTY if the initiative was counted in - /// @audit CRIT HERE | The math on removing messes stuff up - /// Prob need to remove this + // / @audit CRIT HERE | The math on removing messes stuff up + // / Prob need to remove this state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( state.countedVoteLQTYAverageTimestamp, initiativeState.averageStakingTimestampVoteLQTY, From 0ae541f402271773407e7bdc05184a70df6a69bb Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 11:52:47 +0200 Subject: [PATCH 79/93] chore: future --- test/recon/properties/GovernanceProperties.sol | 3 +++ test/recon/targets/GovernanceTargets.sol | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/test/recon/properties/GovernanceProperties.sol b/test/recon/properties/GovernanceProperties.sol index 2667d52..72ef463 100644 --- a/test/recon/properties/GovernanceProperties.sol +++ b/test/recon/properties/GovernanceProperties.sol @@ -69,5 +69,8 @@ abstract contract GovernanceProperties is BeforeAfter { } } + // Function sound total math + + } \ No newline at end of file diff --git a/test/recon/targets/GovernanceTargets.sol b/test/recon/targets/GovernanceTargets.sol index 794aab3..4406ed6 100644 --- a/test/recon/targets/GovernanceTargets.sol +++ b/test/recon/targets/GovernanceTargets.sol @@ -45,9 +45,14 @@ abstract contract GovernanceTargets is BaseTargetFunctions, Properties { } } - function governance_allocateLQTY(int88[] calldata _deltaLQTYVotes, int88[] calldata _deltaLQTYVetos) withChecks public { - governance.allocateLQTY(deployedInitiatives, _deltaLQTYVotes, _deltaLQTYVetos); - } + // 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) withChecks public { + // governance.allocateLQTY(deployedInitiatives, _deltaLQTYVotes, _deltaLQTYVetos); + // } function governance_claimForInitiative(uint8 initiativeIndex) withChecks public { address initiative = _getDeployedInitiative(initiativeIndex); From 3927a884caf7266678c005aa66309405d738a13e Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 12:34:58 +0200 Subject: [PATCH 80/93] feat: broken key test --- src/Governance.sol | 8 +- test/recon/CryticToFoundry.sol | 62 +++++++-------- test/recon/Properties.sol | 4 +- .../recon/properties/GovernanceProperties.sol | 77 +++++++++++++++++++ 4 files changed, 116 insertions(+), 35 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 3545fb3..70cdeee 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -544,6 +544,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // allocate the voting and vetoing LQTY to the initiative initiativeState.voteLQTY = add(initiativeState.voteLQTY, deltaLQTYVotes); initiativeState.vetoLQTY = add(initiativeState.vetoLQTY, deltaLQTYVetos); + // assert(initiativeState.voteLQTY != 0); // Votes are non zero + // assert(deltaLQTYVotes != 0) // Votes are non zero // update the initiative's state initiativeStates[initiative] = initiativeState; @@ -583,7 +585,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance allocation.atEpoch = currentEpoch; require(!(allocation.voteLQTY != 0 && allocation.vetoLQTY != 0), "Governance: vote-and-veto"); lqtyAllocatedByUserToInitiative[msg.sender][initiative] = allocation; - + userState.allocatedLQTY = add(userState.allocatedLQTY, deltaLQTYVotes + deltaLQTYVetos); emit AllocateLQTY(msg.sender, initiative, deltaLQTYVotes, deltaLQTYVetos, currentEpoch); @@ -598,6 +600,9 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance "Governance: insufficient-or-allocated-lqty" ); + // assert(state.countedVoteLQTY != 0); + // assert(state.countedVoteLQTYAverageTimestamp != 0); + /// Update storage globalState = state; userStates[msg.sender] = userState; } @@ -630,6 +635,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance state.countedVoteLQTY - initiativeState.voteLQTY ); state.countedVoteLQTY -= initiativeState.voteLQTY; + globalState = state; /// weeks * 2^16 > u32 so the contract will stop working before this is an issue diff --git a/test/recon/CryticToFoundry.sol b/test/recon/CryticToFoundry.sol index 9074abe..a1d11a9 100644 --- a/test/recon/CryticToFoundry.sol +++ b/test/recon/CryticToFoundry.sol @@ -6,50 +6,46 @@ import {Test} from "forge-std/Test.sol"; import {TargetFunctions} from "./TargetFunctions.sol"; import {FoundryAsserts} from "@chimera/FoundryAsserts.sol"; +import {console} from "forge-std/console.sol"; + + contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts { function setUp() public { setup(); } -// forge test --match-test test_property_GV01_0 -vv +// forge test --match-test test_property_sum_of_lqty_global_user_matches_0 -vv -function test_property_GV01_0() public { - - vm.roll(block.number + 4921); - vm.warp(block.timestamp + 277805); - vm.prank(0x0000000000000000000000000000000000020000); - helper_deployInitiative(); - - vm.roll(block.number + 17731); - vm.warp(block.timestamp + 661456); - vm.prank(0x0000000000000000000000000000000000010000); - helper_deployInitiative(); - - vm.roll(block.number + 41536); - vm.warp(block.timestamp + 1020941); - vm.prank(0x0000000000000000000000000000000000010000); - helper_deployInitiative(); - - vm.roll(block.number + 41536); - vm.warp(block.timestamp + 1020941); +function test_property_sum_of_lqty_global_user_matches_0() public { + vm.roll(block.number + 54184); + vm.warp(block.timestamp + 65199); vm.prank(0x0000000000000000000000000000000000010000); - helper_deployInitiative(); + governance_depositLQTY(10618051687797035123500145); - vm.roll(block.number + 41536); - vm.warp(block.timestamp + 1020941); - vm.prank(0x0000000000000000000000000000000000020000); - helper_deployInitiative(); + vm.roll(block.number + 103930); + vm.warp(block.timestamp + 635494); + vm.prank(0x0000000000000000000000000000000000030000); + governance_allocateLQTY_clamped_single_initiative(0, 0, 1231231); + + (uint88 user_allocatedLQTY, ) = governance.userStates(user); + + assertTrue(user_allocatedLQTY > 0, "Something is allocated"); + + // Allocates `10597250933489619569146227` + ( + uint88 countedVoteLQTY, uint32 countedVoteLQTYAverageTimestamp + // uint32 after_user_countedVoteLQTYAverageTimestamp // TODO: How do we do this? + ) = governance.globalState(); + console.log("countedVoteLQTYAverageTimestamp", countedVoteLQTYAverageTimestamp); + assertTrue(countedVoteLQTY > 0, "Something is counted"); + - vm.roll(block.number + 61507); - vm.warp(block.timestamp + 1049774); + vm.roll(block.number + 130098); + vm.warp(block.timestamp + 1006552); vm.prank(0x0000000000000000000000000000000000020000); - governance_registerInitiative(22); - - vm.roll(block.number + 61507); - vm.warp(block.timestamp + 1049774); - vm.prank(0x0000000000000000000000000000000000030000); - property_GV01(); + property_sum_of_lqty_global_user_matches(); } + } \ No newline at end of file diff --git a/test/recon/Properties.sol b/test/recon/Properties.sol index 661df0f..feab8fd 100644 --- a/test/recon/Properties.sol +++ b/test/recon/Properties.sol @@ -5,5 +5,7 @@ import {BeforeAfter} from "./BeforeAfter.sol"; import {GovernanceProperties} from "./properties/GovernanceProperties.sol"; import {BribeInitiativeProperties} from "./properties/BribeInitiativeProperties.sol"; -abstract contract Properties is GovernanceProperties, BribeInitiativeProperties { + +abstract contract Properties is GovernanceProperties { + /// @audit TODO: Add `BribeInitiativeProperties` } \ No newline at end of file diff --git a/test/recon/properties/GovernanceProperties.sol b/test/recon/properties/GovernanceProperties.sol index 72ef463..43a76b6 100644 --- a/test/recon/properties/GovernanceProperties.sol +++ b/test/recon/properties/GovernanceProperties.sol @@ -70,6 +70,83 @@ abstract contract GovernanceProperties is BeforeAfter { } // Function sound total math + + /// The Sum of LQTY allocated by Users matches the global state + function property_sum_of_lqty_global_user_matches() public { + // Get state + // Get all users + // Sum up all voted users + // Total must match + ( + uint88 totalCountedLQTY, + // uint32 after_user_countedVoteLQTYAverageTimestamp // TODO: How do we do this? + ) = governance.globalState(); + + uint256 totalUserCountedLQTY; + for(uint256 i; i < users.length; i++) { + (uint88 user_allocatedLQTY, ) = governance.userStates(users[i]); + totalUserCountedLQTY += user_allocatedLQTY; + } + + eq(totalCountedLQTY, totalUserCountedLQTY, "Global vs SUM(Users_lqty) must match"); + } + + /// The Sum of LQTY allocated to Initiatives matches the Sum of LQTY allocated by users + function property_sum_of_lqty_initiative_user_matches() public { + // Get Initiatives + // Get all users + // Sum up all voted users & initiatives + // Total must match + uint256 totalInitiativesCountedLQTY; + for(uint256 i; i < deployedInitiatives.length; i++) { + ( + uint88 after_user_voteLQTY, + , + , + , + + ) = governance.initiativeStates(deployedInitiatives[i]); + totalInitiativesCountedLQTY += after_user_voteLQTY; + } + + + uint256 totalUserCountedLQTY; + for(uint256 i; i < users.length; i++) { + (uint88 user_allocatedLQTY, ) = governance.userStates(users[i]); + totalUserCountedLQTY += user_allocatedLQTY; + } + + eq(totalInitiativesCountedLQTY, totalUserCountedLQTY, "SUM(Initiatives_lqty) vs SUM(Users_lqty) must match"); + } + + /// The Sum of LQTY allocated to Initiatives matches the global state + function property_sum_of_lqty_global_initiatives_matches() public { + // Get Initiatives + // Get State + // Sum up all initiatives + // Total must match + ( + uint88 totalCountedLQTY, + // uint32 after_user_countedVoteLQTYAverageTimestamp // TODO: How do we do this? + ) = governance.globalState(); + + uint256 totalInitiativesCountedLQTY; + for(uint256 i; i < deployedInitiatives.length; i++) { + ( + uint88 after_user_voteLQTY, + , + , + , + + ) = governance.initiativeStates(deployedInitiatives[i]); + totalInitiativesCountedLQTY += after_user_voteLQTY; + } + + eq(totalCountedLQTY, totalInitiativesCountedLQTY, "Global vs SUM(Initiatives_lqty) must match"); + + } + + // TODO: also `lqtyAllocatedByUserToInitiative` From 56b162d090b5d928fddf18ba55dacf792637bc84 Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 14:07:30 +0200 Subject: [PATCH 81/93] chore: cleanup --- src/Governance.sol | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index 70cdeee..cedf95d 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -544,8 +544,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // allocate the voting and vetoing LQTY to the initiative initiativeState.voteLQTY = add(initiativeState.voteLQTY, deltaLQTYVotes); initiativeState.vetoLQTY = add(initiativeState.vetoLQTY, deltaLQTYVetos); - // assert(initiativeState.voteLQTY != 0); // Votes are non zero - // assert(deltaLQTYVotes != 0) // Votes are non zero // update the initiative's state initiativeStates[initiative] = initiativeState; @@ -600,13 +598,18 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance "Governance: insufficient-or-allocated-lqty" ); - // assert(state.countedVoteLQTY != 0); - // assert(state.countedVoteLQTYAverageTimestamp != 0); /// Update storage + emit EmitState(globalState.countedVoteLQTY, globalState.countedVoteLQTYAverageTimestamp); + emit EmitState(state.countedVoteLQTY, state.countedVoteLQTYAverageTimestamp); + globalState = state; + emit EmitState(globalState.countedVoteLQTY, globalState.countedVoteLQTYAverageTimestamp); + emit EmitState(state.countedVoteLQTY, state.countedVoteLQTYAverageTimestamp); userStates[msg.sender] = userState; } + event EmitState(uint88 countedVoteLQTY, uint32 countedVoteLQTYAverageTimestamp); + /// @inheritdoc IGovernance function unregisterInitiative(address _initiative) external nonReentrant { /// Enforce FSM From 7d375b13d01232ea070081cfe8e53df9a0843fb5 Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 14:07:52 +0200 Subject: [PATCH 82/93] chore: fixes to properties --- .../recon/properties/GovernanceProperties.sol | 109 +++++++++++------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/test/recon/properties/GovernanceProperties.sol b/test/recon/properties/GovernanceProperties.sol index 43a76b6..076f836 100644 --- a/test/recon/properties/GovernanceProperties.sol +++ b/test/recon/properties/GovernanceProperties.sol @@ -71,53 +71,56 @@ abstract contract GovernanceProperties is BeforeAfter { // Function sound total math + // NOTE: Global vs USer 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 - function property_sum_of_lqty_global_user_matches() public { - // Get state - // Get all users - // Sum up all voted users - // Total must match - ( - uint88 totalCountedLQTY, - // uint32 after_user_countedVoteLQTYAverageTimestamp // TODO: How do we do this? - ) = governance.globalState(); - - uint256 totalUserCountedLQTY; - for(uint256 i; i < users.length; i++) { - (uint88 user_allocatedLQTY, ) = governance.userStates(users[i]); - totalUserCountedLQTY += user_allocatedLQTY; - } - - eq(totalCountedLQTY, totalUserCountedLQTY, "Global vs SUM(Users_lqty) must match"); - } + // function property_sum_of_lqty_global_user_matches() public { + // // Get state + // // Get all users + // // Sum up all voted users + // // Total must match + // ( + // uint88 totalCountedLQTY, + // // uint32 after_user_countedVoteLQTYAverageTimestamp // TODO: How do we do this? + // ) = governance.globalState(); + + // uint256 totalUserCountedLQTY; + // for(uint256 i; i < users.length; i++) { + // (uint88 user_allocatedLQTY, ) = governance.userStates(users[i]); + // totalUserCountedLQTY += user_allocatedLQTY; + // } + + // eq(totalCountedLQTY, totalUserCountedLQTY, "Global vs SUM(Users_lqty) must match"); + // } /// The Sum of LQTY allocated to Initiatives matches the Sum of LQTY allocated by users - function property_sum_of_lqty_initiative_user_matches() public { - // Get Initiatives - // Get all users - // Sum up all voted users & initiatives - // Total must match - uint256 totalInitiativesCountedLQTY; - for(uint256 i; i < deployedInitiatives.length; i++) { - ( - uint88 after_user_voteLQTY, - , - , - , + // function property_sum_of_lqty_initiative_user_matches() public { + // // Get Initiatives + // // Get all users + // // Sum up all voted users & initiatives + // // Total must match + // uint256 totalInitiativesCountedLQTY; + // for(uint256 i; i < deployedInitiatives.length; i++) { + // ( + // uint88 after_user_voteLQTY, + // , + // , + // , - ) = governance.initiativeStates(deployedInitiatives[i]); - totalInitiativesCountedLQTY += after_user_voteLQTY; - } + // ) = governance.initiativeStates(deployedInitiatives[i]); + // totalInitiativesCountedLQTY += after_user_voteLQTY; + // } - uint256 totalUserCountedLQTY; - for(uint256 i; i < users.length; i++) { - (uint88 user_allocatedLQTY, ) = governance.userStates(users[i]); - totalUserCountedLQTY += user_allocatedLQTY; - } + // uint256 totalUserCountedLQTY; + // for(uint256 i; i < users.length; i++) { + // (uint88 user_allocatedLQTY, ) = governance.userStates(users[i]); + // totalUserCountedLQTY += user_allocatedLQTY; + // } - eq(totalInitiativesCountedLQTY, totalUserCountedLQTY, "SUM(Initiatives_lqty) vs SUM(Users_lqty) must match"); - } + // eq(totalInitiativesCountedLQTY, totalUserCountedLQTY, "SUM(Initiatives_lqty) vs SUM(Users_lqty) must match"); + // } /// The Sum of LQTY allocated to Initiatives matches the global state function property_sum_of_lqty_global_initiatives_matches() public { @@ -147,7 +150,31 @@ abstract contract GovernanceProperties is BeforeAfter { } // TODO: also `lqtyAllocatedByUserToInitiative` - + // For each user, for each initiative, allocation is correct + function property_sum_of_user_initiative_allocations() public { + for(uint256 i; i < deployedInitiatives.length; i++) { + ( + uint88 initiative_voteLQTY, + uint88 initiative_vetoLQTY, + , + , + + ) = governance.initiativeStates(deployedInitiatives[i]); + + + // Grab all users and sum up their participations + uint256 totalUserVotes; + uint256 totalUserVetos; + for(uint256 i; i < users.length; i++) { + (uint88 vote_allocated, uint88 veto_allocated, ) = governance.lqtyAllocatedByUserToInitiative(users[i], deployedInitiatives[i]); + totalUserVotes += vote_allocated; + totalUserVetos += veto_allocated; + } + + eq(initiative_voteLQTY, totalUserVotes + initiative_voteLQTY, "Sum of users, matches initiative votes"); + eq(initiative_vetoLQTY, totalUserVetos + initiative_vetoLQTY, "Sum of users, matches initiative vetos"); + } + } } \ No newline at end of file From 568f189a41e2641d3ab35dc09cdcc482282006d4 Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 14:08:00 +0200 Subject: [PATCH 83/93] chore: sample broken pro --- test/recon/CryticToFoundry.sol | 74 ++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/test/recon/CryticToFoundry.sol b/test/recon/CryticToFoundry.sol index a1d11a9..0ec2058 100644 --- a/test/recon/CryticToFoundry.sol +++ b/test/recon/CryticToFoundry.sol @@ -13,39 +13,61 @@ contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts { function setUp() public { setup(); } + -// forge test --match-test test_property_sum_of_lqty_global_user_matches_0 -vv +// forge test --match-test test_property_sum_of_user_initiative_allocations_0 -vv -function test_property_sum_of_lqty_global_user_matches_0() public { - vm.roll(block.number + 54184); - vm.warp(block.timestamp + 65199); +function test_property_sum_of_user_initiative_allocations_0() public { + + vm.roll(2); + vm.warp(86000000 + 2); vm.prank(0x0000000000000000000000000000000000010000); - governance_depositLQTY(10618051687797035123500145); + helper_deployInitiative(); - vm.roll(block.number + 103930); - vm.warp(block.timestamp + 635494); + vm.roll(20338); + vm.warp(86000000 + 359683); vm.prank(0x0000000000000000000000000000000000030000); - governance_allocateLQTY_clamped_single_initiative(0, 0, 1231231); - - (uint88 user_allocatedLQTY, ) = governance.userStates(user); - - assertTrue(user_allocatedLQTY > 0, "Something is allocated"); - - // Allocates `10597250933489619569146227` - ( - uint88 countedVoteLQTY, uint32 countedVoteLQTYAverageTimestamp - // uint32 after_user_countedVoteLQTYAverageTimestamp // TODO: How do we do this? - ) = governance.globalState(); - console.log("countedVoteLQTYAverageTimestamp", countedVoteLQTYAverageTimestamp); - assertTrue(countedVoteLQTY > 0, "Something is counted"); - + helper_deployInitiative(); + + vm.roll(35511); + vm.warp(86000000 + 718072); + vm.prank(0x0000000000000000000000000000000000030000); + helper_deployInitiative(); + + vm.roll(94412); + vm.warp(86000000 + 999244); + vm.prank(0x0000000000000000000000000000000000010000); + helper_deployInitiative(); + + vm.roll(161790); + vm.warp(86000000 + 2651694); + vm.prank(0x0000000000000000000000000000000000020000); + governance_depositLQTY(646169017059856542762865); + + vm.roll(186721); + vm.warp(86000000 + 2815428); + vm.prank(0x0000000000000000000000000000000000020000); + governance_registerInitiative(63); - vm.roll(block.number + 130098); - vm.warp(block.timestamp + 1006552); + vm.roll(257296); + vm.warp(86000000 + 3261349); vm.prank(0x0000000000000000000000000000000000020000); - property_sum_of_lqty_global_user_matches(); + helper_deployInitiative(); + + vm.roll(333543); + vm.warp(86000000 + 4091708); + vm.prank(0x0000000000000000000000000000000000020000); + helper_deployInitiative(); + + vm.roll(368758); + vm.warp(86000000 + 4314243); + vm.prank(0x0000000000000000000000000000000000020000); + governance_allocateLQTY_clamped_single_initiative(3, 29956350487679649024950075925, 0); + + vm.roll(375687); + vm.warp(86000000 + 4704876); + vm.prank(0x0000000000000000000000000000000000020000); + property_sum_of_user_initiative_allocations(); } - - } \ No newline at end of file From 506bf03dfd2c683c7f11422aa6c4f659e0d78d27 Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 14:57:13 +0200 Subject: [PATCH 84/93] feat: view acounting properties --- test/recon/CryticToFoundry.sol | 75 +++++-------- .../recon/properties/GovernanceProperties.sol | 105 ++++++++---------- 2 files changed, 75 insertions(+), 105 deletions(-) diff --git a/test/recon/CryticToFoundry.sol b/test/recon/CryticToFoundry.sol index 0ec2058..7609e52 100644 --- a/test/recon/CryticToFoundry.sol +++ b/test/recon/CryticToFoundry.sol @@ -15,59 +15,40 @@ contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts { } -// forge test --match-test test_property_sum_of_user_initiative_allocations_0 -vv +// forge test --match-test test_property_sum_of_lqty_global_initiatives_matches_0 -vv -function test_property_sum_of_user_initiative_allocations_0() public { +function test_property_sum_of_lqty_global_initiatives_matches_0() public { - vm.roll(2); - vm.warp(86000000 + 2); - vm.prank(0x0000000000000000000000000000000000010000); - helper_deployInitiative(); - - vm.roll(20338); - vm.warp(86000000 + 359683); + vm.roll(13649); + vm.warp(999999999 + 274226); vm.prank(0x0000000000000000000000000000000000030000); - helper_deployInitiative(); + governance_depositLQTY(132009924662042920942); - vm.roll(35511); - vm.warp(86000000 + 718072); + vm.roll(23204); + vm.warp(999999999 + 765086); vm.prank(0x0000000000000000000000000000000000030000); - helper_deployInitiative(); - - vm.roll(94412); - vm.warp(86000000 + 999244); - vm.prank(0x0000000000000000000000000000000000010000); - helper_deployInitiative(); - - vm.roll(161790); - vm.warp(86000000 + 2651694); - vm.prank(0x0000000000000000000000000000000000020000); - governance_depositLQTY(646169017059856542762865); - - vm.roll(186721); - vm.warp(86000000 + 2815428); - vm.prank(0x0000000000000000000000000000000000020000); - governance_registerInitiative(63); - - vm.roll(257296); - vm.warp(86000000 + 3261349); - vm.prank(0x0000000000000000000000000000000000020000); - helper_deployInitiative(); - - vm.roll(333543); - vm.warp(86000000 + 4091708); - vm.prank(0x0000000000000000000000000000000000020000); - helper_deployInitiative(); - - vm.roll(368758); - vm.warp(86000000 + 4314243); - vm.prank(0x0000000000000000000000000000000000020000); - governance_allocateLQTY_clamped_single_initiative(3, 29956350487679649024950075925, 0); - - vm.roll(375687); - vm.warp(86000000 + 4704876); + governance_allocateLQTY_clamped_single_initiative(0, 6936608807263793400734754831, 0); + + +console.log("length", users.length); +console.log("length", deployedInitiatives.length); + vm.roll(52745); + vm.warp(999999999 + 1351102); vm.prank(0x0000000000000000000000000000000000020000); - property_sum_of_user_initiative_allocations(); + + ( + uint88 totalCountedLQTY, + // uint32 after_user_countedVoteLQTYAverageTimestamp // TODO: How do we do this? + ) = governance.globalState(); + + (uint88 user_voteLQTY, ) = _getAllUserAllocations(users[2]); + console.log("totalCountedLQTY", totalCountedLQTY); + console.log("user_voteLQTY", user_voteLQTY); + + assertEq(user_voteLQTY, totalCountedLQTY, "Sum matches"); + + property_sum_of_lqty_global_user_matches(); } + } \ No newline at end of file diff --git a/test/recon/properties/GovernanceProperties.sol b/test/recon/properties/GovernanceProperties.sol index 076f836..60cf416 100644 --- a/test/recon/properties/GovernanceProperties.sol +++ b/test/recon/properties/GovernanceProperties.sol @@ -75,78 +75,55 @@ abstract contract GovernanceProperties is BeforeAfter { // 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 - // function property_sum_of_lqty_global_user_matches() public { - // // Get state - // // Get all users - // // Sum up all voted users - // // Total must match - // ( - // uint88 totalCountedLQTY, - // // uint32 after_user_countedVoteLQTYAverageTimestamp // TODO: How do we do this? - // ) = governance.globalState(); - - // uint256 totalUserCountedLQTY; - // for(uint256 i; i < users.length; i++) { - // (uint88 user_allocatedLQTY, ) = governance.userStates(users[i]); - // totalUserCountedLQTY += user_allocatedLQTY; - // } - - // eq(totalCountedLQTY, totalUserCountedLQTY, "Global vs SUM(Users_lqty) must match"); - // } - - /// The Sum of LQTY allocated to Initiatives matches the Sum of LQTY allocated by users - // function property_sum_of_lqty_initiative_user_matches() public { - // // Get Initiatives - // // Get all users - // // Sum up all voted users & initiatives - // // Total must match - // uint256 totalInitiativesCountedLQTY; - // for(uint256 i; i < deployedInitiatives.length; i++) { - // ( - // uint88 after_user_voteLQTY, - // , - // , - // , - - // ) = governance.initiativeStates(deployedInitiatives[i]); - // totalInitiativesCountedLQTY += after_user_voteLQTY; - // } - - - // uint256 totalUserCountedLQTY; - // for(uint256 i; i < users.length; i++) { - // (uint88 user_allocatedLQTY, ) = governance.userStates(users[i]); - // totalUserCountedLQTY += user_allocatedLQTY; - // } - - // eq(totalInitiativesCountedLQTY, totalUserCountedLQTY, "SUM(Initiatives_lqty) vs SUM(Users_lqty) must match"); - // } - - /// The Sum of LQTY allocated to Initiatives matches the global state - function property_sum_of_lqty_global_initiatives_matches() public { - // Get Initiatives - // Get State - // Sum up all initiatives + // NOTE: Sum of positive votes + function property_sum_of_lqty_global_user_matches() public { + // Get state + // Get all users + // Sum up all voted users // Total must match ( uint88 totalCountedLQTY, // uint32 after_user_countedVoteLQTYAverageTimestamp // TODO: How do we do this? ) = governance.globalState(); - uint256 totalInitiativesCountedLQTY; + uint256 totalUserCountedLQTY; + for(uint256 i; i < users.length; i++) { + // Only sum up user votes + (uint88 user_voteLQTY, ) = _getAllUserAllocations(users[i]); + totalUserCountedLQTY += user_voteLQTY; + } + + eq(totalCountedLQTY, totalUserCountedLQTY, "Global vs SUM(Users_lqty) must match"); + } + + /// The Sum of LQTY allocated to Initiatives matches the Sum of LQTY allocated by users + function property_sum_of_lqty_initiative_user_matches() public { + // Get Initiatives + // Get all users + // Sum up all voted users & initiatives + // Total must match + uint256 totalInitiativesCountedVoteLQTY; + uint256 totalInitiativesCountedVetoLQTY; for(uint256 i; i < deployedInitiatives.length; i++) { ( - uint88 after_user_voteLQTY, - , + uint88 voteLQTY, + uint88 vetoLQTY, , , ) = governance.initiativeStates(deployedInitiatives[i]); - totalInitiativesCountedLQTY += after_user_voteLQTY; + totalInitiativesCountedVoteLQTY += voteLQTY; + totalInitiativesCountedVetoLQTY += vetoLQTY; } - eq(totalCountedLQTY, totalInitiativesCountedLQTY, "Global vs SUM(Initiatives_lqty) must match"); + uint256 totalUserCountedLQTY; + for(uint256 i; i < users.length; i++) { + (uint88 user_allocatedLQTY, ) = governance.userStates(users[i]); + totalUserCountedLQTY += user_allocatedLQTY; + } + + eq(totalInitiativesCountedVoteLQTY + totalInitiativesCountedVetoLQTY, totalUserCountedLQTY, "SUM(Initiatives_lqty) vs SUM(Users_lqty) must match"); } // TODO: also `lqtyAllocatedByUserToInitiative` @@ -166,7 +143,7 @@ abstract contract GovernanceProperties is BeforeAfter { uint256 totalUserVotes; uint256 totalUserVetos; for(uint256 i; i < users.length; i++) { - (uint88 vote_allocated, uint88 veto_allocated, ) = governance.lqtyAllocatedByUserToInitiative(users[i], deployedInitiatives[i]); + (uint88 vote_allocated, uint88 veto_allocated) = _getUserAllocation(users[i], deployedInitiatives[i]); totalUserVotes += vote_allocated; totalUserVetos += veto_allocated; } @@ -176,5 +153,17 @@ abstract contract GovernanceProperties is BeforeAfter { } } + // View vs non view + + function _getUserAllocation(address theUser, address initiative) internal view returns (uint88 votes, uint88 vetos) { + (votes, vetos, ) = governance.lqtyAllocatedByUserToInitiative(theUser, initiative); + } + function _getAllUserAllocations(address theUser) internal view 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; + } + } } \ No newline at end of file From 60f12aed06268172a74f10747e908db0b7e570de Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 15:06:16 +0200 Subject: [PATCH 85/93] fix: dorky bug --- test/recon/CryticToFoundry.sol | 56 +++++++++---------- .../recon/properties/GovernanceProperties.sol | 12 ++-- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/test/recon/CryticToFoundry.sol b/test/recon/CryticToFoundry.sol index 7609e52..c395dae 100644 --- a/test/recon/CryticToFoundry.sol +++ b/test/recon/CryticToFoundry.sol @@ -15,40 +15,40 @@ contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts { } -// forge test --match-test test_property_sum_of_lqty_global_initiatives_matches_0 -vv - -function test_property_sum_of_lqty_global_initiatives_matches_0() public { +// forge test --match-test test_property_sum_of_user_initiative_allocations_0 -vv +function test_property_sum_of_user_initiative_allocations_0() public { - vm.roll(13649); - vm.warp(999999999 + 274226); + vm.roll(39122); + vm.warp(999999999 +285913); + vm.prank(0x0000000000000000000000000000000000020000); + governance_depositLQTY(42075334510194471637767337); + + vm.roll(39152); + vm.warp(999999999 +613771); vm.prank(0x0000000000000000000000000000000000030000); - governance_depositLQTY(132009924662042920942); + helper_deployInitiative(); - vm.roll(23204); - vm.warp(999999999 + 765086); + vm.roll(69177); + vm.warp(999999999 +936185); + vm.prank(0x0000000000000000000000000000000000030000); + governance_allocateLQTY_clamped_single_initiative(0, 0, 1696172787721902493372875218); + + vm.roll(76883); + vm.warp(999999999 +1310996); + vm.prank(0x0000000000000000000000000000000000030000); + helper_deployInitiative(); + + vm.roll(94823); + vm.warp(999999999 +1329974); + vm.prank(0x0000000000000000000000000000000000010000); + helper_deployInitiative(); + + vm.roll(94907); + vm.warp(999999999 +1330374); vm.prank(0x0000000000000000000000000000000000030000); - governance_allocateLQTY_clamped_single_initiative(0, 6936608807263793400734754831, 0); - - -console.log("length", users.length); -console.log("length", deployedInitiatives.length); - vm.roll(52745); - vm.warp(999999999 + 1351102); - vm.prank(0x0000000000000000000000000000000000020000); - - ( - uint88 totalCountedLQTY, - // uint32 after_user_countedVoteLQTYAverageTimestamp // TODO: How do we do this? - ) = governance.globalState(); - - (uint88 user_voteLQTY, ) = _getAllUserAllocations(users[2]); - console.log("totalCountedLQTY", totalCountedLQTY); - console.log("user_voteLQTY", user_voteLQTY); - - assertEq(user_voteLQTY, totalCountedLQTY, "Sum matches"); - property_sum_of_lqty_global_user_matches(); } + } \ No newline at end of file diff --git a/test/recon/properties/GovernanceProperties.sol b/test/recon/properties/GovernanceProperties.sol index 60cf416..6554648 100644 --- a/test/recon/properties/GovernanceProperties.sol +++ b/test/recon/properties/GovernanceProperties.sol @@ -129,27 +129,27 @@ abstract contract GovernanceProperties is BeforeAfter { // TODO: also `lqtyAllocatedByUserToInitiative` // For each user, for each initiative, allocation is correct function property_sum_of_user_initiative_allocations() public { - for(uint256 i; i < deployedInitiatives.length; i++) { + for(uint256 x; x < deployedInitiatives.length; x++) { ( uint88 initiative_voteLQTY, uint88 initiative_vetoLQTY, , , - ) = governance.initiativeStates(deployedInitiatives[i]); + ) = governance.initiativeStates(deployedInitiatives[x]); // Grab all users and sum up their participations uint256 totalUserVotes; uint256 totalUserVetos; - for(uint256 i; i < users.length; i++) { - (uint88 vote_allocated, uint88 veto_allocated) = _getUserAllocation(users[i], deployedInitiatives[i]); + for(uint256 y; y < users.length; y++) { + (uint88 vote_allocated, uint88 veto_allocated) = _getUserAllocation(users[y], deployedInitiatives[x]); totalUserVotes += vote_allocated; totalUserVetos += veto_allocated; } - eq(initiative_voteLQTY, totalUserVotes + initiative_voteLQTY, "Sum of users, matches initiative votes"); - eq(initiative_vetoLQTY, totalUserVetos + initiative_vetoLQTY, "Sum of users, matches initiative vetos"); + eq(initiative_voteLQTY, totalUserVotes, "Sum of users, matches initiative votes"); + eq(initiative_vetoLQTY, totalUserVetos, "Sum of users, matches initiative vetos"); } } From c81f05d9c29887a68d152cc89fd384ffe866b85c Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 15:52:49 +0200 Subject: [PATCH 86/93] feat: broken property --- test/recon/CryticToFoundry.sol | 46 ++++++++++++++++------------------ 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/test/recon/CryticToFoundry.sol b/test/recon/CryticToFoundry.sol index c395dae..f5039e7 100644 --- a/test/recon/CryticToFoundry.sol +++ b/test/recon/CryticToFoundry.sol @@ -14,41 +14,37 @@ contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts { setup(); } - -// forge test --match-test test_property_sum_of_user_initiative_allocations_0 -vv -function test_property_sum_of_user_initiative_allocations_0() public { - - vm.roll(39122); - vm.warp(999999999 +285913); - vm.prank(0x0000000000000000000000000000000000020000); - governance_depositLQTY(42075334510194471637767337); +// forge test --match-test test_property_sum_of_lqty_global_user_matches_0 -vv + +function test_property_sum_of_lqty_global_user_matches_0() public { - vm.roll(39152); - vm.warp(999999999 +613771); + vm.roll(161622); + vm.warp(9999999999 + 1793404); vm.prank(0x0000000000000000000000000000000000030000); - helper_deployInitiative(); + property_sum_of_lqty_global_user_matches(); + + vm.roll(273284); + vm.warp(9999999999 + 3144198); + vm.prank(0x0000000000000000000000000000000000020000); + governance_depositLQTY(3501478328989062228745782); - vm.roll(69177); - vm.warp(999999999 +936185); + vm.roll(273987); + vm.warp(9999999999 + 3148293); vm.prank(0x0000000000000000000000000000000000030000); - governance_allocateLQTY_clamped_single_initiative(0, 0, 1696172787721902493372875218); + governance_allocateLQTY_clamped_single_initiative(0, 5285836763643083359055120749, 0); - vm.roll(76883); - vm.warp(999999999 +1310996); + vm.roll(303163); + vm.warp(9999999999 + 3234641); vm.prank(0x0000000000000000000000000000000000030000); - helper_deployInitiative(); + governance_unregisterInitiative(0); - vm.roll(94823); - vm.warp(999999999 +1329974); + vm.roll(303170); + vm.warp(9999999999 + 3234929); vm.prank(0x0000000000000000000000000000000000010000); - helper_deployInitiative(); - - vm.roll(94907); - vm.warp(999999999 +1330374); - vm.prank(0x0000000000000000000000000000000000030000); - + property_sum_of_lqty_global_user_matches(); } + } \ No newline at end of file From 15411923eb6db75dfa280418ee3188c08f95ecf9 Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 15:54:33 +0200 Subject: [PATCH 87/93] feat: trophies --- test/recon/CryticToFoundry.sol | 36 +--------------- test/recon/trophies/TrophiesToFoundry.sol | 52 +++++++++++++++++++++++ 2 files changed, 53 insertions(+), 35 deletions(-) create mode 100644 test/recon/trophies/TrophiesToFoundry.sol diff --git a/test/recon/CryticToFoundry.sol b/test/recon/CryticToFoundry.sol index f5039e7..1b9caa4 100644 --- a/test/recon/CryticToFoundry.sol +++ b/test/recon/CryticToFoundry.sol @@ -12,39 +12,5 @@ import {console} from "forge-std/console.sol"; contract CryticToFoundry is Test, TargetFunctions, FoundryAsserts { function setUp() public { setup(); - } - -// forge test --match-test test_property_sum_of_lqty_global_user_matches_0 -vv - -function test_property_sum_of_lqty_global_user_matches_0() public { - - vm.roll(161622); - vm.warp(9999999999 + 1793404); - vm.prank(0x0000000000000000000000000000000000030000); - property_sum_of_lqty_global_user_matches(); - - vm.roll(273284); - vm.warp(9999999999 + 3144198); - vm.prank(0x0000000000000000000000000000000000020000); - governance_depositLQTY(3501478328989062228745782); - - vm.roll(273987); - vm.warp(9999999999 + 3148293); - vm.prank(0x0000000000000000000000000000000000030000); - governance_allocateLQTY_clamped_single_initiative(0, 5285836763643083359055120749, 0); - - vm.roll(303163); - vm.warp(9999999999 + 3234641); - vm.prank(0x0000000000000000000000000000000000030000); - governance_unregisterInitiative(0); - - vm.roll(303170); - vm.warp(9999999999 + 3234929); - vm.prank(0x0000000000000000000000000000000000010000); - property_sum_of_lqty_global_user_matches(); -} - - - - + } } \ No newline at end of file diff --git a/test/recon/trophies/TrophiesToFoundry.sol b/test/recon/trophies/TrophiesToFoundry.sol new file mode 100644 index 0000000..88cff06 --- /dev/null +++ b/test/recon/trophies/TrophiesToFoundry.sol @@ -0,0 +1,52 @@ + +// 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 {console} from "forge-std/console.sol"; + + +contract TrophiesToFoundry is Test, TargetFunctions, FoundryAsserts { + function setUp() public { + setup(); + } + +// forge test --match-test test_property_sum_of_lqty_global_user_matches_0 -vv +// NOTE: This property breaks and that's the correct behaviour +// Because we remove the counted votes from total state +// Then the user votes will remain allocated +// But they are allocated to a DISABLED strategy +// Due to this, the count breaks +// We can change the property to ignore DISABLED strategies +// Or we would have to rethink the architecture +function test_property_sum_of_lqty_global_user_matches_0() public { + + vm.roll(161622); + vm.warp(9999999999 + 1793404); + vm.prank(0x0000000000000000000000000000000000030000); + property_sum_of_lqty_global_user_matches(); + + vm.roll(273284); + vm.warp(9999999999 + 3144198); + vm.prank(0x0000000000000000000000000000000000020000); + governance_depositLQTY(3501478328989062228745782); + + vm.roll(273987); + vm.warp(9999999999 + 3148293); + vm.prank(0x0000000000000000000000000000000000030000); + governance_allocateLQTY_clamped_single_initiative(0, 5285836763643083359055120749, 0); + + vm.roll(303163); + vm.warp(9999999999 + 3234641); + vm.prank(0x0000000000000000000000000000000000030000); + governance_unregisterInitiative(0); + + vm.roll(303170); + vm.warp(9999999999 + 3234929); + vm.prank(0x0000000000000000000000000000000000010000); + property_sum_of_lqty_global_user_matches(); +} +} \ No newline at end of file From 85eecddfd1680c63ce2da965105102713d174be3 Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 16:01:50 +0200 Subject: [PATCH 88/93] chore: cleanuop --- src/Governance.sol | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index cedf95d..ac7a025 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -515,7 +515,6 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance } /// === UPDATE ACCOUNTING === /// - // == INITIATIVE STATE == // // deep copy of the initiative's state before the allocation @@ -576,6 +575,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance ); state.countedVoteLQTY += initiativeState.voteLQTY; + // == USER ALLOCATION == // + // allocate the voting and vetoing LQTY to the initiative Allocation memory allocation = lqtyAllocatedByUserToInitiative[msg.sender][initiative]; allocation.voteLQTY = add(allocation.voteLQTY, deltaLQTYVotes); @@ -583,6 +584,8 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance allocation.atEpoch = currentEpoch; require(!(allocation.voteLQTY != 0 && allocation.vetoLQTY != 0), "Governance: vote-and-veto"); lqtyAllocatedByUserToInitiative[msg.sender][initiative] = allocation; + + // == USER STATE == // userState.allocatedLQTY = add(userState.allocatedLQTY, deltaLQTYVotes + deltaLQTYVetos); @@ -598,18 +601,10 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance "Governance: insufficient-or-allocated-lqty" ); - /// Update storage - emit EmitState(globalState.countedVoteLQTY, globalState.countedVoteLQTYAverageTimestamp); - emit EmitState(state.countedVoteLQTY, state.countedVoteLQTYAverageTimestamp); - globalState = state; - emit EmitState(globalState.countedVoteLQTY, globalState.countedVoteLQTYAverageTimestamp); - emit EmitState(state.countedVoteLQTY, state.countedVoteLQTYAverageTimestamp); userStates[msg.sender] = userState; } - event EmitState(uint88 countedVoteLQTY, uint32 countedVoteLQTYAverageTimestamp); - /// @inheritdoc IGovernance function unregisterInitiative(address _initiative) external nonReentrant { /// Enforce FSM @@ -629,8 +624,9 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance assert(initiativeState.lastEpochClaim < currentEpoch - 1); // recalculate the average staking timestamp for all counted voting LQTY if the initiative was counted in - // / @audit CRIT HERE | The math on removing messes stuff up - // / Prob need to remove this + /// @audit Trophy: `test_property_sum_of_lqty_global_user_matches_0` + // Removing votes from state desynchs the state until all users remove their votes from the initiative + state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp( state.countedVoteLQTYAverageTimestamp, initiativeState.averageStakingTimestampVoteLQTY, From 4581c3eb8bd79bc374d417e532c423f94063efdb Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 16:15:58 +0200 Subject: [PATCH 89/93] feat: e2e test + make invariant tests more reliable --- test/E2E.t.sol | 141 +++++++++++++++++++++++++++++++++++++++++++ test/recon/Setup.sol | 3 + 2 files changed, 144 insertions(+) create mode 100644 test/E2E.t.sol diff --git a/test/E2E.t.sol b/test/E2E.t.sol new file mode 100644 index 0000000..2f44ba9 --- /dev/null +++ b/test/E2E.t.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {Test, console2} from "forge-std/Test.sol"; +import {VmSafe} from "forge-std/Vm.sol"; +import {console} from "forge-std/console.sol"; + +import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; + +import {IGovernance} from "../src/interfaces/IGovernance.sol"; +import {ILQTY} from "../src/interfaces/ILQTY.sol"; + +import {BribeInitiative} from "../src/BribeInitiative.sol"; +import {Governance} from "../src/Governance.sol"; +import {UserProxy} from "../src/UserProxy.sol"; + +import {PermitParams} from "../src/utils/Types.sol"; + +import {MockInitiative} from "./mocks/MockInitiative.sol"; + +contract E2ETests is Test { + IERC20 private constant lqty = IERC20(address(0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D)); + IERC20 private constant lusd = IERC20(address(0x5f98805A4E8be255a32880FDeC7F6728C6568bA0)); + address private constant stakingV1 = address(0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d); + address private constant user = address(0xF977814e90dA44bFA03b6295A0616a897441aceC); + address private constant user2 = address(0x10C9cff3c4Faa8A60cB8506a7A99411E6A199038); + address private constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); + + uint128 private constant REGISTRATION_FEE = 1e18; + uint128 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; + uint128 private constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; + uint16 private constant REGISTRATION_WARM_UP_PERIOD = 4; + uint16 private constant UNREGISTRATION_AFTER_EPOCHS = 4; + uint128 private constant VOTING_THRESHOLD_FACTOR = 0.04e18; + uint88 private constant MIN_CLAIM = 500e18; + uint88 private constant MIN_ACCRUAL = 1000e18; + uint32 private constant EPOCH_DURATION = 604800; + uint32 private constant EPOCH_VOTING_CUTOFF = 518400; + + Governance private governance; + address[] private initialInitiatives; + + address private baseInitiative2; + address private baseInitiative3; + address private baseInitiative1; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl("mainnet"), 20430000); + + baseInitiative1 = address( + new BribeInitiative( + address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 3)), + address(lusd), + address(lqty) + ) + ); + + baseInitiative2 = address( + new BribeInitiative( + address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 2)), + address(lusd), + address(lqty) + ) + ); + + baseInitiative3 = address( + new BribeInitiative( + address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 1)), + address(lusd), + address(lqty) + ) + ); + + initialInitiatives.push(baseInitiative1); + initialInitiatives.push(baseInitiative2); + + governance = new Governance( + address(lqty), + address(lusd), + stakingV1, + address(lusd), + IGovernance.Configuration({ + registrationFee: REGISTRATION_FEE, + registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, + unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, + registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, + unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, + votingThresholdFactor: VOTING_THRESHOLD_FACTOR, + minClaim: MIN_CLAIM, + minAccrual: MIN_ACCRUAL, + epochStart: uint32(block.timestamp - EPOCH_DURATION), /// @audit KEY + epochDuration: EPOCH_DURATION, + epochVotingCutoff: EPOCH_VOTING_CUTOFF + }), + initialInitiatives + ); + } + + // forge test --match-test test_initialInitiativesCanBeVotedOnAtStart -vv + function test_initialInitiativesCanBeVotedOnAtStart() public { + /// @audit NOTE: In order for this to work, the constructor must set the start time a week behind + /// This will make the initiatives work on the first epoch + vm.startPrank(user); + // Check that we can vote on the first epoch, right after deployment + _deposit(1000e18); + + console.log("epoch", governance.epoch()); + _allocate(baseInitiative1, 1e18, 0); // Doesn't work due to cool down I think + + // And for sanity, you cannot vote on new ones, they need to be added first + deal(address(lusd), address(user), REGISTRATION_FEE); + lusd.approve(address(governance), REGISTRATION_FEE); + governance.registerInitiative(address(0x123123)); + + vm.expectRevert(); + _allocate(address(0x123123), 1e18, 0); + + // Whereas in next week it will work + vm.warp(block.timestamp + EPOCH_DURATION); + _allocate(address(0x123123), 1e18, 0); + } + + function _deposit(uint88 amt) internal { + address userProxy = governance.deployUserProxy(); + + lqty.approve(address(userProxy), amt); + governance.depositLQTY(amt); + } + + function _allocate(address initiative, int88 votes, int88 vetos) internal { + address[] memory initiatives = new address[](1); + initiatives[0] = initiative; + int88[] memory deltaLQTYVotes = new int88[](1); + deltaLQTYVotes[0] = votes; + int88[] memory deltaLQTYVetos = new int88[](1); + deltaLQTYVetos[0] = vetos; + + governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); + } + +} \ No newline at end of file diff --git a/test/recon/Setup.sol b/test/recon/Setup.sol index 0738c68..5eaa734 100644 --- a/test/recon/Setup.sol +++ b/test/recon/Setup.sol @@ -47,6 +47,9 @@ abstract contract Setup is BaseSetup { function setup() internal virtual override { + // Random TS that is realistic + vm.warp(1729087439); + vm.roll(block.number + 1); users.push(user); users.push(user2); From b6fc28a96f53b3b6be170d2d806b1e8011b7c4e9 Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 16:42:07 +0200 Subject: [PATCH 90/93] chore: further simplify trophy --- test/recon/trophies/TrophiesToFoundry.sol | 27 +++++++++++++---------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/test/recon/trophies/TrophiesToFoundry.sol b/test/recon/trophies/TrophiesToFoundry.sol index 88cff06..43c8649 100644 --- a/test/recon/trophies/TrophiesToFoundry.sol +++ b/test/recon/trophies/TrophiesToFoundry.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import {Test} from "forge-std/Test.sol"; import {TargetFunctions} from "../TargetFunctions.sol"; +import {Governance} from "src/Governance.sol"; import {FoundryAsserts} from "@chimera/FoundryAsserts.sol"; import {console} from "forge-std/console.sol"; @@ -23,30 +24,32 @@ contract TrophiesToFoundry is Test, TargetFunctions, FoundryAsserts { // We can change the property to ignore DISABLED strategies // Or we would have to rethink the architecture function test_property_sum_of_lqty_global_user_matches_0() public { - + vm.roll(161622); - vm.warp(9999999999 + 1793404); + vm.warp(block.timestamp + 1793404); vm.prank(0x0000000000000000000000000000000000030000); property_sum_of_lqty_global_user_matches(); vm.roll(273284); - vm.warp(9999999999 + 3144198); + vm.warp(block.timestamp + 3144198); vm.prank(0x0000000000000000000000000000000000020000); governance_depositLQTY(3501478328989062228745782); vm.roll(273987); - vm.warp(9999999999 + 3148293); + vm.warp(block.timestamp + 3148293); vm.prank(0x0000000000000000000000000000000000030000); governance_allocateLQTY_clamped_single_initiative(0, 5285836763643083359055120749, 0); - - vm.roll(303163); - vm.warp(9999999999 + 3234641); - vm.prank(0x0000000000000000000000000000000000030000); + + governance_unregisterInitiative(0); - - vm.roll(303170); - vm.warp(9999999999 + 3234929); - vm.prank(0x0000000000000000000000000000000000010000); property_sum_of_lqty_global_user_matches(); } + + function _getInitiativeStatus(address _initiative) internal returns (uint256) { + (Governance.InitiativeStatus status, , ) = governance.getInitiativeState(_getDeployedInitiative(0)); + return uint256(status); + } + + + } \ No newline at end of file From e4ef357c7a98eb5193e47e21724d23175b663f80 Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 16:47:43 +0200 Subject: [PATCH 91/93] feat: document the vote vs veto bug --- test/VoteVsVetBug.t.sol | 155 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 test/VoteVsVetBug.t.sol diff --git a/test/VoteVsVetBug.t.sol b/test/VoteVsVetBug.t.sol new file mode 100644 index 0000000..212e825 --- /dev/null +++ b/test/VoteVsVetBug.t.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {Test, console2} from "forge-std/Test.sol"; +import {VmSafe} from "forge-std/Vm.sol"; +import {console} from "forge-std/console.sol"; + +import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; + +import {IGovernance} from "../src/interfaces/IGovernance.sol"; +import {ILQTY} from "../src/interfaces/ILQTY.sol"; + +import {BribeInitiative} from "../src/BribeInitiative.sol"; +import {Governance} from "../src/Governance.sol"; +import {UserProxy} from "../src/UserProxy.sol"; + +import {PermitParams} from "../src/utils/Types.sol"; + +import {MockInitiative} from "./mocks/MockInitiative.sol"; + +contract VoteVsVetoBug is Test { + IERC20 private constant lqty = IERC20(address(0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D)); + IERC20 private constant lusd = IERC20(address(0x5f98805A4E8be255a32880FDeC7F6728C6568bA0)); + address private constant stakingV1 = address(0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d); + address private constant user = address(0xF977814e90dA44bFA03b6295A0616a897441aceC); + address private constant user2 = address(0x10C9cff3c4Faa8A60cB8506a7A99411E6A199038); + address private constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); + + uint128 private constant REGISTRATION_FEE = 1e18; + uint128 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; + uint128 private constant UNREGISTRATION_THRESHOLD_FACTOR = 4e18; + uint16 private constant REGISTRATION_WARM_UP_PERIOD = 4; + uint16 private constant UNREGISTRATION_AFTER_EPOCHS = 4; + uint128 private constant VOTING_THRESHOLD_FACTOR = 0.04e18; + uint88 private constant MIN_CLAIM = 500e18; + uint88 private constant MIN_ACCRUAL = 1000e18; + uint32 private constant EPOCH_DURATION = 604800; + uint32 private constant EPOCH_VOTING_CUTOFF = 518400; + + Governance private governance; + address[] private initialInitiatives; + + address private baseInitiative2; + address private baseInitiative3; + address private baseInitiative1; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl("mainnet"), 20430000); + + baseInitiative1 = address( + new BribeInitiative( + address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 3)), + address(lusd), + address(lqty) + ) + ); + + baseInitiative2 = address( + new BribeInitiative( + address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 2)), + address(lusd), + address(lqty) + ) + ); + + baseInitiative3 = address( + new BribeInitiative( + address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 1)), + address(lusd), + address(lqty) + ) + ); + + initialInitiatives.push(baseInitiative1); + initialInitiatives.push(baseInitiative2); + + governance = new Governance( + address(lqty), + address(lusd), + stakingV1, + address(lusd), + IGovernance.Configuration({ + registrationFee: REGISTRATION_FEE, + registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, + unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, + registrationWarmUpPeriod: REGISTRATION_WARM_UP_PERIOD, + unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, + votingThresholdFactor: VOTING_THRESHOLD_FACTOR, + minClaim: MIN_CLAIM, + minAccrual: MIN_ACCRUAL, + epochStart: uint32(block.timestamp - EPOCH_DURATION), /// @audit KEY + epochDuration: EPOCH_DURATION, + epochVotingCutoff: EPOCH_VOTING_CUTOFF + }), + initialInitiatives + ); + } + + // forge test --match-test test_voteVsVeto -vv + // See: https://miro.com/app/board/uXjVLRmQqYk=/?share_link_id=155340627460 + function test_voteVsVeto() public { + // Vetos can suppress votes + // Votes can be casted anywhere + + // Accounting issue + // Votes that are vetoed result in a loss of yield to the rest of the initiatives + // Since votes are increasing the denominator, while resulting in zero rewards + + // Game theory isse + // Additionally, vetos that fail to block an initiative are effectively a total loss to those that cast them + // They are objectively worse than voting something else + // Instead, it would be best to change the veto to reduce the amount of tokens sent to an initiative + + // We can do this via the following logic: + /** + + Vote vs Veto + + If you veto -> The vote is decreased + If you veto past the votes, the vote is not decreased, as we cannot create a negative vote amount, it needs to be net of the two + + Same for removing a veto, you can bring back votes, but you cannot bring back votes that didn’t exist + + So for this + Total votes + = + Sum votes - vetos + + But more specifically it needs to be the clamped delta of the two + */ + + // Demo = Vote on something + // Vote on something that gets vetoed + // Show that the result causes the only initiative to win to receive less than 100% of rewards + } + + function _deposit(uint88 amt) internal { + address userProxy = governance.deployUserProxy(); + + lqty.approve(address(userProxy), amt); + governance.depositLQTY(amt); + } + + function _allocate(address initiative, int88 votes, int88 vetos) internal { + address[] memory initiatives = new address[](1); + initiatives[0] = initiative; + int88[] memory deltaLQTYVotes = new int88[](1); + deltaLQTYVotes[0] = votes; + int88[] memory deltaLQTYVetos = new int88[](1); + deltaLQTYVetos[0] = vetos; + + governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); + } + +} \ No newline at end of file From 62ed071e829928532fcf00cc954505eeb8ca49f9 Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 16:57:32 +0200 Subject: [PATCH 92/93] feat: e2e test for unregistering --- src/Governance.sol | 3 +- test/E2E.t.sol | 54 +++++++++++++++++++++++ test/recon/Setup.sol | 5 +++ test/recon/trophies/TrophiesToFoundry.sol | 6 --- 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/Governance.sol b/src/Governance.sol index ac7a025..4cd789d 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -425,8 +425,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // == Unregister Condition == // - /// @audit epoch() - 1 because we can have Now - 1 and that's not a removal case | TODO: Double check | Worst case QA, off by one epoch - /// This shifts the logic by 1 epoch + // 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 diff --git a/test/E2E.t.sol b/test/E2E.t.sol index 2f44ba9..c98fd5c 100644 --- a/test/E2E.t.sol +++ b/test/E2E.t.sol @@ -120,6 +120,55 @@ contract E2ETests is Test { _allocate(address(0x123123), 1e18, 0); } + + // forge test --match-test test_deregisterIsSound -vv + function test_deregisterIsSound() public { + + // Deregistration works as follows: + // We stop voting + // We wait for `UNREGISTRATION_AFTER_EPOCHS` + // The initiative is removed + vm.startPrank(user); + // Check that we can vote on the first epoch, right after deployment + _deposit(1000e18); + + console.log("epoch", governance.epoch()); + _allocate(baseInitiative1, 1e18, 0); // Doesn't work due to cool down I think + + + // And for sanity, you cannot vote on new ones, they need to be added first + deal(address(lusd), address(user), REGISTRATION_FEE); + lusd.approve(address(governance), REGISTRATION_FEE); + + address newInitiative = address(0x123123); + governance.registerInitiative(newInitiative); + assertEq(uint256(Governance.InitiativeStatus.COOLDOWN) , _getInitiativeStatus(newInitiative), "Cooldown"); + + uint256 skipCount; + + // Whereas in next week it will work + vm.warp(block.timestamp + EPOCH_DURATION); // 1 + _allocate(newInitiative, 100, 0); // Will not meet the treshold + ++skipCount; + assertEq(uint256(Governance.InitiativeStatus.SKIP) ,_getInitiativeStatus(newInitiative), "SKIP"); + + // Cooldown on epoch Staert + + vm.warp(block.timestamp + EPOCH_DURATION); // 2 + ++skipCount; + assertEq(uint256(Governance.InitiativeStatus.SKIP) ,_getInitiativeStatus(newInitiative), "SKIP"); + + 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"); + } + function _deposit(uint88 amt) internal { address userProxy = governance.deployUserProxy(); @@ -138,4 +187,9 @@ contract E2ETests is Test { governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); } + function _getInitiativeStatus(address _initiative) internal returns (uint256) { + (Governance.InitiativeStatus status, , ) = governance.getInitiativeState(_initiative); + return uint256(status); + } + } \ No newline at end of file diff --git a/test/recon/Setup.sol b/test/recon/Setup.sol index 5eaa734..485ff6f 100644 --- a/test/recon/Setup.sol +++ b/test/recon/Setup.sol @@ -107,5 +107,10 @@ abstract contract Setup is BaseSetup { function _getRandomUser(uint8 index) internal returns (address randomUser) { return users[index % users.length]; } + + function _getInitiativeStatus(address _initiative) internal returns (uint256) { + (Governance.InitiativeStatus status, , ) = governance.getInitiativeState(_getDeployedInitiative(0)); + return uint256(status); + } } \ No newline at end of file diff --git a/test/recon/trophies/TrophiesToFoundry.sol b/test/recon/trophies/TrophiesToFoundry.sol index 43c8649..b0e8c37 100644 --- a/test/recon/trophies/TrophiesToFoundry.sol +++ b/test/recon/trophies/TrophiesToFoundry.sol @@ -45,11 +45,5 @@ function test_property_sum_of_lqty_global_user_matches_0() public { property_sum_of_lqty_global_user_matches(); } - function _getInitiativeStatus(address _initiative) internal returns (uint256) { - (Governance.InitiativeStatus status, , ) = governance.getInitiativeState(_getDeployedInitiative(0)); - return uint256(status); - } - - } \ No newline at end of file From e26ab76dab65791ed91f0b21cc8762e66054997d Mon Sep 17 00:00:00 2001 From: gallo Date: Wed, 16 Oct 2024 17:33:25 +0200 Subject: [PATCH 93/93] chore: improved README --- README.md | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8faecc2..968d20a 100644 --- a/README.md +++ b/README.md @@ -51,35 +51,33 @@ Claims for Initiatives which have met the minimum qualifying threshold, can be c in which they are awarded. Failure to do so will result in the unclaimed portion being reused in the following epoch. As Initiatives are assigned to arbitrary addresses, they can be used for any purpose, including EOAs, Multisigs, or smart contracts designed -for targetted purposes. Smart contracts should be designed in a way that they can support BOLD and include any additional logic about -how BOLD is to be used. +for targetted purposes. Smart contracts should be designed in a way that they can support BOLD and include any additional logic about how BOLD is to be used. + +### Malicious Initiatives + +It's important to note that initiatives could be malicious, and the system does it's best effort to prevent any DOS to happen, however, a malicious initiative could drain all rewards if voted on. ## Voting -Users with LQTY staked in Governance.sol, can allocate LQTY in the same epoch in which they were deposited. But the -effective voting power at that point would be insignificant. +Users with LQTY staked in Governance.sol, can allocate LQTY in the same epoch in which they were deposited. But the effective voting power at that point would be insignificant. Votes can take two forms, a vote for an Initiative or a veto vote. Initiatives which have received vetoes which are both: three times greater than the minimum qualifying threshold, and greater than the number of votes for will not be eligible for claims by being excluded from the vote count and maybe deregistered as an Initiative. Users may split their votes for and veto votes across any number of initiatives. But cannot vote for and veto vote the same Initiative. -Each epoch is split into two parts, a six day period where both votes for and veto votes take place, and a final 24 hour period where votes -can only be made as veto votes. This is designed to give a period where any detrimental distributions can be mitigated should there be -sufficient will to do so by voters, but is not envisaged to be a regular occurance. +Each epoch is split into two parts, a six day period where both votes for and veto votes take place, and a final 24 hour period where votes can only be made as veto votes. This is designed to give a period where any detrimental distributions can be mitigated should there be sufficient will to do so by voters, but is not envisaged to be a regular occurance. ## Snapshots Snapshots of results from the voting activity of an epoch takes place on an initiative by initiative basis in a permissionless manner. -User interactions or direct calls following the closure of an epoch trigger the snapshot logic which makes a Claim available to a -qualifying Initiative. +User interactions or direct calls following the closure of an epoch trigger the snapshot logic which makes a Claim available to a qualifying Initiative. ## Bribing LQTY depositors can also receive bribes in the form of ERC20s in exchange for voting for a specified initiative. This is done externally to the Governance.sol logic and should be implemented at the initiative level. -BaseInitiative.sol is a reference implementation which allows for bribes to be set and paid in BOLD + another token, -all claims for bribes are made by directly interacting with the implemented BaseInitiative contract. +BaseInitiative.sol is a reference implementation which allows for bribes to be set and paid in BOLD + another token, all claims for bribes are made by directly interacting with the implemented BaseInitiative contract. ## Example Initiatives @@ -95,3 +93,18 @@ Claiming and depositing to gauges must be done manually after each epoch in whic ### Uniswap v4 Simple hook for Uniswap v4 which implements a donate to a preconfigured pool. Allowing for adjustments to liquidity positions to make Claims which are smoothed over a vesting epoch. + +## Known Issues + +### Vetoed Initiatives and Initiatives that receive votes that are below the treshold cause a loss of emissions to the voted initiatives + +Because the system counts: valid_votes / total_votes +By definition, initiatives that increase the total_votes without receiving any rewards are stealing the rewards from other initiatives + +The rewards will be re-queued in the next epoch + +see: `test_voteVsVeto` as well as the miro and comments + +### User Votes, Initiative Votes and Global State Votes can desynchronize + +See `test_property_sum_of_lqty_global_user_matches_0`