From db7c0333d879173506c0f3ba561cb9fcad090301 Mon Sep 17 00:00:00 2001 From: Storming0x <6074987+storming0x@users.noreply.github.com> Date: Thu, 2 Mar 2023 15:57:53 -0600 Subject: [PATCH 1/9] Feat/gov compatibility (#26) * feat: Make Timelock compatible with other implementations of gov * fix: failing tests * feat: state method compatible in serpentorBravo * feat: compatible vote interface methods * feat: compatible admin role * feat: compatible proposals interface --------- Co-authored-by: storming0x <storm0x@storm0x.com> --- .gas-snapshot | 116 ++--- foundry_test/SerpentorBravo.t.sol | 474 +++++++++++++-------- foundry_test/Timelock.t.sol | 173 +++----- foundry_test/interfaces/SerpentorBravo.sol | 37 +- foundry_test/interfaces/Timelock.sol | 14 +- src/SerpentorBravo.vy | 322 +++++++++----- src/Timelock.vy | 139 +++--- 7 files changed, 720 insertions(+), 555 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 7efe61d..b5de489 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,67 +1,67 @@ -SerpentorBravoTest:testCanSubmitProposal(uint256) (runs: 256, μ: 727385, ~: 727385) -SerpentorBravoTest:testCannotCancelProposalIfProposerIsAboveThreshold(uint256,address,address) (runs: 256, μ: 726520, ~: 726520) -SerpentorBravoTest:testCannotCancelWhitelistedProposerBelowThreshold(uint256,uint256,address) (runs: 256, μ: 756802, ~: 758513) -SerpentorBravoTest:testCannotExecuteProposalIfNotQueued() (gas: 720712) -SerpentorBravoTest:testCannotProposeBelowThreshold(uint256) (runs: 256, μ: 224431, ~: 224432) -SerpentorBravoTest:testCannotProposeIfLastProposalIsActive(uint256) (runs: 256, μ: 794013, ~: 794013) -SerpentorBravoTest:testCannotProposeIfLastProposalIsPending(uint256) (runs: 256, μ: 791696, ~: 791696) -SerpentorBravoTest:testCannotProposeTooManyActions(uint256,uint8) (runs: 256, μ: 174743, ~: 174708) -SerpentorBravoTest:testCannotProposeZeroActions(uint256) (runs: 256, μ: 222629, ~: 222630) -SerpentorBravoTest:testCannotQueueProposalIfNotSucceeded() (gas: 738405) -SerpentorBravoTest:testCannotSetProposalThresholdOutsideRange(uint32) (runs: 256, μ: 13974, ~: 13967) -SerpentorBravoTest:testCannotSetVotingDelayOutsideRange(address,uint32) (runs: 256, μ: 16124, ~: 16127) -SerpentorBravoTest:testCannotSetVotingPeriodOutsideRange(address,uint32) (runs: 256, μ: 16634, ~: 16640) +SerpentorBravoTest:testCanSubmitProposal(uint256) (runs: 256, μ: 1051527, ~: 1051527) +SerpentorBravoTest:testCannotCancelProposalIfProposerIsAboveThreshold(uint256,address,address) (runs: 256, μ: 1043102, ~: 1043102) +SerpentorBravoTest:testCannotCancelWhitelistedProposerBelowThreshold(uint256,uint256,address) (runs: 256, μ: 1073333, ~: 1075121) +SerpentorBravoTest:testCannotExecuteProposalIfNotQueued() (gas: 1037342) +SerpentorBravoTest:testCannotProposeBelowThreshold(uint256) (runs: 256, μ: 225095, ~: 225096) +SerpentorBravoTest:testCannotProposeIfLastProposalIsActive(uint256) (runs: 256, μ: 1117095, ~: 1117095) +SerpentorBravoTest:testCannotProposeIfLastProposalIsPending(uint256) (runs: 256, μ: 1114800, ~: 1114800) +SerpentorBravoTest:testCannotProposeTooManyActions(uint256,uint8) (runs: 256, μ: 179808, ~: 179796) +SerpentorBravoTest:testCannotProposeZeroOperations(uint256) (runs: 256, μ: 223295, ~: 223296) +SerpentorBravoTest:testCannotQueueProposalIfNotSucceeded() (gas: 1043162) +SerpentorBravoTest:testCannotSetProposalThresholdOutsideRange(uint32) (runs: 256, μ: 13952, ~: 13945) +SerpentorBravoTest:testCannotSetVotingDelayOutsideRange(address,uint32) (runs: 256, μ: 16091, ~: 16094) +SerpentorBravoTest:testCannotSetVotingPeriodOutsideRange(address,uint32) (runs: 256, μ: 16612, ~: 16618) SerpentorBravoTest:testCannotSetWhitelistedAccount(address,uint256) (runs: 256, μ: 17540, ~: 17540) -SerpentorBravoTest:testCannotVoteMoreThanOnce(uint256,address,uint8) (runs: 256, μ: 763453, ~: 773024) -SerpentorBravoTest:testCannotVoteOnInactiveProposal(uint256,address,uint8) (runs: 256, μ: 714631, ~: 714631) -SerpentorBravoTest:testCannotVoteWithInvalidOption(uint256,address,uint8) (runs: 256, μ: 716891, ~: 716891) -SerpentorBravoTest:testGetAction() (gas: 734654) -SerpentorBravoTest:testOnlyPendingQueenCanAcceptThrone(address) (runs: 256, μ: 39324, ~: 39308) +SerpentorBravoTest:testCannotVoteMoreThanOnce(uint256,address,uint8) (runs: 256, μ: 1085813, ~: 1095461) +SerpentorBravoTest:testCannotVoteOnInactiveProposal(uint256,address,uint8) (runs: 256, μ: 1036980, ~: 1036980) +SerpentorBravoTest:testCannotVoteWithInvalidOption(uint256,address,uint8) (runs: 256, μ: 1039240, ~: 1039240) +SerpentorBravoTest:testGetAction() (gas: 1332672) +SerpentorBravoTest:testOnlyPendingAdminCanAcceptThrone(address) (runs: 256, μ: 39256, ~: 39240) +SerpentorBravoTest:testRandomAcctCannotSetNewAdmin(address) (runs: 256, μ: 14320, ~: 14320) SerpentorBravoTest:testRandomAcctCannotSetNewKnight(address) (runs: 256, μ: 14376, ~: 14376) -SerpentorBravoTest:testRandomAcctCannotSetNewQueen(address) (runs: 256, μ: 14234, ~: 14234) SerpentorBravoTest:testRandomAcctCannotSetProposalThreshold(address,uint256) (runs: 256, μ: 15205, ~: 15205) -SerpentorBravoTest:testRandomAcctCannotSetVotingDelay(address,uint256) (runs: 256, μ: 14604, ~: 14604) +SerpentorBravoTest:testRandomAcctCannotSetVotingDelay(address,uint256) (runs: 256, μ: 14582, ~: 14582) SerpentorBravoTest:testRandomAcctCannotSetVotingPeriod(address,uint256) (runs: 256, μ: 14627, ~: 14627) -SerpentorBravoTest:testRandomAcctCannotTakeOverThrone(address) (runs: 256, μ: 14281, ~: 14281) -SerpentorBravoTest:testSetNewKnight(address) (runs: 256, μ: 24775, ~: 24775) +SerpentorBravoTest:testRandomAcctCannotTakeOverThrone(address) (runs: 256, μ: 14259, ~: 14259) +SerpentorBravoTest:testSetNewKnight(address) (runs: 256, μ: 24753, ~: 24753) +SerpentorBravoTest:testSetWhitelistedAccountAsAdmin(address,uint256) (runs: 256, μ: 41634, ~: 41634) SerpentorBravoTest:testSetWhitelistedAccountAsKnight(address,uint256) (runs: 256, μ: 43666, ~: 43666) -SerpentorBravoTest:testSetWhitelistedAccountAsQueen(address,uint256) (runs: 256, μ: 41591, ~: 41591) -SerpentorBravoTest:testSetup() (gas: 83869) -SerpentorBravoTest:testShouldCancelProposalIfProposerIsBelowThreshold(uint256,uint256,address,address) (runs: 256, μ: 821841, ~: 823785) -SerpentorBravoTest:testShouldCancelQueuedProposal(address[7]) (runs: 256, μ: 2291565, ~: 2291476) -SerpentorBravoTest:testShouldCancelWhenSenderIsProposerAndProposalActive(uint256,address) (runs: 256, μ: 787143, ~: 787143) -SerpentorBravoTest:testShouldCancelWhitelistedProposerBelowThresholdAsKnight(uint256,uint256) (runs: 256, μ: 822444, ~: 825010) -SerpentorBravoTest:testShouldComputeDomainSeparatorCorrectly() (gas: 8804) -SerpentorBravoTest:testShouldExecuteQueuedProposal(address[7]) (runs: 256, μ: 2496754, ~: 2496665) -SerpentorBravoTest:testShouldHandleProposalDefeatedCorrectly(address[7]) (runs: 256, μ: 2073726, ~: 2073638) -SerpentorBravoTest:testShouldQueueProposal(address[7]) (runs: 256, μ: 2267303, ~: 2267215) -SerpentorBravoTest:testShouldRevertExecutionIfTrxReverts(address[7]) (runs: 256, μ: 2327075, ~: 2326986) +SerpentorBravoTest:testSetup() (gas: 83935) +SerpentorBravoTest:testShouldCancelProposalIfProposerIsBelowThreshold(uint256,uint256,address,address) (runs: 256, μ: 1124895, ~: 1126839) +SerpentorBravoTest:testShouldCancelQueuedProposal(address[7]) (runs: 256, μ: 2518157, ~: 2518068) +SerpentorBravoTest:testShouldCancelWhenSenderIsProposerAndProposalActive(uint256,address) (runs: 256, μ: 1090182, ~: 1090182) +SerpentorBravoTest:testShouldCancelWhitelistedProposerBelowThresholdAsKnight(uint256,uint256) (runs: 256, μ: 1125512, ~: 1128078) +SerpentorBravoTest:testShouldComputeDomainSeparatorCorrectly() (gas: 8762) +SerpentorBravoTest:testShouldExecuteQueuedProposal(address[7]) (runs: 256, μ: 2723217, ~: 2723129) +SerpentorBravoTest:testShouldHandleProposalDefeatedCorrectly(address[7]) (runs: 256, μ: 2331123, ~: 2331035) +SerpentorBravoTest:testShouldQueueProposal(address[7]) (runs: 256, μ: 2509235, ~: 2509147) +SerpentorBravoTest:testShouldRevertExecutionIfTrxReverts(address[7]) (runs: 256, μ: 2561053, ~: 2561053) SerpentorBravoTest:testShouldSetProposalThreshold(uint256) (runs: 256, μ: 23370, ~: 23414) -SerpentorBravoTest:testShouldSetVotingDelay(address,uint256) (runs: 256, μ: 26049, ~: 26049) -SerpentorBravoTest:testShouldSetVotingPeriod(address,uint256) (runs: 256, μ: 25992, ~: 26003) -SerpentorBravoTest:testShouldVote(uint256,address,uint8) (runs: 256, μ: 902352, ~: 912156) -SerpentorBravoTest:testShouldVoteBySig(uint256,uint8,uint248) (runs: 256, μ: 919104, ~: 928831) -SerpentorBravoTest:testShouldVoteWithReason(uint256,address,uint8) (runs: 256, μ: 903783, ~: 913587) -TimelockTest:testCannotExecNonExistingTrx() (gas: 59863) -TimelockTest:testCannotExecQueuedTrxAfterGracePeriod(uint256) (runs: 256, μ: 58678, ~: 58678) -TimelockTest:testCannotExecQueuedTrxBeforeETA() (gas: 57320) +SerpentorBravoTest:testShouldSetVotingDelay(address,uint256) (runs: 256, μ: 26027, ~: 26027) +SerpentorBravoTest:testShouldSetVotingPeriod(address,uint256) (runs: 256, μ: 26003, ~: 26003) +SerpentorBravoTest:testShouldVoteBySig(uint256,uint8,uint248) (runs: 256, μ: 1241322, ~: 1251127) +SerpentorBravoTest:testShouldVoteWithReason(uint256,address,uint8) (runs: 256, μ: 1225995, ~: 1235799) +SerpentorBravoTest:testShouldcastVote(uint256,address,uint8) (runs: 256, μ: 1224606, ~: 1234410) +TimelockTest:testCannotExecNonExistingTrx() (gas: 59503) +TimelockTest:testCannotExecQueuedTrxAfterGracePeriod(uint256) (runs: 256, μ: 58511, ~: 58511) +TimelockTest:testCannotExecQueuedTrxBeforeETA() (gas: 57094) TimelockTest:testDelayCannotBeAboveMax(uint256) (runs: 256, μ: 9483, ~: 9483) -TimelockTest:testDelayCannotBeBelowMinimum(uint256) (runs: 256, μ: 9361, ~: 9361) -TimelockTest:testOnlyPendingQueenCanAcceptThrone() (gas: 31741) +TimelockTest:testDelayCannotBeBelowMinimum(uint256) (runs: 256, μ: 9406, ~: 9406) +TimelockTest:testOnlyPendingAdminCanAcceptAdmin() (gas: 31742) TimelockTest:testOnlySelfCanSetDelay(uint256) (runs: 256, μ: 19650, ~: 19650) -TimelockTest:testQueueTrxEtaCannotBeInvalid() (gas: 18952) -TimelockTest:testRandomAcctCannotCancelQueueTrx(address) (runs: 256, μ: 17434, ~: 17434) -TimelockTest:testRandomAcctCannotExecQueuedTrx(address) (runs: 256, μ: 56403, ~: 56403) -TimelockTest:testRandomAcctCannotQueueTrx(address) (runs: 256, μ: 17477, ~: 17477) +TimelockTest:testQueueTrxEtaCannotBeInvalid() (gas: 18178) +TimelockTest:testRandomAcctCannotCancelQueueTrx(address) (runs: 256, μ: 16626, ~: 16626) +TimelockTest:testRandomAcctCannotExecQueuedTrx(address) (runs: 256, μ: 56133, ~: 56133) +TimelockTest:testRandomAcctCannotQueueTrx(address) (runs: 256, μ: 16657, ~: 16657) TimelockTest:testRandomAcctCannotSetDelay(address) (runs: 256, μ: 9449, ~: 9449) -TimelockTest:testRandomAcctCannotSetNewQueen(address) (runs: 256, μ: 9824, ~: 9824) -TimelockTest:testRandomAcctCannotTakeOverThrone(address) (runs: 256, μ: 13975, ~: 13975) -TimelockTest:testRandomAcctCantExecQueuedTrx(address) (runs: 256, μ: 17462, ~: 17462) -TimelockTest:testSetup() (gas: 16130) -TimelockTest:testShouldCancelQueuedTrx() (gas: 48198) -TimelockTest:testShouldExecQueuedTrxCorrectly() (gas: 64946) -TimelockTest:testShouldExecQueuedTrxWithCallerEthTransferCorrectly() (gas: 99300) -TimelockTest:testShouldExecQueuedTrxWithSignatureCorrectly() (gas: 65311) -TimelockTest:testShouldExecQueuedTrxWithTimelockEthTransferCorrectly() (gas: 93128) -TimelockTest:testShouldQueueTrx() (gas: 54829) -TimelockTest:testTimelockCanReceiveEther() (gas: 15267) +TimelockTest:testRandomAcctCannotSetNewAdmin(address) (runs: 256, μ: 9834, ~: 9834) +TimelockTest:testRandomAcctCannotTakeOverAdmin(address) (runs: 256, μ: 13952, ~: 13952) +TimelockTest:testRandomAcctCantExecQueuedTrx(address) (runs: 256, μ: 16654, ~: 16654) +TimelockTest:testSetup() (gas: 16108) +TimelockTest:testShouldCancelQueuedTrx() (gas: 47997) +TimelockTest:testShouldExecQueuedTrxCorrectly() (gas: 64743) +TimelockTest:testShouldExecQueuedTrxWithCallerEthTransferCorrectly() (gas: 99095) +TimelockTest:testShouldExecQueuedTrxWithSignatureCorrectly() (gas: 65108) +TimelockTest:testShouldExecQueuedTrxWithTimelockEthTransferCorrectly() (gas: 92873) +TimelockTest:testShouldQueueTrx() (gas: 54694) +TimelockTest:testTimelockCanReceiveEther() (gas: 15323) \ No newline at end of file diff --git a/foundry_test/SerpentorBravo.t.sol b/foundry_test/SerpentorBravo.t.sol index 4ff18ad..4e16639 100644 --- a/foundry_test/SerpentorBravo.t.sol +++ b/foundry_test/SerpentorBravo.t.sol @@ -9,7 +9,6 @@ import {SigUtils} from "./utils/SigUtils.sol"; import {console} from "forge-std/console.sol"; import { SerpentorBravo, - ProposalAction, Proposal, ProposalState, Receipt @@ -42,7 +41,7 @@ contract SerpentorBravoTest is ExtendedTest { uint256 public constant transferAmount = 1e18; uint public delay = 2 days; - address public queen = address(1); + address public admin = address(1); address public proposer = address(2); address public smallVoter = address(3); address public mediumVoter = address(4); @@ -61,7 +60,10 @@ contract SerpentorBravoTest is ExtendedTest { event ProposalCreated( uint256 id, address indexed proposer, - ProposalAction[] actions, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, uint256 startBlock, uint256 endBlock, string description @@ -84,7 +86,7 @@ contract SerpentorBravoTest is ExtendedTest { event VotingPeriodSet(uint256 oldVotingPeriod, uint256 newVotingPeriod); event ProposalThresholdSet(uint256 oldProposalThreshold, uint256 newProposalThreshold); - event NewQueen(address indexed oldQueen, address indexed newQueen); + event NewAdmin(address indexed oldAdmin, address indexed newAdmin); event NewKnight(address indexed oldKnight, address indexed newKnight); function setUp() public { @@ -93,7 +95,7 @@ contract SerpentorBravoTest is ExtendedTest { console.log("address for token: ", address(token)); // deploy timelock - bytes memory timelockArgs = abi.encode(queen, delay); + bytes memory timelockArgs = abi.encode(admin, delay); timelock = Timelock(vyperDeployer.deployContract("src/", "Timelock", timelockArgs)); console.log("address for timelock: ", address(timelock)); @@ -128,9 +130,9 @@ contract SerpentorBravoTest is ExtendedTest { // setup coupled governance between serpentor and timelock hoax(address(timelock)); - timelock.setPendingQueen(address(serpentor)); + timelock.setPendingAdmin(address(serpentor)); hoax(address(serpentor)); - timelock.acceptThrone(); + timelock.acceptAdmin(); hoax(address(timelock)); serpentor.setKnight(knight); hoax(address(knight)); @@ -157,9 +159,9 @@ contract SerpentorBravoTest is ExtendedTest { assertEq(serpentor.proposalThreshold(), THRESHOLD); assertEq(serpentor.quorumVotes(), QUORUM_VOTES); assertEq(serpentor.initialProposalId(), 0); - assertEq(serpentor.queen(), address(timelock)); - assertEq(serpentor.pendingQueen(), address(0)); - assertEq(timelock.queen(), address(serpentor)); + assertEq(serpentor.admin(), address(timelock)); + assertEq(serpentor.pendingAdmin(), address(0)); + assertEq(timelock.admin(), address(serpentor)); assertEq(serpentor.knight(), knight); assertTrue(serpentor.isWhitelisted(whitelistedProposer)); // check tests have correct starting balance of tokens @@ -180,15 +182,18 @@ contract SerpentorBravoTest is ExtendedTest { skip(2 days); assertEq(token.getPriorVotes(yoloProposer, block.number), votes); - ProposalAction[] memory actions; + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; vm.expectRevert(bytes("!threshold")); //execute hoax(yoloProposer); - serpentor.propose(actions, "test proposal"); + serpentor.propose(targets, values, signatures, calldatas, "test proposal"); } - function testCannotProposeZeroActions(uint256 votes) public { + function testCannotProposeZeroOperations(uint256 votes) public { vm.assume(votes > THRESHOLD && votes < type(uint128).max); // setup address yoloProposer = address(0xBEEF); @@ -196,12 +201,15 @@ contract SerpentorBravoTest is ExtendedTest { skip(2 days); assertEq(token.getPriorVotes(yoloProposer, block.number), votes); - ProposalAction[] memory actions; - vm.expectRevert(bytes("!no_actions")); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + vm.expectRevert(bytes("!no_targets")); //execute hoax(yoloProposer); - serpentor.propose(actions, "test proposal"); + serpentor.propose(targets, values, signatures, calldatas, "test proposal"); } function testShouldComputeDomainSeparatorCorrectly() public { @@ -221,7 +229,7 @@ contract SerpentorBravoTest is ExtendedTest { } function testCannotProposeTooManyActions(uint256 votes, uint8 size) public { - uint256 maxActions = serpentor.proposalMaxActions(); + uint256 maxActions = serpentor.proposalMaxOperations(); uint256 threshold = serpentor.proposalThreshold(); // if maxActions is a big number, tests runs out of gas vm.assume(votes > threshold && size > maxActions && size <= maxActions + 5); @@ -234,23 +242,24 @@ contract SerpentorBravoTest is ExtendedTest { // transfer 1e18 token to grantee bytes memory callData = abi.encodeWithSelector(IERC20.transfer.selector, grantee, transferAmount); - ProposalAction memory testAction = ProposalAction({ - target: address(token), - amount: 0, - signature: "", - callData: callData - }); - - ProposalAction[] memory actions = new ProposalAction[](size); + address[] memory targets = new address[](size); + uint256[] memory values = new uint256[](size); + string[] memory signatures = new string[](size); + bytes[] memory calldatas = new bytes[](size); // fill up action array - for (uint i = 0; i < size; i++) - actions[i] = testAction; + for (uint i = 0; i < size; i++) { + targets[i] = address(token); + values[i] = 0; + signatures[i] = ""; + calldatas[i] = callData; + } + // vyper reverts if array is longer than Max without data vm.expectRevert(); //execute hoax(yoloProposer); - serpentor.propose(actions, "test proposal"); + serpentor.propose(targets, values, signatures, calldatas, "test proposal"); } function testCanSubmitProposal(uint256 votes) public { @@ -258,18 +267,22 @@ contract SerpentorBravoTest is ExtendedTest { vm.assume(votes > threshold && votes < type(uint128).max); // setup address grantProposer = address(0xBEEF); - ProposalAction[] memory actions = _setupTestProposal(grantProposer, votes); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(grantProposer, votes); //setup for event checks uint256 expectedStartBlock = block.number + serpentor.votingDelay(); uint256 expectedEndBlock = expectedStartBlock + serpentor.votingPeriod(); vm.expectEmit(false, true, false, false); - emit ProposalCreated(1, grantProposer, actions, expectedStartBlock, expectedEndBlock, "send grant to contributor"); + emit ProposalCreated(1, grantProposer, targets, values, signatures, calldatas, expectedStartBlock, expectedEndBlock, "send grant to contributor"); // execute hoax(grantProposer); - uint256 proposalId = serpentor.propose(actions, "send grant to contributor"); + uint256 proposalId = serpentor.propose(targets, values, signatures, calldatas, "send grant to contributor"); Proposal memory proposal = serpentor.proposals(proposalId); - uint8 state = serpentor.ordinalState(proposalId); + uint8 state = serpentor.state(proposalId); // asserts assertEq(serpentor.proposalCount(), proposalId); @@ -277,7 +290,7 @@ contract SerpentorBravoTest is ExtendedTest { assertEq(proposal.id, proposalId); assertEq(proposal.proposer, grantProposer); assertEq(proposal.eta, 0); - assertEq(proposal.actions.length, actions.length); + assertEq(proposal.targets.length, targets.length); assertEq(proposal.startBlock, expectedStartBlock); assertEq(proposal.endBlock, expectedEndBlock); assertEq(proposal.forVotes, 0); @@ -294,20 +307,21 @@ contract SerpentorBravoTest is ExtendedTest { // setup first proposal address grantProposer = address(0xBEEF); - ProposalAction[] memory firstProposalActions = _setupTestProposal(grantProposer, votes); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(grantProposer, votes); + hoax(grantProposer); - uint256 proposalId = serpentor.propose(firstProposalActions, "send grant to contributor"); - uint8 state = serpentor.ordinalState(proposalId); + uint256 proposalId = serpentor.propose(targets, values, signatures, calldatas, "send grant to contributor"); + uint8 state = serpentor.state(proposalId); assertTrue(state == uint8(ProposalState.PENDING)); - ProposalAction[] memory secondProposalActions = new ProposalAction[](1); - // copy action - secondProposalActions[0] = firstProposalActions[0]; - // execute vm.expectRevert(bytes("!latestPropId_state")); hoax(grantProposer); - serpentor.propose(secondProposalActions, "send second grant to contributor"); + serpentor.propose(targets, values, signatures, calldatas, "send second grant to contributor"); } function testCannotProposeIfLastProposalIsActive(uint256 votes) public { @@ -316,20 +330,22 @@ contract SerpentorBravoTest is ExtendedTest { // setup first proposal address grantProposer = address(0xBEEF); - ProposalAction[] memory firstProposalActions = _setupTestProposal(grantProposer, votes); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(grantProposer, votes); hoax(grantProposer); - uint256 proposalId = serpentor.propose(firstProposalActions, "send grant to contributor"); + uint256 proposalId = serpentor.propose(targets, values, signatures, calldatas, "send grant to contributor"); // increase block.number after startBlock vm.roll(serpentor.votingDelay() + 2); - uint8 state = serpentor.ordinalState(proposalId); + uint8 state = serpentor.state(proposalId); assertEq(state,uint8(ProposalState.ACTIVE)); - ProposalAction[] memory secondProposalActions = new ProposalAction[](1); - secondProposalActions[0] = firstProposalActions[0]; // execute vm.expectRevert(bytes("!latestPropId_state")); hoax(grantProposer); - serpentor.propose(secondProposalActions, "send second grant to contributor"); + serpentor.propose(targets, values, signatures, calldatas, "send second grant to contributor"); } function testShouldCancelWhenSenderIsProposerAndProposalActive(uint256 votes, address grantProposer) public { @@ -337,12 +353,16 @@ contract SerpentorBravoTest is ExtendedTest { vm.assume(_isNotReservedAddress(grantProposer)); vm.assume(votes > threshold && votes < type(uint128).max); // setup proposal - ProposalAction[] memory proposalActions = _setupTestProposal(grantProposer, votes); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(grantProposer, votes); hoax(grantProposer); - uint256 proposalId = serpentor.propose(proposalActions, "send grant to contributor"); + uint256 proposalId = serpentor.propose(targets, values, signatures, calldatas, "send grant to contributor"); // increase block.number after startBlock vm.roll(serpentor.votingDelay() + 2); - uint8 state = serpentor.ordinalState(proposalId); + uint8 state = serpentor.state(proposalId); assertEq(state,uint8(ProposalState.ACTIVE)); // setup event vm.expectEmit(false, false, false, false); @@ -351,7 +371,7 @@ contract SerpentorBravoTest is ExtendedTest { // execute hoax(grantProposer); serpentor.cancel(proposalId); - state = serpentor.ordinalState(proposalId); + state = serpentor.state(proposalId); Proposal memory updatedProposal = serpentor.proposals(proposalId); // asserts @@ -370,15 +390,19 @@ contract SerpentorBravoTest is ExtendedTest { uint256 threshold = serpentor.proposalThreshold(); // if maxActions is a big number, tests runs out of gas vm.assume(votes > threshold && votes < type(uint128).max); + // setup proposal - - ProposalAction[] memory proposalActions = _setupTestProposal(grantProposer, votes); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(grantProposer, votes); hoax(grantProposer); - uint256 proposalId = serpentor.propose(proposalActions, "send grant to contributor"); + uint256 proposalId = serpentor.propose(targets, values, signatures, calldatas, "send grant to contributor"); // increase block.number after startBlock vm.roll(serpentor.votingDelay() + 2); - uint8 state = serpentor.ordinalState(proposalId); + uint8 state = serpentor.state(proposalId); assertEq(state,uint8(ProposalState.ACTIVE)); // setup event vm.expectRevert(bytes("!threshold")); @@ -400,8 +424,12 @@ contract SerpentorBravoTest is ExtendedTest { vm.assume(votes > threshold && votes < type(uint128).max); vm.assume(updatedVotes < threshold); // setup proposal - ProposalAction[] memory proposalActions = _setupTestProposal(grantProposer, votes); - uint256 proposalId = _submitActiveTestProposal(proposalActions, grantProposer); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(grantProposer, votes); + uint256 proposalId = _submitActiveTestProposal(targets, values, signatures, calldatas, grantProposer); // proposer goes below uint256 balanceOut = votes - updatedVotes; @@ -416,7 +444,7 @@ contract SerpentorBravoTest is ExtendedTest { // execute hoax(randomAcct); serpentor.cancel(proposalId); - uint256 state = serpentor.ordinalState(proposalId); + uint256 state = serpentor.state(proposalId); Proposal memory updatedProposal = serpentor.proposals(proposalId); // asserts @@ -437,10 +465,14 @@ contract SerpentorBravoTest is ExtendedTest { // setup proposal uint256 expectedETA; uint256 proposalId; - ProposalAction[] memory proposalActions = _setupTestProposal(grantProposer, threshold + 1); - (proposalId, expectedETA) = _submitQueuedTestProposal(voters, proposalActions, grantProposer); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(grantProposer, threshold + 1); + (proposalId, expectedETA) = _submitQueuedTestProposal(voters, targets, values, signatures, calldatas, grantProposer); Proposal memory proposal = serpentor.proposals(proposalId); - bytes32 expectedTxHash = _getTrxHash(proposal.actions[0], expectedETA); + bytes32 expectedTxHash = _getTrxHash(proposal.targets[0], proposal.values[0], proposal.signatures[0], proposal.calldatas[0], expectedETA); uint256 proposerBalance = token.balanceOf(grantProposer); // proposer goes below hoax(grantProposer); @@ -453,7 +485,7 @@ contract SerpentorBravoTest is ExtendedTest { serpentor.cancel(proposalId); // asserts - assertEq(serpentor.ordinalState(proposalId), uint8(ProposalState.CANCELED)); + assertEq(serpentor.state(proposalId), uint8(ProposalState.CANCELED)); assertFalse(timelock.queuedTransactions(expectedTxHash)); } @@ -467,8 +499,12 @@ contract SerpentorBravoTest is ExtendedTest { vm.assume(updatedVotes < threshold); vm.assume(_isNotReservedAddress(randomAcct)); // setup - ProposalAction[] memory proposalActions = _setupTestProposal(whitelistedProposer, votes); - uint256 proposalId = _submitActiveTestProposal(proposalActions, whitelistedProposer); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(whitelistedProposer, votes); + uint256 proposalId = _submitActiveTestProposal(targets, values, signatures, calldatas, whitelistedProposer); // proposer goes below uint256 balanceOut = votes - updatedVotes; @@ -492,8 +528,12 @@ contract SerpentorBravoTest is ExtendedTest { vm.assume(votes > threshold && votes < type(uint128).max); vm.assume(updatedVotes < threshold); // setup - ProposalAction[] memory proposalActions = _setupTestProposal(whitelistedProposer, votes); - uint256 proposalId = _submitActiveTestProposal(proposalActions, whitelistedProposer); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(whitelistedProposer, votes); + uint256 proposalId = _submitActiveTestProposal(targets, values, signatures, calldatas, whitelistedProposer); // proposer goes below uint256 balanceOut = votes - updatedVotes; @@ -509,7 +549,7 @@ contract SerpentorBravoTest is ExtendedTest { hoax(knight); serpentor.cancel(proposalId); - uint256 state = serpentor.ordinalState(proposalId); + uint256 state = serpentor.state(proposalId); Proposal memory updatedProposal = serpentor.proposals(proposalId); // asserts @@ -517,7 +557,7 @@ contract SerpentorBravoTest is ExtendedTest { assertEq(state,uint8(ProposalState.CANCELED)); } - function testSetWhitelistedAccountAsQueen(address randomAcct, uint256 expiration) public { + function testSetWhitelistedAccountAsAdmin(address randomAcct, uint256 expiration) public { // setup vm.assume(_isNotReservedAddress(randomAcct)); vm.assume(expiration > block.timestamp + 10 days && expiration < type(uint128).max); @@ -560,13 +600,17 @@ contract SerpentorBravoTest is ExtendedTest { vm.assume(_isNotReservedAddress(voter)); vm.assume(support > 2); // setup - ProposalAction[] memory proposalActions = _setupTestProposal(whitelistedProposer, votes); - uint256 proposalId = _submitActiveTestProposal(proposalActions, whitelistedProposer); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(whitelistedProposer, votes); + uint256 proposalId = _submitActiveTestProposal(targets, values, signatures, calldatas, whitelistedProposer); vm.expectRevert(bytes("!vote_type")); // execute hoax(voter); - serpentor.vote(proposalId, support); // invalid + serpentor.castVote(proposalId, support); // invalid } function testCannotVoteMoreThanOnce(uint256 votes, address voter, uint8 support) public { @@ -575,16 +619,20 @@ contract SerpentorBravoTest is ExtendedTest { vm.assume(_isNotReservedAddress(voter)); vm.assume(support <= 2); // setup - ProposalAction[] memory proposalActions = _setupTestProposal(whitelistedProposer, votes); - uint256 proposalId = _submitActiveTestProposal(proposalActions, whitelistedProposer); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(whitelistedProposer, votes); + uint256 proposalId = _submitActiveTestProposal(targets, values, signatures, calldatas, whitelistedProposer); // vote first time hoax(voter); - serpentor.vote(proposalId, support); + serpentor.castVote(proposalId, support); vm.expectRevert(bytes("!hasVoted")); // execute hoax(voter); - serpentor.vote(proposalId, support); + serpentor.castVote(proposalId, support); } function testCannotVoteOnInactiveProposal(uint256 votes, address voter, uint8 support) public { @@ -593,16 +641,20 @@ contract SerpentorBravoTest is ExtendedTest { vm.assume(_isNotReservedAddress(voter)); vm.assume(support <= 2); // setup - ProposalAction[] memory proposalActions = _setupTestProposal(whitelistedProposer, votes); - uint256 proposalId = _submitPendingTestProposal(proposalActions, whitelistedProposer); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(whitelistedProposer, votes); + uint256 proposalId = _submitPendingTestProposal(targets, values, signatures, calldatas, whitelistedProposer); vm.expectRevert(bytes("!active")); // execute hoax(voter); - serpentor.vote(proposalId, support); + serpentor.castVote(proposalId, support); } - function testShouldVote( + function testShouldcastVote( uint256 votes, address voter, uint8 support @@ -616,8 +668,12 @@ contract SerpentorBravoTest is ExtendedTest { // setup voter votes deal(address(token), voter, votes); - ProposalAction[] memory proposalActions = _setupTestProposal(whitelistedProposer, threshold + 1); - uint256 proposalId = _submitActiveTestProposal(proposalActions, whitelistedProposer); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(whitelistedProposer, threshold + 1); + uint256 proposalId = _submitActiveTestProposal(targets, values, signatures, calldatas, whitelistedProposer); // setup event vm.expectEmit(true, false, false, false); @@ -625,7 +681,7 @@ contract SerpentorBravoTest is ExtendedTest { // execute hoax(voter); - serpentor.vote(proposalId, support); + serpentor.castVote(proposalId, support); Proposal memory proposal = serpentor.proposals(proposalId); Receipt memory receipt = serpentor.getReceipt(proposalId, voter); @@ -650,8 +706,12 @@ contract SerpentorBravoTest is ExtendedTest { // setup voter votes deal(address(token), voter, votes); - ProposalAction[] memory proposalActions = _setupTestProposal(whitelistedProposer, threshold + 1); - uint256 proposalId = _submitActiveTestProposal(proposalActions, whitelistedProposer); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(whitelistedProposer, threshold + 1); + uint256 proposalId = _submitActiveTestProposal(targets, values, signatures, calldatas, whitelistedProposer); // setup event vm.expectEmit(true, false, false, false); @@ -659,7 +719,7 @@ contract SerpentorBravoTest is ExtendedTest { // execute hoax(voter); - serpentor.voteWithReason(proposalId, support, "test"); + serpentor.castVoteWithReason(proposalId, support, "test"); Proposal memory proposal = serpentor.proposals(proposalId); Receipt memory receipt = serpentor.getReceipt(proposalId, voter); @@ -684,12 +744,18 @@ contract SerpentorBravoTest is ExtendedTest { // generate voter from privateKey address voter = vm.addr(voterPrivateKey); vm.assume(_isNotReservedAddress(voter)); - + uint256 proposalId = 0; // setup voter votes deal(address(token), voter, votes); - - ProposalAction[] memory proposalActions = _setupTestProposal(whitelistedProposer, threshold + 1); - uint256 proposalId = _submitActiveTestProposal(proposalActions, whitelistedProposer); + // avoid stack too deep + { + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(whitelistedProposer, threshold + 1); + proposalId = _submitActiveTestProposal(targets, values, signatures, calldatas, whitelistedProposer); + } // create ballot SigUtils.Ballot memory ballot = SigUtils.Ballot({ proposalId: proposalId, @@ -708,7 +774,7 @@ contract SerpentorBravoTest is ExtendedTest { // execute hoax(address(0xdeadbeef)); // relayer - serpentor.voteBySig(proposalId, support, v,r,s); + serpentor.castVoteBySig(proposalId, support, v,r,s); Proposal memory proposal = serpentor.proposals(proposalId); Receipt memory receipt = serpentor.getReceipt(proposalId, voter); @@ -722,8 +788,12 @@ contract SerpentorBravoTest is ExtendedTest { function testCannotQueueProposalIfNotSucceeded() public { // setup uint256 threshold = serpentor.proposalThreshold(); - ProposalAction[] memory proposalActions = _setupTestProposal(whitelistedProposer, threshold + 1); - uint256 proposalId = _submitActiveTestProposal(proposalActions, whitelistedProposer); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(whitelistedProposer, threshold + 1); + uint256 proposalId = _submitActiveTestProposal(targets, values, signatures, calldatas, whitelistedProposer); // proposal still active cant be queued vm.expectRevert(bytes("!succeeded")); @@ -739,15 +809,19 @@ contract SerpentorBravoTest is ExtendedTest { vm.assume(_noReservedAddress(voters)); vm.assume(_noDuplicates(voters)); uint256 threshold = serpentor.proposalThreshold(); - ProposalAction[] memory proposalActions = _setupTestProposal(whitelistedProposer, threshold + 1); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(whitelistedProposer, threshold + 1); // execute - uint256 proposalId = _submitDefeatedTestProposal(voters, proposalActions, whitelistedProposer); + uint256 proposalId = _submitDefeatedTestProposal(voters, targets, values, signatures, calldatas, whitelistedProposer); Proposal memory proposal = serpentor.proposals(proposalId); // asserts - assertEq(serpentor.ordinalState(proposalId), uint8(ProposalState.DEFEATED)); + assertEq(serpentor.state(proposalId), uint8(ProposalState.DEFEATED)); assertEq(proposal.eta, 0); } @@ -760,16 +834,20 @@ contract SerpentorBravoTest is ExtendedTest { uint256 threshold = serpentor.proposalThreshold(); uint256 expectedETA; uint256 proposalId; - ProposalAction[] memory proposalActions = _setupTestProposal(whitelistedProposer, threshold + 1); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(whitelistedProposer, threshold + 1); // execute - (proposalId, expectedETA) = _submitQueuedTestProposal(voters, proposalActions, whitelistedProposer); + (proposalId, expectedETA) = _submitQueuedTestProposal(voters, targets, values, signatures, calldatas, whitelistedProposer); Proposal memory proposal = serpentor.proposals(proposalId); - bytes32 expectedTxHash = _getTrxHash(proposal.actions[0], expectedETA); + bytes32 expectedTxHash = _getTrxHash(proposal.targets[0], proposal.values[0], proposal.signatures[0], proposal.calldatas[0], expectedETA); // asserts - assertEq(serpentor.ordinalState(proposalId), uint8(ProposalState.QUEUED)); + assertEq(serpentor.state(proposalId), uint8(ProposalState.QUEUED)); assertEq(proposal.eta, expectedETA); assertTrue(timelock.queuedTransactions(expectedTxHash)); } @@ -777,10 +855,14 @@ contract SerpentorBravoTest is ExtendedTest { function testCannotExecuteProposalIfNotQueued() public { // setup uint256 threshold = serpentor.proposalThreshold(); - ProposalAction[] memory proposalActions = _setupTestProposal(whitelistedProposer, threshold + 1); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(whitelistedProposer, threshold + 1); // setup active proposal - uint256 proposalId = _submitActiveTestProposal(proposalActions, whitelistedProposer); + uint256 proposalId = _submitActiveTestProposal(targets, values, signatures, calldatas, whitelistedProposer); vm.expectRevert(bytes("!queued")); // execute hoax(smallVoter); @@ -796,13 +878,18 @@ contract SerpentorBravoTest is ExtendedTest { uint256 threshold = serpentor.proposalThreshold(); uint256 expectedETA; uint256 proposalId; - ProposalAction[] memory proposalActions = _setupTestProposal(whitelistedProposer, threshold + 1); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(whitelistedProposer, threshold + 1); + // setup queued proposal - (proposalId, expectedETA) = _submitQueuedTestProposal(voters, proposalActions, whitelistedProposer); + (proposalId, expectedETA) = _submitQueuedTestProposal(voters, targets, values, signatures, calldatas, whitelistedProposer); Proposal memory proposal = serpentor.proposals(proposalId); - bytes32 expectedTxHash = _getTrxHash(proposal.actions[0], expectedETA); + bytes32 expectedTxHash = _getTrxHash(proposal.targets[0], proposal.values[0], proposal.signatures[0], proposal.calldatas[0], expectedETA); skip(expectedETA + 1); // timelock does not have enough funds for proposal so trx will revert @@ -826,13 +913,17 @@ contract SerpentorBravoTest is ExtendedTest { uint256 threshold = serpentor.proposalThreshold(); uint256 expectedETA; uint256 proposalId; - ProposalAction[] memory proposalActions = _setupTestProposal(whitelistedProposer, threshold + 1); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + (targets, values, signatures, calldatas) = _setupTestProposal(whitelistedProposer, threshold + 1); // setup queued proposal - (proposalId, expectedETA) = _submitQueuedTestProposal(voters, proposalActions, whitelistedProposer); + (proposalId, expectedETA) = _submitQueuedTestProposal(voters, targets, values, signatures, calldatas, whitelistedProposer); Proposal memory proposal = serpentor.proposals(proposalId); - bytes32 expectedTxHash = _getTrxHash(proposal.actions[0], expectedETA); + bytes32 expectedTxHash = _getTrxHash(proposal.targets[0], proposal.values[0], proposal.signatures[0], proposal.calldatas[0], expectedETA); // assert balance of grantee before proposal execution is none assertEq(token.balanceOf(grantee), 0); assertTrue(timelock.queuedTransactions(expectedTxHash)); @@ -849,7 +940,7 @@ contract SerpentorBravoTest is ExtendedTest { proposal = serpentor.proposals(proposalId); // asserts - assertEq(serpentor.ordinalState(proposalId), uint8(ProposalState.EXECUTED)); + assertEq(serpentor.state(proposalId), uint8(ProposalState.EXECUTED)); assertFalse(timelock.queuedTransactions(expectedTxHash)); assertEq(token.balanceOf(grantee), transferAmount); } @@ -857,25 +948,33 @@ contract SerpentorBravoTest is ExtendedTest { function testGetAction() public { // setup uint256 threshold = serpentor.proposalThreshold(); - ProposalAction[] memory expectedActions = _setupTestProposal(whitelistedProposer, threshold + 1); - uint256 proposalId = _submitActiveTestProposal(expectedActions, whitelistedProposer); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + address[] memory expectedTargets; + uint256[] memory expectedValues; + string[] memory expectedSignatures; + bytes[] memory expectedCalldatas; + (expectedTargets, expectedValues, expectedSignatures, expectedCalldatas) = _setupTestProposal(whitelistedProposer, threshold + 1); + uint256 proposalId = _submitActiveTestProposal(expectedTargets, expectedValues, expectedSignatures, expectedCalldatas, whitelistedProposer); // execute - ProposalAction[] memory actions = serpentor.getActions(proposalId); + (targets, values, signatures, calldatas) = serpentor.getActions(proposalId); // asserts - assertEq(actions.length, expectedActions.length); - assertEq(actions[0].target, expectedActions[0].target); - assertEq(actions[0].amount, expectedActions[0].amount); - assertEq(actions[0].signature, expectedActions[0].signature); - assertEq(actions[0].callData, expectedActions[0].callData); + assertEq(targets.length, targets.length); + assertEq(targets[0], expectedTargets[0]); + assertEq(values[0], expectedValues[0]); + assertEq(signatures[0], expectedSignatures[0]); + assertEq(calldatas[0], expectedCalldatas[0]); } function testRandomAcctCannotSetVotingPeriod(address random, uint256 newVotingPeriod) public { vm.assume(_isNotReservedAddress(random)); vm.assume(newVotingPeriod >= MIN_VOTING_PERIOD && newVotingPeriod <= MAX_VOTING_PERIOD); // setup - vm.expectRevert(bytes("!queen")); + vm.expectRevert(bytes("!admin")); // execute vm.prank(random); serpentor.setVotingPeriod(newVotingPeriod); @@ -884,11 +983,11 @@ contract SerpentorBravoTest is ExtendedTest { function testCannotSetVotingPeriodOutsideRange(address random, uint32 newVotingPeriod) public { vm.assume(_isNotReservedAddress(random)); vm.assume(newVotingPeriod == 0 || newVotingPeriod == 1 || newVotingPeriod > MAX_VOTING_PERIOD); - address currentQueen = serpentor.queen(); + address currentAdmin = serpentor.admin(); // setup vm.expectRevert(bytes("!votingPeriod")); // execute - hoax(currentQueen); + hoax(currentAdmin); serpentor.setVotingPeriod(newVotingPeriod); } @@ -896,13 +995,13 @@ contract SerpentorBravoTest is ExtendedTest { vm.assume(_isNotReservedAddress(random)); vm.assume(newVotingPeriod >= MIN_VOTING_PERIOD && newVotingPeriod <= MAX_VOTING_PERIOD); // setup - address currentQueen = serpentor.queen(); + address currentAdmin = serpentor.admin(); uint256 oldVotingPeriod = serpentor.votingPeriod(); // setup event vm.expectEmit(false, false, false, false); emit VotingPeriodSet(oldVotingPeriod, newVotingPeriod); // execute - vm.prank(currentQueen); + vm.prank(currentAdmin); serpentor.setVotingPeriod(newVotingPeriod); // asserts @@ -913,7 +1012,7 @@ contract SerpentorBravoTest is ExtendedTest { vm.assume(_isNotReservedAddress(random)); vm.assume(newProposalThreshold >= MIN_PROPOSAL_THRESHOLD && newProposalThreshold <= MAX_PROPOSAL_THRESHOLD); // setup - vm.expectRevert(bytes("!queen")); + vm.expectRevert(bytes("!admin")); // execute hoax(random); serpentor.setProposalThreshold(newProposalThreshold); @@ -921,73 +1020,73 @@ contract SerpentorBravoTest is ExtendedTest { function testCannotSetProposalThresholdOutsideRange(uint32 newProposalThreshold) public { vm.assume(newProposalThreshold == 0 || newProposalThreshold == 1 || newProposalThreshold > MAX_PROPOSAL_THRESHOLD); - address currentQueen = serpentor.queen(); + address currentAdmin = serpentor.admin(); // setup vm.expectRevert(bytes("!threshold")); // execute - hoax(currentQueen); + hoax(currentAdmin); serpentor.setProposalThreshold(newProposalThreshold); } function testShouldSetProposalThreshold(uint256 newProposalThreshold) public { vm.assume(newProposalThreshold >= MIN_PROPOSAL_THRESHOLD && newProposalThreshold <= MAX_PROPOSAL_THRESHOLD); // setup - address currentQueen = serpentor.queen(); + address currentAdmin = serpentor.admin(); uint256 oldProposalThreshold = serpentor.proposalThreshold(); // setup event vm.expectEmit(false, false, false, false); emit ProposalThresholdSet(oldProposalThreshold, newProposalThreshold); // execute - vm.prank(currentQueen); + vm.prank(currentAdmin); serpentor.setProposalThreshold(newProposalThreshold); // asserts assertEq(serpentor.proposalThreshold(), newProposalThreshold); } - function testRandomAcctCannotSetNewQueen(address random) public { + function testRandomAcctCannotSetNewAdmin(address random) public { vm.assume(_isNotReservedAddress(random)); // setup - vm.expectRevert(bytes("!queen")); + vm.expectRevert(bytes("!admin")); // execute vm.prank(random); - serpentor.setPendingQueen(random); + serpentor.setPendingAdmin(random); } - function testRandomAcctCannotTakeOverThrone(address random) public { + function testRandomAcctCannotTakeOverAdmin(address random) public { vm.assume(_isNotReservedAddress(random)); // setup - vm.expectRevert(bytes("!pendingQueen")); + vm.expectRevert(bytes("!pendingAdmin")); // execute vm.prank(random); - serpentor.acceptThrone(); + serpentor.acceptAdmin(); } - function testOnlyPendingQueenCanAcceptThrone(address futureQueen) public { + function testOnlyPendingAdminCanAcceptAdmin(address futureAdmin) public { // setup - vm.assume(_isNotReservedAddress(futureQueen)); - address oldQueen = serpentor.queen(); - // setup pendingQueen + vm.assume(_isNotReservedAddress(futureAdmin)); + address oldAdmin = serpentor.admin(); + // setup pendingAdmin vm.prank(address(timelock)); - serpentor.setPendingQueen(futureQueen); - assertEq(serpentor.pendingQueen(), futureQueen); + serpentor.setPendingAdmin(futureAdmin); + assertEq(serpentor.pendingAdmin(), futureAdmin); //setup for event checks vm.expectEmit(true, true, false, false); - emit NewQueen(oldQueen, futureQueen); + emit NewAdmin(oldAdmin, futureAdmin); // execute - vm.prank(futureQueen); - serpentor.acceptThrone(); + vm.prank(futureAdmin); + serpentor.acceptAdmin(); // asserts - assertEq(serpentor.queen(), futureQueen); - assertEq(serpentor.pendingQueen(), address(0)); + assertEq(serpentor.admin(), futureAdmin); + assertEq(serpentor.pendingAdmin(), address(0)); } function testRandomAcctCannotSetNewKnight(address random) public { vm.assume(_isNotReservedAddress(random)); // setup - vm.expectRevert(bytes("!queen")); + vm.expectRevert(bytes("!admin")); // execute vm.prank(random); serpentor.setKnight(random); @@ -995,7 +1094,7 @@ contract SerpentorBravoTest is ExtendedTest { function testSetNewKnight(address newKnight) public { vm.assume(_isNotReservedAddress(newKnight)); - address currentQueen = serpentor.queen(); + address currentAdmin = serpentor.admin(); address oldKnight = serpentor.knight(); //setup for event checks @@ -1003,7 +1102,7 @@ contract SerpentorBravoTest is ExtendedTest { emit NewKnight(oldKnight, newKnight); // execute - vm.prank(currentQueen); + vm.prank(currentAdmin); serpentor.setKnight(newKnight); } @@ -1011,10 +1110,10 @@ contract SerpentorBravoTest is ExtendedTest { vm.assume(_isNotReservedAddress(random)); vm.assume(newVotingDelay == 0 || newVotingDelay > MAXIMUM_DELAY); // setup - address currentQueen = serpentor.queen(); + address currentAdmin = serpentor.admin(); vm.expectRevert(bytes("!votingDelay")); // execute - vm.prank(currentQueen); + vm.prank(currentAdmin); serpentor.setVotingDelay(newVotingDelay); } @@ -1022,7 +1121,7 @@ contract SerpentorBravoTest is ExtendedTest { vm.assume(_isNotReservedAddress(random)); vm.assume(newVotingDelay >= MINIMUM_DELAY && newVotingDelay <= MAXIMUM_DELAY); // setup - vm.expectRevert(bytes("!queen")); + vm.expectRevert(bytes("!admin")); // execute vm.prank(random); serpentor.setVotingDelay(newVotingDelay); @@ -1032,13 +1131,13 @@ contract SerpentorBravoTest is ExtendedTest { vm.assume(_isNotReservedAddress(random)); vm.assume(newVotingDelay >= MINIMUM_DELAY && newVotingDelay <= MAXIMUM_DELAY); // setup - address currentQueen = serpentor.queen(); + address currentAdmin = serpentor.admin(); uint256 oldVotingDelay = serpentor.votingDelay(); // setup event vm.expectEmit(false, false, false, false); emit VotingDelaySet(oldVotingDelay, newVotingDelay); // execute - vm.prank(currentQueen); + vm.prank(currentAdmin); serpentor.setVotingDelay(newVotingDelay); // asserts @@ -1084,7 +1183,7 @@ contract SerpentorBravoTest is ExtendedTest { function _setupTestProposal( address grantProposer, uint256 votes - ) internal returns (ProposalAction[] memory) { + ) internal returns (address[] memory targets, uint256[] memory amounts, string[] memory signatures, bytes[] memory calldatas) { deal(address(token), grantProposer, votes); skip(2 days); @@ -1092,22 +1191,23 @@ contract SerpentorBravoTest is ExtendedTest { // transfer 1e18 token to grantee bytes memory callData = abi.encodeWithSelector(IERC20.transfer.selector, grantee, transferAmount); - ProposalAction memory testAction = ProposalAction({ - target: address(token), - amount: 0, - signature: "", - callData: callData - }); - - ProposalAction[] memory actions = new ProposalAction[](1); - actions[0] = testAction; + targets = new address[](1); + amounts = new uint256[](1); + signatures = new string[](1); + calldatas = new bytes[](1); - return actions; + targets[0] = address(token); + amounts[0] = 0; + signatures[0] = ""; + calldatas[0] = callData; } function _submitQueuedTestProposal( address[ARR_SIZE] memory voters, - ProposalAction[] memory proposalActions, + address[] memory targets, + uint256[] memory amounts, + string[] memory signatures, + bytes[] memory calldatas, address _proposer ) internal returns (uint256 proposalId , uint256 expectedETA) { uint256[ARR_SIZE] memory votes = _setupVotingBalancesToQuorum(voters); @@ -1116,14 +1216,14 @@ contract SerpentorBravoTest is ExtendedTest { uint256 voteCount = _countVotes(votes); assertTrue(voteCount > serpentor.quorumVotes()); - proposalId = _submitActiveTestProposal(proposalActions, _proposer); + proposalId = _submitActiveTestProposal(targets, amounts, signatures, calldatas, _proposer); _executeVoting(voters, proposalId, 1); // for Proposal memory proposal = serpentor.proposals(proposalId); vm.roll(proposal.endBlock + 2); - assertEq(serpentor.ordinalState(proposalId), uint8(ProposalState.SUCCEEDED)); + assertEq(serpentor.state(proposalId), uint8(ProposalState.SUCCEEDED)); expectedETA = block.timestamp + timelock.delay(); //setup event @@ -1137,7 +1237,10 @@ contract SerpentorBravoTest is ExtendedTest { function _submitDefeatedTestProposal( address[ARR_SIZE] memory voters, - ProposalAction[] memory proposalActions, + address[] memory targets, + uint256[] memory amounts, + string[] memory signatures, + bytes[] memory calldatas, address _proposer ) internal returns (uint256 proposalId) { uint256[ARR_SIZE] memory votes = _setupVotingBalancesToQuorum(voters); @@ -1146,14 +1249,14 @@ contract SerpentorBravoTest is ExtendedTest { uint256 voteCount = _countVotes(votes); assertTrue(voteCount > serpentor.quorumVotes()); - proposalId = _submitActiveTestProposal(proposalActions, _proposer); + proposalId = _submitActiveTestProposal(targets, amounts, signatures, calldatas, _proposer); _executeVoting(voters, proposalId, 0); // against Proposal memory proposal = serpentor.proposals(proposalId); vm.roll(proposal.endBlock + 2); - assertEq(serpentor.ordinalState(proposalId), uint8(ProposalState.DEFEATED)); + assertEq(serpentor.state(proposalId), uint8(ProposalState.DEFEATED)); } function _executeVoting( @@ -1164,36 +1267,42 @@ contract SerpentorBravoTest is ExtendedTest { // execute voting for (uint i = 0; i < voters.length; i++) { hoax(voters[i]); - serpentor.vote(proposalId, support); + serpentor.castVote(proposalId, support); } } function _submitActiveTestProposal( - ProposalAction[] memory proposalActions, + address[] memory targets, + uint256[] memory amounts, + string[] memory signatures, + bytes[] memory calldatas, address _proposer ) internal returns (uint256) { // submit proposal hoax(_proposer); - uint256 proposalId = serpentor.propose(proposalActions, "send grant to contributor"); + uint256 proposalId = serpentor.propose(targets, amounts, signatures, calldatas, "send grant to contributor"); // increase block.number after startBlock vm.roll(serpentor.votingDelay() + 2); - uint8 state = serpentor.ordinalState(proposalId); + uint8 state = serpentor.state(proposalId); assertEq(state,uint8(ProposalState.ACTIVE)); return proposalId; } function _submitPendingTestProposal( - ProposalAction[] memory proposalActions, + address[] memory targets, + uint256[] memory amounts, + string[] memory signatures, + bytes[] memory calldatas, address _proposer ) internal returns (uint256) { // submit proposal hoax(_proposer); - uint256 proposalId = serpentor.propose(proposalActions, "send grant to contributor"); + uint256 proposalId = serpentor.propose(targets, amounts, signatures, calldatas, "send grant to contributor"); // increase block.number after startBlock - uint8 state = serpentor.ordinalState(proposalId); + uint8 state = serpentor.state(proposalId); assertEq(state,uint8(ProposalState.PENDING)); return proposalId; @@ -1201,7 +1310,7 @@ contract SerpentorBravoTest is ExtendedTest { function _setupReservedAddress() internal { reservedList = [ - queen, + admin, proposer, smallVoter, mediumVoter, @@ -1257,14 +1366,17 @@ contract SerpentorBravoTest is ExtendedTest { } function _getTrxHash( - ProposalAction memory action, + address target, + uint256 amount, + string memory signature, + bytes memory callData, uint eta ) internal pure returns (bytes32) { bytes32 trxHash = keccak256(abi.encode( - action.target, - action.amount, - action.signature, - action.callData, + target, + amount, + signature, + callData, eta )); diff --git a/foundry_test/Timelock.t.sol b/foundry_test/Timelock.t.sol index b76741b..da0e5b3 100644 --- a/foundry_test/Timelock.t.sol +++ b/foundry_test/Timelock.t.sol @@ -10,7 +10,7 @@ import {Timelock, Transaction} from "./interfaces/Timelock.sol"; contract TimelockTest is ExtendedTest { VyperDeployer private vyperDeployer = new VyperDeployer(); Timelock private timelock; - address public queen = address(1); + address public admin = address(1); address public holder = address(2); address public grantee = address(3); @@ -22,13 +22,13 @@ contract TimelockTest is ExtendedTest { // events event NewDelay(uint256 newDelay); - event NewQueen(address indexed newQueen); + event NewAdmin(address indexed newAdmin); event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); function setUp() public { - bytes memory args = abi.encode(queen, delay); + bytes memory args = abi.encode(admin, delay); timelock = Timelock(vyperDeployer.deployContract("src/", "Timelock", args)); console.log("address for timelock: ", address(timelock)); @@ -38,7 +38,7 @@ contract TimelockTest is ExtendedTest { function testSetup() public { assertNeq(address(timelock), address(0)); - assertEq(address(timelock.queen()), queen); + assertEq(address(timelock.admin()), admin); assertEq(timelock.delay(), delay); assertEq(timelock.delay(), MINIMUM_DELAY); } @@ -84,60 +84,52 @@ contract TimelockTest is ExtendedTest { timelock.setDelay(newDelay); } - function testRandomAcctCannotSetNewQueen(address random) public { + function testRandomAcctCannotSetNewAdmin(address random) public { vm.assume(random != address(timelock)); // setup vm.expectRevert(bytes("!Timelock")); // execute vm.prank(random); - timelock.setPendingQueen(random); + timelock.setPendingAdmin(random); } - function testRandomAcctCannotTakeOverThrone(address random) public { - vm.assume(random != queen && random != address(0)); + function testRandomAcctCannotTakeOverAdmin(address random) public { + vm.assume(random != admin && random != address(0)); // setup - vm.expectRevert(bytes("!pendingQueen")); + vm.expectRevert(bytes("!pendingAdmin")); // execute vm.prank(random); - timelock.acceptThrone(); + timelock.acceptAdmin(); } - function testOnlyPendingQueenCanAcceptThrone() public { + function testOnlyPendingAdminCanAcceptAdmin() public { // setup - address futureQueen = address(0xBEEF); - // setup pendingQueen + address futureAdmin = address(0xBEEF); + // setup pendingAdmin vm.prank(address(timelock)); - timelock.setPendingQueen(futureQueen); - assertEq(timelock.pendingQueen(), futureQueen); + timelock.setPendingAdmin(futureAdmin); + assertEq(timelock.pendingAdmin(), futureAdmin); //setup for event checks vm.expectEmit(true, false, false, false); - emit NewQueen(futureQueen); + emit NewAdmin(futureAdmin); // execute - vm.prank(futureQueen); - timelock.acceptThrone(); + vm.prank(futureAdmin); + timelock.acceptAdmin(); // asserts - assertEq(timelock.queen(), futureQueen); - assertEq(timelock.pendingQueen(), address(0)); + assertEq(timelock.admin(), futureAdmin); + assertEq(timelock.pendingAdmin(), address(0)); } function testRandomAcctCannotQueueTrx(address random) public { - vm.assume(random != queen); + vm.assume(random != admin); // setup - vm.expectRevert(bytes("!queen")); - - Transaction memory emptyTrx = Transaction({ - target: address(timelock), - amount: 0, - eta: block.timestamp + 10 days, - signature: "", - callData: "" - }); + vm.expectRevert(bytes("!admin")); // execute vm.prank(random); - timelock.queueTransaction(emptyTrx); + timelock.queueTransaction(address(timelock), 0, "", "", block.timestamp + 10 days); } function testQueueTrxEtaCannotBeInvalid() public { @@ -145,17 +137,10 @@ contract TimelockTest is ExtendedTest { vm.expectRevert(bytes("!eta")); uint256 badEta = block.timestamp; - Transaction memory emptyTrx = Transaction({ - target: address(timelock), - amount: 0, - eta: badEta, - signature: "", - callData: "" - }); // execute - vm.prank(address(queen)); - timelock.queueTransaction(emptyTrx); + vm.prank(address(admin)); + timelock.queueTransaction(address(timelock), 0, "", "", badEta); } function testShouldQueueTrx() public { @@ -167,8 +152,8 @@ contract TimelockTest is ExtendedTest { bytes memory callData = abi.encodeWithSelector(Timelock.setDelay.selector, newDelay); uint256 amount = 0; string memory signature = ""; - Transaction memory testTrx; bytes32 expectedTrxHash; + Transaction memory testTrx; (testTrx, expectedTrxHash) =_getTransactionAndHash( target, amount, @@ -181,29 +166,21 @@ contract TimelockTest is ExtendedTest { emit QueueTransaction(expectedTrxHash, target, amount, signature, callData, eta); // execute - vm.prank(address(queen)); - bytes32 trxHash = timelock.queueTransaction(testTrx); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); // asserts assertEq(expectedTrxHash, trxHash); assertTrue(timelock.queuedTransactions(trxHash)); } function testRandomAcctCannotCancelQueueTrx(address random) public { - vm.assume(random != queen); + vm.assume(random != admin); // setup - vm.expectRevert(bytes("!queen")); - - Transaction memory emptyTrx = Transaction({ - target: address(timelock), - amount: 0, - eta: block.timestamp + 10 days, - signature: "", - callData: "" - }); + vm.expectRevert(bytes("!admin")); // execute vm.prank(address(0xABCD)); - timelock.cancelTransaction(emptyTrx); + timelock.cancelTransaction(address(timelock), 0, "", "", block.timestamp + 10 days); } function testShouldCancelQueuedTrx() public { @@ -224,8 +201,8 @@ contract TimelockTest is ExtendedTest { eta ); - vm.prank(address(queen)); - bytes32 trxHash = timelock.queueTransaction(testTrx); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); assertTrue(timelock.queuedTransactions(trxHash)); //setup for event checks @@ -233,33 +210,25 @@ contract TimelockTest is ExtendedTest { emit CancelTransaction(expectedTrxHash, target, amount, signature, callData, eta); // execute - vm.prank(address(queen)); - timelock.cancelTransaction(testTrx); + vm.prank(address(admin)); + timelock.cancelTransaction(target, amount, signature, callData, eta); // asserts assertFalse(timelock.queuedTransactions(trxHash)); } function testRandomAcctCantExecQueuedTrx(address random) public { - vm.assume(random != queen); + vm.assume(random != admin); // setup - vm.expectRevert(bytes("!queen")); - - Transaction memory emptyTrx = Transaction({ - target: address(timelock), - amount: 0, - eta: block.timestamp + 10 days, - signature: "", - callData: "" - }); + vm.expectRevert(bytes("!admin")); // execute vm.prank(random); - timelock.cancelTransaction(emptyTrx); + timelock.cancelTransaction(address(timelock), 0, "", "", block.timestamp + 10 days); } function testRandomAcctCannotExecQueuedTrx(address random) public { - vm.assume(random != queen); + vm.assume(random != admin); // setup uint256 newDelay = 5 days; uint256 eta = block.timestamp + delay + 2 days; @@ -277,14 +246,14 @@ contract TimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(queen)); - bytes32 trxHash = timelock.queueTransaction(testTrx); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); assertTrue(timelock.queuedTransactions(trxHash)); - vm.expectRevert(bytes("!queen")); + vm.expectRevert(bytes("!admin")); // execute vm.prank(random); - timelock.executeTransaction(testTrx); + timelock.executeTransaction(target, amount, signature, callData, eta); } function testCannotExecNonExistingTrx() public { @@ -306,8 +275,8 @@ contract TimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(queen)); - bytes32 trxHash = timelock.queueTransaction(queuedTransaction); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); assertTrue(timelock.queuedTransactions(trxHash)); Transaction memory wrongTrx; @@ -322,8 +291,8 @@ contract TimelockTest is ExtendedTest { vm.expectRevert(bytes("!queued_trx")); // execute - vm.prank(address(queen)); - timelock.executeTransaction(wrongTrx); + vm.prank(address(admin)); + timelock.executeTransaction(target, amount, signature, "", eta); } function testCannotExecQueuedTrxBeforeETA() public { @@ -344,15 +313,15 @@ contract TimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(queen)); - bytes32 trxHash = timelock.queueTransaction(testTrx); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); assertTrue(timelock.queuedTransactions(trxHash)); skip(2 days); // short of ETA vm.expectRevert(bytes("!eta")); // execute - vm.prank(address(queen)); - timelock.executeTransaction(testTrx); + vm.prank(address(admin)); + timelock.executeTransaction(target, amount, signature, callData, eta); } function testCannotExecQueuedTrxAfterGracePeriod(uint256 executionTime) public { @@ -375,14 +344,14 @@ contract TimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(queen)); - bytes32 trxHash = timelock.queueTransaction(testTrx); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); assertTrue(timelock.queuedTransactions(trxHash)); skip(executionTime); // skip to time of execution passed gracePeriod vm.expectRevert(bytes("!staled_trx")); // execute - vm.prank(address(queen)); - timelock.executeTransaction(testTrx); + vm.prank(address(admin)); + timelock.executeTransaction(target, amount, signature, callData, eta); } @@ -404,8 +373,8 @@ contract TimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(queen)); - bytes32 trxHash = timelock.queueTransaction(testTrx); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); assertTrue(timelock.queuedTransactions(trxHash)); skip(eta + 1); // 1 pass eta //setup for event checks @@ -413,8 +382,8 @@ contract TimelockTest is ExtendedTest { emit ExecuteTransaction(expectedTrxHash, target, amount, signature, callData, eta); // execute - vm.prank(address(queen)); - bytes memory response = timelock.executeTransaction(testTrx); + vm.prank(address(admin)); + bytes memory response = timelock.executeTransaction(target, amount, signature, callData, eta); // asserts assertEq(string(response), string("")); @@ -439,8 +408,8 @@ contract TimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(queen)); - bytes32 trxHash = timelock.queueTransaction(testTrx); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); assertTrue(timelock.queuedTransactions(trxHash)); skip(eta + 1); // 1 pass eta //setup for event checks @@ -448,8 +417,8 @@ contract TimelockTest is ExtendedTest { emit ExecuteTransaction(expectedTrxHash, target, amount, signature, callData, eta); // execute - vm.prank(address(queen)); - bytes memory response = timelock.executeTransaction(testTrx); + vm.prank(address(admin)); + bytes memory response = timelock.executeTransaction(target, amount, signature, callData, eta); // asserts assertEq(string(response), string("")); @@ -474,8 +443,8 @@ contract TimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(queen)); - bytes32 trxHash = timelock.queueTransaction(testTrx); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); assertTrue(timelock.queuedTransactions(trxHash)); skip(eta + 1); // 1 pass deal(address(timelock), 11 ether); @@ -485,8 +454,8 @@ contract TimelockTest is ExtendedTest { emit ExecuteTransaction(expectedTrxHash, target, amount, signature, callData, eta); // execute - hoax(address(queen), 1 ether); - timelock.executeTransaction(testTrx); + hoax(address(admin), 1 ether); + timelock.executeTransaction(target, amount, signature, callData, eta); // asserts assertEq(grantee.balance, amount); @@ -510,8 +479,8 @@ contract TimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(queen)); - bytes32 trxHash = timelock.queueTransaction(testTrx); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); assertTrue(timelock.queuedTransactions(trxHash)); skip(eta + 1); // 1 pass assertEq(address(timelock).balance, 0); @@ -521,8 +490,8 @@ contract TimelockTest is ExtendedTest { emit ExecuteTransaction(expectedTrxHash, target, amount, signature, callData, eta); // execute - hoax(address(queen), 10 ether); - timelock.executeTransaction{value: amount}(testTrx); + hoax(address(admin), 10 ether); + timelock.executeTransaction{value: amount}(target, amount, signature, callData, eta); // asserts assertEq(grantee.balance, amount); diff --git a/foundry_test/interfaces/SerpentorBravo.sol b/foundry_test/interfaces/SerpentorBravo.sol index efe7057..6741664 100644 --- a/foundry_test/interfaces/SerpentorBravo.sol +++ b/foundry_test/interfaces/SerpentorBravo.sol @@ -12,20 +12,15 @@ enum ProposalState { EXPIRED, EXECUTED } - - -struct ProposalAction { - address target; - uint256 amount; - string signature; - bytes callData; -} struct Proposal { uint256 id; address proposer; uint256 eta; - ProposalAction[] actions; + address[] targets; + uint256[] values; + string[] signatures; + bytes[] calldatas; uint256 startBlock; uint256 endBlock; uint256 forVotes; @@ -43,8 +38,8 @@ struct Receipt { interface SerpentorBravo { // view functions - function queen() external view returns (address); - function pendingQueen() external view returns (address); + function admin() external view returns (address); + function pendingAdmin() external view returns (address); function knight() external view returns (address); function timelock() external view returns (address); function token() external view returns (address); @@ -53,28 +48,28 @@ interface SerpentorBravo { function quorumVotes() external view returns (uint256); function proposalThreshold() external view returns (uint256); function initialProposalId() external view returns (uint256); - function proposalMaxActions() external view returns (uint256); + function proposalMaxOperations() external view returns (uint256); function proposalCount() external view returns (uint256); function proposals(uint256 proposalId) external view returns (Proposal memory); function latestProposalIds(address account) external view returns (uint256); - function state(uint256 proposalId) external view returns (ProposalState); - function ordinalState(uint256 proposalId) external view returns (uint8); + function state(uint256 proposalId) external view returns (uint8); + function enumState(uint256 proposalId) external view returns (ProposalState); function isWhitelisted(address account) external view returns (bool); function getReceipt(uint256 proposalId, address voter) external view returns (Receipt memory); - function getActions(uint256 proposalId) external view returns (ProposalAction[] memory); + function getActions(uint256 proposalId) external view returns (address[] memory targets, uint[] memory values, string[] memory signatures, bytes[] memory calldatas); function domainSeparator() external view returns (bytes32); function name() external view returns (string memory); // state changing funcs - function setPendingQueen(address newQueen) external; - function acceptThrone() external; - function propose(ProposalAction[] calldata actions, string calldata description) external returns (uint256); + function setPendingAdmin(address newAdmin) external; + function acceptAdmin() external; + function propose(address[] memory targets, uint[] memory values, string[] memory signatures, bytes[] memory calldatas, string memory description) external returns (uint256); function cancel(uint256 proposalId) external; function setWhitelistAccountExpiration(address account, uint256 expiration) external; function setKnight(address newKnight) external; - function vote(uint256 proposalId, uint8 support) external; - function voteWithReason(uint256 proposalId, uint8 support, string calldata reason) external; - function voteBySig(uint256 proposalId, uint8 support, uint8 v, bytes32 r, bytes32 s) external; + function castVote(uint256 proposalId, uint8 support) external; + function castVoteWithReason(uint256 proposalId, uint8 support, string calldata reason) external; + function castVoteBySig(uint256 proposalId, uint8 support, uint8 v, bytes32 r, bytes32 s) external; function setVotingDelay(uint256 votingDelay) external; function setVotingPeriod(uint256 votingPeriod) external; function setProposalThreshold(uint256 proposalThreshold) external; diff --git a/foundry_test/interfaces/Timelock.sol b/foundry_test/interfaces/Timelock.sol index 7aa0078..a4c72e8 100644 --- a/foundry_test/interfaces/Timelock.sol +++ b/foundry_test/interfaces/Timelock.sol @@ -10,15 +10,15 @@ struct Transaction { } interface Timelock { - function queen() external view returns (address); - function pendingQueen() external view returns (address); + function admin() external view returns (address); + function pendingAdmin() external view returns (address); function delay() external view returns (uint); function setDelay(uint256 newDelay) external; - function setPendingQueen(address pendingqueen) external; + function setPendingAdmin(address pendingadmin) external; function GRACE_PERIOD() external view returns (uint); - function acceptThrone() external; + function acceptAdmin() external; function queuedTransactions(bytes32 hash) external view returns (bool); - function queueTransaction(Transaction calldata trx) external returns (bytes32); - function cancelTransaction(Transaction calldata trx) external; - function executeTransaction(Transaction calldata trx) external payable returns (bytes memory); + function queueTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external returns (bytes32); + function cancelTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external; + function executeTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external payable returns (bytes memory); } \ No newline at end of file diff --git a/src/SerpentorBravo.vy b/src/SerpentorBravo.vy index f3aeca8..46d3d37 100644 --- a/src/SerpentorBravo.vy +++ b/src/SerpentorBravo.vy @@ -41,28 +41,14 @@ BALLOT_TYPEHASH: constant(bytes32) = keccak256("Ballot(uint256 proposalId,uint8 # interfaces - -# timelock struct -# @notice a single transaction to be executed by the timelock -struct Transaction: - # @notice the target address for calls to be made - target: address - # @notice The value (i.e. msg.value) to be passed to the calls to be made - amount: uint256 - # @notice The estimated time for execution of the trx - eta: uint256 - # @notice The function signature to be called - signature: String[METHOD_SIG_SIZE] - # @notice The calldata to be passed to the call - callData: Bytes[CALL_DATA_LEN] - +# @dev compatible interface for timelock implementations interface Timelock: def delay() -> uint256: view def GRACE_PERIOD() -> uint256: view def queuedTransactions(hash: bytes32) -> bool: view - def queueTransaction(trx:Transaction) -> bytes32: nonpayable - def cancelTransaction(trx:Transaction): nonpayable - def executeTransaction(trx:Transaction) -> Bytes[MAX_DATA_LEN]: payable + def queueTransaction(target: address, amount: uint256, signature: String[METHOD_SIG_SIZE], data: Bytes[CALL_DATA_LEN], eta: uint256) -> bytes32: nonpayable + def cancelTransaction(target: address, amount: uint256, signature: String[METHOD_SIG_SIZE], data: Bytes[CALL_DATA_LEN], eta: uint256): nonpayable + def executeTransaction(target: address, amount: uint256, signature: String[METHOD_SIG_SIZE], data: Bytes[CALL_DATA_LEN], eta: uint256) -> Bytes[MAX_DATA_LEN]: payable # @dev Comp compatible interface to get Voting weight of account at block number. Some tokens implement 'balanceOfAt' but this call can be adapted to integrate with 'balanceOfAt' interface GovToken: @@ -86,11 +72,11 @@ struct ProposalAction: # @notice the target address for calls to be made target: address # @notice The value (i.e. msg.value) to be passed to the calls to be made - amount: uint256 + value: uint256 # @notice The function signature to be called signature: String[METHOD_SIG_SIZE] # @notice The calldata to be passed to the call - callData: Bytes[CALL_DATA_LEN] + calldata: Bytes[CALL_DATA_LEN] # @notice Ballot receipt record for a voter struct Receipt: @@ -108,7 +94,37 @@ struct Proposal: proposer: address # @notice The timestamp that the proposal will be available for execution, set once the vote succeeds eta: uint256 - # @notice The ordered list of actions this proposal will execute + # @notice the ordered list of target addresses for calls to be made + targets: DynArray[address, MAX_POSSIBLE_OPERATIONS] + # @notice the ordered list of values (i.e. msg.value) to be passed to the calls to be made + values: DynArray[uint256, MAX_POSSIBLE_OPERATIONS] + # @notice the ordered list of function signatures to be called + signatures: DynArray[String[METHOD_SIG_SIZE], MAX_POSSIBLE_OPERATIONS] + # @notice the ordered list of calldatas to be passed to each call + calldatas: DynArray[Bytes[CALL_DATA_LEN], MAX_POSSIBLE_OPERATIONS] + # @notice The block at which voting begins: holders must delegate their votes prior to this block + startBlock: uint256 + # @notice The block at which voting ends: votes must be cast prior to this block + endBlock: uint256 + # @notice Current number of votes in favor of this proposal + forVotes: uint256 + # @notice Current number of votes in opposition to this proposal + againstVotes: uint256 + # @notice Current number of votes for abstaining for this proposal + abstainVotes: uint256 + # @notice Flag marking whether the proposal has been canceled + canceled: bool + # @notice Flag marking whether the proposal has been executed + executed: bool + +struct ProposalCore: + # @notice Unique id for looking up a proposal + id: uint256 + # @notice Creator of the proposal + proposer: address + # @notice The timestamp that the proposal will be available for execution, set once the vote succeeds + eta: uint256 + # @notice the ordered list of ProposalActions to be executed actions: DynArray[ProposalAction, MAX_POSSIBLE_OPERATIONS] # @notice The block at which voting begins: holders must delegate their votes prior to this block startBlock: uint256 @@ -125,10 +141,11 @@ struct Proposal: # @notice Flag marking whether the proposal has been executed executed: bool + # @notice empress for this contract -queen: public(address) +admin: public(address) # @notice pending empress for this contract -pendingQueen: public(address) +pendingAdmin: public(address) # @notice whitelist guardian role for this contract knight: public(address) @@ -149,12 +166,12 @@ timelock: public(immutable(address)) token: public(immutable(address)) # @notice The total number of proposals proposalCount: public(uint256) -# @notice The storage record of all proposals ever proposed -proposals: public(HashMap[uint256, Proposal]) # @notice The latest proposal for each proposer latestProposalIds: public(HashMap[address, uint256]) # @notice Stores the expiration of account whitelist status as a timestamp whitelistAccountExpirations: public(HashMap[address, uint256]) +# @notice The storage record of all proposals ever proposed +_proposals: HashMap[uint256, ProposalCore] # @notice Receipts of ballots for the entire set of voters, proposal_id -> voter_address -> receipt receipts: HashMap[uint256, HashMap[address, Receipt]] @@ -163,11 +180,14 @@ receipts: HashMap[uint256, HashMap[address, Receipt]] # ///// EVENTS ///// # @notice An event emitted when a new proposal is created event ProposalCreated: - id: uint256 + proposalId: uint256 proposer: indexed(address) - actions: DynArray[ProposalAction, MAX_POSSIBLE_OPERATIONS] - startBlock: uint256 - endBlock: uint256 + targets: DynArray[address, MAX_POSSIBLE_OPERATIONS] + values: DynArray[uint256, MAX_POSSIBLE_OPERATIONS] + signatures: DynArray[String[METHOD_SIG_SIZE], MAX_POSSIBLE_OPERATIONS] + calldatas: DynArray[Bytes[CALL_DATA_LEN], MAX_POSSIBLE_OPERATIONS] + voteStart: uint256 + voteEnd: uint256 description: String[STR_LEN] # @notice An event emitted when a proposal has been queued in the Timelock @@ -216,15 +236,15 @@ event WhitelistAccountExpirationSet: account: indexed(address) expiration: uint256 -# @notice Event emitted when pendingQueen is set -event NewPendingQueen: - oldPendingQueen: indexed(address) - newPendingQueen: indexed(address) +# @notice Event emitted when pendingAdmin is set +event NewPendingAdmin: + oldPendingAdmin: indexed(address) + newPendingAdmin: indexed(address) -# @notice Event emitted when new queen is set -event NewQueen: - oldQueen: indexed(address) - newQueen: indexed(address) +# @notice Event emitted when new admin is set +event NewAdmin: + oldAdmin: indexed(address) + newAdmin: indexed(address) # @notice Event emitted when knight is set event NewKnight: @@ -234,7 +254,7 @@ event NewKnight: @external def __init__( timelockAddr: address, - queen: address, + admin: address, tokenAddr: address, votingPeriod: uint256, votingDelay: uint256, @@ -256,11 +276,11 @@ def __init__( """ assert timelockAddr != empty(address), "!timelock" assert tokenAddr != empty(address), "!token" - assert queen != empty(address), "!queen" + assert admin != empty(address), "!admin" assert votingPeriod >= MIN_VOTING_PERIOD and votingPeriod <= MAX_VOTING_PERIOD, "!votingPeriod" assert votingDelay >= MIN_VOTING_DELAY and votingDelay <= MAX_VOTING_DELAY, "!votingDelay" assert proposalThreshold >= MIN_PROPOSAL_THRESHOLD and proposalThreshold <= MAX_PROPOSAL_THRESHOLD, "!proposalThreshold" - self.queen = queen + self.admin = admin self.votingPeriod = votingPeriod self.votingDelay = votingDelay self.proposalThreshold = proposalThreshold @@ -272,21 +292,28 @@ def __init__( @external def propose( - actions: DynArray[ProposalAction, MAX_POSSIBLE_OPERATIONS], + targets: DynArray[address, MAX_POSSIBLE_OPERATIONS], + values: DynArray[uint256, MAX_POSSIBLE_OPERATIONS], + signatures: DynArray[String[METHOD_SIG_SIZE], MAX_POSSIBLE_OPERATIONS], + calldatas: DynArray[Bytes[CALL_DATA_LEN], MAX_POSSIBLE_OPERATIONS], description: String[STR_LEN] ) -> uint256: """ @notice Function used to propose a new proposal. Sender must have voting power above the proposal threshold - @param actions Array of ProposalAction struct with target, value, signature and calldata for executing + @param targets Array of addresses to call + @param values Array of values to send to each target + @param signatures Array of function signatures on each target + @param calldatas Array of calldata to call on each target @param description String description of the proposal @return Proposal id of new proposal """ # check voting power or whitelist access assert GovToken(token).getPriorVotes(msg.sender, block.number - 1) > self.proposalThreshold or self._isWhitelisted(msg.sender), "!threshold" - assert len(actions) != 0, "!no_actions" - assert len(actions) <= MAX_POSSIBLE_OPERATIONS, "!too_many_actions" + assert len(targets) != 0, "!no_targets" + assert len(targets) <= MAX_POSSIBLE_OPERATIONS, "!too_many_operations" + assert len(targets) == len(values) and len(targets) == len(signatures) and len(targets) == len(calldatas), "!ops_length_mismatch" latestProposalId: uint256 = self.latestProposalIds[msg.sender] if latestProposalId != 0: @@ -298,7 +325,20 @@ def propose( self.proposalCount += 1 - newProposal: Proposal = Proposal({ + actions: DynArray[ProposalAction, MAX_POSSIBLE_OPERATIONS] = [] + numActions: uint256 = len(targets) + + for i in range(MAX_POSSIBLE_OPERATIONS): + if i >= numActions: + break + actions.append(ProposalAction({ + target: targets[i], + value: values[i], + signature: signatures[i], + calldata: calldatas[i] + })) + + newProposal: ProposalCore = ProposalCore({ id: self.proposalCount, proposer: msg.sender, eta: 0, @@ -312,10 +352,10 @@ def propose( executed: False }) - self.proposals[newProposal.id] = newProposal + self._proposals[newProposal.id] = newProposal self.latestProposalIds[newProposal.proposer] = newProposal.id - log ProposalCreated(newProposal.id, msg.sender, actions, startBlock, endBlock, description) + log ProposalCreated(newProposal.id, msg.sender, targets,values, signatures, calldatas, startBlock, endBlock, description) return newProposal.id @@ -328,9 +368,9 @@ def queue(proposalId: uint256): """ assert self._state(proposalId) == ProposalState.SUCCEEDED, "!succeeded" eta: uint256 = block.timestamp + Timelock(timelock).delay() - for action in self.proposals[proposalId].actions: + for action in self._proposals[proposalId].actions: self._queueOrRevertInternal(action, eta) - self.proposals[proposalId].eta = eta + self._proposals[proposalId].eta = eta log ProposalQueued(proposalId, eta) @external @@ -341,11 +381,10 @@ def execute(proposalId: uint256): @param proposalId The id of the proposal to execute """ assert self._state(proposalId) == ProposalState.QUEUED, "!queued" - proposalEta: uint256 = self.proposals[proposalId].eta - self.proposals[proposalId].executed = True - for action in self.proposals[proposalId].actions: - trx: Transaction = self._buildTrx(action, proposalEta) - Timelock(timelock).executeTransaction(trx, value=action.amount) + proposalEta: uint256 = self._proposals[proposalId].eta + self._proposals[proposalId].executed = True + for action in self._proposals[proposalId].actions: + Timelock(timelock).executeTransaction(action.target, action.value, action.signature, action.calldata, proposalEta, value=action.value) log ProposalExecuted(proposalId) @@ -357,8 +396,8 @@ def cancel(proposalId: uint256): """ assert self._state(proposalId) != ProposalState.EXECUTED, "!cancel_executed" # proposer can cancel - proposer: address = self.proposals[proposalId].proposer - proposalEta: uint256 = self.proposals[proposalId].eta + proposer: address = self._proposals[proposalId].proposer + proposalEta: uint256 = self._proposals[proposalId].eta if msg.sender != proposer: # Whitelisted proposers can't be canceled for falling below proposal threshold unless msg.sender is knight @@ -367,15 +406,14 @@ def cancel(proposalId: uint256): else: assert GovToken(token).getPriorVotes(proposer, block.number - 1) < self.proposalThreshold, "!threshold" - self.proposals[proposalId].canceled = True - for action in self.proposals[proposalId].actions: - trx: Transaction = self._buildTrx(action, proposalEta) - Timelock(timelock).cancelTransaction(trx) + self._proposals[proposalId].canceled = True + for action in self._proposals[proposalId].actions: + Timelock(timelock).cancelTransaction(action.target, action.value, action.signature, action.calldata, proposalEta) log ProposalCanceled(proposalId) @external -def vote(proposalId: uint256, support: uint8): +def castVote(proposalId: uint256, support: uint8): """ @notice Cast a vote for a proposal @param proposalId The id of the proposal @@ -384,7 +422,7 @@ def vote(proposalId: uint256, support: uint8): log VoteCast(msg.sender, proposalId, support, self._vote(msg.sender, proposalId, support), "") @external -def voteWithReason(proposalId: uint256, support: uint8, reason: String[STR_LEN]): +def castVoteWithReason(proposalId: uint256, support: uint8, reason: String[STR_LEN]): """ @notice Cast a vote for a proposal with a reason string @param proposalId The id of the proposal @@ -393,7 +431,7 @@ def voteWithReason(proposalId: uint256, support: uint8, reason: String[STR_LEN]) log VoteCast(msg.sender, proposalId, support, self._vote(msg.sender, proposalId, support), reason) @external -def voteBySig(proposalId: uint256, support: uint8, v: uint8, r: bytes32, s: bytes32): +def castVoteBySig(proposalId: uint256, support: uint8, v: uint8, r: bytes32, s: bytes32): """ @notice Cast a vote for a proposal by signature @dev External function that accepts EIP-712 signatures for voting on proposals. @@ -423,7 +461,7 @@ def setVotingDelay(newVotingDelay: uint256): @notice Admin function for setting the voting delay @param newVotingDelay new voting delay, in blocks """ - assert msg.sender == self.queen, "!queen" + assert msg.sender == self.admin, "!admin" assert newVotingDelay >= MIN_VOTING_DELAY and newVotingDelay <= MAX_VOTING_DELAY, "!votingDelay" oldVotingDelay: uint256 = self.votingDelay self.votingDelay = newVotingDelay @@ -436,7 +474,7 @@ def setVotingPeriod(newVotingPeriod: uint256): @notice Admin function for setting the voting period @param newVotingPeriod new voting period, in blocks """ - assert msg.sender == self.queen, "!queen" + assert msg.sender == self.admin, "!admin" assert newVotingPeriod >= MIN_VOTING_PERIOD and newVotingPeriod <= MAX_VOTING_PERIOD, "!votingPeriod" oldVotingPeriod: uint256 = self.votingPeriod self.votingPeriod = newVotingPeriod @@ -449,7 +487,7 @@ def setProposalThreshold(newProposalThreshold: uint256): @notice Admin function for setting the proposal threshold @param newProposalThreshold must be in required range """ - assert msg.sender == self.queen, "!queen" + assert msg.sender == self.admin, "!admin" assert newProposalThreshold >= MIN_PROPOSAL_THRESHOLD and newProposalThreshold <= MAX_PROPOSAL_THRESHOLD, "!threshold" oldProposalThreshold: uint256 = self.proposalThreshold self.proposalThreshold = newProposalThreshold @@ -464,40 +502,40 @@ def setWhitelistAccountExpiration(account: address, expiration: uint256): @param expiration Expiration for account whitelist status as timestamp (if now < expiration, whitelisted) """ - assert msg.sender == self.queen or msg.sender == self.knight, "!access" + assert msg.sender == self.admin or msg.sender == self.knight, "!access" self.whitelistAccountExpirations[account] = expiration log WhitelistAccountExpirationSet(account, expiration) @external -def setPendingQueen(newPendingQueen: address): +def setPendingAdmin(newPendingAdmin: address): """ - @notice Begins transfer of crown and governor rights. The new queen must call `acceptThrone` - @dev Admin function to begin exchange of queen. The newPendingQueen must call `acceptThrone` to finalize the transfer. - @param newPendingQueen New pending queen. + @notice Begins transfer of crown and governor rights. The new admin must call `acceptThrone` + @dev Admin function to begin exchange of admin. The newPendingAdmin must call `acceptThrone` to finalize the transfer. + @param newPendingAdmin New pending admin. """ - assert msg.sender == self.queen, "!queen" - oldPendingQueen: address = self.pendingQueen - self.pendingQueen = newPendingQueen + assert msg.sender == self.admin, "!admin" + oldPendingAdmin: address = self.pendingAdmin + self.pendingAdmin = newPendingAdmin - log NewPendingQueen(oldPendingQueen, newPendingQueen) + log NewPendingAdmin(oldPendingAdmin, newPendingAdmin) @external -def acceptThrone(): +def acceptAdmin(): """ @notice Accepts transfer of crown and governor rights - @dev msg.sender must be pendingQueen + @dev msg.sender must be pendingAdmin """ - assert msg.sender == self.pendingQueen, "!pendingQueen" + assert msg.sender == self.pendingAdmin, "!pendingAdmin" # save values for events - oldQueen: address = self.queen + oldAdmin: address = self.admin # new ruler - self.queen = self.pendingQueen + self.admin = self.pendingAdmin # clean up - self.pendingQueen = empty(address) + self.pendingAdmin = empty(address) - log NewQueen(oldQueen, msg.sender) - log NewPendingQueen(msg.sender, empty(address)) + log NewAdmin(oldAdmin, msg.sender) + log NewPendingAdmin(msg.sender, empty(address)) @@ -507,7 +545,7 @@ def setKnight(newKnight: address): @notice Admin function for setting the knight for this contract @param newKnight Account configured to be the knight, set to 0x0 to remove knight """ - assert msg.sender == self.queen, "!queen" + assert msg.sender == self.admin, "!admin" oldKnight: address = self.knight self.knight = newKnight @@ -515,11 +553,11 @@ def setKnight(newKnight: address): @external @view -def state(proposalId: uint256) -> ProposalState: +def enumState(proposalId: uint256) -> ProposalState: """ @notice returns enum value of proposalId @dev when calling this method from ABI interfaces be aware enums in vyper have a different enumeration from solidity enums. - @dev also check `ordinalState()` method + @dev also check `state()` method @param proposalId Id of proposal """ return self._state(proposalId) @@ -527,11 +565,11 @@ def state(proposalId: uint256) -> ProposalState: @external @view -def ordinalState(proposalId: uint256) -> uint8: +def state(proposalId: uint256) -> uint8: """ @notice returns ordinal value of proposalId which is different from enum value - @dev function to support compatibility with solidity enums - @dev also check `state()` method + @dev function to support compatibility with solidity enums and gov contracts + @dev also check `enumState()` method @param proposalId Id of proposal """ proposalState: ProposalState = self._state(proposalId) @@ -559,13 +597,36 @@ def isWhitelisted(account: address) -> bool: @external @view -def getActions(proposalId: uint256) -> DynArray[ProposalAction, MAX_POSSIBLE_OPERATIONS]: +def getActions(proposalId: uint256) -> ( + DynArray[address, MAX_POSSIBLE_OPERATIONS], + DynArray[uint256, MAX_POSSIBLE_OPERATIONS], + DynArray[String[METHOD_SIG_SIZE], MAX_POSSIBLE_OPERATIONS], + DynArray[Bytes[CALL_DATA_LEN], MAX_POSSIBLE_OPERATIONS] +): """ @notice Gets actions of a proposal @param proposalId the id of the proposal @return Targets, values, signatures, and calldatas of the proposal actions """ - return self.proposals[proposalId].actions + + targets: DynArray[address, MAX_POSSIBLE_OPERATIONS] = [] + values: DynArray[uint256, MAX_POSSIBLE_OPERATIONS] = [] + signatures: DynArray[String[METHOD_SIG_SIZE], MAX_POSSIBLE_OPERATIONS] = [] + calldatas: DynArray[Bytes[CALL_DATA_LEN], MAX_POSSIBLE_OPERATIONS] = [] + + actions: DynArray[ProposalAction, MAX_POSSIBLE_OPERATIONS] = self._proposals[proposalId].actions + + numActions: uint256 = len(actions) + + for i in range(MAX_POSSIBLE_OPERATIONS): + if i >= numActions: + break + targets.append(actions[i].target) + values.append(actions[i].value) + signatures.append(actions[i].signature) + calldatas.append(actions[i].calldata) + + return targets, values, signatures, calldatas @external @view @@ -580,7 +641,7 @@ def getReceipt(proposalId: uint256, voter: address) -> Receipt: @external @view -def proposalMaxActions() -> uint256: +def proposalMaxOperations() -> uint256: return MAX_POSSIBLE_OPERATIONS @external @@ -602,6 +663,44 @@ def domainSeparator() -> bytes32: """ return self._domainSeparator() +@external +@view +def proposals(proposalId: uint256) -> Proposal: + """ + @notice Gets a Proposal By ID + @return Proposal Struct + """ + proposal: ProposalCore = self._proposals[proposalId] + + targets: DynArray[address, MAX_POSSIBLE_OPERATIONS] = [] + values: DynArray[uint256, MAX_POSSIBLE_OPERATIONS] = [] + signatures: DynArray[String[METHOD_SIG_SIZE], MAX_POSSIBLE_OPERATIONS] = [] + calldatas: DynArray[Bytes[CALL_DATA_LEN], MAX_POSSIBLE_OPERATIONS] = [] + + for action in proposal.actions: + targets.append(action.target) + values.append(action.value) + signatures.append(action.signature) + calldatas.append(action.calldata) + + return Proposal({ + id: proposal.id, + proposer: proposal.proposer, + eta: proposal.eta, + targets: targets, + values: values, + signatures: signatures, + calldatas: calldatas, + startBlock: proposal.startBlock, + endBlock: proposal.endBlock, + forVotes: proposal.forVotes, + againstVotes: proposal.againstVotes, + abstainVotes: proposal.abstainVotes, + canceled: proposal.canceled, + executed: proposal.executed + }) + + @external @view def name() -> String[20]: @@ -609,22 +708,9 @@ def name() -> String[20]: @internal def _queueOrRevertInternal(action: ProposalAction, eta: uint256): - trxHash: bytes32 = keccak256(_abi_encode(action.target, action.amount, action.signature, action.callData, eta)) + trxHash: bytes32 = keccak256(_abi_encode(action.target, action.value, action.signature, action.calldata, eta)) assert Timelock(timelock).queuedTransactions(trxHash) != True, "!duplicate_trx" - timelockTrx: Transaction = self._buildTrx(action, eta) - Timelock(timelock).queueTransaction(timelockTrx) - -@internal -def _buildTrx(action: ProposalAction, eta: uint256) -> Transaction: - timelockTrx: Transaction = Transaction({ - target: action.target, - amount: action.amount, - eta: eta, - signature: action.signature, - callData: action.callData, - }) - - return timelockTrx + Timelock(timelock).queueTransaction(action.target, action.value, action.signature, action.calldata, eta) @internal @view @@ -652,14 +738,14 @@ def _vote(voter: address, proposalId: uint256, support: uint8) -> uint256: assert support <= 2, "!vote_type" assert self._getHasVoted(proposalId, voter) == False, "!hasVoted" # @dev use min of current block and proposal startBlock instead ? - votes:uint256 = GovToken(token).getPriorVotes(voter, self.proposals[proposalId].startBlock) + votes:uint256 = GovToken(token).getPriorVotes(voter, self._proposals[proposalId].startBlock) if support == 0: - self.proposals[proposalId].againstVotes += votes + self._proposals[proposalId].againstVotes += votes elif support == 1: - self.proposals[proposalId].forVotes += votes + self._proposals[proposalId].forVotes += votes elif support == 2: - self.proposals[proposalId].abstainVotes += votes + self._proposals[proposalId].abstainVotes += votes self.receipts[proposalId][voter].hasVoted = True self.receipts[proposalId][voter].support = support @@ -672,19 +758,19 @@ def _vote(voter: address, proposalId: uint256, support: uint8) -> uint256: def _state(proposalId: uint256) -> ProposalState: assert self.proposalCount >= proposalId and proposalId > INITIAL_PROPOSAL_ID, "!proposalId" - if self.proposals[proposalId].canceled: + if self._proposals[proposalId].canceled: return ProposalState.CANCELED - elif block.number <= self.proposals[proposalId].startBlock: + elif block.number <= self._proposals[proposalId].startBlock: return ProposalState.PENDING - elif block.number <= self.proposals[proposalId].endBlock: + elif block.number <= self._proposals[proposalId].endBlock: return ProposalState.ACTIVE - elif self.proposals[proposalId].forVotes <= self.proposals[proposalId].againstVotes or self.proposals[proposalId].forVotes < QUORUM_VOTES: + elif self._proposals[proposalId].forVotes <= self._proposals[proposalId].againstVotes or self._proposals[proposalId].forVotes < QUORUM_VOTES: return ProposalState.DEFEATED - elif self.proposals[proposalId].eta == 0: + elif self._proposals[proposalId].eta == 0: return ProposalState.SUCCEEDED - elif self.proposals[proposalId].executed: + elif self._proposals[proposalId].executed: return ProposalState.EXECUTED - elif block.timestamp > self.proposals[proposalId].eta + Timelock(timelock).GRACE_PERIOD(): + elif block.timestamp > self._proposals[proposalId].eta + Timelock(timelock).GRACE_PERIOD(): return ProposalState.EXPIRED else: return ProposalState.QUEUED diff --git a/src/Timelock.vy b/src/Timelock.vy index 04a7cb5..ef6ad16 100644 --- a/src/Timelock.vy +++ b/src/Timelock.vy @@ -5,37 +5,15 @@ @license GNU AGPLv3 @author yearn.finance @notice - A timelock contract implementation in vyper. Designed to work with close integration + A timelock contract implementation in vyper. Designed to work with most governance voting contracts and close integration with SerpentorBravo, a governance contract for on-chain voting of proposals and execution. """ -MAX_DATA_LEN: constant(uint256) = 16608 -CALL_DATA_LEN: constant(uint256) = 16483 -METHOD_SIG_SIZE: constant(uint256) = 1024 -DAY: constant(uint256) = 86400 -GRACE_PERIOD: constant(uint256) = 14 * DAY -MINIMUM_DELAY: constant(uint256) = 2 * DAY -MAXIMUM_DELAY: constant(uint256) = 30 * DAY - -# @notice a single transaction to be executed by the timelock -struct Transaction: - # @notice the target address for calls to be made - target: address - # @notice The value (i.e. msg.value) to be passed to the calls to be made - amount: uint256 - # @notice The estimated time for execution of the trx - eta: uint256 - # @notice The function signature to be called - signature: String[METHOD_SIG_SIZE] - # @notice The calldata to be passed to the call - callData: Bytes[CALL_DATA_LEN] - - -event NewQueen: - newQueen: indexed(address) +event NewAdmin: + newAdmin: indexed(address) -event NewPendingQueen: - newPendingQueen: indexed(address) +event NewPendingAdmin: + newPendingAdmin: indexed(address) event NewDelay: newDelay: uint256 @@ -64,24 +42,31 @@ event QueueTransaction: data: Bytes[CALL_DATA_LEN] eta: uint256 -queen: public(address) -pendingQueen: public(address) +MAX_DATA_LEN: constant(uint256) = 16608 +CALL_DATA_LEN: constant(uint256) = 16483 +METHOD_SIG_SIZE: constant(uint256) = 1024 +DAY: constant(uint256) = 86400 +GRACE_PERIOD: constant(uint256) = 14 * DAY +MINIMUM_DELAY: constant(uint256) = 2 * DAY +MAXIMUM_DELAY: constant(uint256) = 30 * DAY + +admin: public(address) +pendingAdmin: public(address) delay: public(uint256) queuedTransactions: public(HashMap[bytes32, bool]) - @external -def __init__(queen: address, delay: uint256): +def __init__(admin: address, delay: uint256): """ @notice Deploys the timelock with initial values - @param queen The contract that rules over the timelock + @param admin The contract that rules over the timelock @param delay The delay for timelock """ assert delay >= MINIMUM_DELAY, "Delay must exceed minimum delay" assert delay <= MAXIMUM_DELAY, "Delay must not exceed maximum delay" - assert queen != empty(address), "!queen" - self.queen = queen + assert admin != empty(address), "!admin" + self.admin = admin self.delay = delay @external @@ -104,105 +89,123 @@ def setDelay(delay: uint256): log NewDelay(delay) @external -def acceptThrone(): +def acceptAdmin(): """ @notice - updates `pendingQueen` to queen. - msg.sender must be `pendingQueen` + updates `pendingAdmin` to admin. + msg.sender must be `pendingAdmin` """ - assert msg.sender == self.pendingQueen, "!pendingQueen" - self.queen = msg.sender - self.pendingQueen = empty(address) + assert msg.sender == self.pendingAdmin, "!pendingAdmin" + self.admin = msg.sender + self.pendingAdmin = empty(address) - log NewQueen(msg.sender) - log NewPendingQueen(empty(address)) + log NewAdmin(msg.sender) + log NewPendingAdmin(empty(address)) @external -def setPendingQueen(pendingQueen: address): +def setPendingAdmin(pendingAdmin: address): """ @notice - Updates `pendingQueen` value + Updates `pendingAdmin` value msg.sender must be this contract - @param pendingQueen The proposed new queen for the contract + @param pendingAdmin The proposed new admin for the contract """ assert msg.sender == self, "!Timelock" - self.pendingQueen = pendingQueen + self.pendingAdmin = pendingAdmin - log NewPendingQueen(pendingQueen) + log NewPendingAdmin(pendingAdmin) @external -def queueTransaction(trx: Transaction) -> bytes32: +def queueTransaction( + target: address, + amount: uint256, + signature: String[METHOD_SIG_SIZE], + data: Bytes[CALL_DATA_LEN], + eta: uint256 +) -> bytes32: """ @notice adds transaction to execution queue @param trx Transaction to queue """ - assert msg.sender == self.queen, "!queen" - assert trx.eta >= block.timestamp + self.delay, "!eta" + assert msg.sender == self.admin, "!admin" + assert eta >= block.timestamp + self.delay, "!eta" - trxHash: bytes32 = keccak256(_abi_encode(trx.target, trx.amount, trx.signature, trx.callData, trx.eta)) + trxHash: bytes32 = keccak256(_abi_encode(target, amount, signature, data, eta)) self.queuedTransactions[trxHash] = True - log QueueTransaction(trxHash, trx.target, trx.amount, trx.signature, trx.callData, trx.eta) + log QueueTransaction(trxHash, target, amount, signature, data, eta) return trxHash @external -def cancelTransaction(trx: Transaction): +def cancelTransaction( + target: address, + amount: uint256, + signature: String[METHOD_SIG_SIZE], + data: Bytes[CALL_DATA_LEN], + eta: uint256 +): """ @notice cancels a queued transaction @param trx Transaction to cancel """ - assert msg.sender == self.queen, "!queen" + assert msg.sender == self.admin, "!admin" - trxHash: bytes32 = keccak256(_abi_encode(trx.target, trx.amount, trx.signature, trx.callData, trx.eta)) + trxHash: bytes32 = keccak256(_abi_encode(target, amount, signature, data, eta)) self.queuedTransactions[trxHash] = False - log CancelTransaction(trxHash, trx.target, trx.amount, trx.signature, trx.callData, trx.eta) + log CancelTransaction(trxHash, target, amount, signature, data, eta) @payable @external -def executeTransaction(trx: Transaction) -> Bytes[MAX_DATA_LEN]: +def executeTransaction( + target: address, + amount: uint256, + signature: String[METHOD_SIG_SIZE], + data: Bytes[CALL_DATA_LEN], + eta: uint256 +) -> Bytes[MAX_DATA_LEN]: """ @notice executes a queued transaction @param trx Transaction to execute """ - assert msg.sender == self.queen, "!queen" + assert msg.sender == self.admin, "!admin" - trxHash: bytes32 = keccak256(_abi_encode(trx.target, trx.amount, trx.signature, trx.callData, trx.eta)) + trxHash: bytes32 = keccak256(_abi_encode(target, amount, signature, data, eta)) assert self.queuedTransactions[trxHash], "!queued_trx" - assert block.timestamp >= trx.eta, "!eta" - assert block.timestamp <= trx.eta + GRACE_PERIOD, "!staled_trx" + assert block.timestamp >= eta, "!eta" + assert block.timestamp <= eta + GRACE_PERIOD, "!staled_trx" self.queuedTransactions[trxHash] = False callData: Bytes[MAX_DATA_LEN] = b"" - if len(trx.signature) == 0: + if len(signature) == 0: # @dev use provided data directly - callData = trx.callData + callData = data else: # @dev use signature + data - sig_hash: bytes32 = keccak256(trx.signature) + sig_hash: bytes32 = keccak256(signature) func_sig: bytes4 = convert(slice(sig_hash, 0, 4), bytes4) - callData = concat(func_sig, trx.callData) + callData = concat(func_sig, data) success: bool = False response: Bytes[MAX_DATA_LEN] = b"" success, response = raw_call( - trx.target, + target, callData, max_outsize=MAX_DATA_LEN, - value=trx.amount, + value=amount, revert_on_failure=False ) assert success, "!trx_revert" - log ExecuteTransaction(trxHash, trx.target, trx.amount, trx.signature, trx.callData, trx.eta) + log ExecuteTransaction(trxHash, target, amount, signature, data, eta) return response From ea24d98617cb9b1fce5db10b3011999117f554c5 Mon Sep 17 00:00:00 2001 From: Storming0x <6074987+storming0x@users.noreply.github.com> Date: Fri, 3 Mar 2023 17:25:44 -0600 Subject: [PATCH 2/9] Feat/dual timelock phase2 (#27) * chore: initial working test and repo harness * feat: implement fast queue operations * feat: more tests for dualtimelock --------- Co-authored-by: storming0x <storm0x@storm0x.com> --- foundry_test/DualTimelock.t.sol | 994 +++++++++++++++++++++++ foundry_test/interfaces/DualTimelock.sol | 37 + src/DualTimelock.vy | 435 ++++++++++ 3 files changed, 1466 insertions(+) create mode 100644 foundry_test/DualTimelock.t.sol create mode 100644 foundry_test/interfaces/DualTimelock.sol create mode 100644 src/DualTimelock.vy diff --git a/foundry_test/DualTimelock.t.sol b/foundry_test/DualTimelock.t.sol new file mode 100644 index 0000000..fa8e7c1 --- /dev/null +++ b/foundry_test/DualTimelock.t.sol @@ -0,0 +1,994 @@ +// SPDX-License-Identifier: AGPL +pragma solidity ^0.8.16; + +import "@openzeppelin/token/ERC20/ERC20.sol"; + +import {ExtendedTest} from "./utils/ExtendedTest.sol"; +import {VyperDeployer} from "../lib/utils/VyperDeployer.sol"; + +import {console} from "forge-std/console.sol"; +import {DualTimelock, Transaction} from "./interfaces/DualTimelock.sol"; +import {GovToken} from "./utils/GovToken.sol"; + +contract DualTimelockTest is ExtendedTest { + VyperDeployer private vyperDeployer = new VyperDeployer(); + DualTimelock private timelock; + ERC20 private token; + address public admin = address(1); + address public holder = address(2); + address public grantee = address(3); + address public fastTrack = address(4); + + uint public constant GRACE_PERIOD = 14 days; + uint public constant MINIMUM_DELAY = 2 days; + uint public constant MAXIMUM_DELAY = 30 days; + uint256 public delay = 2 days; + uint256 public fastTrackDelay = 1 days; + + // events + + event NewDelay(uint256 newDelay); + event NewFastTrackDelay(uint256 newDelay); + event NewAdmin(address indexed newAdmin); + event NewPendingAdmin(address indexed newPendingAdmin); + event NewFastTrack(address indexed newFastTrack); + event NewPendingFastTrack(address indexed newPendingFastTrack); + event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + event QueueFastTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + event CancelFastTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + event ExecuteFastTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + + function setUp() public { + bytes memory args = abi.encode(admin, fastTrack, delay, fastTrackDelay); + timelock = DualTimelock(vyperDeployer.deployContract("src/", "DualTimelock", args)); + console.log("address for DualTimelock: ", address(timelock)); + + // deploy token + token = ERC20(new GovToken(18)); + console.log("address for GovToken: ", address(token)); + + // vm traces + vm.label(address(timelock), "DualTimelock"); + vm.label(address(token), "Token"); + + deal(address(token), address(timelock), 1000e18); + } + + function testSetup() public { + assertNeq(address(timelock), address(0)); + assertEq(address(timelock.admin()), admin); + assertEq(address(timelock.fastTrack()), fastTrack); + assertEq(timelock.delay(), delay); + assertEq(timelock.delay(), MINIMUM_DELAY); + assertEq(timelock.fastTrackDelay(), fastTrackDelay); + + assertEq(token.balanceOf(address(timelock)), 1000e18); + } + + function testRandomAcctCannotSetDelay(address random) public { + vm.assume(random != address(timelock)); + vm.expectRevert("!Timelock"); + + vm.prank(random); + timelock.setDelay(5 days); + } + + function testRandomAcctCannotSetFastTrackDelay(address random) public { + vm.assume(random != address(timelock)); + vm.expectRevert("!Timelock"); + + vm.prank(random); + timelock.setFastTrackDelay(0 days); + } + + function testOnlySelfCanSetDelay(uint256 newDelay) public { + vm.assume(newDelay >= MINIMUM_DELAY && newDelay <= MAXIMUM_DELAY); + //setup + //setup for event checks + vm.expectEmit(false, false, false, false); + emit NewDelay(newDelay); + // execute + vm.prank(address(timelock)); + timelock.setDelay(newDelay); + // asserts + assertEq(timelock.delay(), newDelay); + } + + function testOnlySelfCanSetFastTrackDelay(uint256 newDelay) public { + vm.assume(newDelay >= 0 && newDelay < MINIMUM_DELAY); + //setup + //setup for event checks + vm.expectEmit(false, false, false, false); + emit NewFastTrackDelay(newDelay); + // execute + vm.prank(address(timelock)); + timelock.setFastTrackDelay(newDelay); + // asserts + assertEq(timelock.fastTrackDelay(), newDelay); + } + + function testSetFastTrackDelayCannotBeGreaterThanDelay(uint256 newDelay) public { + uint currentDelay = timelock.delay(); + vm.assume(newDelay > currentDelay); + //setup + vm.expectRevert("!fastTrackDelay < delay"); + // execute + vm.prank(address(timelock)); + timelock.setFastTrackDelay(newDelay); + } + + + function testDelayCannotBeBelowMinimum(uint256 newDelay) public { + vm.assume(newDelay < MINIMUM_DELAY); + // setup + vm.expectRevert("!MINIMUM_DELAY"); + // execute + vm.prank(address(timelock)); + // delay minimum in contract is 2 days + timelock.setDelay(newDelay); + } + + function testDelayCannotBeAboveMax(uint256 newDelay) public { + vm.assume(newDelay > MAXIMUM_DELAY && newDelay <= 1000 days); + // setup + vm.expectRevert("!MAXIMUM_DELAY"); + // execute + vm.prank(address(timelock)); + // delay maximum in contract is 30 days + timelock.setDelay(newDelay); + } + + function testRandomAcctCannotSetNewAdmin(address random) public { + vm.assume(random != address(timelock)); + // setup + vm.expectRevert(bytes("!Timelock")); + // execute + vm.prank(random); + timelock.setPendingAdmin(random); + } + + function testRandomAcctCannotSetNewFastTrack(address random) public { + vm.assume(random != address(timelock)); + // setup + vm.expectRevert(bytes("!Timelock")); + // execute + vm.prank(random); + timelock.setPendingFastTrack(random); + } + + function testRandomAcctCannotTakeOverAdmin(address random) public { + vm.assume(random != admin && random != address(0)); + // setup + vm.expectRevert(bytes("!pendingAdmin")); + // execute + vm.prank(random); + timelock.acceptAdmin(); + } + + function testRandomAcctCannotTakeOverFastTrack(address random) public { + vm.assume(random != fastTrack && random != address(0)); + // setup + vm.expectRevert(bytes("!pendingFastTrack")); + // execute + vm.prank(random); + timelock.acceptFastTrack(); + } + + function testOnlyPendingAdminCanAcceptAdmin() public { + // setup + address futureAdmin = address(0xBEEF); + // setup pendingAdmin + vm.prank(address(timelock)); + timelock.setPendingAdmin(futureAdmin); + assertEq(timelock.pendingAdmin(), futureAdmin); + //setup for event checks + vm.expectEmit(true, false, false, false); + emit NewAdmin(futureAdmin); + + // execute + vm.prank(futureAdmin); + timelock.acceptAdmin(); + + // asserts + assertEq(timelock.admin(), futureAdmin); + assertEq(timelock.pendingAdmin(), address(0)); + } + + function testOnlyPendingFastTrackCanCallAcceptFastTrack() public { + // setup + address futureFastTrack = address(0xBEEF); + // setup pendingAdmin + vm.prank(address(timelock)); + timelock.setPendingFastTrack(futureFastTrack); + assertEq(timelock.pendingFastTrack(), futureFastTrack); + //setup for event checks + vm.expectEmit(true, false, false, false); + emit NewFastTrack(futureFastTrack); + + // execute + vm.prank(futureFastTrack); + timelock.acceptFastTrack(); + + // asserts + assertEq(timelock.fastTrack(), futureFastTrack); + assertEq(timelock.pendingFastTrack(), address(0)); + } + + function testRandomAcctCannotQueueTrx(address random) public { + vm.assume(random != admin); + // setup + vm.expectRevert(bytes("!admin")); + + // execute + vm.prank(random); + timelock.queueTransaction(address(timelock), 0, "", "", block.timestamp + 10 days); + } + + function testRandomAcctCannotQueueFastTrx(address random) public { + vm.assume(random != fastTrack); + // setup + vm.expectRevert(bytes("!fastTrack")); + + // execute + vm.prank(random); + timelock.queueFastTransaction(address(timelock), 0, "", "", block.timestamp + 10 days); + } + + function testQueueTrxEtaCannotBeInvalid() public { + // setup + vm.expectRevert(bytes("!eta")); + + uint256 badEta = block.timestamp; + + // execute + vm.prank(address(admin)); + timelock.queueTransaction(address(timelock), 0, "", "", badEta); + } + + function testQueueFastTrxEtaCannotBeInvalid() public { + // setup + vm.expectRevert(bytes("!eta")); + + uint256 badEta = block.timestamp; + + // execute + vm.prank(address(fastTrack)); + timelock.queueFastTransaction(address(grantee), 0, "", "", badEta); + } + + function testShouldQueueTrx() public { + // setup + uint256 newDelay = 5 days; + uint256 eta = block.timestamp + delay + 2 days; + address target = address(timelock); + bytes memory callData = abi.encodeWithSelector(DualTimelock.setDelay.selector, newDelay); + uint256 amount = 0; + string memory signature = ""; + bytes32 expectedTrxHash; + Transaction memory testTrx; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + //setup for event checks + vm.expectEmit(true, true, false, false); + emit QueueTransaction(expectedTrxHash, target, amount, signature, callData, eta); + + // execute + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); + // asserts + assertEq(expectedTrxHash, trxHash); + assertTrue(timelock.queuedTransactions(trxHash)); + } + + function testShouldQueueFastTrx() public { + // setup + uint256 eta = block.timestamp + 1 days; + address target = address(token); + bytes memory callData = abi.encodeWithSelector(ERC20.transfer.selector, grantee, 1000); + uint256 amount = 0; + string memory signature = ""; + bytes32 expectedTrxHash; + Transaction memory testTrx; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + //setup for event checks + vm.expectEmit(true, true, false, false); + emit QueueFastTransaction(expectedTrxHash, target, amount, signature, callData, eta); + + // execute + vm.prank(address(fastTrack)); + bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); + // asserts + assertEq(expectedTrxHash, trxHash); + assertTrue(timelock.queuedFastTransactions(trxHash)); + } + + function testFastTrackCannotTargetTimelock() public { + // setup + uint256 eta = block.timestamp + 1 days; + // cannot call timelock + address target = address(timelock); + bytes memory callData = abi.encodeWithSelector(DualTimelock.setDelay.selector, 5 days); + uint256 amount = 0; + string memory signature = ""; + bytes32 expectedTrxHash; + Transaction memory testTrx; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + //setup for expect revert + vm.expectRevert(bytes("!self")); + + // execute + vm.prank(address(fastTrack)); + timelock.queueFastTransaction(target, amount, signature, callData, eta); + } + + function testRandomAcctCannotCancelQueueTrx(address random) public { + vm.assume(random != admin); + // setup + vm.expectRevert(bytes("!admin")); + + // execute + vm.prank(address(0xABCD)); + timelock.cancelTransaction(address(timelock), 0, "", "", block.timestamp + 10 days); + } + + function testRandomAcctCannotCancelFastQueueTrx(address random) public { + vm.assume(random != fastTrack); + // setup + vm.expectRevert(bytes("!fastTrack")); + + // execute + vm.prank(address(0xABCD)); + timelock.cancelFastTransaction(address(token), 0, "", "", block.timestamp + 1 days); + } + + function testShouldCancelQueuedTrx() public { + // setup + uint256 newDelay = 5 days; + uint256 eta = block.timestamp + delay + 2 days; + address target = address(timelock); + bytes memory callData = abi.encodeWithSelector(DualTimelock.setDelay.selector, newDelay); + uint256 amount = 0; + string memory signature = ""; + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedTransactions(trxHash)); + + //setup for event checks + vm.expectEmit(true, true, false, false); + emit CancelTransaction(expectedTrxHash, target, amount, signature, callData, eta); + + // execute + vm.prank(address(admin)); + timelock.cancelTransaction(target, amount, signature, callData, eta); + + // asserts + assertFalse(timelock.queuedTransactions(trxHash)); + } + + function testShouldCancelFastQueuedTrx() public { + // setup + uint256 eta = block.timestamp + 1 days; + address target = address(token); + bytes memory callData = abi.encodeWithSelector(ERC20.transfer.selector, grantee, 1000); + uint256 amount = 0; + string memory signature = ""; + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + + vm.prank(address(fastTrack)); + bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedFastTransactions(trxHash)); + + //setup for event checks + vm.expectEmit(true, true, false, false); + emit CancelFastTransaction(expectedTrxHash, target, amount, signature, callData, eta); + + // execute + vm.prank(address(fastTrack)); + timelock.cancelFastTransaction(target, amount, signature, callData, eta); + + // asserts + assertFalse(timelock.queuedFastTransactions(trxHash)); + } + + function testRandomAcctCannotExecQueuedTrx(address random) public { + vm.assume(random != admin); + // setup + uint256 newDelay = 5 days; + uint256 eta = block.timestamp + delay + 2 days; + address target = address(timelock); + bytes memory callData = abi.encodeWithSelector(DualTimelock.setDelay.selector, newDelay); + uint256 amount = 0; + string memory signature = ""; + + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedTransactions(trxHash)); + + vm.expectRevert(bytes("!admin")); + // execute + vm.prank(random); + timelock.executeTransaction(target, amount, signature, callData, eta); + } + + function testRandomAcctCannotExecQueuedFastTrx(address random) public { + vm.assume(random != fastTrack); + // setup + uint256 eta = block.timestamp + 1 days; + address target = address(token); + bytes memory callData = abi.encodeWithSelector(ERC20.transfer.selector, grantee, 1000); + uint256 amount = 0; + string memory signature = ""; + + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(fastTrack)); + bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedFastTransactions(trxHash)); + + vm.expectRevert(bytes("!fastTrack")); + // execute + vm.prank(random); + timelock.executeFastTransaction(target, amount, signature, callData, eta); + } + + function testCannotExecNonExistingTrx() public { + // setup + // setup + uint256 newDelay = 5 days; + uint256 eta = block.timestamp + delay + 2 days; + address target = address(timelock); + bytes memory callData = abi.encodeWithSelector(DualTimelock.setDelay.selector, newDelay); + uint256 amount = 0; + string memory signature = ""; + + Transaction memory queuedTransaction; + bytes32 expectedTrxHash; + (queuedTransaction, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedTransactions(trxHash)); + + Transaction memory wrongTrx; + bytes32 wrongTrxHash; + (wrongTrx, wrongTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + "", + eta + ); + + vm.expectRevert(bytes("!queued_trx")); + // execute + vm.prank(address(admin)); + timelock.executeTransaction(target, amount, signature, "", eta); + } + + function testCannotExecNonExistingFastTrx() public { + // setup + uint256 eta = block.timestamp + 1 days; + address target = address(token); + bytes memory callData = abi.encodeWithSelector(ERC20.transfer.selector, grantee, 1000); + uint256 amount = 0; + string memory signature = ""; + + Transaction memory queuedTransaction; + bytes32 expectedTrxHash; + (queuedTransaction, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(fastTrack)); + bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedFastTransactions(trxHash)); + + Transaction memory wrongTrx; + bytes32 wrongTrxHash; + (wrongTrx, wrongTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + "", + eta + ); + + vm.expectRevert(bytes("!queued_trx")); + // execute + vm.prank(address(fastTrack)); + timelock.executeFastTransaction(target, amount, signature, "", eta); + } + + function testCannotExecQueuedTrxBeforeETA() public { + // setup + uint256 newDelay = 5 days; + uint256 eta = block.timestamp + delay + 2 days; + address target = address(timelock); + bytes memory callData = abi.encodeWithSelector(DualTimelock.setDelay.selector, newDelay); + uint256 amount = 0; + string memory signature = ""; + + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedTransactions(trxHash)); + + skip(2 days); // short of ETA + vm.expectRevert(bytes("!eta")); + // execute + vm.prank(address(admin)); + timelock.executeTransaction(target, amount, signature, callData, eta); + } + + function testCannotExecQueuedFastTrxBeforeETA() public { + // setup + uint256 eta = block.timestamp + 1 days; + address target = address(token); + bytes memory callData = abi.encodeWithSelector(ERC20.transfer.selector, grantee, 1000); + uint256 amount = 0; + string memory signature = ""; + + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(fastTrack)); + bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedFastTransactions(trxHash)); + + skip(12 hours); // short of ETA + vm.expectRevert(bytes("!eta")); + // execute + vm.prank(address(fastTrack)); + timelock.executeFastTransaction(target, amount, signature, callData, eta); + } + + function testCannotExecQueuedTrxAfterGracePeriod(uint256 executionTime) public { + uint256 eta = block.timestamp + delay + 2 days; + uint256 gracePeriod = timelock.GRACE_PERIOD(); + vm.assume(executionTime > eta + gracePeriod && executionTime < type(uint128).max); + // setup + uint256 newDelay = 5 days; + address target = address(timelock); + bytes memory callData = abi.encodeWithSelector(DualTimelock.setDelay.selector, newDelay); + uint256 amount = 0; + string memory signature = ""; + + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedTransactions(trxHash)); + skip(executionTime); // skip to time of execution passed gracePeriod + vm.expectRevert(bytes("!staled_trx")); + // execute + vm.prank(address(admin)); + timelock.executeTransaction(target, amount, signature, callData, eta); + } + + function testCannotExecQueuedFastTrxAfterGracePeriod(uint256 executionTime) public { + uint256 eta = block.timestamp + 1 days; + uint256 gracePeriod = timelock.GRACE_PERIOD(); + vm.assume(executionTime > eta + gracePeriod && executionTime < type(uint128).max); + // setup + address target = address(token); + bytes memory callData = abi.encodeWithSelector(ERC20.transfer.selector, grantee, 1000); + uint256 amount = 0; + string memory signature = ""; + + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(fastTrack)); + bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedFastTransactions(trxHash)); + skip(executionTime); // skip to time of execution passed gracePeriod + vm.expectRevert(bytes("!staled_trx")); + // execute + vm.prank(address(fastTrack)); + timelock.executeFastTransaction(target, amount, signature, callData, eta); + } + + + function testShouldExecQueuedTrxCorrectly() public { + // setup + uint256 newDelay = 5 days; + uint256 eta = block.timestamp + delay + 2 days; + address target = address(timelock); + bytes memory callData = abi.encodeWithSelector(DualTimelock.setDelay.selector, newDelay); + uint256 amount = 0; + string memory signature = ""; + + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedTransactions(trxHash)); + skip(eta + 1); // 1 pass eta + //setup for event checks + vm.expectEmit(true, true, false, false); + emit ExecuteTransaction(expectedTrxHash, target, amount, signature, callData, eta); + + // execute + vm.prank(address(admin)); + bytes memory response = timelock.executeTransaction(target, amount, signature, callData, eta); + + // asserts + assertEq(string(response), string("")); + assertEq(timelock.delay(), newDelay); + } + + function testShouldExecQueuedFastTrxCorrectly() public { + // setup + uint256 eta = block.timestamp + 1 days; + address target = address(token); + bytes memory callData = abi.encodeWithSelector(ERC20.transfer.selector, grantee, 1000); + uint256 amount = 0; + string memory signature = ""; + + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(fastTrack)); + bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedFastTransactions(trxHash)); + skip(eta + 1); // 1 pass eta + //setup for event checks + vm.expectEmit(true, true, false, false); + emit ExecuteFastTransaction(expectedTrxHash, target, amount, signature, callData, eta); + + // execute + vm.prank(address(fastTrack)); + timelock.executeFastTransaction(target, amount, signature, callData, eta); + + // asserts + assertEq(token.balanceOf(grantee), 1000); + } + + function testShouldExecQueuedTrxWithSignatureCorrectly() public { + // setup + uint256 newDelay = 5 days; + uint256 eta = block.timestamp + delay + 2 days; + address target = address(timelock); + bytes memory callData = abi.encode(newDelay); + uint256 amount = 0; + string memory signature = "setDelay(uint256)"; + + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedTransactions(trxHash)); + skip(eta + 1); // 1 pass eta + //setup for event checks + vm.expectEmit(true, true, false, false); + emit ExecuteTransaction(expectedTrxHash, target, amount, signature, callData, eta); + + // execute + vm.prank(address(admin)); + bytes memory response = timelock.executeTransaction(target, amount, signature, callData, eta); + + // asserts + assertEq(string(response), string("")); + assertEq(timelock.delay(), newDelay); + } + + function testShouldExecQueuedFastTrxWithSignatureCorrectly() public { + // setup + uint256 eta = block.timestamp + 1 days; + address target = address(token); + bytes memory callData = abi.encode(grantee, 1000); + uint256 amount = 0; + string memory signature = "transfer(address,uint256)"; + + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(fastTrack)); + bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedFastTransactions(trxHash)); + skip(eta + 1); // 1 pass eta + //setup for event checks + vm.expectEmit(true, true, false, false); + emit ExecuteFastTransaction(expectedTrxHash, target, amount, signature, callData, eta); + + // execute + vm.prank(address(fastTrack)); + timelock.executeFastTransaction(target, amount, signature, callData, eta); + + // asserts + assertEq(token.balanceOf(grantee), 1000); + } + + function testShouldExecQueuedTrxWithTimelockEthTransferCorrectly() public { + // setup + uint256 eta = block.timestamp + delay + 2 days; + address target = address(grantee); + bytes memory callData; + uint256 amount = 10 ether; + string memory signature = ""; + assertEq(grantee.balance, 0); + + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedTransactions(trxHash)); + skip(eta + 1); // 1 pass + deal(address(timelock), 11 ether); + + //setup for event checks + vm.expectEmit(true, true, false, false); + emit ExecuteTransaction(expectedTrxHash, target, amount, signature, callData, eta); + + // execute + hoax(address(admin), 1 ether); + timelock.executeTransaction(target, amount, signature, callData, eta); + + // asserts + assertEq(grantee.balance, amount); + assertEq(address(timelock).balance, 1 ether); + } + + function testShouldExecQueuedFastTrxWithTimelockEthTransferCorrectly() public { + // setup + uint256 eta = block.timestamp + 1 days; + address target = address(grantee); + bytes memory callData; + uint256 amount = 10 ether; + string memory signature = ""; + assertEq(grantee.balance, 0); + + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(fastTrack)); + bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedFastTransactions(trxHash)); + skip(eta + 1); // 1 pass + deal(address(timelock), 11 ether); + + //setup for event checks + vm.expectEmit(true, true, false, false); + emit ExecuteFastTransaction(expectedTrxHash, target, amount, signature, callData, eta); + + // execute + hoax(address(fastTrack), 1 ether); + timelock.executeFastTransaction(target, amount, signature, callData, eta); + + // asserts + assertEq(grantee.balance, amount); + assertEq(address(timelock).balance, 1 ether); + } + + function testShouldExecQueuedTrxWithCallerEthTransferCorrectly() public { + // setup + uint256 eta = block.timestamp + delay + 2 days; + address target = address(grantee); + bytes memory callData; + uint256 amount = 10 ether; + string memory signature = ""; + + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedTransactions(trxHash)); + skip(eta + 1); // 1 pass + assertEq(address(timelock).balance, 0); + + //setup for event checks + vm.expectEmit(true, true, false, false); + emit ExecuteTransaction(expectedTrxHash, target, amount, signature, callData, eta); + + // execute + hoax(address(admin), 10 ether); + timelock.executeTransaction{value: amount}(target, amount, signature, callData, eta); + + // asserts + assertEq(grantee.balance, amount); + assertEq(address(timelock).balance, 0); + } + + function testShouldExecQueuedFastTrxWithCallerEthTransferCorrectly() public { + // setup + uint256 eta = block.timestamp + 1 days; + address target = address(grantee); + bytes memory callData; + uint256 amount = 10 ether; + string memory signature = ""; + + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(fastTrack)); + bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedFastTransactions(trxHash)); + skip(eta + 1); // 1 pass + assertEq(address(timelock).balance, 0); + + //setup for event checks + vm.expectEmit(true, true, false, false); + emit ExecuteFastTransaction(expectedTrxHash, target, amount, signature, callData, eta); + + // execute + hoax(address(fastTrack), 10 ether); + timelock.executeFastTransaction{value: amount}(target, amount, signature, callData, eta); + + // asserts + assertEq(grantee.balance, amount); + assertEq(address(timelock).balance, 0); + } + + function testTimelockCanReceiveEther() public { + // setup eth balance + uint256 amount = 10 ether; + deal(address(this), 100 ether); + assertEq(address(timelock).balance, 0 ether); + + payable(address(timelock)).transfer(amount); + + assertEq(address(timelock).balance, amount); + } + + + function _getTransactionAndHash( + address target, + uint256 amount, + string memory signature, + bytes memory callData, + uint eta + ) internal pure returns (Transaction memory, bytes32) { + Transaction memory testTrx = Transaction({ + target: target, + amount: amount, + eta: eta, + signature: signature, + callData: callData + }); + + bytes32 trxHash = keccak256(abi.encode(target, amount, signature, callData, eta)); + + return (testTrx, trxHash); + } + +} diff --git a/foundry_test/interfaces/DualTimelock.sol b/foundry_test/interfaces/DualTimelock.sol new file mode 100644 index 0000000..317b670 --- /dev/null +++ b/foundry_test/interfaces/DualTimelock.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: AGPL +pragma solidity ^0.8.16; + +struct Transaction { + address target; + uint256 amount; + uint256 eta; + string signature; + bytes callData; +} + +interface DualTimelock { + // compatible interface with other Timelocks + function admin() external view returns (address); + function pendingAdmin() external view returns (address); + function delay() external view returns (uint); + function setDelay(uint256 newDelay) external; + function setPendingAdmin(address pendingadmin) external; + function GRACE_PERIOD() external view returns (uint); + function acceptAdmin() external; + function queuedTransactions(bytes32 hash) external view returns (bool); + function queueTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external returns (bytes32); + function cancelTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external; + function executeTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external payable returns (bytes memory); + + // DualTimelock specific functions + function fastTrack() external view returns (address); + function pendingFastTrack() external view returns (address); + function fastTrackDelay() external view returns (uint); + function setFastTrackDelay(uint256 newDelay) external; + function acceptFastTrack() external; + function setPendingFastTrack(address pendingFastTrack) external; + function queuedFastTransactions(bytes32 hash) external view returns (bool); + function queueFastTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external returns (bytes32); + function cancelFastTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external; + function executeFastTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external payable returns (bytes memory); +} \ No newline at end of file diff --git a/src/DualTimelock.vy b/src/DualTimelock.vy new file mode 100644 index 0000000..e023b0f --- /dev/null +++ b/src/DualTimelock.vy @@ -0,0 +1,435 @@ +# @version 0.3.7 + +""" +@title Yearn Dual Time lock implementation +@license GNU AGPLv3 +@author yearn.finance +@notice + A timelock contract implementation in vyper that manages two queues with different delay configurations. + The first queue is for governance actions compatible with other governor type systems, and the second queue is for faster operational actions. + The operational actions will be used for actions that are not critical to the protocol, but are still + time sensitive. The governance actions will be used for actions that are critical to the protocol, + and require a larger delay. + Designed to work with most governance voting contracts and close integration + with SerpentorBravo. + The second queue for operational actions is used for fast tracking actions that are generated by pre-approved contracts + with limited access and very specific functionality. +""" + +event NewAdmin: + newAdmin: indexed(address) + +event NewFastTrack: + newFastTrack: indexed(address) + +event NewPendingAdmin: + newPendingAdmin: indexed(address) + +event NewPendingFastTrack: + newPendingFastTrack: indexed(address) + +event NewDelay: + newDelay: uint256 + +event NewFastTrackDelay: + newFastTrackDelay: uint256 + +event CancelTransaction: + txHash: indexed(bytes32) + target: indexed(address) + value: uint256 + signature: String[METHOD_SIG_SIZE] + data: Bytes[CALL_DATA_LEN] + eta: uint256 + +event ExecuteTransaction: + txHash: indexed(bytes32) + target: indexed(address) + value: uint256 + signature: String[METHOD_SIG_SIZE] + data: Bytes[CALL_DATA_LEN] + eta: uint256 + +event QueueTransaction: + txHash: indexed(bytes32) + target: indexed(address) + value: uint256 + signature: String[METHOD_SIG_SIZE] + data: Bytes[CALL_DATA_LEN] + eta: uint256 + +event QueueFastTransaction: + txHash: indexed(bytes32) + target: indexed(address) + value: uint256 + signature: String[METHOD_SIG_SIZE] + data: Bytes[CALL_DATA_LEN] + eta: uint256 + +event CancelFastTransaction: + txHash: indexed(bytes32) + target: indexed(address) + value: uint256 + signature: String[METHOD_SIG_SIZE] + data: Bytes[CALL_DATA_LEN] + eta: uint256 + +event ExecuteFastTransaction: + txHash: indexed(bytes32) + target: indexed(address) + value: uint256 + signature: String[METHOD_SIG_SIZE] + data: Bytes[CALL_DATA_LEN] + eta: uint256 + +MAX_DATA_LEN: constant(uint256) = 16608 +CALL_DATA_LEN: constant(uint256) = 16483 +METHOD_SIG_SIZE: constant(uint256) = 1024 +DAY: constant(uint256) = 86400 +GRACE_PERIOD: constant(uint256) = 14 * DAY +MINIMUM_DELAY: constant(uint256) = 2 * DAY +MAXIMUM_DELAY: constant(uint256) = 30 * DAY + +admin: public(address) +pendingAdmin: public(address) +delay: public(uint256) +queuedTransactions: public(HashMap[bytes32, bool]) + +fastTrack: public(address) +pendingFastTrack: public(address) +fastTrackDelay: public(uint256) +queuedFastTransactions: public(HashMap[bytes32, bool]) + +@external +def __init__(admin: address, fastTrack: address, delay: uint256, fastTrackDelay: uint256): + """ + @notice Deploys the timelock with initial values + @param admin The contract that rules over the timelock + @param fastTrack The contract that rules over the fast track queued transactions. Can be 0x0. + @param delay The delay for timelock + @param fastTrackDelay The delay for fast track timelock + """ + + assert delay >= MINIMUM_DELAY, "Delay must exceed minimum delay" + assert delay <= MAXIMUM_DELAY, "Delay must not exceed maximum delay" + assert delay > fastTrackDelay, "Delay must be greater than fast track delay" + assert admin != empty(address), "!admin" + self.admin = admin + self.fastTrack = fastTrack + self.delay = delay + self.fastTrackDelay = fastTrackDelay + + +@external +@payable +def __default__(): + pass + +@external +def setDelay(delay: uint256): + """ + @notice + Updates delay to new value + @param delay The delay for timelock + """ + assert msg.sender == self, "!Timelock" + assert delay >= MINIMUM_DELAY, "!MINIMUM_DELAY" + assert delay <= MAXIMUM_DELAY, "!MAXIMUM_DELAY" + self.delay = delay + + log NewDelay(delay) + +@external +def setFastTrackDelay(fastTrackDelay: uint256): + """ + @notice + Updates fast track delay to new value + @param fastTrackDelay The delay for fast track timelock + """ + assert msg.sender == self, "!Timelock" + assert fastTrackDelay < self.delay, "!fastTrackDelay < delay" + self.fastTrackDelay = fastTrackDelay + + log NewFastTrackDelay(fastTrackDelay) + +@external +def acceptAdmin(): + """ + @notice + updates `pendingAdmin` to admin. + msg.sender must be `pendingAdmin` + """ + assert msg.sender == self.pendingAdmin, "!pendingAdmin" + self.admin = msg.sender + self.pendingAdmin = empty(address) + + log NewAdmin(msg.sender) + log NewPendingAdmin(empty(address)) + +@external +def setPendingAdmin(pendingAdmin: address): + """ + @notice + Updates `pendingAdmin` value + msg.sender must be this contract + @param pendingAdmin The proposed new admin for the contract + """ + assert msg.sender == self, "!Timelock" + self.pendingAdmin = pendingAdmin + + log NewPendingAdmin(pendingAdmin) + +@external +def acceptFastTrack(): + """ + @notice + updates `pendingFastTrack` to fastTrack. + msg.sender must be `pendingFastTrack` + """ + assert msg.sender == self.pendingFastTrack, "!pendingFastTrack" + self.fastTrack = msg.sender + self.pendingFastTrack = empty(address) + log NewFastTrack(msg.sender) + log NewPendingFastTrack(empty(address)) + + +@external +def setPendingFastTrack(pendingFastTrack: address): + """ + @notice + Updates `pendingFastTrack` value + msg.sender must be this contract + @param pendingFastTrack The proposed new fast track contract for the contract + """ + assert msg.sender == self, "!Timelock" + self.pendingFastTrack = pendingFastTrack + + log NewPendingFastTrack(pendingFastTrack) + +@external +def queueTransaction( + target: address, + amount: uint256, + signature: String[METHOD_SIG_SIZE], + data: Bytes[CALL_DATA_LEN], + eta: uint256 +) -> bytes32: + """ + @notice + adds transaction to execution queue + @param target The address of the contract to execute + @param amount The amount of ether to send to the contract + @param signature The signature of the function to execute + @param data The data to send to the contract + @param eta The timestamp when the transaction can be executed + + @return txHash The hash of the transaction + """ + assert msg.sender == self.admin, "!admin" + assert eta >= block.timestamp + self.delay, "!eta" + + trxHash: bytes32 = keccak256(_abi_encode(target, amount, signature, data, eta)) + self.queuedTransactions[trxHash] = True + + log QueueTransaction(trxHash, target, amount, signature, data, eta) + + return trxHash + +@external +def cancelTransaction( + target: address, + amount: uint256, + signature: String[METHOD_SIG_SIZE], + data: Bytes[CALL_DATA_LEN], + eta: uint256 +): + """ + @notice + cancels a queued transaction + @param target The address of the contract to execute + @param amount The amount of ether to send to the contract + @param signature The signature of the function to execute + @param data The data to send to the contract + @param eta The timestamp when the transaction can be executed + """ + assert msg.sender == self.admin, "!admin" + + trxHash: bytes32 = keccak256(_abi_encode(target, amount, signature, data, eta)) + self.queuedTransactions[trxHash] = False + + log CancelTransaction(trxHash, target, amount, signature, data, eta) + +@payable +@external +def executeTransaction( + target: address, + amount: uint256, + signature: String[METHOD_SIG_SIZE], + data: Bytes[CALL_DATA_LEN], + eta: uint256 +) -> Bytes[MAX_DATA_LEN]: + """ + @notice + executes a queued transaction + @param target The address of the contract to execute + @param amount The amount of ether to send to the contract + @param signature The signature of the function to execute + @param data The data to send to the contract + @param eta The timestamp when the transaction can be executed + + @return response The response from the transaction + """ + assert msg.sender == self.admin, "!admin" + + trxHash: bytes32 = keccak256(_abi_encode(target, amount, signature, data, eta)) + assert self.queuedTransactions[trxHash], "!queued_trx" + assert block.timestamp >= eta, "!eta" + assert block.timestamp <= eta + GRACE_PERIOD, "!staled_trx" + + self.queuedTransactions[trxHash] = False + + callData: Bytes[MAX_DATA_LEN] = b"" + + if len(signature) == 0: + # @dev use provided data directly + callData = data + else: + # @dev use signature + data + sig_hash: bytes32 = keccak256(signature) + func_sig: bytes4 = convert(slice(sig_hash, 0, 4), bytes4) + callData = concat(func_sig, data) + + success: bool = False + response: Bytes[MAX_DATA_LEN] = b"" + + success, response = raw_call( + target, + callData, + max_outsize=MAX_DATA_LEN, + value=amount, + revert_on_failure=False + ) + + assert success, "!trx_revert" + + log ExecuteTransaction(trxHash, target, amount, signature, data, eta) + + return response + +@external +def queueFastTransaction( + target: address, + amount: uint256, + signature: String[METHOD_SIG_SIZE], + data: Bytes[CALL_DATA_LEN], + eta: uint256 +) -> bytes32: + """ + @notice + adds transaction to fast execution queue + fast execution queue cannot target this timelock contract + @param target The address of the contract to execute + @param amount The amount of ether to send to the contract + @param signature The signature of the function to execute + @param data The data to send to the contract + @param eta The timestamp when the transaction can be executed + + @return txHash The hash of the transaction + """ + assert msg.sender == self.fastTrack, "!fastTrack" + assert target != self, "!self" + assert eta >= block.timestamp + self.fastTrackDelay, "!eta" + + trxHash: bytes32 = keccak256(_abi_encode(target, amount, signature, data, eta)) + self.queuedFastTransactions[trxHash] = True + + log QueueFastTransaction(trxHash, target, amount, signature, data, eta) + + return trxHash + +@external +def cancelFastTransaction( + target: address, + amount: uint256, + signature: String[METHOD_SIG_SIZE], + data: Bytes[CALL_DATA_LEN], + eta: uint256 +): + """ + @notice + cancels a queued fast transaction + @param target The address of the contract to execute + @param amount The amount of ether to send to the contract + @param signature The signature of the function to execute + @param data The data to send to the contract + @param eta The timestamp when the transaction can be executed + """ + assert msg.sender == self.fastTrack, "!fastTrack" + + trxHash: bytes32 = keccak256(_abi_encode(target, amount, signature, data, eta)) + self.queuedFastTransactions[trxHash] = False + + log CancelFastTransaction(trxHash, target, amount, signature, data, eta) + +@payable +@external +def executeFastTransaction( + target: address, + amount: uint256, + signature: String[METHOD_SIG_SIZE], + data: Bytes[CALL_DATA_LEN], + eta: uint256 +) -> Bytes[MAX_DATA_LEN]: + """ + @notice + executes a queued fast transaction + @param target The address of the contract to execute + @param amount The amount of ether to send to the contract + @param signature The signature of the function to execute + @param data The data to send to the contract + @param eta The timestamp when the transaction can be executed + + @return response The response from the transaction + """ + assert msg.sender == self.fastTrack, "!fastTrack" + + trxHash: bytes32 = keccak256(_abi_encode(target, amount, signature, data, eta)) + assert self.queuedFastTransactions[trxHash], "!queued_trx" + assert block.timestamp >= eta, "!eta" + assert block.timestamp <= eta + GRACE_PERIOD, "!staled_trx" + + self.queuedFastTransactions[trxHash] = False + + callData: Bytes[MAX_DATA_LEN] = b"" + + if len(signature) == 0: + # @dev use provided data directly + callData = data + else: + # @dev use signature + data + sig_hash: bytes32 = keccak256(signature) + func_sig: bytes4 = convert(slice(sig_hash, 0, 4), bytes4) + callData = concat(func_sig, data) + + success: bool = False + response: Bytes[MAX_DATA_LEN] = b"" + + success, response = raw_call( + target, + callData, + max_outsize=MAX_DATA_LEN, + value=amount, + revert_on_failure=False + ) + + assert success, "!trx_revert" + + log ExecuteFastTransaction(trxHash, target, amount, signature, data, eta) + + return response + + +@external +@view +def GRACE_PERIOD() -> uint256: + return GRACE_PERIOD \ No newline at end of file From e94ec0fbe10200cb84e595d8e63e01f85a1f5dcd Mon Sep 17 00:00:00 2001 From: Storming0x <6074987+storming0x@users.noreply.github.com> Date: Tue, 7 Mar 2023 11:34:44 -0600 Subject: [PATCH 3/9] feat: more tests for dualtimelock (#29) Co-authored-by: storming0x <storm0x@storm0x.com> --- foundry_test/DualTimelock.t.sol | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/foundry_test/DualTimelock.t.sol b/foundry_test/DualTimelock.t.sol index fa8e7c1..74dfd98 100644 --- a/foundry_test/DualTimelock.t.sol +++ b/foundry_test/DualTimelock.t.sol @@ -676,6 +676,34 @@ contract DualTimelockTest is ExtendedTest { timelock.executeFastTransaction(target, amount, signature, callData, eta); } + function testCannotExecFastTrxInIncorrectQueue() public { + // setup + address target = address(token); + bytes memory callData = abi.encodeWithSelector(ERC20.transfer.selector, grantee, 1000); + uint256 amount = 0; + string memory signature = ""; + uint256 eta = block.timestamp + delay; + + Transaction memory testTrx; + bytes32 expectedTrxHash; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + vm.prank(address(admin)); + bytes32 trxHash = timelock.queueTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedTransactions(trxHash)); + skip(eta + 1); // 1 pass eta + + vm.expectRevert(bytes("!queued_trx")); + // execute + vm.prank(address(fastTrack)); + timelock.executeFastTransaction(target, amount, signature, callData, eta); + } + function testShouldExecQueuedTrxCorrectly() public { // setup From 6fc6905d7c81264d12a94408b1c7ba498c6756e4 Mon Sep 17 00:00:00 2001 From: Storming0x <6074987+storming0x@users.noreply.github.com> Date: Fri, 24 Mar 2023 14:26:18 -0600 Subject: [PATCH 4/9] Feat/lean track (#30) * feat: initial fast track commits * feat: initial createMotion logic * fix: add target checks to timelock * feat: motion create * feat: queue motion * feat: refactor names * feat: queue and enact motions * feat: object to motions * feat: cancel motions * feat: add gas snapshot --------- Co-authored-by: storming0x <storm0x@storm0x.com> --- .gas-snapshot | 188 ++- foundry_test/DualTimelock.t.sol | 263 +++-- foundry_test/LeanTrack.t.sol | 1347 ++++++++++++++++++++++ foundry_test/interfaces/DualTimelock.sol | 20 +- foundry_test/interfaces/LeanTrack.sol | 55 + foundry_test/utils/GovToken.sol | 5 + src/DualTimelock.vy | 118 +- src/LeanTrack.vy | 538 +++++++++ 8 files changed, 2317 insertions(+), 217 deletions(-) create mode 100644 foundry_test/LeanTrack.t.sol create mode 100644 foundry_test/interfaces/LeanTrack.sol create mode 100644 src/LeanTrack.vy diff --git a/.gas-snapshot b/.gas-snapshot index b5de489..3f9f678 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,47 +1,149 @@ -SerpentorBravoTest:testCanSubmitProposal(uint256) (runs: 256, μ: 1051527, ~: 1051527) -SerpentorBravoTest:testCannotCancelProposalIfProposerIsAboveThreshold(uint256,address,address) (runs: 256, μ: 1043102, ~: 1043102) -SerpentorBravoTest:testCannotCancelWhitelistedProposerBelowThreshold(uint256,uint256,address) (runs: 256, μ: 1073333, ~: 1075121) -SerpentorBravoTest:testCannotExecuteProposalIfNotQueued() (gas: 1037342) -SerpentorBravoTest:testCannotProposeBelowThreshold(uint256) (runs: 256, μ: 225095, ~: 225096) -SerpentorBravoTest:testCannotProposeIfLastProposalIsActive(uint256) (runs: 256, μ: 1117095, ~: 1117095) -SerpentorBravoTest:testCannotProposeIfLastProposalIsPending(uint256) (runs: 256, μ: 1114800, ~: 1114800) -SerpentorBravoTest:testCannotProposeTooManyActions(uint256,uint8) (runs: 256, μ: 179808, ~: 179796) -SerpentorBravoTest:testCannotProposeZeroOperations(uint256) (runs: 256, μ: 223295, ~: 223296) -SerpentorBravoTest:testCannotQueueProposalIfNotSucceeded() (gas: 1043162) -SerpentorBravoTest:testCannotSetProposalThresholdOutsideRange(uint32) (runs: 256, μ: 13952, ~: 13945) -SerpentorBravoTest:testCannotSetVotingDelayOutsideRange(address,uint32) (runs: 256, μ: 16091, ~: 16094) -SerpentorBravoTest:testCannotSetVotingPeriodOutsideRange(address,uint32) (runs: 256, μ: 16612, ~: 16618) +DualTimelockTest:testCannotExecFastTrxInIncorrectQueue() (gas: 68064) +DualTimelockTest:testCannotExecNonExistingFastTrx() (gas: 64681) +DualTimelockTest:testCannotExecNonExistingTrx() (gas: 59857) +DualTimelockTest:testCannotExecQueuedFastTrxAfterGracePeriod(uint256) (runs: 256, μ: 63859, ~: 63859) +DualTimelockTest:testCannotExecQueuedFastTrxBeforeETA() (gas: 62291) +DualTimelockTest:testCannotExecQueuedTrxAfterGracePeriod(uint256) (runs: 256, μ: 58950, ~: 58950) +DualTimelockTest:testCannotExecQueuedTrxBeforeETA() (gas: 57338) +DualTimelockTest:testDelayCannotBeAboveMax(uint256) (runs: 256, μ: 9529, ~: 9529) +DualTimelockTest:testDelayCannotBeBelowMinimum(uint256) (runs: 256, μ: 9429, ~: 9429) +DualTimelockTest:testLeanTrackCannotTargetLeanTrack() (gas: 18578) +DualTimelockTest:testLeanTrackCannotTargetTimelock() (gas: 18615) +DualTimelockTest:testLeanTrackCannotTargetTimelockAdmin() (gas: 22706) +DualTimelockTest:testOnlyPendingAdminCanAcceptAdmin() (gas: 32147) +DualTimelockTest:testOnlyPendingLeanTrackCanCallAcceptLeanTrack() (gas: 32440) +DualTimelockTest:testOnlySelfCanSetDelay(uint256) (runs: 256, μ: 17542, ~: 17608) +DualTimelockTest:testOnlySelfCanSetLeanTrackDelay(uint256) (runs: 256, μ: 19289, ~: 19732) +DualTimelockTest:testQueueFastTrxEtaCannotBeInvalid() (gas: 22621) +DualTimelockTest:testQueueTrxEtaCannotBeInvalid() (gas: 18293) +DualTimelockTest:testRandomAcctCannotCancelFastQueueTrx(address) (runs: 256, μ: 18927, ~: 18927) +DualTimelockTest:testRandomAcctCannotCancelQueueTrx(address) (runs: 256, μ: 16793, ~: 16793) +DualTimelockTest:testRandomAcctCannotExecQueuedFastTrx(address) (runs: 256, μ: 61390, ~: 61390) +DualTimelockTest:testRandomAcctCannotExecQueuedTrx(address) (runs: 256, μ: 56422, ~: 56422) +DualTimelockTest:testRandomAcctCannotQueueFastTrx(address) (runs: 256, μ: 16791, ~: 16791) +DualTimelockTest:testRandomAcctCannotQueueTrx(address) (runs: 256, μ: 16727, ~: 16727) +DualTimelockTest:testRandomAcctCannotSetDelay(address) (runs: 256, μ: 9495, ~: 9495) +DualTimelockTest:testRandomAcctCannotSetLeanTrackDelay(address) (runs: 256, μ: 9517, ~: 9517) +DualTimelockTest:testRandomAcctCannotSetNewAdmin(address) (runs: 256, μ: 9968, ~: 9968) +DualTimelockTest:testRandomAcctCannotSetNewLeanTrack(address) (runs: 256, μ: 10004, ~: 10004) +DualTimelockTest:testRandomAcctCannotTakeOverAdmin(address) (runs: 256, μ: 14065, ~: 14065) +DualTimelockTest:testRandomAcctCannotTakeOverLeanTrack(address) (runs: 256, μ: 14089, ~: 14089) +DualTimelockTest:testSetLeanTrackDelayCannotBeGreaterThanDelay(uint256) (runs: 256, μ: 12570, ~: 12570) +DualTimelockTest:testSetup() (gas: 34936) +DualTimelockTest:testShouldCancelFastQueuedTrx() (gas: 52856) +DualTimelockTest:testShouldCancelQueuedTrx() (gas: 48428) +DualTimelockTest:testShouldExecQueuedFastTrxCorrectly() (gas: 100846) +DualTimelockTest:testShouldExecQueuedFastTrxWithCallerEthTransferCorrectly() (gas: 99607) +DualTimelockTest:testShouldExecQueuedFastTrxWithSignatureCorrectly() (gas: 101158) +DualTimelockTest:testShouldExecQueuedFastTrxWithTimelockEthTransferCorrectly() (gas: 93329) +DualTimelockTest:testShouldExecQueuedTrxCorrectly() (gas: 65148) +DualTimelockTest:testShouldExecQueuedTrxWithCallerEthTransferCorrectly() (gas: 99450) +DualTimelockTest:testShouldExecQueuedTrxWithSignatureCorrectly() (gas: 65535) +DualTimelockTest:testShouldExecQueuedTrxWithTimelockEthTransferCorrectly() (gas: 93150) +DualTimelockTest:testShouldQueueFastTrx() (gas: 60076) +DualTimelockTest:testShouldQueueTrx() (gas: 54969) +DualTimelockTest:testTimelockCanReceiveEther() (gas: 15290) +LeanTrackTest:testCanObjectToMotionReturnsFalseIfMotionDoesntExist(address,uint256) (runs: 256, μ: 767396, ~: 767396) +LeanTrackTest:testCanObjectToMotionReturnsFalseIfMotionIsQueued(address,uint256) (runs: 256, μ: 1042922, ~: 1042922) +LeanTrackTest:testCanObjectToMotionReturnsFalseIfMotionTimeForQueueHasPassed(address,uint256) (runs: 256, μ: 859422, ~: 859422) +LeanTrackTest:testCanObjectToMotionReturnsFalseIfObjectorAlreadyObjected(address,uint256) (runs: 256, μ: 1008125, ~: 1008125) +LeanTrackTest:testCanObjectToMotionReturnsFalseIfVotingBalanceIsZero(address) (runs: 256, μ: 766932, ~: 766932) +LeanTrackTest:testCanObjectToMotionReturnsTrue(address,uint256) (runs: 256, μ: 867912, ~: 867918) +LeanTrackTest:testCannotAddExecutorTwice(address) (runs: 256, μ: 43814, ~: 43814) +LeanTrackTest:testCannotAddFactoryTwice() (gas: 18434) +LeanTrackTest:testCannotCancelUnexistingMotion(address) (runs: 256, μ: 777012, ~: 777012) +LeanTrackTest:testCannotCreateMotionWhenPaused() (gas: 118656) +LeanTrackTest:testCannotCreateMotionWithDifferentLenArrays() (gas: 98201) +LeanTrackTest:testCannotCreateMotionWithTooManyOperations() (gas: 40127) +LeanTrackTest:testCannotCreateMotionWithZeroOps() (gas: 91382) +LeanTrackTest:testCannotEnactMotionBeforeEta(uint256,address) (runs: 256, μ: 1128649, ~: 1107545) +LeanTrackTest:testCannotEnactMotionThatIsntQueued(uint256,address) (runs: 256, μ: 903444, ~: 886929) +LeanTrackTest:testCannotEnactMotionWhenPaused(uint256,address) (runs: 256, μ: 1072533, ~: 1051603) +LeanTrackTest:testCannotEnactUnexistingMotion(uint256,address,uint256) (runs: 256, μ: 1152563, ~: 1129095) +LeanTrackTest:testCannotObjectToMotionAfterTimeForQueuePasses(uint256,address) (runs: 256, μ: 901614, ~: 885099) +LeanTrackTest:testCannotObjectToMotionThatDoesntExist(uint256,address,uint256) (runs: 256, μ: 1150341, ~: 1126873) +LeanTrackTest:testCannotObjectToMotionWithZeroVotingBalance(address) (runs: 256, μ: 767163, ~: 767163) +LeanTrackTest:testCannotObjectToQueuedMotion(uint256,address) (runs: 256, μ: 1126257, ~: 1105153) +LeanTrackTest:testCannotObjectTwiceToSameMotion(address,uint256) (runs: 256, μ: 1009448, ~: 1009448) +LeanTrackTest:testCannotQueueMotionBeforeEta(uint256,address) (runs: 256, μ: 887151, ~: 871037) +LeanTrackTest:testCannotQueueMotionTwice(uint256,address) (runs: 256, μ: 1123882, ~: 1102777) +LeanTrackTest:testCannotQueueMotionWhenPaused(uint256,address) (runs: 256, μ: 833205, ~: 817265) +LeanTrackTest:testCannotQueueUnexistingMotion(uint256,address,uint256) (runs: 256, μ: 910561, ~: 892689) +LeanTrackTest:testCannotRemoveExecutorThatDoesNotExist(address) (runs: 256, μ: 19039, ~: 19039) +LeanTrackTest:testCannotRemoveFactoryThatDoesNotExist(address) (runs: 256, μ: 19020, ~: 19020) +LeanTrackTest:testKnightCanCancelMotionAfterBeingQueued() (gas: 779012) +LeanTrackTest:testKnightCanCancelMotionBeforeQueued(address) (runs: 256, μ: 609795, ~: 609779) +LeanTrackTest:testKnightCannotBeAddressZero() (gas: 14145) +LeanTrackTest:testMotionFactoryDurationCannotBeLessThanMinimum(address,uint256,uint32) (runs: 256, μ: 19933, ~: 19933) +LeanTrackTest:testMotionFactoryObjectionsThresholdCannotBeGreaterThanMaximum(address,uint256) (runs: 256, μ: 19527, ~: 19527) +LeanTrackTest:testMotionFactoryObjectionsThresholdCannotBeLessThanMinimum(address,uint256) (runs: 256, μ: 19433, ~: 19433) +LeanTrackTest:testOnlyAdminCanAddExecutor(address) (runs: 256, μ: 14644, ~: 14644) +LeanTrackTest:testOnlyAdminCanRemoveExecutor(address) (runs: 256, μ: 14678, ~: 14678) +LeanTrackTest:testOnlyAdminCanRemoveFactory(address,uint256,uint32) (runs: 256, μ: 97597, ~: 97597) +LeanTrackTest:testOnlyAdminCanSetKnight(address) (runs: 256, μ: 14745, ~: 14745) +LeanTrackTest:testOnlyApprovedFactoryCanCreateMotion(address) (runs: 256, μ: 103482, ~: 103482) +LeanTrackTest:testOnlyExecutorsCanCallEnactMotion(uint256,address) (runs: 256, μ: 1045259, ~: 1024329) +LeanTrackTest:testOnlyKnightCanPauseLeanTrack(address) (runs: 256, μ: 14785, ~: 14785) +LeanTrackTest:testProposerCanCancelMotionAfterBeingQueued() (gas: 777359) +LeanTrackTest:testProposerCanCancelMotionBeforeQueued(address) (runs: 256, μ: 608195, ~: 608179) +LeanTrackTest:testRandomAcctCannotAddMotionFactory(address) (runs: 256, μ: 14653, ~: 14653) +LeanTrackTest:testRandomAcctCannotCanCancelMotion(address) (runs: 256, μ: 756081, ~: 756081) +LeanTrackTest:testSetup() (gas: 50098) +LeanTrackTest:testShouldAddExecutor(address) (runs: 256, μ: 42414, ~: 42414) +LeanTrackTest:testShouldAddMotionFactory(address,uint256,uint32) (runs: 256, μ: 94935, ~: 94935) +LeanTrackTest:testShouldCreateMotion(uint8) (runs: 256, μ: 978212, ~: 858082) +LeanTrackTest:testShouldEnactQueuedMotion(uint256,address) (runs: 256, μ: 1003708, ~: 978478) +LeanTrackTest:testShouldObjectToMotionWithVotingPowerLessThanThreshold(address,uint256) (runs: 256, μ: 927211, ~: 927211) +LeanTrackTest:testShouldPauseLeanTrack() (gas: 39492) +LeanTrackTest:testShouldQueueMotion(uint256,address) (runs: 256, μ: 1075072, ~: 1053051) +LeanTrackTest:testShouldRejectMotionIfObjectionsReachedAboveThreshold(address,uint256) (runs: 256, μ: 769906, ~: 769919) +LeanTrackTest:testShouldRemoveExecutor(address) (runs: 256, μ: 34840, ~: 34824) +LeanTrackTest:testShouldRemoveFactory(address,uint256,uint32) (runs: 256, μ: 77252, ~: 77259) +LeanTrackTest:testShouldUseLowerVotingBalanceForObjection(address,uint256) (runs: 256, μ: 873400, ~: 873400) +SerpentorBravoTest:testCanSubmitProposal(uint256) (runs: 256, μ: 1329049, ~: 1329049) +SerpentorBravoTest:testCannotCancelProposalIfProposerIsAboveThreshold(uint256,address,address) (runs: 256, μ: 1043325, ~: 1043325) +SerpentorBravoTest:testCannotCancelWhitelistedProposerBelowThreshold(uint256,uint256,address) (runs: 256, μ: 1073721, ~: 1075432) +SerpentorBravoTest:testCannotExecuteProposalIfNotQueued() (gas: 1037587) +SerpentorBravoTest:testCannotProposeBelowThreshold(uint256) (runs: 256, μ: 225262, ~: 225263) +SerpentorBravoTest:testCannotProposeIfLastProposalIsActive(uint256) (runs: 256, μ: 1117319, ~: 1117319) +SerpentorBravoTest:testCannotProposeIfLastProposalIsPending(uint256) (runs: 256, μ: 1115001, ~: 1115001) +SerpentorBravoTest:testCannotProposeTooManyActions(uint256,uint8) (runs: 256, μ: 179947, ~: 179917) +SerpentorBravoTest:testCannotProposeZeroOperations(uint256) (runs: 256, μ: 223462, ~: 223463) +SerpentorBravoTest:testCannotQueueProposalIfNotSucceeded() (gas: 1043407) +SerpentorBravoTest:testCannotSetProposalThresholdOutsideRange(uint32) (runs: 256, μ: 13953, ~: 13946) +SerpentorBravoTest:testCannotSetVotingDelayOutsideRange(address,uint32) (runs: 256, μ: 16092, ~: 16095) +SerpentorBravoTest:testCannotSetVotingPeriodOutsideRange(address,uint32) (runs: 256, μ: 16634, ~: 16641) SerpentorBravoTest:testCannotSetWhitelistedAccount(address,uint256) (runs: 256, μ: 17540, ~: 17540) -SerpentorBravoTest:testCannotVoteMoreThanOnce(uint256,address,uint8) (runs: 256, μ: 1085813, ~: 1095461) -SerpentorBravoTest:testCannotVoteOnInactiveProposal(uint256,address,uint8) (runs: 256, μ: 1036980, ~: 1036980) -SerpentorBravoTest:testCannotVoteWithInvalidOption(uint256,address,uint8) (runs: 256, μ: 1039240, ~: 1039240) -SerpentorBravoTest:testGetAction() (gas: 1332672) -SerpentorBravoTest:testOnlyPendingAdminCanAcceptThrone(address) (runs: 256, μ: 39256, ~: 39240) -SerpentorBravoTest:testRandomAcctCannotSetNewAdmin(address) (runs: 256, μ: 14320, ~: 14320) -SerpentorBravoTest:testRandomAcctCannotSetNewKnight(address) (runs: 256, μ: 14376, ~: 14376) -SerpentorBravoTest:testRandomAcctCannotSetProposalThreshold(address,uint256) (runs: 256, μ: 15205, ~: 15205) -SerpentorBravoTest:testRandomAcctCannotSetVotingDelay(address,uint256) (runs: 256, μ: 14582, ~: 14582) -SerpentorBravoTest:testRandomAcctCannotSetVotingPeriod(address,uint256) (runs: 256, μ: 14627, ~: 14627) -SerpentorBravoTest:testRandomAcctCannotTakeOverThrone(address) (runs: 256, μ: 14259, ~: 14259) -SerpentorBravoTest:testSetNewKnight(address) (runs: 256, μ: 24753, ~: 24753) -SerpentorBravoTest:testSetWhitelistedAccountAsAdmin(address,uint256) (runs: 256, μ: 41634, ~: 41634) -SerpentorBravoTest:testSetWhitelistedAccountAsKnight(address,uint256) (runs: 256, μ: 43666, ~: 43666) -SerpentorBravoTest:testSetup() (gas: 83935) -SerpentorBravoTest:testShouldCancelProposalIfProposerIsBelowThreshold(uint256,uint256,address,address) (runs: 256, μ: 1124895, ~: 1126839) -SerpentorBravoTest:testShouldCancelQueuedProposal(address[7]) (runs: 256, μ: 2518157, ~: 2518068) -SerpentorBravoTest:testShouldCancelWhenSenderIsProposerAndProposalActive(uint256,address) (runs: 256, μ: 1090182, ~: 1090182) -SerpentorBravoTest:testShouldCancelWhitelistedProposerBelowThresholdAsKnight(uint256,uint256) (runs: 256, μ: 1125512, ~: 1128078) -SerpentorBravoTest:testShouldComputeDomainSeparatorCorrectly() (gas: 8762) -SerpentorBravoTest:testShouldExecuteQueuedProposal(address[7]) (runs: 256, μ: 2723217, ~: 2723129) -SerpentorBravoTest:testShouldHandleProposalDefeatedCorrectly(address[7]) (runs: 256, μ: 2331123, ~: 2331035) -SerpentorBravoTest:testShouldQueueProposal(address[7]) (runs: 256, μ: 2509235, ~: 2509147) -SerpentorBravoTest:testShouldRevertExecutionIfTrxReverts(address[7]) (runs: 256, μ: 2561053, ~: 2561053) -SerpentorBravoTest:testShouldSetProposalThreshold(uint256) (runs: 256, μ: 23370, ~: 23414) -SerpentorBravoTest:testShouldSetVotingDelay(address,uint256) (runs: 256, μ: 26027, ~: 26027) -SerpentorBravoTest:testShouldSetVotingPeriod(address,uint256) (runs: 256, μ: 26003, ~: 26003) -SerpentorBravoTest:testShouldVoteBySig(uint256,uint8,uint248) (runs: 256, μ: 1241322, ~: 1251127) -SerpentorBravoTest:testShouldVoteWithReason(uint256,address,uint8) (runs: 256, μ: 1225995, ~: 1235799) -SerpentorBravoTest:testShouldcastVote(uint256,address,uint8) (runs: 256, μ: 1224606, ~: 1234410) +SerpentorBravoTest:testCannotVoteMoreThanOnce(uint256,address,uint8) (runs: 256, μ: 1086036, ~: 1095684) +SerpentorBravoTest:testCannotVoteOnInactiveProposal(uint256,address,uint8) (runs: 256, μ: 1037202, ~: 1037202) +SerpentorBravoTest:testCannotVoteWithInvalidOption(uint256,address,uint8) (runs: 256, μ: 1039485, ~: 1039485) +SerpentorBravoTest:testGetAction() (gas: 1332982) +SerpentorBravoTest:testOnlyPendingAdminCanAcceptAdmin(address) (runs: 256, μ: 39328, ~: 39312) +SerpentorBravoTest:testRandomAcctCannotSetNewAdmin(address) (runs: 256, μ: 14298, ~: 14298) +SerpentorBravoTest:testRandomAcctCannotSetNewKnight(address) (runs: 256, μ: 14354, ~: 14354) +SerpentorBravoTest:testRandomAcctCannotSetProposalThreshold(address,uint256) (runs: 256, μ: 15183, ~: 15183) +SerpentorBravoTest:testRandomAcctCannotSetVotingDelay(address,uint256) (runs: 256, μ: 14670, ~: 14670) +SerpentorBravoTest:testRandomAcctCannotSetVotingPeriod(address,uint256) (runs: 256, μ: 14605, ~: 14605) +SerpentorBravoTest:testRandomAcctCannotTakeOverAdmin(address) (runs: 256, μ: 14325, ~: 14325) +SerpentorBravoTest:testSetNewKnight(address) (runs: 256, μ: 24799, ~: 24799) +SerpentorBravoTest:testSetWhitelistedAccountAsAdmin(address,uint256) (runs: 256, μ: 41612, ~: 41612) +SerpentorBravoTest:testSetWhitelistedAccountAsKnight(address,uint256) (runs: 256, μ: 43644, ~: 43644) +SerpentorBravoTest:testSetup() (gas: 84735) +SerpentorBravoTest:testShouldCancelProposalIfProposerIsBelowThreshold(uint256,uint256,address,address) (runs: 256, μ: 1402436, ~: 1404380) +SerpentorBravoTest:testShouldCancelQueuedProposal(address[7]) (runs: 256, μ: 3048455, ~: 3048455) +SerpentorBravoTest:testShouldCancelWhenSenderIsProposerAndProposalActive(uint256,address) (runs: 256, μ: 1367657, ~: 1367657) +SerpentorBravoTest:testShouldCancelWhitelistedProposerBelowThresholdAsKnight(uint256,uint256) (runs: 256, μ: 1403162, ~: 1405728) +SerpentorBravoTest:testShouldComputeDomainSeparatorCorrectly() (gas: 8805) +SerpentorBravoTest:testShouldExecuteQueuedProposal(address[7]) (runs: 256, μ: 3507619, ~: 3507619) +SerpentorBravoTest:testShouldHandleProposalDefeatedCorrectly(address[7]) (runs: 256, μ: 2839334, ~: 2839334) +SerpentorBravoTest:testShouldQueueProposal(address[7]) (runs: 256, μ: 3061939, ~: 3061939) +SerpentorBravoTest:testShouldRevertExecutionIfTrxReverts(address[7]) (runs: 256, μ: 3117649, ~: 3117649) +SerpentorBravoTest:testShouldSetProposalThreshold(uint256) (runs: 256, μ: 23395, ~: 23461) +SerpentorBravoTest:testShouldSetVotingDelay(address,uint256) (runs: 256, μ: 26063, ~: 26074) +SerpentorBravoTest:testShouldSetVotingPeriod(address,uint256) (runs: 256, μ: 26050, ~: 26050) +SerpentorBravoTest:testShouldVoteBySig(uint256,uint8,uint248) (runs: 256, μ: 1519152, ~: 1528957) +SerpentorBravoTest:testShouldVoteWithReason(uint256,address,uint8) (runs: 256, μ: 1503559, ~: 1503556) +SerpentorBravoTest:testShouldcastVote(uint256,address,uint8) (runs: 256, μ: 1502192, ~: 1502189) TimelockTest:testCannotExecNonExistingTrx() (gas: 59503) TimelockTest:testCannotExecQueuedTrxAfterGracePeriod(uint256) (runs: 256, μ: 58511, ~: 58511) TimelockTest:testCannotExecQueuedTrxBeforeETA() (gas: 57094) diff --git a/foundry_test/DualTimelock.t.sol b/foundry_test/DualTimelock.t.sol index 74dfd98..31a0396 100644 --- a/foundry_test/DualTimelock.t.sol +++ b/foundry_test/DualTimelock.t.sol @@ -2,11 +2,10 @@ pragma solidity ^0.8.16; import "@openzeppelin/token/ERC20/ERC20.sol"; +import {console} from "forge-std/console.sol"; import {ExtendedTest} from "./utils/ExtendedTest.sol"; import {VyperDeployer} from "../lib/utils/VyperDeployer.sol"; - -import {console} from "forge-std/console.sol"; import {DualTimelock, Transaction} from "./interfaces/DualTimelock.sol"; import {GovToken} from "./utils/GovToken.sol"; @@ -17,31 +16,31 @@ contract DualTimelockTest is ExtendedTest { address public admin = address(1); address public holder = address(2); address public grantee = address(3); - address public fastTrack = address(4); + address public leanTrack = address(4); uint public constant GRACE_PERIOD = 14 days; uint public constant MINIMUM_DELAY = 2 days; uint public constant MAXIMUM_DELAY = 30 days; uint256 public delay = 2 days; - uint256 public fastTrackDelay = 1 days; + uint256 public leanTrackDelay = 1 days; // events event NewDelay(uint256 newDelay); - event NewFastTrackDelay(uint256 newDelay); + event NewLeanTrackDelay(uint256 newDelay); event NewAdmin(address indexed newAdmin); event NewPendingAdmin(address indexed newPendingAdmin); - event NewFastTrack(address indexed newFastTrack); - event NewPendingFastTrack(address indexed newPendingFastTrack); + event NewLeanTrack(address indexed newLeanTrack); + event NewPendingLeanTrack(address indexed newPendingLeanTrack); event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); - event QueueFastTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); - event CancelFastTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); - event ExecuteFastTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + event QueueRapidTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + event CancelRapidTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); + event ExecuteRapidTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); function setUp() public { - bytes memory args = abi.encode(admin, fastTrack, delay, fastTrackDelay); + bytes memory args = abi.encode(admin, leanTrack, delay, leanTrackDelay); timelock = DualTimelock(vyperDeployer.deployContract("src/", "DualTimelock", args)); console.log("address for DualTimelock: ", address(timelock)); @@ -59,10 +58,10 @@ contract DualTimelockTest is ExtendedTest { function testSetup() public { assertNeq(address(timelock), address(0)); assertEq(address(timelock.admin()), admin); - assertEq(address(timelock.fastTrack()), fastTrack); + assertEq(address(timelock.leanTrack()), leanTrack); assertEq(timelock.delay(), delay); assertEq(timelock.delay(), MINIMUM_DELAY); - assertEq(timelock.fastTrackDelay(), fastTrackDelay); + assertEq(timelock.leanTrackDelay(), leanTrackDelay); assertEq(token.balanceOf(address(timelock)), 1000e18); } @@ -75,12 +74,12 @@ contract DualTimelockTest is ExtendedTest { timelock.setDelay(5 days); } - function testRandomAcctCannotSetFastTrackDelay(address random) public { + function testRandomAcctCannotSetLeanTrackDelay(address random) public { vm.assume(random != address(timelock)); vm.expectRevert("!Timelock"); vm.prank(random); - timelock.setFastTrackDelay(0 days); + timelock.setLeanTrackDelay(0 days); } function testOnlySelfCanSetDelay(uint256 newDelay) public { @@ -96,27 +95,27 @@ contract DualTimelockTest is ExtendedTest { assertEq(timelock.delay(), newDelay); } - function testOnlySelfCanSetFastTrackDelay(uint256 newDelay) public { + function testOnlySelfCanSetLeanTrackDelay(uint256 newDelay) public { vm.assume(newDelay >= 0 && newDelay < MINIMUM_DELAY); //setup //setup for event checks vm.expectEmit(false, false, false, false); - emit NewFastTrackDelay(newDelay); + emit NewLeanTrackDelay(newDelay); // execute vm.prank(address(timelock)); - timelock.setFastTrackDelay(newDelay); + timelock.setLeanTrackDelay(newDelay); // asserts - assertEq(timelock.fastTrackDelay(), newDelay); + assertEq(timelock.leanTrackDelay(), newDelay); } - function testSetFastTrackDelayCannotBeGreaterThanDelay(uint256 newDelay) public { + function testSetLeanTrackDelayCannotBeGreaterThanDelay(uint256 newDelay) public { uint currentDelay = timelock.delay(); vm.assume(newDelay > currentDelay); //setup - vm.expectRevert("!fastTrackDelay < delay"); + vm.expectRevert("!leanTrackDelay < delay"); // execute vm.prank(address(timelock)); - timelock.setFastTrackDelay(newDelay); + timelock.setLeanTrackDelay(newDelay); } @@ -149,13 +148,13 @@ contract DualTimelockTest is ExtendedTest { timelock.setPendingAdmin(random); } - function testRandomAcctCannotSetNewFastTrack(address random) public { + function testRandomAcctCannotSetNewLeanTrack(address random) public { vm.assume(random != address(timelock)); // setup vm.expectRevert(bytes("!Timelock")); // execute vm.prank(random); - timelock.setPendingFastTrack(random); + timelock.setPendingLeanTrack(random); } function testRandomAcctCannotTakeOverAdmin(address random) public { @@ -167,13 +166,13 @@ contract DualTimelockTest is ExtendedTest { timelock.acceptAdmin(); } - function testRandomAcctCannotTakeOverFastTrack(address random) public { - vm.assume(random != fastTrack && random != address(0)); + function testRandomAcctCannotTakeOverLeanTrack(address random) public { + vm.assume(random != leanTrack && random != address(0)); // setup - vm.expectRevert(bytes("!pendingFastTrack")); + vm.expectRevert(bytes("!pendingLeanTrack")); // execute vm.prank(random); - timelock.acceptFastTrack(); + timelock.acceptLeanTrack(); } function testOnlyPendingAdminCanAcceptAdmin() public { @@ -196,24 +195,24 @@ contract DualTimelockTest is ExtendedTest { assertEq(timelock.pendingAdmin(), address(0)); } - function testOnlyPendingFastTrackCanCallAcceptFastTrack() public { + function testOnlyPendingLeanTrackCanCallAcceptLeanTrack() public { // setup - address futureFastTrack = address(0xBEEF); + address futureLeanTrack = address(0xBEEF); // setup pendingAdmin vm.prank(address(timelock)); - timelock.setPendingFastTrack(futureFastTrack); - assertEq(timelock.pendingFastTrack(), futureFastTrack); + timelock.setPendingLeanTrack(futureLeanTrack); + assertEq(timelock.pendingLeanTrack(), futureLeanTrack); //setup for event checks vm.expectEmit(true, false, false, false); - emit NewFastTrack(futureFastTrack); + emit NewLeanTrack(futureLeanTrack); // execute - vm.prank(futureFastTrack); - timelock.acceptFastTrack(); + vm.prank(futureLeanTrack); + timelock.acceptLeanTrack(); // asserts - assertEq(timelock.fastTrack(), futureFastTrack); - assertEq(timelock.pendingFastTrack(), address(0)); + assertEq(timelock.leanTrack(), futureLeanTrack); + assertEq(timelock.pendingLeanTrack(), address(0)); } function testRandomAcctCannotQueueTrx(address random) public { @@ -227,13 +226,13 @@ contract DualTimelockTest is ExtendedTest { } function testRandomAcctCannotQueueFastTrx(address random) public { - vm.assume(random != fastTrack); + vm.assume(random != leanTrack); // setup - vm.expectRevert(bytes("!fastTrack")); + vm.expectRevert(bytes("!leanTrack")); // execute vm.prank(random); - timelock.queueFastTransaction(address(timelock), 0, "", "", block.timestamp + 10 days); + timelock.queueRapidTransaction(address(timelock), 0, "", "", block.timestamp + 10 days); } function testQueueTrxEtaCannotBeInvalid() public { @@ -254,8 +253,8 @@ contract DualTimelockTest is ExtendedTest { uint256 badEta = block.timestamp; // execute - vm.prank(address(fastTrack)); - timelock.queueFastTransaction(address(grantee), 0, "", "", badEta); + vm.prank(address(leanTrack)); + timelock.queueRapidTransaction(address(grantee), 0, "", "", badEta); } function testShouldQueueTrx() public { @@ -305,17 +304,17 @@ contract DualTimelockTest is ExtendedTest { ); //setup for event checks vm.expectEmit(true, true, false, false); - emit QueueFastTransaction(expectedTrxHash, target, amount, signature, callData, eta); + emit QueueRapidTransaction(expectedTrxHash, target, amount, signature, callData, eta); // execute - vm.prank(address(fastTrack)); - bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); + vm.prank(address(leanTrack)); + bytes32 trxHash = timelock.queueRapidTransaction(target, amount, signature, callData, eta); // asserts assertEq(expectedTrxHash, trxHash); - assertTrue(timelock.queuedFastTransactions(trxHash)); + assertTrue(timelock.queuedRapidTransactions(trxHash)); } - function testFastTrackCannotTargetTimelock() public { + function testLeanTrackCannotTargetTimelock() public { // setup uint256 eta = block.timestamp + 1 days; // cannot call timelock @@ -333,11 +332,61 @@ contract DualTimelockTest is ExtendedTest { eta ); //setup for expect revert - vm.expectRevert(bytes("!self")); + vm.expectRevert(bytes("!target")); + + // execute + vm.prank(address(leanTrack)); + timelock.queueRapidTransaction(target, amount, signature, callData, eta); + } + + function testLeanTrackCannotTargetTimelockAdmin() public { + // setup + uint256 eta = block.timestamp + 1 days; + // cannot call timelock + address target = address(admin); + bytes memory callData = abi.encodeWithSelector(DualTimelock.setDelay.selector, 5 days); + uint256 amount = 0; + string memory signature = ""; + bytes32 expectedTrxHash; + Transaction memory testTrx; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + //setup for expect revert + vm.expectRevert(bytes("!target")); + + // execute + vm.prank(address(leanTrack)); + timelock.queueRapidTransaction(target, amount, signature, callData, eta); + } + + function testLeanTrackCannotTargetLeanTrack() public { + // setup + uint256 eta = block.timestamp + 1 days; + // cannot call timelock + address target = address(leanTrack); + bytes memory callData = abi.encodeWithSelector(DualTimelock.setDelay.selector, 5 days); + uint256 amount = 0; + string memory signature = ""; + bytes32 expectedTrxHash; + Transaction memory testTrx; + (testTrx, expectedTrxHash) =_getTransactionAndHash( + target, + amount, + signature, + callData, + eta + ); + //setup for expect revert + vm.expectRevert(bytes("!target")); // execute - vm.prank(address(fastTrack)); - timelock.queueFastTransaction(target, amount, signature, callData, eta); + vm.prank(address(leanTrack)); + timelock.queueRapidTransaction(target, amount, signature, callData, eta); } function testRandomAcctCannotCancelQueueTrx(address random) public { @@ -351,13 +400,13 @@ contract DualTimelockTest is ExtendedTest { } function testRandomAcctCannotCancelFastQueueTrx(address random) public { - vm.assume(random != fastTrack); + vm.assume(random != leanTrack); // setup - vm.expectRevert(bytes("!fastTrack")); + vm.expectRevert(bytes("!leanTrack")); // execute vm.prank(address(0xABCD)); - timelock.cancelFastTransaction(address(token), 0, "", "", block.timestamp + 1 days); + timelock.cancelRapidTransaction(address(token), 0, "", "", block.timestamp + 1 days); } function testShouldCancelQueuedTrx() public { @@ -411,20 +460,20 @@ contract DualTimelockTest is ExtendedTest { eta ); - vm.prank(address(fastTrack)); - bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); - assertTrue(timelock.queuedFastTransactions(trxHash)); + vm.prank(address(leanTrack)); + bytes32 trxHash = timelock.queueRapidTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedRapidTransactions(trxHash)); //setup for event checks vm.expectEmit(true, true, false, false); - emit CancelFastTransaction(expectedTrxHash, target, amount, signature, callData, eta); + emit CancelRapidTransaction(expectedTrxHash, target, amount, signature, callData, eta); // execute - vm.prank(address(fastTrack)); - timelock.cancelFastTransaction(target, amount, signature, callData, eta); + vm.prank(address(leanTrack)); + timelock.cancelRapidTransaction(target, amount, signature, callData, eta); // asserts - assertFalse(timelock.queuedFastTransactions(trxHash)); + assertFalse(timelock.queuedRapidTransactions(trxHash)); } function testRandomAcctCannotExecQueuedTrx(address random) public { @@ -457,7 +506,7 @@ contract DualTimelockTest is ExtendedTest { } function testRandomAcctCannotExecQueuedFastTrx(address random) public { - vm.assume(random != fastTrack); + vm.assume(random != leanTrack); // setup uint256 eta = block.timestamp + 1 days; address target = address(token); @@ -474,14 +523,14 @@ contract DualTimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(fastTrack)); - bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); - assertTrue(timelock.queuedFastTransactions(trxHash)); + vm.prank(address(leanTrack)); + bytes32 trxHash = timelock.queueRapidTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedRapidTransactions(trxHash)); - vm.expectRevert(bytes("!fastTrack")); + vm.expectRevert(bytes("!leanTrack")); // execute vm.prank(random); - timelock.executeFastTransaction(target, amount, signature, callData, eta); + timelock.executeRapidTransaction(target, amount, signature, callData, eta); } function testCannotExecNonExistingTrx() public { @@ -540,9 +589,9 @@ contract DualTimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(fastTrack)); - bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); - assertTrue(timelock.queuedFastTransactions(trxHash)); + vm.prank(address(leanTrack)); + bytes32 trxHash = timelock.queueRapidTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedRapidTransactions(trxHash)); Transaction memory wrongTrx; bytes32 wrongTrxHash; @@ -556,8 +605,8 @@ contract DualTimelockTest is ExtendedTest { vm.expectRevert(bytes("!queued_trx")); // execute - vm.prank(address(fastTrack)); - timelock.executeFastTransaction(target, amount, signature, "", eta); + vm.prank(address(leanTrack)); + timelock.executeRapidTransaction(target, amount, signature, "", eta); } function testCannotExecQueuedTrxBeforeETA() public { @@ -606,15 +655,15 @@ contract DualTimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(fastTrack)); - bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); - assertTrue(timelock.queuedFastTransactions(trxHash)); + vm.prank(address(leanTrack)); + bytes32 trxHash = timelock.queueRapidTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedRapidTransactions(trxHash)); skip(12 hours); // short of ETA vm.expectRevert(bytes("!eta")); // execute - vm.prank(address(fastTrack)); - timelock.executeFastTransaction(target, amount, signature, callData, eta); + vm.prank(address(leanTrack)); + timelock.executeRapidTransaction(target, amount, signature, callData, eta); } function testCannotExecQueuedTrxAfterGracePeriod(uint256 executionTime) public { @@ -666,14 +715,14 @@ contract DualTimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(fastTrack)); - bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); - assertTrue(timelock.queuedFastTransactions(trxHash)); + vm.prank(address(leanTrack)); + bytes32 trxHash = timelock.queueRapidTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedRapidTransactions(trxHash)); skip(executionTime); // skip to time of execution passed gracePeriod vm.expectRevert(bytes("!staled_trx")); // execute - vm.prank(address(fastTrack)); - timelock.executeFastTransaction(target, amount, signature, callData, eta); + vm.prank(address(leanTrack)); + timelock.executeRapidTransaction(target, amount, signature, callData, eta); } function testCannotExecFastTrxInIncorrectQueue() public { @@ -700,8 +749,8 @@ contract DualTimelockTest is ExtendedTest { vm.expectRevert(bytes("!queued_trx")); // execute - vm.prank(address(fastTrack)); - timelock.executeFastTransaction(target, amount, signature, callData, eta); + vm.prank(address(leanTrack)); + timelock.executeRapidTransaction(target, amount, signature, callData, eta); } @@ -757,17 +806,17 @@ contract DualTimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(fastTrack)); - bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); - assertTrue(timelock.queuedFastTransactions(trxHash)); + vm.prank(address(leanTrack)); + bytes32 trxHash = timelock.queueRapidTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedRapidTransactions(trxHash)); skip(eta + 1); // 1 pass eta //setup for event checks vm.expectEmit(true, true, false, false); - emit ExecuteFastTransaction(expectedTrxHash, target, amount, signature, callData, eta); + emit ExecuteRapidTransaction(expectedTrxHash, target, amount, signature, callData, eta); // execute - vm.prank(address(fastTrack)); - timelock.executeFastTransaction(target, amount, signature, callData, eta); + vm.prank(address(leanTrack)); + timelock.executeRapidTransaction(target, amount, signature, callData, eta); // asserts assertEq(token.balanceOf(grantee), 1000); @@ -825,17 +874,17 @@ contract DualTimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(fastTrack)); - bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); - assertTrue(timelock.queuedFastTransactions(trxHash)); + vm.prank(address(leanTrack)); + bytes32 trxHash = timelock.queueRapidTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedRapidTransactions(trxHash)); skip(eta + 1); // 1 pass eta //setup for event checks vm.expectEmit(true, true, false, false); - emit ExecuteFastTransaction(expectedTrxHash, target, amount, signature, callData, eta); + emit ExecuteRapidTransaction(expectedTrxHash, target, amount, signature, callData, eta); // execute - vm.prank(address(fastTrack)); - timelock.executeFastTransaction(target, amount, signature, callData, eta); + vm.prank(address(leanTrack)); + timelock.executeRapidTransaction(target, amount, signature, callData, eta); // asserts assertEq(token.balanceOf(grantee), 1000); @@ -896,19 +945,19 @@ contract DualTimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(fastTrack)); - bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); - assertTrue(timelock.queuedFastTransactions(trxHash)); + vm.prank(address(leanTrack)); + bytes32 trxHash = timelock.queueRapidTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedRapidTransactions(trxHash)); skip(eta + 1); // 1 pass deal(address(timelock), 11 ether); //setup for event checks vm.expectEmit(true, true, false, false); - emit ExecuteFastTransaction(expectedTrxHash, target, amount, signature, callData, eta); + emit ExecuteRapidTransaction(expectedTrxHash, target, amount, signature, callData, eta); // execute - hoax(address(fastTrack), 1 ether); - timelock.executeFastTransaction(target, amount, signature, callData, eta); + hoax(address(leanTrack), 1 ether); + timelock.executeRapidTransaction(target, amount, signature, callData, eta); // asserts assertEq(grantee.balance, amount); @@ -968,19 +1017,19 @@ contract DualTimelockTest is ExtendedTest { callData, eta ); - vm.prank(address(fastTrack)); - bytes32 trxHash = timelock.queueFastTransaction(target, amount, signature, callData, eta); - assertTrue(timelock.queuedFastTransactions(trxHash)); + vm.prank(address(leanTrack)); + bytes32 trxHash = timelock.queueRapidTransaction(target, amount, signature, callData, eta); + assertTrue(timelock.queuedRapidTransactions(trxHash)); skip(eta + 1); // 1 pass assertEq(address(timelock).balance, 0); //setup for event checks vm.expectEmit(true, true, false, false); - emit ExecuteFastTransaction(expectedTrxHash, target, amount, signature, callData, eta); + emit ExecuteRapidTransaction(expectedTrxHash, target, amount, signature, callData, eta); // execute - hoax(address(fastTrack), 10 ether); - timelock.executeFastTransaction{value: amount}(target, amount, signature, callData, eta); + hoax(address(leanTrack), 10 ether); + timelock.executeRapidTransaction{value: amount}(target, amount, signature, callData, eta); // asserts assertEq(grantee.balance, amount); diff --git a/foundry_test/LeanTrack.t.sol b/foundry_test/LeanTrack.t.sol new file mode 100644 index 0000000..a5474ee --- /dev/null +++ b/foundry_test/LeanTrack.t.sol @@ -0,0 +1,1347 @@ +// SPDX-License-Identifier: AGPL +pragma solidity ^0.8.16; + +import "@openzeppelin/token/ERC20/ERC20.sol"; +import {console} from "forge-std/console.sol"; + +import {ExtendedTest} from "./utils/ExtendedTest.sol"; +import {VyperDeployer} from "../lib/utils/VyperDeployer.sol"; +import {DualTimelock} from "./interfaces/DualTimelock.sol"; +import { + LeanTrack, + Factory, + Motion +} from "./interfaces/LeanTrack.sol"; +import {GovToken} from "./utils/GovToken.sol"; + +contract LeanTrackTest is ExtendedTest { + VyperDeployer private vyperDeployer = new VyperDeployer(); + DualTimelock private timelock; + ERC20 private token; + LeanTrack private leanTrack; + GovToken private govToken; + + uint256 public delay = 2 days; + uint256 public leanTrackDelay = 1 days; + uint256 public factoryMotionDuration = 1 days; + uint256 public constant HUNDRED_PCT = 10000; + uint256 public constant TOKEN_SUPPLY = 30000 * 10**uint256(18); + uint256 public constant QUORUM = 2000; // 20% + uint256 public constant QUORUM_AMOUNT = TOKEN_SUPPLY * QUORUM / HUNDRED_PCT; + uint256 public constant transferAmount = 1e18; + uint256 public constant MAX_OPERATIONS = 10; + uint256 public constant MIN_OBJECTIONS_THRESHOLD = 100; // 1% + uint256 public constant MAX_OBJECTIONS_THRESHOLD = 3000; // 30% + uint256 public constant MIN_MOTION_DURATION = 16 hours; + + address public admin = address(1); + address public factory = address(2); + address public objectoor = address(3); + address public mediumVoter = address(4); + address public whaleVoter1 = address(5); + address public whaleVoter2 = address(6); + address public knight = address(7); + address public smallVoter = address(8); + address public grantee = address(0xABCD); + address public executor = address(7); + + // test helper fields + address[] public reservedList; + mapping(address => bool) public isVoter; // for tracking duplicates in fuzzing + mapping(address => bool) public reserved; // for tracking duplicates in fuzzing + + + // events + event MotionCreated( + uint256 indexed motionId, + address indexed proposer, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, + uint256 eta, + uint256 snapshotBlock, + uint256 objectionsThreshold + ); + + event MotionQueued( + uint256 indexed motionId, + bytes32[] txHashes, + uint256 eta + ); + + event MotionObjected( + uint256 indexed motionId, + address indexed objector, + uint256 objectorsBalance, + uint256 newObjectionsAmount, + uint256 newObjectionsAmountPct + ); + + event MotionRejected( + uint256 indexed motionId + ); + + event MotionCanceled( + uint256 indexed motionId + ); + + event MotionFactoryAdded( + address indexed factory, + uint256 objectionsThreshold, + uint256 motionDuration + ); + + event MotionFactoryRemoved( + address indexed factory + ); + + event ExecutorAdded( + address indexed executor + ); + + event ExecutorRemoved( + address indexed executor + ); + + event Paused( + address indexed account + ); + + event Unpaused( + address indexed account + ); + + function setUp() public { + // deploy token + govToken = new GovToken(18); + token = ERC20(govToken); + console.log("address for GovToken: ", address(token)); + + bytes memory args = abi.encode(admin, address(0), delay, leanTrackDelay); + timelock = DualTimelock(vyperDeployer.deployContract("src/", "DualTimelock", args)); + console.log("address for DualTimelock: ", address(timelock)); + + bytes memory argsLeanTrack = abi.encode(address(token), admin, address(timelock), knight); + leanTrack = LeanTrack(vyperDeployer.deployContract("src/", "LeanTrack", argsLeanTrack)); + console.log("address for LeanTrack: ", address(leanTrack)); + + hoax(address(timelock)); + timelock.setPendingLeanTrack(address(leanTrack)); + + hoax(address(knight)); + leanTrack.acceptTimelockAccess(); + + + _setupReservedAddress(); + // setup factory + hoax(admin); + leanTrack.addMotionFactory(factory, QUORUM, factoryMotionDuration); + + // add executor + hoax(admin); + leanTrack.addExecutor(address(knight)); + + // vm traces + vm.label(address(timelock), "DualTimelock"); + vm.label(address(token), "Token"); + vm.label(factory, "factory"); + vm.label(objectoor, "objectoor"); + vm.label(smallVoter, "smallVoter"); + vm.label(mediumVoter, "mediumVoter"); + vm.label(whaleVoter1, "whaleVoter1"); + vm.label(whaleVoter2, "whaleVoter2"); + vm.label(knight, "knight"); + vm.label(grantee, "grantee"); + + // setup token balances + deal(address(token), objectoor, QUORUM); + deal(address(token), smallVoter, 1e18); + deal(address(token), mediumVoter, 10e18); + deal(address(token), whaleVoter1, 300e18); + deal(address(token), whaleVoter2, 250e18); + deal(address(token), address(timelock), 1000e18); + } + + function testSetup() public { + assertNeq(address(timelock), address(0)); + assertNeq(address(leanTrack), address(0)); + + assertEq(address(timelock.admin()), admin); + assertEq(timelock.delay(), delay); + assertEq(timelock.leanTrackDelay(), leanTrackDelay); + assertEq(leanTrack.admin(), admin); + assertEq(leanTrack.token(), address(token)); + assertTrue(leanTrack.factories(factory).isFactory); + assertEq(leanTrack.factories(factory).objectionsThreshold, QUORUM); + assertEq(leanTrack.factories(factory).motionDuration, factoryMotionDuration); + } + + + function _setupReservedAddress() internal { + reservedList = [ + admin, + factory, + smallVoter, + mediumVoter, + whaleVoter1, + whaleVoter2, + objectoor, + knight, + grantee, + address(0), + address(timelock), + address(leanTrack), + address(token) + ]; + for (uint i = 0; i < reservedList.length; i++) + reserved[reservedList[i]] = true; + } + + function testRandomAcctCannotAddMotionFactory(address random) public { + vm.assume(!reserved[random]); + + // setup + vm.expectRevert(bytes("!admin")); + + //execute + hoax(random); + leanTrack.addMotionFactory(random, MIN_OBJECTIONS_THRESHOLD, MIN_MOTION_DURATION); + } + + function testMotionFactoryDurationCannotBeLessThanMinimum(address random, uint256 objectionsThreshold, uint32 motionDuration) public { + vm.assume(!reserved[random]); + vm.assume(objectionsThreshold >= MIN_OBJECTIONS_THRESHOLD); + vm.assume(motionDuration < MIN_MOTION_DURATION); + + // setup + vm.expectRevert(bytes("!motion_duration")); + + //execute + hoax(admin); + leanTrack.addMotionFactory(random, objectionsThreshold, motionDuration); + } + + function testMotionFactoryObjectionsThresholdCannotBeLessThanMinimum(address random, uint256 objectionsThreshold) public { + vm.assume(!reserved[random]); + vm.assume(objectionsThreshold < MIN_OBJECTIONS_THRESHOLD); + + // setup + vm.expectRevert(bytes("!min_objections_threshold")); + + //execute + hoax(admin); + leanTrack.addMotionFactory(random, objectionsThreshold, MIN_MOTION_DURATION); + } + + function testMotionFactoryObjectionsThresholdCannotBeGreaterThanMaximum(address random, uint256 objectionsThreshold) public { + vm.assume(!reserved[random]); + vm.assume(objectionsThreshold > MAX_OBJECTIONS_THRESHOLD); + + // setup + vm.expectRevert(bytes("!max_objections_threshold")); + + //execute + hoax(admin); + leanTrack.addMotionFactory(random, objectionsThreshold, MIN_MOTION_DURATION); + } + + function testCannotAddFactoryTwice() public { + // setup + vm.expectRevert(bytes("!factory_exists")); + + //execute + hoax(admin); + leanTrack.addMotionFactory(factory, MIN_OBJECTIONS_THRESHOLD, 1 days); + } + + function testShouldAddMotionFactory(address random, uint256 objectionsThreshold, uint32 motionDuration) public { + vm.assume(!reserved[random]); + vm.assume(objectionsThreshold >= MIN_OBJECTIONS_THRESHOLD && objectionsThreshold <= MAX_OBJECTIONS_THRESHOLD); + vm.assume(motionDuration >= MIN_MOTION_DURATION); + + // setup + vm.expectEmit(false, false, false, false); + emit MotionFactoryAdded(factory, objectionsThreshold, motionDuration); + + //execute + hoax(admin); + leanTrack.addMotionFactory(random, objectionsThreshold, motionDuration); + + // assert + assertTrue(leanTrack.factories(random).isFactory); + assertEq(leanTrack.factories(random).motionDuration, motionDuration); + assertEq(leanTrack.factories(random).objectionsThreshold, objectionsThreshold); + } + + function testOnlyApprovedFactoryCanCreateMotion(address random) public { + vm.assume(!reserved[random]); + vm.assume(random != factory); + + // setup + address[] memory targets = new address[](1); + uint256[] memory values = new uint256[](1); + string[] memory signatures = new string[](1); + bytes[] memory calldatas = new bytes[](1); + targets[0] = address(token); + values[0] = 0; + signatures[0] = ""; + calldatas[0] = abi.encodeWithSelector(IERC20.transfer.selector, grantee, transferAmount); + vm.expectRevert(bytes("!factory")); + + //execute + hoax(random); + leanTrack.createMotion(targets, values, signatures, calldatas); + } + + function testShouldRemoveFactory(address random, uint256 objectionsThreshold, uint32 motionDuration) public { + vm.assume(!reserved[random]); + vm.assume(objectionsThreshold >= MIN_OBJECTIONS_THRESHOLD && objectionsThreshold <= MAX_OBJECTIONS_THRESHOLD); + vm.assume(motionDuration >= MIN_MOTION_DURATION); + + // setup + vm.expectEmit(false, false, false, false); + emit MotionFactoryAdded(factory, objectionsThreshold, motionDuration); + // add factory + hoax(admin); + leanTrack.addMotionFactory(random, objectionsThreshold, motionDuration); + + // assert factory added + assertTrue(leanTrack.factories(random).isFactory); + assertEq(leanTrack.factories(random).motionDuration, motionDuration); + assertEq(leanTrack.factories(random).objectionsThreshold, objectionsThreshold); + + // execute remove factory + vm.expectEmit(false, false, false, false); + emit MotionFactoryRemoved(random); + + //execute + hoax(admin); + leanTrack.removeMotionFactory(random); + + // assert factory removed + assertTrue(!leanTrack.factories(random).isFactory); + } + + function testOnlyAdminCanRemoveFactory(address random, uint256 objectionsThreshold, uint32 motionDuration) public { + vm.assume(!reserved[random]); + vm.assume(objectionsThreshold >= MIN_OBJECTIONS_THRESHOLD && objectionsThreshold <= MAX_OBJECTIONS_THRESHOLD); + vm.assume(motionDuration >= MIN_MOTION_DURATION); + + // setup + vm.expectEmit(false, false, false, false); + emit MotionFactoryAdded(factory, objectionsThreshold, motionDuration); + // add factory + hoax(admin); + leanTrack.addMotionFactory(random, objectionsThreshold, motionDuration); + + // assert factory added + assertTrue(leanTrack.factories(random).isFactory); + assertEq(leanTrack.factories(random).motionDuration, motionDuration); + assertEq(leanTrack.factories(random).objectionsThreshold, objectionsThreshold); + + // setup + vm.expectRevert(bytes("!admin")); + + //execute + hoax(random); + leanTrack.removeMotionFactory(random); + } + + function testCannotRemoveFactoryThatDoesNotExist(address random) public { + vm.assume(!reserved[random]); + + // setup + vm.expectRevert(bytes("!factory_exists")); + + //execute + hoax(admin); + leanTrack.removeMotionFactory(random); + } + + function testOnlyAdminCanAddExecutor(address random) public { + vm.assume(!reserved[random]); + + // setup + vm.expectRevert(bytes("!admin")); + + //execute + hoax(random); + leanTrack.addExecutor(random); + } + + function testCannotAddExecutorTwice(address random) public { + vm.assume(!reserved[random]); + + // setup + vm.expectEmit(false, false, false, false); + emit ExecutorAdded(random); + + //execute + hoax(admin); + leanTrack.addExecutor(random); + + // setup + vm.expectRevert(bytes("!executor_exists")); + + //execute + hoax(admin); + leanTrack.addExecutor(random); + } + + function testShouldAddExecutor(address random) public { + vm.assume(!reserved[random]); + + // setup + vm.expectEmit(false, false, false, false); + emit ExecutorAdded(random); + + //execute + hoax(admin); + leanTrack.addExecutor(random); + + // assert + assertTrue(leanTrack.executors(random)); + } + + function testOnlyAdminCanRemoveExecutor(address random) public { + vm.assume(!reserved[random]); + + // setup + vm.expectRevert(bytes("!admin")); + + //execute + hoax(random); + leanTrack.removeExecutor(random); + } + + function testCannotRemoveExecutorThatDoesNotExist(address random) public { + vm.assume(!reserved[random]); + + // setup + vm.expectRevert(bytes("!executor_exists")); + + //execute + hoax(admin); + leanTrack.removeExecutor(random); + } + + function testShouldRemoveExecutor(address random) public { + vm.assume(!reserved[random]); + + // setup + vm.expectEmit(false, false, false, false); + emit ExecutorAdded(random); + + //execute + hoax(admin); + leanTrack.addExecutor(random); + + // assert + assertTrue(leanTrack.executors(random)); + + // setup + vm.expectEmit(false, false, false, false); + emit ExecutorRemoved(random); + + //execute + hoax(admin); + leanTrack.removeExecutor(random); + + // assert + assertTrue(!leanTrack.executors(random)); + } + + function testOnlyAdminCanSetKnight(address random) public { + vm.assume(!reserved[random]); + + // setup + vm.expectRevert(bytes("!admin")); + + //execute + hoax(random); + leanTrack.setKnight(random); + } + + function testKnightCannotBeAddressZero() public { + // setup + vm.expectRevert(bytes("!knight")); + + //execute + hoax(admin); + leanTrack.setKnight(address(0)); + } + + function testOnlyKnightCanPauseLeanTrack(address random) public { + vm.assume(!reserved[random]); + + // setup + vm.expectRevert(bytes("!knight")); + + //execute + hoax(random); + leanTrack.pause(); + } + + function testShouldPauseLeanTrack() public { + // setup + vm.expectEmit(false, false, false, false); + emit Paused(knight); + + //execute + hoax(knight); + leanTrack.pause(); + + // assert + assertTrue(leanTrack.paused()); + } + + + // MOTION TESTS + + function testCannotCreateMotionWithZeroOps() public { + // setup + address[] memory targets = new address[](0); + uint256[] memory values = new uint256[](0); + string[] memory signatures = new string[](0); + bytes[] memory calldatas = new bytes[](0); + vm.expectRevert(bytes("!no_targets")); + + //execute + hoax(factory); + leanTrack.createMotion(targets, values, signatures, calldatas); + } + + function testCannotCreateMotionWhenPaused() public { + // setup + hoax(knight); + leanTrack.pause(); + + vm.expectRevert(bytes("!paused")); + + //execute + hoax(factory); + leanTrack.createMotion(new address[](0), new uint256[](0), new string[](0), new bytes[](0)); + } + + function testCannotCreateMotionWithDifferentLenArrays() public { + // setup + address[] memory targets = new address[](1); + uint256[] memory values = new uint256[](2); + string[] memory signatures = new string[](1); + bytes[] memory calldatas = new bytes[](1); + targets[0] = address(token); + values[0] = 0; + values[1] = 0; + signatures[0] = ""; + calldatas[0] = abi.encodeWithSelector(IERC20.transfer.selector, grantee, transferAmount); + vm.expectRevert(bytes("!len_mismatch")); + + //execute + hoax(factory); + leanTrack.createMotion(targets, values, signatures, calldatas); + } + + function testCannotCreateMotionWithTooManyOperations() public { + // setup + address[] memory targets = new address[](MAX_OPERATIONS + 1); + uint256[] memory values = new uint256[](MAX_OPERATIONS + 1); + string[] memory signatures = new string[](MAX_OPERATIONS + 1); + bytes[] memory calldatas = new bytes[](MAX_OPERATIONS + 1); + for (uint i = 0; i < MAX_OPERATIONS + 1; i++) { + targets[i] = address(token); + values[i] = 0; + signatures[i] = ""; + calldatas[i] = abi.encodeWithSelector(IERC20.transfer.selector, grantee, transferAmount); + } + // vyper reverts + vm.expectRevert(); + + //execute + hoax(factory); + leanTrack.createMotion(targets, values, signatures, calldatas); + } + + function testShouldCreateMotion(uint8 operations) public { + vm.assume(operations > 0 && operations <= MAX_OPERATIONS); + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + bytes32[] memory hashes; + uint256 totalAmount; + + (targets, values, signatures, calldatas, hashes, totalAmount) = _createMotionTrxs(operations); + + // setup + vm.expectEmit(true, true, false, false); + emit MotionCreated( + 1, + factory, + targets, + values, + signatures, + calldatas, + block.timestamp + leanTrack.factories(factory).motionDuration, + block.number, + leanTrack.factories(factory).objectionsThreshold + ); + + //execute + hoax(factory); + uint256 motionId = leanTrack.createMotion(targets, values, signatures, calldatas); + + // assert + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.proposer, factory); + assertEq(motion.timeForQueue, block.timestamp + leanTrack.factories(factory).motionDuration); + assertEq(motion.objectionsThreshold, leanTrack.factories(factory).objectionsThreshold); + assertEq(motion.objections, 0); + assertEq(motion.targets.length, targets.length); + assertEq(motion.values.length, values.length); + assertEq(motion.signatures.length, signatures.length); + assertEq(motion.calldatas.length, calldatas.length); + for (uint i = 0; i < targets.length; i++) { + assertEq(motion.targets[i], targets[i]); + assertEq(motion.values[i], values[i]); + assertEq(motion.signatures[i], signatures[i]); + assertEq(motion.calldatas[i], calldatas[i]); + } + assertEq(leanTrack.lastMotionId(), 1); + } + + function testCannotQueueMotionWhenPaused(uint256 operations, address random) public { + vm.assume(operations > 0 && operations <= MAX_OPERATIONS); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(operations); + hoax(knight); + leanTrack.pause(); + vm.expectRevert(bytes("!paused")); + + //execute + hoax(random); + leanTrack.queueMotion(motionId); + } + + function testCannotQueueMotionBeforeEta(uint256 operations, address random) public { + vm.assume(operations > 0 && operations <= MAX_OPERATIONS); + vm.assume(!reserved[random]); + // setup + + uint256 motionId; + (motionId,) = _createMotion(operations); + vm.expectRevert(bytes("!timeForQueue")); + + //execute + hoax(random); + leanTrack.queueMotion(motionId); + } + + function testCannotQueueUnexistingMotion(uint256 operations, address random, uint256 unexistingMotiondId) public { + vm.assume(operations > 0 && operations <= MAX_OPERATIONS); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(operations); // motion 1 + vm.assume(unexistingMotiondId != motionId); + vm.expectRevert(bytes("!motion_exists")); + + //execute + hoax(random); + leanTrack.queueMotion(unexistingMotiondId); // doesnt exist + } + + function testShouldQueueMotion(uint256 operations, address random) public { + vm.assume(operations > 0 && operations <= MAX_OPERATIONS); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + uint256 totalAmount; + bytes32[] memory trxHashes; + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + // create test motion transactions + (targets, values, signatures, calldatas, trxHashes, totalAmount) = _createMotionTrxs(operations); + + hoax(factory); + motionId = leanTrack.createMotion(targets, values, signatures, calldatas); + + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + + vm.expectEmit(true, true, false, false); + emit MotionQueued(motionId, trxHashes, motion.timeForQueue + leanTrackDelay); + + vm.warp(motion.timeForQueue); //skip to time for queue + + //execute + hoax(random); + bytes32[] memory queuedTrxHashes = leanTrack.queueMotion(motionId); + + //assert motion has been queued and eta has been set + motion = leanTrack.motions(motionId); + assertEq(motion.isQueued, true); + assertEq(motion.eta, motion.timeForQueue + leanTrackDelay); + // check trx hashes where correctly queued + for (uint i = 0; i < trxHashes.length; i++) { + assertEq(trxHashes[i], queuedTrxHashes[i]); + assertEq(timelock.queuedRapidTransactions(queuedTrxHashes[i]), true); + } + // check that the motion is not queued again + assertEq(leanTrack.motions(motionId).isQueued, true); + } + + function testCannotQueueMotionTwice(uint256 operations, address random) public { + vm.assume(operations > 0 && operations <= MAX_OPERATIONS); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(operations); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + + vm.warp(motion.timeForQueue); //skip to eta + + //execute + hoax(random); + leanTrack.queueMotion(motionId); + + vm.expectRevert(bytes("!motion_queued")); + leanTrack.queueMotion(motionId); + } + + function testCannotEnactMotionWhenPaused(uint256 operations, address random) public { + vm.assume(operations > 0 && operations <= MAX_OPERATIONS); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(operations); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + + vm.warp(motion.timeForQueue); //skip to eta + + //execute + hoax(random); + leanTrack.queueMotion(motionId); + + hoax(knight); + leanTrack.pause(); + vm.expectRevert(bytes("!paused")); + + //execute + hoax(executor); + leanTrack.enactMotion(motionId); + } + + function testOnlyExecutorsCanCallEnactMotion(uint256 operations, address random) public { + vm.assume(operations > 0 && operations <= MAX_OPERATIONS); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(operations); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + + vm.warp(motion.timeForQueue); //skip to eta + + //execute + hoax(random); + leanTrack.queueMotion(motionId); + + vm.expectRevert(bytes("!executor")); + hoax(random); + leanTrack.enactMotion(motionId); + } + + function testCannotEnactUnexistingMotion(uint256 operations, address random, uint256 unexistingMotiondId) public { + vm.assume(operations > 0 && operations <= MAX_OPERATIONS); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(operations); // motion 1 + vm.assume(unexistingMotiondId != motionId); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + + vm.warp(motion.timeForQueue); //skip to eta + + //execute + hoax(random); + leanTrack.queueMotion(motionId); + + vm.expectRevert(bytes("!motion_exists")); + hoax(executor); + leanTrack.enactMotion(unexistingMotiondId); // doesnt exist + } + + function testCannotEnactMotionThatIsntQueued(uint256 operations, address random) public { + vm.assume(operations > 0 && operations <= MAX_OPERATIONS); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(operations); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + + vm.expectRevert(bytes("!motion_queued")); + hoax(executor); + leanTrack.enactMotion(motionId); + } + + function testCannotEnactMotionBeforeEta(uint256 operations, address random) public { + vm.assume(operations > 0 && operations <= MAX_OPERATIONS); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(operations); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + + vm.warp(motion.timeForQueue); //skip to eta + + //execute + hoax(random); + leanTrack.queueMotion(motionId); + + vm.expectRevert(bytes("!eta")); + hoax(executor); + leanTrack.enactMotion(motionId); // not enough time has passed since delay + } + + function testShouldEnactQueuedMotion(uint256 operations, address random) public { + vm.assume(operations > 0 && operations <= MAX_OPERATIONS); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + uint256 expectedAmount; + address[] memory targets; + uint256[] memory values; + string[] memory signatures; + bytes[] memory calldatas; + bytes32[] memory trxHashes; + uint256 eta = block.timestamp + factoryMotionDuration + leanTrackDelay; + (targets, values, signatures, calldatas, trxHashes, expectedAmount) = _createMotionTrxs(operations); + // create motion + hoax(factory); + motionId = leanTrack.createMotion(targets, values, signatures, calldatas); + + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + + vm.warp(motion.timeForQueue); //skip to eta + + // setup queue motion + hoax(random); + leanTrack.queueMotion(motionId); + + motion = leanTrack.motions(motionId); + assertTrue(motion.eta != 0); + assertEq(motion.eta, eta); + vm.warp(motion.eta); //skip to eta + + //execute + hoax(executor); + leanTrack.enactMotion(motionId); + + //assert motion has been enacted + motion = leanTrack.motions(motionId); + assertEq(motion.id, 0); + // check transaction was executed + assertEq(token.balanceOf(grantee), expectedAmount); + } + + function testCannotObjectToMotionThatDoesntExist(uint256 operations, address random, uint256 unexistingMotiondId) public { + vm.assume(operations > 0 && operations <= MAX_OPERATIONS); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(operations); // motion 1 + vm.assume(unexistingMotiondId != motionId); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + + vm.warp(motion.timeForQueue); //skip to eta + + //execute + hoax(random); + leanTrack.queueMotion(motionId); + + vm.expectRevert(bytes("!motion_exists")); + hoax(objectoor); + leanTrack.objectToMotion(unexistingMotiondId); // doesnt exist + } + + function testCannotObjectToQueuedMotion(uint256 operations, address random) public { + vm.assume(operations > 0 && operations <= MAX_OPERATIONS); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(operations); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + + vm.warp(motion.timeForQueue); //skip to eta + + //execute + hoax(random); + leanTrack.queueMotion(motionId); + + vm.expectRevert(bytes("!motion_queued")); + hoax(objectoor); + leanTrack.objectToMotion(motionId); // already queued + } + + function testCannotObjectToMotionAfterTimeForQueuePasses(uint256 operations, address random) public { + vm.assume(operations > 0 && operations <= MAX_OPERATIONS); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(operations); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + + vm.warp(motion.timeForQueue + 1); //skip to eta without queueing the motion + + vm.expectRevert(bytes("!timeForQueue")); + hoax(objectoor); + leanTrack.objectToMotion(motionId); // timeForQueue has passed + } + + function testShouldObjectToMotionWithVotingPowerLessThanThreshold(address random, uint256 votingBalance) public { + vm.assume(votingBalance > 0 && votingBalance < QUORUM_AMOUNT); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.objections, 0); + assertEq(motion.objectionsThreshold, QUORUM); + deal(address(token), random, votingBalance); + uint256 votingBalanceForObjector = token.balanceOf(random); + uint256 votingBalanceForObjectorPct = (votingBalanceForObjector * HUNDRED_PCT) / TOKEN_SUPPLY; + + // check for event + vm.expectEmit(false, false, false, false); + emit MotionObjected(motionId, random, votingBalanceForObjector, votingBalanceForObjector, votingBalanceForObjectorPct); + + //execute + hoax(random); + leanTrack.objectToMotion(motionId); + + //assert motion has correct amount of objections + motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.objections, votingBalanceForObjector); + } + + function testShouldUseLowerVotingBalanceForObjection(address random, uint256 votingBalance) public { + vm.assume(votingBalance > 0 && votingBalance < QUORUM_AMOUNT); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.objections, 0); + assertEq(motion.objectionsThreshold, QUORUM); + // setup voting power + govToken._setUseBalanceOfForVotingPower(false); + // has lower balance at the time of the motion snapshot block + govToken._setVotingPower(random, motion.snapshotBlock, votingBalance); + uint256 votingBalanceForObjector = govToken.getPriorVotes(random, motion.snapshotBlock); + uint256 votingBalanceForObjectorPct = (votingBalanceForObjector * HUNDRED_PCT) / TOKEN_SUPPLY; + + // skip block numbers + vm.roll(block.number + 2); + // give objector more voting power than Quorum at current block number + govToken._setVotingPower(random, block.number, QUORUM_AMOUNT + 1); + + // check for event + vm.expectEmit(false, false, false, false); + emit MotionObjected(motionId, random, votingBalanceForObjector, votingBalanceForObjector, votingBalanceForObjectorPct); + + //execute + hoax(random); + leanTrack.objectToMotion(motionId); + + //assert motion has correct amount of objections + motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + // should use lower voting balance + assertEq(motion.objections, votingBalanceForObjector); + } + + function testCannotObjectTwiceToSameMotion(address random, uint256 votingBalance) public { + vm.assume(votingBalance > 0 && votingBalance < QUORUM_AMOUNT); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.objections, 0); + assertEq(motion.objectionsThreshold, QUORUM); + deal(address(token), random, votingBalance); + uint256 votingBalanceForObjector = token.balanceOf(random); + uint256 votingBalanceForObjectorPct = (votingBalanceForObjector * HUNDRED_PCT) / TOKEN_SUPPLY; + + // check for event + vm.expectEmit(false, false, false, false); + emit MotionObjected(motionId, random, votingBalanceForObjector, votingBalanceForObjector, votingBalanceForObjectorPct); + + //execute + hoax(random); + leanTrack.objectToMotion(motionId); + + //assert motion has correct amount of objections + motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.objections, votingBalanceForObjector); + + vm.expectRevert(bytes("!already_objected")); + hoax(random); + leanTrack.objectToMotion(motionId); // already objected + } + + function testCannotObjectToMotionWithZeroVotingBalance(address random) public { + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.objections, 0); + assertEq(motion.objectionsThreshold, QUORUM); + + vm.expectRevert(bytes("!voting_balance")); + hoax(random); + leanTrack.objectToMotion(motionId); // zero voting balance + } + + function testShouldRejectMotionIfObjectionsReachedAboveThreshold(address random, uint256 votingBalance) public { + vm.assume(votingBalance >= QUORUM_AMOUNT && votingBalance < TOKEN_SUPPLY); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.objections, 0); + assertEq(motion.objectionsThreshold, QUORUM); + deal(address(token), random, votingBalance); + uint256 votingBalanceForObjector = token.balanceOf(random); + uint256 votingBalanceForObjectorPct = (votingBalanceForObjector * HUNDRED_PCT) / TOKEN_SUPPLY; + + // check for event + vm.expectEmit(false, false, false, false); + emit MotionObjected(motionId, random, votingBalanceForObjector, votingBalanceForObjector, votingBalanceForObjectorPct); + + // check for event + vm.expectEmit(false, false, false, false); + emit MotionRejected(motionId); + + //execute + hoax(random); // has more than quorum + leanTrack.objectToMotion(motionId); // should reject motion + + //assert motion has been deleted + motion = leanTrack.motions(motionId); + assertEq(motion.id, 0); + } + + function testRandomAcctCannotCanCancelMotion(address random) public { + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + + vm.expectRevert(bytes("!access")); + hoax(random); + leanTrack.cancelMotion(motionId); + } + + function testCannotCancelUnexistingMotion(address random) public { + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.proposer, factory); + + vm.expectRevert(bytes("!motion_exists")); + hoax(factory); + leanTrack.cancelMotion(motionId + 1); + } + + function testProposerCanCancelMotionBeforeQueued(address random) public { + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.proposer, factory); + + // check for event + vm.expectEmit(false, false, false, false); + emit MotionCanceled(motionId); + + //execute + hoax(factory); + leanTrack.cancelMotion(motionId); + + //assert motion has been deleted + motion = leanTrack.motions(motionId); + assertEq(motion.id, 0); + } + + function testKnightCanCancelMotionBeforeQueued(address random) public { + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.proposer, factory); + + // check for event + vm.expectEmit(false, false, false, false); + emit MotionCanceled(motionId); + + //execute + hoax(knight); + leanTrack.cancelMotion(motionId); + + //assert motion has been deleted + motion = leanTrack.motions(motionId); + assertEq(motion.id, 0); + } + + function testProposerCanCancelMotionAfterBeingQueued() public { + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.proposer, factory); + + // skip to timeForQueue + vm.warp(motion.timeForQueue); + + hoax(factory); + bytes32[] memory queuedTrxHashes = leanTrack.queueMotion(motionId); + + //assert trxHashes are queued in timelock + for (uint256 i = 0; i < queuedTrxHashes.length; i++) { + assertTrue(timelock.queuedRapidTransactions(queuedTrxHashes[i])); + } + + // check for event + vm.expectEmit(false, false, false, false); + emit MotionCanceled(motionId); + + //execute + hoax(factory); + leanTrack.cancelMotion(motionId); + + //assert motion has been deleted + motion = leanTrack.motions(motionId); + assertEq(motion.id, 0); + + //assert trxHashes are no longer queued in timelock + for (uint256 i = 0; i < queuedTrxHashes.length; i++) { + assertFalse(timelock.queuedRapidTransactions(queuedTrxHashes[i])); + } + } + + function testKnightCanCancelMotionAfterBeingQueued() public { + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.proposer, factory); + + // skip to timeForQueue + vm.warp(motion.timeForQueue); + + hoax(factory); + bytes32[] memory queuedTrxHashes = leanTrack.queueMotion(motionId); + + //assert trxHashes are queued in timelock + for (uint256 i = 0; i < queuedTrxHashes.length; i++) { + assertTrue(timelock.queuedRapidTransactions(queuedTrxHashes[i])); + } + + // check for event + vm.expectEmit(false, false, false, false); + emit MotionCanceled(motionId); + + //execute + hoax(knight); + leanTrack.cancelMotion(motionId); + + //assert motion has been deleted + motion = leanTrack.motions(motionId); + assertEq(motion.id, 0); + + //assert trxHashes are no longer queued in timelock + for (uint256 i = 0; i < queuedTrxHashes.length; i++) { + assertFalse(timelock.queuedRapidTransactions(queuedTrxHashes[i])); + } + } + + function testCanObjectToMotionReturnsTrue(address random, uint256 votingBalance) public { + vm.assume(votingBalance >= QUORUM_AMOUNT && votingBalance < TOKEN_SUPPLY); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.objections, 0); + assertEq(motion.objectionsThreshold, QUORUM); + deal(address(token), random, votingBalance); + + assertTrue(leanTrack.canObjectToMotion(motionId, random)); + } + + function testCanObjectToMotionReturnsFalseIfMotionDoesntExist(address random, uint256 votingBalance) public { + vm.assume(votingBalance > 0 && votingBalance < QUORUM_AMOUNT); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + + assertFalse(leanTrack.canObjectToMotion(motionId + 1, random)); + } + + function testCanObjectToMotionReturnsFalseIfMotionIsQueued(address random, uint256 votingBalance) public { + vm.assume(votingBalance > 0 && votingBalance < QUORUM_AMOUNT); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + deal(address(token), random, votingBalance); + + // skip to time when motion is queued + vm.warp(motion.timeForQueue); + + //execute + leanTrack.queueMotion(motionId); + + assertFalse(leanTrack.canObjectToMotion(motionId, random)); + } + + function testCanObjectToMotionReturnsFalseIfMotionTimeForQueueHasPassed(address random, uint256 votingBalance) public { + vm.assume(votingBalance > 0 && votingBalance < QUORUM_AMOUNT); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + deal(address(token), random, votingBalance); + + // skip to time when motion is queued + vm.warp(motion.timeForQueue + 1); + + assertFalse(leanTrack.canObjectToMotion(motionId, random)); + } + + function testCanObjectToMotionReturnsFalseIfObjectorAlreadyObjected(address random, uint256 votingBalance) public { + vm.assume(votingBalance > 0 && votingBalance < QUORUM_AMOUNT); + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.objections, 0); + assertEq(motion.objectionsThreshold, QUORUM); + deal(address(token), random, votingBalance); + uint256 votingBalanceForObjector = token.balanceOf(random); + uint256 votingBalanceForObjectorPct = (votingBalanceForObjector * HUNDRED_PCT) / TOKEN_SUPPLY; + + // check for event + vm.expectEmit(false, false, false, false); + emit MotionObjected(motionId, random, votingBalanceForObjector, votingBalanceForObjector, votingBalanceForObjectorPct); + + //execute + hoax(random); // has more than quorum + leanTrack.objectToMotion(motionId); // should reject motion + + //assert motion objections were counted + motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.objections, votingBalanceForObjector); + + assertFalse(leanTrack.canObjectToMotion(motionId, random)); + } + + function testCanObjectToMotionReturnsFalseIfVotingBalanceIsZero(address random) public { + vm.assume(!reserved[random]); + // setup + uint256 motionId; + (motionId,) = _createMotion(1); + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + assertEq(motion.objections, 0); + assertEq(token.balanceOf(random), 0); + + assertFalse(leanTrack.canObjectToMotion(motionId, random)); + } + + + function _createMotionTrxs(uint256 operations) private view returns ( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + bytes32[] memory trxHashes, + uint256 totalAmount + ) { + targets = new address[](operations); + values = new uint256[](operations); + signatures = new string[](operations); + calldatas = new bytes[](operations); + trxHashes = new bytes32[](operations); + uint256 eta = block.timestamp + factoryMotionDuration + leanTrackDelay; + for (uint i = 0; i < operations; i++) { + targets[i] = address(token); + values[i] = 0; + signatures[i] = ""; + // vary the amount of tokens to transfer to avoid hash collision + calldatas[i] = abi.encodeWithSelector(IERC20.transfer.selector, grantee, transferAmount + i); + totalAmount += transferAmount + i; + trxHashes[i] = keccak256(abi.encode(targets[i], values[i], signatures[i], calldatas[i], eta)); + } + return (targets, values, signatures, calldatas, trxHashes, totalAmount); + } + + function _createMotion(uint256 operations) private returns (uint256, bytes32[] memory) { + address [] memory targets; + uint256 [] memory values; + string[] memory signatures; + bytes[] memory calldatas; + bytes32[] memory trxHashes; + uint256 totalAmount; + (targets, values, signatures, calldatas, trxHashes, totalAmount) = _createMotionTrxs(operations); + + hoax(factory); + uint256 motionId = leanTrack.createMotion(targets, values, signatures, calldatas); + + return (motionId, trxHashes); + } +} diff --git a/foundry_test/interfaces/DualTimelock.sol b/foundry_test/interfaces/DualTimelock.sol index 317b670..ac73fbf 100644 --- a/foundry_test/interfaces/DualTimelock.sol +++ b/foundry_test/interfaces/DualTimelock.sol @@ -24,14 +24,14 @@ interface DualTimelock { function executeTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external payable returns (bytes memory); // DualTimelock specific functions - function fastTrack() external view returns (address); - function pendingFastTrack() external view returns (address); - function fastTrackDelay() external view returns (uint); - function setFastTrackDelay(uint256 newDelay) external; - function acceptFastTrack() external; - function setPendingFastTrack(address pendingFastTrack) external; - function queuedFastTransactions(bytes32 hash) external view returns (bool); - function queueFastTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external returns (bytes32); - function cancelFastTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external; - function executeFastTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external payable returns (bytes memory); + function leanTrack() external view returns (address); + function pendingLeanTrack() external view returns (address); + function leanTrackDelay() external view returns (uint); + function setLeanTrackDelay(uint256 newDelay) external; + function acceptLeanTrack() external; + function setPendingLeanTrack(address pendingLeanTrack) external; + function queuedRapidTransactions(bytes32 hash) external view returns (bool); + function queueRapidTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external returns (bytes32); + function cancelRapidTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external; + function executeRapidTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) external payable returns (bytes memory); } \ No newline at end of file diff --git a/foundry_test/interfaces/LeanTrack.sol b/foundry_test/interfaces/LeanTrack.sol new file mode 100644 index 0000000..aa12932 --- /dev/null +++ b/foundry_test/interfaces/LeanTrack.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: AGPL +pragma solidity ^0.8.16; + +// struct for motion factory settings +struct Factory { + uint256 objectionsThreshold; + uint256 motionDuration; + bool isFactory; +} +// struct for a motion +struct Motion { + uint256 id; + address proposer; + address[] targets; + uint256[] values; + string[] signatures; + bytes[] calldatas; + uint256 timeForQueue; + uint256 snapshotBlock; + uint256 objections; + uint256 objectionsThreshold; + uint256 eta; + bool isQueued; +} + +interface LeanTrack { + // view functions + function admin() external view returns (address); + function pendingAdmin() external view returns (address); + function token() external view returns (address); + function factories(address) external view returns (Factory memory); + function motions(uint256) external view returns (Motion memory); + function lastMotionId() external view returns (uint256); + function executors(address) external view returns (bool); + function timelock() external view returns (address); + function paused() external view returns (bool); + function knight() external view returns (address); + function canObjectToMotion(uint256 motionId, address objector) external view returns (bool); + + // non-view functions + function acceptTimelockAccess() external; + function setKnight(address knight) external; + function pause() external; + function unpause() external; + function addMotionFactory(address factory, uint256 objectionThreshold, uint256 motionDuration) external; + function removeMotionFactory(address factory) external; + function setMotionFactorySettings(address factory, uint256 objectionThreshold, uint256 motionDuration) external; + function addExecutor(address executor) external; + function removeExecutor(address executor) external; + function createMotion(address[] memory targets, uint256[] memory values, string[] memory signatures, bytes[] memory calldatas) external returns (uint256); + function queueMotion(uint256 motionId) external returns (bytes32[] memory); + function enactMotion(uint256 motionId) external; + function cancelMotion(uint256 motionId) external; + function objectToMotion(uint256 motionId) external; +} diff --git a/foundry_test/utils/GovToken.sol b/foundry_test/utils/GovToken.sol index 9baf162..8a7d940 100644 --- a/foundry_test/utils/GovToken.sol +++ b/foundry_test/utils/GovToken.sol @@ -45,5 +45,10 @@ contract GovToken is ERC20 { return votingPower[account][blockNumber]; } + function totalSupplyAt(uint blockNumber) external view returns (uint256) { + blockNumber; // silence warning + return totalSupply(); + } + } \ No newline at end of file diff --git a/src/DualTimelock.vy b/src/DualTimelock.vy index e023b0f..7fbf0df 100644 --- a/src/DualTimelock.vy +++ b/src/DualTimelock.vy @@ -19,20 +19,20 @@ event NewAdmin: newAdmin: indexed(address) -event NewFastTrack: - newFastTrack: indexed(address) +event NewLeanTrack: + newLeanTrack: indexed(address) event NewPendingAdmin: newPendingAdmin: indexed(address) -event NewPendingFastTrack: - newPendingFastTrack: indexed(address) +event NewPendingLeanTrack: + newPendingLeanTrack: indexed(address) event NewDelay: newDelay: uint256 -event NewFastTrackDelay: - newFastTrackDelay: uint256 +event NewLeanTrackDelay: + newLeanTrackDelay: uint256 event CancelTransaction: txHash: indexed(bytes32) @@ -58,7 +58,7 @@ event QueueTransaction: data: Bytes[CALL_DATA_LEN] eta: uint256 -event QueueFastTransaction: +event QueueRapidTransaction: txHash: indexed(bytes32) target: indexed(address) value: uint256 @@ -66,7 +66,7 @@ event QueueFastTransaction: data: Bytes[CALL_DATA_LEN] eta: uint256 -event CancelFastTransaction: +event CancelRapidTransaction: txHash: indexed(bytes32) target: indexed(address) value: uint256 @@ -74,7 +74,7 @@ event CancelFastTransaction: data: Bytes[CALL_DATA_LEN] eta: uint256 -event ExecuteFastTransaction: +event ExecuteRapidTransaction: txHash: indexed(bytes32) target: indexed(address) value: uint256 @@ -95,29 +95,29 @@ pendingAdmin: public(address) delay: public(uint256) queuedTransactions: public(HashMap[bytes32, bool]) -fastTrack: public(address) -pendingFastTrack: public(address) -fastTrackDelay: public(uint256) -queuedFastTransactions: public(HashMap[bytes32, bool]) +leanTrack: public(address) +pendingLeanTrack: public(address) +leanTrackDelay: public(uint256) +queuedRapidTransactions: public(HashMap[bytes32, bool]) @external -def __init__(admin: address, fastTrack: address, delay: uint256, fastTrackDelay: uint256): +def __init__(admin: address, leanTrack: address, delay: uint256, leanTrackDelay: uint256): """ @notice Deploys the timelock with initial values @param admin The contract that rules over the timelock - @param fastTrack The contract that rules over the fast track queued transactions. Can be 0x0. + @param leanTrack The contract that rules over the lean track queued transactions. Can be 0x0. @param delay The delay for timelock - @param fastTrackDelay The delay for fast track timelock + @param leanTrackDelay The delay for lean track timelock """ assert delay >= MINIMUM_DELAY, "Delay must exceed minimum delay" assert delay <= MAXIMUM_DELAY, "Delay must not exceed maximum delay" - assert delay > fastTrackDelay, "Delay must be greater than fast track delay" + assert delay > leanTrackDelay, "Delay must be greater than lean track delay" assert admin != empty(address), "!admin" self.admin = admin - self.fastTrack = fastTrack + self.leanTrack = leanTrack self.delay = delay - self.fastTrackDelay = fastTrackDelay + self.leanTrackDelay = leanTrackDelay @external @@ -140,17 +140,17 @@ def setDelay(delay: uint256): log NewDelay(delay) @external -def setFastTrackDelay(fastTrackDelay: uint256): +def setLeanTrackDelay(leanTrackDelay: uint256): """ @notice - Updates fast track delay to new value - @param fastTrackDelay The delay for fast track timelock + Updates lean track delay to new value + @param leanTrackDelay The delay for lean track timelock """ assert msg.sender == self, "!Timelock" - assert fastTrackDelay < self.delay, "!fastTrackDelay < delay" - self.fastTrackDelay = fastTrackDelay + assert leanTrackDelay < self.delay, "!leanTrackDelay < delay" + self.leanTrackDelay = leanTrackDelay - log NewFastTrackDelay(fastTrackDelay) + log NewLeanTrackDelay(leanTrackDelay) @external def acceptAdmin(): @@ -180,31 +180,31 @@ def setPendingAdmin(pendingAdmin: address): log NewPendingAdmin(pendingAdmin) @external -def acceptFastTrack(): +def acceptLeanTrack(): """ @notice - updates `pendingFastTrack` to fastTrack. - msg.sender must be `pendingFastTrack` + updates `pendingLeanTrack` to leanTrack. + msg.sender must be `pendingLeanTrack` """ - assert msg.sender == self.pendingFastTrack, "!pendingFastTrack" - self.fastTrack = msg.sender - self.pendingFastTrack = empty(address) - log NewFastTrack(msg.sender) - log NewPendingFastTrack(empty(address)) + assert msg.sender == self.pendingLeanTrack, "!pendingLeanTrack" + self.leanTrack = msg.sender + self.pendingLeanTrack = empty(address) + log NewLeanTrack(msg.sender) + log NewPendingLeanTrack(empty(address)) @external -def setPendingFastTrack(pendingFastTrack: address): +def setPendingLeanTrack(pendingLeanTrack: address): """ @notice - Updates `pendingFastTrack` value + Updates `pendingLeanTrack` value msg.sender must be this contract - @param pendingFastTrack The proposed new fast track contract for the contract + @param pendingLeanTrack The proposed new lean track contract for the contract """ assert msg.sender == self, "!Timelock" - self.pendingFastTrack = pendingFastTrack + self.pendingLeanTrack = pendingLeanTrack - log NewPendingFastTrack(pendingFastTrack) + log NewPendingLeanTrack(pendingLeanTrack) @external def queueTransaction( @@ -317,7 +317,7 @@ def executeTransaction( return response @external -def queueFastTransaction( +def queueRapidTransaction( target: address, amount: uint256, signature: String[METHOD_SIG_SIZE], @@ -326,8 +326,8 @@ def queueFastTransaction( ) -> bytes32: """ @notice - adds transaction to fast execution queue - fast execution queue cannot target this timelock contract + adds transaction to rapid execution queue + rapid execution queue cannot target this timelock contract @param target The address of the contract to execute @param amount The amount of ether to send to the contract @param signature The signature of the function to execute @@ -336,19 +336,23 @@ def queueFastTransaction( @return txHash The hash of the transaction """ - assert msg.sender == self.fastTrack, "!fastTrack" - assert target != self, "!self" - assert eta >= block.timestamp + self.fastTrackDelay, "!eta" + # @dev minor gas savings + leanTrack: address = self.leanTrack + assert msg.sender == leanTrack, "!leanTrack" + assert target != leanTrack, "!target" + assert target != self, "!target" + assert target != self.admin, "!target" + assert eta >= block.timestamp + self.leanTrackDelay, "!eta" trxHash: bytes32 = keccak256(_abi_encode(target, amount, signature, data, eta)) - self.queuedFastTransactions[trxHash] = True + self.queuedRapidTransactions[trxHash] = True - log QueueFastTransaction(trxHash, target, amount, signature, data, eta) + log QueueRapidTransaction(trxHash, target, amount, signature, data, eta) return trxHash @external -def cancelFastTransaction( +def cancelRapidTransaction( target: address, amount: uint256, signature: String[METHOD_SIG_SIZE], @@ -357,23 +361,23 @@ def cancelFastTransaction( ): """ @notice - cancels a queued fast transaction + cancels a queued rapid transaction @param target The address of the contract to execute @param amount The amount of ether to send to the contract @param signature The signature of the function to execute @param data The data to send to the contract @param eta The timestamp when the transaction can be executed """ - assert msg.sender == self.fastTrack, "!fastTrack" + assert msg.sender == self.leanTrack, "!leanTrack" trxHash: bytes32 = keccak256(_abi_encode(target, amount, signature, data, eta)) - self.queuedFastTransactions[trxHash] = False + self.queuedRapidTransactions[trxHash] = False - log CancelFastTransaction(trxHash, target, amount, signature, data, eta) + log CancelRapidTransaction(trxHash, target, amount, signature, data, eta) @payable @external -def executeFastTransaction( +def executeRapidTransaction( target: address, amount: uint256, signature: String[METHOD_SIG_SIZE], @@ -382,7 +386,7 @@ def executeFastTransaction( ) -> Bytes[MAX_DATA_LEN]: """ @notice - executes a queued fast transaction + executes a queued rapid transaction @param target The address of the contract to execute @param amount The amount of ether to send to the contract @param signature The signature of the function to execute @@ -391,14 +395,14 @@ def executeFastTransaction( @return response The response from the transaction """ - assert msg.sender == self.fastTrack, "!fastTrack" + assert msg.sender == self.leanTrack, "!leanTrack" trxHash: bytes32 = keccak256(_abi_encode(target, amount, signature, data, eta)) - assert self.queuedFastTransactions[trxHash], "!queued_trx" + assert self.queuedRapidTransactions[trxHash], "!queued_trx" assert block.timestamp >= eta, "!eta" assert block.timestamp <= eta + GRACE_PERIOD, "!staled_trx" - self.queuedFastTransactions[trxHash] = False + self.queuedRapidTransactions[trxHash] = False callData: Bytes[MAX_DATA_LEN] = b"" @@ -424,7 +428,7 @@ def executeFastTransaction( assert success, "!trx_revert" - log ExecuteFastTransaction(trxHash, target, amount, signature, data, eta) + log ExecuteRapidTransaction(trxHash, target, amount, signature, data, eta) return response diff --git a/src/LeanTrack.vy b/src/LeanTrack.vy new file mode 100644 index 0000000..ee7db6e --- /dev/null +++ b/src/LeanTrack.vy @@ -0,0 +1,538 @@ +# @version 0.3.7 + +""" +@title Yearn LeanTrack an optimistic governance contract +@license GNU AGPLv3 +@author yearn.finance +@notice + A vyper implementation of on-chain optimistic governance contract for motion proposals and management of smart contract calls. +""" + +NAME: constant(String[20]) = "LeanTrack" +# buffer for string descriptions. Can use ipfshash +STR_LEN: constant(uint256) = 4000 +# these values are reasonable estimates from historical onchain data of compound and other gov systems +MAX_DATA_LEN: constant(uint256) = 16608 +CALL_DATA_LEN: constant(uint256) = 16483 +METHOD_SIG_SIZE: constant(uint256) = 1024 +# @notice The maximum number of operations in a motion +MAX_POSSIBLE_OPERATIONS: constant(uint256) = 10 +# @notice lower bound for objection threshold settings +# @dev represented in basis points (1% = 100) +MIN_OBJECTIONS_THRESHOLD: constant(uint256) = 100 +# @notice upper bound for objections threshold settings +# @dev represented in basis points (30% = 3000) +MAX_OBJECTIONS_THRESHOLD: constant(uint256) = 3000 +MIN_MOTION_DURATION: constant(uint256) = 57600 # 16 hours +HUNDRED_PERCENT: constant(uint256) = 10000 # 100% + +### interfaces + +# @dev compatible interface for DualTimelock implementations +# @dev DualTimelock is a contract that can queue and execute transactions. Should be possible to change interface to common timelock interfaces +interface DualTimelock: + def leanTrackDelay() -> uint256: view + def acceptLeanTrack() : nonpayable + def queuedRapidTransactions(hash: bytes32) -> bool: view + def queueRapidTransaction(target: address, amount: uint256, signature: String[METHOD_SIG_SIZE], data: Bytes[CALL_DATA_LEN], eta: uint256) -> bytes32: nonpayable + def cancelRapidTransaction(target: address, amount: uint256, signature: String[METHOD_SIG_SIZE], data: Bytes[CALL_DATA_LEN], eta: uint256): nonpayable + def executeRapidTransaction(target: address, amount: uint256, signature: String[METHOD_SIG_SIZE], data: Bytes[CALL_DATA_LEN], eta: uint256) -> Bytes[MAX_DATA_LEN]: payable + +# @dev Comp compatible interface to get Voting weight of account at block number. Some tokens implement 'balanceOfAt' but this call can be adapted to integrate with 'balanceOfAt' +interface GovToken: + def getPriorVotes(account: address, blockNumber: uint256) -> uint256:view + def totalSupplyAt(blockNumber: uint256) -> uint256: view + +### structs + +# @notice A struct to represent a Factory Settings +struct Factory: + # @notice The objections threshold for the factory proposed motions + objectionsThreshold: uint256 + # @notice the minimum time in seconds that must pass before the factory motions can be queued + motionDuration: uint256 + # @notice is factory flag + isFactory: bool + +# @notice A struct to represent a motion +struct Motion: + # @notice The id of the motion + id: uint256 + # @notice The address of the proposer + proposer: address + # @notice The ordered list of target addresses for calls to be made in motion + targets: DynArray[address, MAX_POSSIBLE_OPERATIONS] + # @notice The ordered list of values (i.e. msg.value) to be passed to the calls to be made in motion + values: DynArray[uint256, MAX_POSSIBLE_OPERATIONS] + # @notice The ordered list of function signatures to be called in motion + signatures: DynArray[String[METHOD_SIG_SIZE], MAX_POSSIBLE_OPERATIONS] + # @notice The ordered list of calldatas to be passed to each call to be made in motion + calldatas: DynArray[Bytes[CALL_DATA_LEN], MAX_POSSIBLE_OPERATIONS] + # @notice The block.timestamp when the motion can be queued to the timelock + timeForQueue: uint256 + # @notice The block number at which the motion was created + snapshotBlock: uint256 + # @notice The number of objections against the motion + objections: uint256 + # @notice The objection threshold to defeat the motion + objectionsThreshold: uint256 + # @notice The timestamp for when the motion can be executed in timelock + eta: uint256 + # @notice The flag to indicate if the motion has been queued to the timelock + isQueued: bool + + +# ///// EVENTS ///// +event MotionFactoryAdded: + factory: indexed(address) + objectionThreshold: uint256 + motionDuration: uint256 + +event MotionFactoryRemoved: + factory: indexed(address) + +event ExecutorAdded: + executor: indexed(address) + +event ExecutorRemoved: + executor: indexed(address) + +event Paused: + account: indexed(address) + +event Unpaused: + account: indexed(address) + +event KnightSet: + knight: indexed(address) + +event MotionCreated: + motionId: indexed(uint256) + proposer: indexed(address) + targets: DynArray[address, MAX_POSSIBLE_OPERATIONS] + values: DynArray[uint256, MAX_POSSIBLE_OPERATIONS] + signatures: DynArray[String[METHOD_SIG_SIZE], MAX_POSSIBLE_OPERATIONS] + calldatas: DynArray[Bytes[CALL_DATA_LEN], MAX_POSSIBLE_OPERATIONS] + timeForQueue: uint256 + snapshotBlock: uint256 + objectionThreshold: uint256 + +event MotionQueued: + motionId: indexed(uint256) + trxHashes: DynArray[bytes32, MAX_POSSIBLE_OPERATIONS] + eta: uint256 + +event MotionEnacted: + motionId: indexed(uint256) + +event MotionObjected: + motionId: indexed(uint256) + objector: indexed(address) + objectorBalance: uint256 + newObjectionsAmount: uint256 + newObjectionsAmountPct: uint256 + +event MotionRejected: + motionId: indexed(uint256) + +event MotionCanceled: + motionId: indexed(uint256) + +### state fields +# @notice The address of the admin +admin: public(address) +# @notice The address of the pending admin +pendingAdmin: public(address) +# @notice The address of the guardian role +knight: public(address) +# @notice Boolean flag to indicate if the contract is paused +paused: public(bool) +# @notice The address of the governance token +token: public(address) +# @notice The address of the timelock +timelock: public(address) +# @notice the last motion id +lastMotionId: public(uint256) +# @notice motions Id => Motion +motions: public(HashMap[uint256, Motion]) +# @notice stores if motion with given id has been object from given address +objections: public(HashMap[uint256, HashMap[address, bool]]) +# @notice factories addresses => Factory +factories: public(HashMap[address, Factory]) +# @notice allowed executors for queued motions +executors: public(HashMap[address, bool]) + +@external +def __init__( + governanceToken: address, + admin: address, + timelock: address, + knight: address +): + """ + @notice + The constructor sets the initial admin and token address. + @param governanceToken: The address of the governance token + @param admin: The address of the admin + @param timelock: The address of the timelock this contract interacts with + """ + + self.admin = admin + self.token = governanceToken + self.timelock = timelock + self.knight = knight + + +@external +def createMotion( + targets: DynArray[address, MAX_POSSIBLE_OPERATIONS], + values: DynArray[uint256, MAX_POSSIBLE_OPERATIONS], + signatures: DynArray[String[METHOD_SIG_SIZE], MAX_POSSIBLE_OPERATIONS], + calldatas: DynArray[Bytes[CALL_DATA_LEN], MAX_POSSIBLE_OPERATIONS] +) -> uint256: + """ + @notice + Create a motion to execute a series of transactions. + @param targets: The addresses of the contracts to call + @param values: The values to send with the transactions + @param signatures: The function signatures of the transactions + @param calldatas: The calldatas of the transactions + + @return motionId: The id of the motion + """ + assert not self.paused, "!paused" + assert len(targets) != 0, "!no_targets" + assert len(targets) <= MAX_POSSIBLE_OPERATIONS, "!too_many_ops" + assert len(targets) == len(values) and len(targets) == len(signatures) and len(targets) == len(calldatas), "!len_mismatch" + assert self.factories[msg.sender].isFactory, "!factory" + + # TODO: add motions limit check + self.lastMotionId += 1 + motionId: uint256 = self.lastMotionId + + motionDuration: uint256 = self.factories[msg.sender].motionDuration + objectionsThreshold: uint256 = self.factories[msg.sender].objectionsThreshold + + motion: Motion = Motion({ + id: motionId, + proposer: msg.sender, + targets: targets, + values: values, + signatures: signatures, + calldatas: calldatas, + timeForQueue: block.timestamp + motionDuration, + snapshotBlock: block.number, + objections: 0, + objectionsThreshold: objectionsThreshold, + eta: 0, + isQueued: False + }) + + self.motions[motionId] = motion + + log MotionCreated( + motionId, + msg.sender, + targets, + values, + signatures, + calldatas, + motion.timeForQueue, + motion.snapshotBlock, + objectionsThreshold + ) + + return motionId + +@external +def queueMotion(motionId: uint256)-> DynArray[bytes32, MAX_POSSIBLE_OPERATIONS]: + """ + @notice + Send motion transactions to be queued in the timelock. + Queue will fail if operation arguments are repeated or already in timelock queue. + @param motionId: The id of the motion + """ + assert not self.paused, "!paused" + motion: Motion = self.motions[motionId] + assert motion.id != 0, "!motion_exists" + assert motion.isQueued == False, "!motion_queued" + assert motion.timeForQueue <= block.timestamp, "!timeForQueue" + + eta: uint256 = block.timestamp + DualTimelock(self.timelock).leanTrackDelay() + + trxHashes: DynArray[bytes32, MAX_POSSIBLE_OPERATIONS] = [] + + numOperations: uint256 = len(motion.targets) + + for i in range(MAX_POSSIBLE_OPERATIONS): + if i >= numOperations: + break + # check hash doesnt exist already in timelock + localHash: bytes32 = keccak256(_abi_encode(motion.targets[i], motion.values[i], motion.signatures[i], motion.calldatas[i], eta)) + assert not DualTimelock(self.timelock).queuedRapidTransactions(localHash), "!trxHash_exists" + trxHash: bytes32 = DualTimelock(self.timelock).queueRapidTransaction( + motion.targets[i], + motion.values[i], + motion.signatures[i], + motion.calldatas[i], + eta + ) + + trxHashes.append(trxHash) + # check motion as queued and set eta + self.motions[motionId].isQueued = True + self.motions[motionId].eta = eta + + log MotionQueued(motionId, trxHashes, eta) + + return trxHashes + +@external +def enactMotion(motionId: uint256): + """ + @notice + Enact an already queued motion to execute a series of transactions. + @param motionId: The id of the motion + """ + assert not self.paused, "!paused" + assert self.executors[msg.sender], "!executor" + motion: Motion = self.motions[motionId] + assert motion.id != 0, "!motion_exists" + assert motion.isQueued == True, "!motion_queued" + assert motion.eta <= block.timestamp, "!eta" + + numOperations: uint256 = len(motion.targets) + for i in range(MAX_POSSIBLE_OPERATIONS): + if i >= numOperations: + break + DualTimelock(self.timelock).executeRapidTransaction( + motion.targets[i], + motion.values[i], + motion.signatures[i], + motion.calldatas[i], + motion.eta + ) + + # delete motion + self.motions[motionId] = empty(Motion) + + log MotionEnacted(motionId) + +@external +def objectToMotion(motionId: uint256): + """ + @notice + Submits an objection to a motion from a "governanceToken" holder with voting power. + @dev + The motion must exist. + The motion must be in the "pending" state. + The sender must not have already objected. + The sender must have voting power. + @param motionId: The id of the motion + """ + motion: Motion = self.motions[motionId] + assert motion.id != 0, "!motion_exists" + assert motion.isQueued == False, "!motion_queued" + assert motion.timeForQueue > block.timestamp, "!timeForQueue" + assert not self.objections[motionId][msg.sender], "!already_objected" + # check voting balance at motion snapshot block and compare to current block number and use the lower one + votingBalance: uint256 = min( + GovToken(self.token).getPriorVotes(msg.sender, motion.snapshotBlock), + GovToken(self.token).getPriorVotes(msg.sender, block.number) + ) + assert votingBalance > 0, "!voting_balance" + totalSupply: uint256 = GovToken(self.token).totalSupplyAt(motion.snapshotBlock) + newObjectionsAmount: uint256 = motion.objections + votingBalance + newObjectionsAmountPct: uint256 = (newObjectionsAmount * HUNDRED_PERCENT) / totalSupply + log MotionObjected(motionId, msg.sender, votingBalance, newObjectionsAmount, newObjectionsAmountPct) + + # update motion objections or delete motion if objections threshold is reached + if newObjectionsAmountPct >= motion.objectionsThreshold: + self.motions[motionId] = empty(Motion) + log MotionRejected(motionId) + else: + self.motions[motionId].objections = newObjectionsAmount + self.objections[motionId][msg.sender] = True + +@external +def cancelMotion(motionId: uint256): + """ + @notice + Cancels a motion. + @dev + The motion must exist. + The motion must be in the "pending" state. + The sender must be the proposer of the motion or the guardian role. + @param motionId: The id of the motion + """ + motion: Motion = self.motions[motionId] + assert motion.id != 0, "!motion_exists" + # only guardian or proposer can cancel motion + assert msg.sender == self.knight or msg.sender == motion.proposer, "!access" + + # if motion is queued, cancel it in timelock + if motion.isQueued: + numOperations: uint256 = len(motion.targets) + for i in range(MAX_POSSIBLE_OPERATIONS): + if i >= numOperations: + break + DualTimelock(self.timelock).cancelRapidTransaction( + motion.targets[i], + motion.values[i], + motion.signatures[i], + motion.calldatas[i], + motion.eta + ) + + # delete motion + self.motions[motionId] = empty(Motion) + + log MotionCanceled(motionId) + +@external +def addMotionFactory( + factory: address, + objectionsThreshold: uint256, + motionDuration: uint256 +): + """ + @notice + Add a factory to the list of approved factories. + @param factory: The address of the factory + @param objectionsThreshold: The objections threshold for the factory proposed motions + @param motionDuration: The duration for the factory motions to be queued + """ + assert msg.sender == self.admin, "!admin" + assert not self.factories[factory].isFactory, "!factory_exists" + assert motionDuration >= MIN_MOTION_DURATION, "!motion_duration" + assert objectionsThreshold >= MIN_OBJECTIONS_THRESHOLD, "!min_objections_threshold" + assert objectionsThreshold <= MAX_OBJECTIONS_THRESHOLD, "!max_objections_threshold" + + self.factories[factory] = Factory({ + objectionsThreshold: objectionsThreshold, + motionDuration: motionDuration, + isFactory: True + }) + + log MotionFactoryAdded(factory, objectionsThreshold, motionDuration) + +@external +def removeMotionFactory(factory: address): + """ + @notice + Remove a factory from the list of approved factories. + @param factory: The address of the factory + """ + assert msg.sender == self.admin, "!admin" + assert self.factories[factory].isFactory, "!factory_exists" + + self.factories[factory] = empty(Factory) + + log MotionFactoryRemoved(factory) + +@external +def addExecutor(executor: address): + """ + @notice + Add an executor to the list of approved executors. + @param executor: The address of the executor + """ + assert msg.sender == self.admin, "!admin" + assert not self.executors[executor], "!executor_exists" + + self.executors[executor] = True + + log ExecutorAdded(executor) + +@external +def removeExecutor(executor: address): + """ + @notice + Remove an executor from the list of approved executors. + @param executor: The address of the executor + """ + assert msg.sender == self.admin, "!admin" + assert self.executors[executor], "!executor_exists" + + self.executors[executor] = False + + log ExecutorRemoved(executor) + +@external +def setKnight(knight: address): + """ + @notice + Set the knight address. + @param knight: The address of the knight + """ + assert msg.sender == self.admin, "!admin" + assert knight != empty(address), "!knight" + + self.knight = knight + + log KnightSet(knight) + +@external +def acceptTimelockAccess(): + """ + @notice + Accept the access to send trxs to timelock. + """ + assert msg.sender == self.knight, "!knight" + DualTimelock(self.timelock).acceptLeanTrack() + +@external +def pause(): + """ + @notice + Emergency method to pause the contract. Only knight can pause. + """ + assert msg.sender == self.knight, "!knight" + assert not self.paused, "!paused" + + self.paused = True + + log Paused(msg.sender) + +@external +def unpause(): + """ + @notice + Unpause the contract. Only knight or admin can unpause. + """ + assert msg.sender == self.knight, "!knight" + assert self.paused, "!unpaused" + + self.paused = False + + log Unpaused(msg.sender) + + +@external +@view +def canObjectToMotion(motionId: uint256, objector: address) -> bool: + """ + @notice + Check if a "governanceToken" holder with voting power can object to a motion. + @param motionId: The id of the motion + @param objector: The address of the objector + @return bool: True if the objector can object to the motion + """ + motion: Motion = self.motions[motionId] + if motion.id == 0: + return False + if motion.isQueued: # motion is queued + return False + if motion.timeForQueue <= block.timestamp: # motion is expired + return False + if self.objections[motionId][objector]: # objector already objected + return False + # check voting balance at motion snapshot block and compare to current block number and use the lower one + votingBalance: uint256 = min( + GovToken(self.token).getPriorVotes(objector, motion.snapshotBlock), + GovToken(self.token).getPriorVotes(objector, block.number) + ) + if votingBalance == 0: # objector has no voting balance + return False + + return True \ No newline at end of file From 0750d9f49702709e7a0a8b4f8a5bcd29573aff71 Mon Sep 17 00:00:00 2001 From: Storming0x <6074987+storming0x@users.noreply.github.com> Date: Wed, 29 Mar 2023 15:54:43 -0600 Subject: [PATCH 5/9] Feat/gas optimizations leantrack (#32) * feat: gas optimizations on leantrack * feat: update gas snapshot and remove comment * feat: update docs --------- Co-authored-by: storming0x <storm0x@storm0x.com> --- .gas-snapshot | 64 ++++++++++++++--------------- README.md | 7 ++++ src/LeanTrack.vy | 105 ++++++++++++++++++++++++++--------------------- 3 files changed, 97 insertions(+), 79 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 3f9f678..af1cd4c 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -43,36 +43,36 @@ DualTimelockTest:testShouldExecQueuedTrxWithTimelockEthTransferCorrectly() (gas: DualTimelockTest:testShouldQueueFastTrx() (gas: 60076) DualTimelockTest:testShouldQueueTrx() (gas: 54969) DualTimelockTest:testTimelockCanReceiveEther() (gas: 15290) -LeanTrackTest:testCanObjectToMotionReturnsFalseIfMotionDoesntExist(address,uint256) (runs: 256, μ: 767396, ~: 767396) -LeanTrackTest:testCanObjectToMotionReturnsFalseIfMotionIsQueued(address,uint256) (runs: 256, μ: 1042922, ~: 1042922) -LeanTrackTest:testCanObjectToMotionReturnsFalseIfMotionTimeForQueueHasPassed(address,uint256) (runs: 256, μ: 859422, ~: 859422) -LeanTrackTest:testCanObjectToMotionReturnsFalseIfObjectorAlreadyObjected(address,uint256) (runs: 256, μ: 1008125, ~: 1008125) -LeanTrackTest:testCanObjectToMotionReturnsFalseIfVotingBalanceIsZero(address) (runs: 256, μ: 766932, ~: 766932) -LeanTrackTest:testCanObjectToMotionReturnsTrue(address,uint256) (runs: 256, μ: 867912, ~: 867918) +LeanTrackTest:testCanObjectToMotionReturnsFalseIfMotionDoesntExist(address,uint256) (runs: 256, μ: 667344, ~: 667344) +LeanTrackTest:testCanObjectToMotionReturnsFalseIfMotionIsQueued(address,uint256) (runs: 256, μ: 963479, ~: 963479) +LeanTrackTest:testCanObjectToMotionReturnsFalseIfMotionTimeForQueueHasPassed(address,uint256) (runs: 256, μ: 780365, ~: 780365) +LeanTrackTest:testCanObjectToMotionReturnsFalseIfObjectorAlreadyObjected(address,uint256) (runs: 256, μ: 850449, ~: 850449) +LeanTrackTest:testCanObjectToMotionReturnsFalseIfVotingBalanceIsZero(address) (runs: 256, μ: 687923, ~: 687923) +LeanTrackTest:testCanObjectToMotionReturnsTrue(address,uint256) (runs: 256, μ: 788909, ~: 788909) LeanTrackTest:testCannotAddExecutorTwice(address) (runs: 256, μ: 43814, ~: 43814) LeanTrackTest:testCannotAddFactoryTwice() (gas: 18434) -LeanTrackTest:testCannotCancelUnexistingMotion(address) (runs: 256, μ: 777012, ~: 777012) +LeanTrackTest:testCannotCancelUnexistingMotion(address) (runs: 256, μ: 676960, ~: 676960) LeanTrackTest:testCannotCreateMotionWhenPaused() (gas: 118656) LeanTrackTest:testCannotCreateMotionWithDifferentLenArrays() (gas: 98201) LeanTrackTest:testCannotCreateMotionWithTooManyOperations() (gas: 40127) LeanTrackTest:testCannotCreateMotionWithZeroOps() (gas: 91382) -LeanTrackTest:testCannotEnactMotionBeforeEta(uint256,address) (runs: 256, μ: 1128649, ~: 1107545) -LeanTrackTest:testCannotEnactMotionThatIsntQueued(uint256,address) (runs: 256, μ: 903444, ~: 886929) -LeanTrackTest:testCannotEnactMotionWhenPaused(uint256,address) (runs: 256, μ: 1072533, ~: 1051603) -LeanTrackTest:testCannotEnactUnexistingMotion(uint256,address,uint256) (runs: 256, μ: 1152563, ~: 1129095) -LeanTrackTest:testCannotObjectToMotionAfterTimeForQueuePasses(uint256,address) (runs: 256, μ: 901614, ~: 885099) -LeanTrackTest:testCannotObjectToMotionThatDoesntExist(uint256,address,uint256) (runs: 256, μ: 1150341, ~: 1126873) -LeanTrackTest:testCannotObjectToMotionWithZeroVotingBalance(address) (runs: 256, μ: 767163, ~: 767163) -LeanTrackTest:testCannotObjectToQueuedMotion(uint256,address) (runs: 256, μ: 1126257, ~: 1105153) -LeanTrackTest:testCannotObjectTwiceToSameMotion(address,uint256) (runs: 256, μ: 1009448, ~: 1009448) -LeanTrackTest:testCannotQueueMotionBeforeEta(uint256,address) (runs: 256, μ: 887151, ~: 871037) -LeanTrackTest:testCannotQueueMotionTwice(uint256,address) (runs: 256, μ: 1123882, ~: 1102777) -LeanTrackTest:testCannotQueueMotionWhenPaused(uint256,address) (runs: 256, μ: 833205, ~: 817265) -LeanTrackTest:testCannotQueueUnexistingMotion(uint256,address,uint256) (runs: 256, μ: 910561, ~: 892689) +LeanTrackTest:testCannotEnactMotionBeforeEta(uint256,address) (runs: 256, μ: 1047228, ~: 1026932) +LeanTrackTest:testCannotEnactMotionThatIsntQueued(uint256,address) (runs: 256, μ: 822195, ~: 806348) +LeanTrackTest:testCannotEnactMotionWhenPaused(uint256,address) (runs: 256, μ: 1071690, ~: 1051394) +LeanTrackTest:testCannotEnactUnexistingMotion(uint256,address,uint256) (runs: 256, μ: 1052302, ~: 1028834) +LeanTrackTest:testCannotObjectToMotionAfterTimeForQueuePasses(uint256,address) (runs: 256, μ: 820542, ~: 804695) +LeanTrackTest:testCannotObjectToMotionThatDoesntExist(uint256,address,uint256) (runs: 256, μ: 1050080, ~: 1026612) +LeanTrackTest:testCannotObjectToMotionWithZeroVotingBalance(address) (runs: 256, μ: 688190, ~: 688190) +LeanTrackTest:testCannotObjectToQueuedMotion(uint256,address) (runs: 256, μ: 1044659, ~: 1024363) +LeanTrackTest:testCannotObjectTwiceToSameMotion(address,uint256) (runs: 256, μ: 851772, ~: 851772) +LeanTrackTest:testCannotQueueMotionBeforeEta(uint256,address) (runs: 256, μ: 806091, ~: 790633) +LeanTrackTest:testCannotQueueMotionTwice(uint256,address) (runs: 256, μ: 1042284, ~: 1021987) +LeanTrackTest:testCannotQueueMotionWhenPaused(uint256,address) (runs: 256, μ: 832723, ~: 817265) +LeanTrackTest:testCannotQueueUnexistingMotion(uint256,address,uint256) (runs: 256, μ: 810509, ~: 792637) LeanTrackTest:testCannotRemoveExecutorThatDoesNotExist(address) (runs: 256, μ: 19039, ~: 19039) LeanTrackTest:testCannotRemoveFactoryThatDoesNotExist(address) (runs: 256, μ: 19020, ~: 19020) -LeanTrackTest:testKnightCanCancelMotionAfterBeingQueued() (gas: 779012) -LeanTrackTest:testKnightCanCancelMotionBeforeQueued(address) (runs: 256, μ: 609795, ~: 609779) +LeanTrackTest:testKnightCanCancelMotionAfterBeingQueued() (gas: 778708) +LeanTrackTest:testKnightCanCancelMotionBeforeQueued(address) (runs: 256, μ: 546443, ~: 546427) LeanTrackTest:testKnightCannotBeAddressZero() (gas: 14145) LeanTrackTest:testMotionFactoryDurationCannotBeLessThanMinimum(address,uint256,uint32) (runs: 256, μ: 19933, ~: 19933) LeanTrackTest:testMotionFactoryObjectionsThresholdCannotBeGreaterThanMaximum(address,uint256) (runs: 256, μ: 19527, ~: 19527) @@ -82,24 +82,24 @@ LeanTrackTest:testOnlyAdminCanRemoveExecutor(address) (runs: 256, μ: 14678, ~: LeanTrackTest:testOnlyAdminCanRemoveFactory(address,uint256,uint32) (runs: 256, μ: 97597, ~: 97597) LeanTrackTest:testOnlyAdminCanSetKnight(address) (runs: 256, μ: 14745, ~: 14745) LeanTrackTest:testOnlyApprovedFactoryCanCreateMotion(address) (runs: 256, μ: 103482, ~: 103482) -LeanTrackTest:testOnlyExecutorsCanCallEnactMotion(uint256,address) (runs: 256, μ: 1045259, ~: 1024329) +LeanTrackTest:testOnlyExecutorsCanCallEnactMotion(uint256,address) (runs: 256, μ: 1044416, ~: 1024120) LeanTrackTest:testOnlyKnightCanPauseLeanTrack(address) (runs: 256, μ: 14785, ~: 14785) -LeanTrackTest:testProposerCanCancelMotionAfterBeingQueued() (gas: 777359) -LeanTrackTest:testProposerCanCancelMotionBeforeQueued(address) (runs: 256, μ: 608195, ~: 608179) +LeanTrackTest:testProposerCanCancelMotionAfterBeingQueued() (gas: 777196) +LeanTrackTest:testProposerCanCancelMotionBeforeQueued(address) (runs: 256, μ: 544984, ~: 544968) LeanTrackTest:testRandomAcctCannotAddMotionFactory(address) (runs: 256, μ: 14653, ~: 14653) -LeanTrackTest:testRandomAcctCannotCanCancelMotion(address) (runs: 256, μ: 756081, ~: 756081) +LeanTrackTest:testRandomAcctCannotCanCancelMotion(address) (runs: 256, μ: 676847, ~: 676847) LeanTrackTest:testSetup() (gas: 50098) LeanTrackTest:testShouldAddExecutor(address) (runs: 256, μ: 42414, ~: 42414) LeanTrackTest:testShouldAddMotionFactory(address,uint256,uint32) (runs: 256, μ: 94935, ~: 94935) -LeanTrackTest:testShouldCreateMotion(uint8) (runs: 256, μ: 978212, ~: 858082) -LeanTrackTest:testShouldEnactQueuedMotion(uint256,address) (runs: 256, μ: 1003708, ~: 978478) -LeanTrackTest:testShouldObjectToMotionWithVotingPowerLessThanThreshold(address,uint256) (runs: 256, μ: 927211, ~: 927211) +LeanTrackTest:testShouldCreateMotion(uint8) (runs: 256, μ: 1011615, ~: 858082) +LeanTrackTest:testShouldEnactQueuedMotion(uint256,address) (runs: 256, μ: 1002854, ~: 978316) +LeanTrackTest:testShouldObjectToMotionWithVotingPowerLessThanThreshold(address,uint256) (runs: 256, μ: 848592, ~: 848592) LeanTrackTest:testShouldPauseLeanTrack() (gas: 39492) -LeanTrackTest:testShouldQueueMotion(uint256,address) (runs: 256, μ: 1075072, ~: 1053051) -LeanTrackTest:testShouldRejectMotionIfObjectionsReachedAboveThreshold(address,uint256) (runs: 256, μ: 769906, ~: 769919) +LeanTrackTest:testShouldQueueMotion(uint256,address) (runs: 256, μ: 1074196, ~: 1052842) +LeanTrackTest:testShouldRejectMotionIfObjectionsReachedAboveThreshold(address,uint256) (runs: 256, μ: 707010, ~: 707024) LeanTrackTest:testShouldRemoveExecutor(address) (runs: 256, μ: 34840, ~: 34824) LeanTrackTest:testShouldRemoveFactory(address,uint256,uint32) (runs: 256, μ: 77252, ~: 77259) -LeanTrackTest:testShouldUseLowerVotingBalanceForObjection(address,uint256) (runs: 256, μ: 873400, ~: 873400) +LeanTrackTest:testShouldUseLowerVotingBalanceForObjection(address,uint256) (runs: 256, μ: 794781, ~: 794781) SerpentorBravoTest:testCanSubmitProposal(uint256) (runs: 256, μ: 1329049, ~: 1329049) SerpentorBravoTest:testCannotCancelProposalIfProposerIsAboveThreshold(uint256,address,address) (runs: 256, μ: 1043325, ~: 1043325) SerpentorBravoTest:testCannotCancelWhitelistedProposerBelowThreshold(uint256,uint256,address) (runs: 256, μ: 1073721, ~: 1075432) diff --git a/README.md b/README.md index 18906b2..49ac1fd 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ A set of smart contracts tools for governance written in vyper ## Contracts * Timelock.vy - "Vyper implementation of a timelock contract for governance" +* DualTimelock.vy - "Timelock that can work with two queues with different delay settings" * SerpentorBravo.vy - "Vyper implementation of a governance contract for on-chain voting on proposals and execution" +* LeanTrack.vy - "Implementation for Optimistic on-chain governance system of motions to govern smart contracts" ## Requirements @@ -76,6 +78,10 @@ ape compile ape test ``` +## Compatibility +This project aims to be compatible with most governance contracts and best practices. +This implementation is mainly designed to work with any token implementing COMP token voting weight functions like `getPriorVotes`, but in most cases minimal changes are required to interact with other smart contracts implementations like OZ voting tokens. + ## Disclaimer This is **experimental software** and is provided on an "as is" and "as available" basis **without any warranties**. @@ -91,5 +97,6 @@ Use at your own risk. ## Acknowledgements - [compound governance](https://github.com/compound-finance/compound-protocol/tree/master/contracts/Governance) +- [Easy Track](https://github.com/lidofinance/easy-track) - [snekmate](https://github.com/pcaversaccio/snekmate) - [vyperDeployer](https://github.com/0xKitsune/Foundry-Vyper/blob/main/lib/utils/VyperDeployer.sol) diff --git a/src/LeanTrack.vy b/src/LeanTrack.vy index ee7db6e..715f963 100644 --- a/src/LeanTrack.vy +++ b/src/LeanTrack.vy @@ -206,7 +206,6 @@ def createMotion( assert len(targets) == len(values) and len(targets) == len(signatures) and len(targets) == len(calldatas), "!len_mismatch" assert self.factories[msg.sender].isFactory, "!factory" - # TODO: add motions limit check self.lastMotionId += 1 motionId: uint256 = self.lastMotionId @@ -253,28 +252,31 @@ def queueMotion(motionId: uint256)-> DynArray[bytes32, MAX_POSSIBLE_OPERATIONS]: @param motionId: The id of the motion """ assert not self.paused, "!paused" - motion: Motion = self.motions[motionId] - assert motion.id != 0, "!motion_exists" - assert motion.isQueued == False, "!motion_queued" - assert motion.timeForQueue <= block.timestamp, "!timeForQueue" - + assert self.motions[motionId].id != 0, "!motion_exists" + assert self.motions[motionId].isQueued == False, "!motion_queued" + assert self.motions[motionId].timeForQueue <= block.timestamp, "!timeForQueue" + eta: uint256 = block.timestamp + DualTimelock(self.timelock).leanTrackDelay() trxHashes: DynArray[bytes32, MAX_POSSIBLE_OPERATIONS] = [] - numOperations: uint256 = len(motion.targets) - + numOperations: uint256 = len(self.motions[motionId].targets) + targets: DynArray[address, MAX_POSSIBLE_OPERATIONS] = self.motions[motionId].targets + values: DynArray[uint256, MAX_POSSIBLE_OPERATIONS] = self.motions[motionId].values + signatures: DynArray[String[METHOD_SIG_SIZE], MAX_POSSIBLE_OPERATIONS] = self.motions[motionId].signatures + calldatas: DynArray[Bytes[CALL_DATA_LEN], MAX_POSSIBLE_OPERATIONS] = self.motions[motionId].calldatas + for i in range(MAX_POSSIBLE_OPERATIONS): if i >= numOperations: break # check hash doesnt exist already in timelock - localHash: bytes32 = keccak256(_abi_encode(motion.targets[i], motion.values[i], motion.signatures[i], motion.calldatas[i], eta)) + localHash: bytes32 = keccak256(_abi_encode(targets[i], values[i], signatures[i], calldatas[i], eta)) assert not DualTimelock(self.timelock).queuedRapidTransactions(localHash), "!trxHash_exists" trxHash: bytes32 = DualTimelock(self.timelock).queueRapidTransaction( - motion.targets[i], - motion.values[i], - motion.signatures[i], - motion.calldatas[i], + targets[i], + values[i], + signatures[i], + calldatas[i], eta ) @@ -282,7 +284,7 @@ def queueMotion(motionId: uint256)-> DynArray[bytes32, MAX_POSSIBLE_OPERATIONS]: # check motion as queued and set eta self.motions[motionId].isQueued = True self.motions[motionId].eta = eta - + log MotionQueued(motionId, trxHashes, eta) return trxHashes @@ -296,21 +298,26 @@ def enactMotion(motionId: uint256): """ assert not self.paused, "!paused" assert self.executors[msg.sender], "!executor" - motion: Motion = self.motions[motionId] - assert motion.id != 0, "!motion_exists" - assert motion.isQueued == True, "!motion_queued" - assert motion.eta <= block.timestamp, "!eta" + assert self.motions[motionId].id != 0, "!motion_exists" + assert self.motions[motionId].isQueued == True, "!motion_queued" + assert self.motions[motionId].eta <= block.timestamp, "!eta" + + numOperations: uint256 = len(self.motions[motionId].targets) + targets: DynArray[address, MAX_POSSIBLE_OPERATIONS] = self.motions[motionId].targets + values: DynArray[uint256, MAX_POSSIBLE_OPERATIONS] = self.motions[motionId].values + signatures: DynArray[String[METHOD_SIG_SIZE], MAX_POSSIBLE_OPERATIONS] = self.motions[motionId].signatures + calldatas: DynArray[Bytes[CALL_DATA_LEN], MAX_POSSIBLE_OPERATIONS] = self.motions[motionId].calldatas + eta: uint256 = self.motions[motionId].eta - numOperations: uint256 = len(motion.targets) for i in range(MAX_POSSIBLE_OPERATIONS): if i >= numOperations: break DualTimelock(self.timelock).executeRapidTransaction( - motion.targets[i], - motion.values[i], - motion.signatures[i], - motion.calldatas[i], - motion.eta + targets[i], + values[i], + signatures[i], + calldatas[i], + eta ) # delete motion @@ -330,24 +337,24 @@ def objectToMotion(motionId: uint256): The sender must have voting power. @param motionId: The id of the motion """ - motion: Motion = self.motions[motionId] - assert motion.id != 0, "!motion_exists" - assert motion.isQueued == False, "!motion_queued" - assert motion.timeForQueue > block.timestamp, "!timeForQueue" + assert self.motions[motionId].id != 0, "!motion_exists" + assert self.motions[motionId].isQueued == False, "!motion_queued" + assert self.motions[motionId].timeForQueue > block.timestamp, "!timeForQueue" assert not self.objections[motionId][msg.sender], "!already_objected" # check voting balance at motion snapshot block and compare to current block number and use the lower one + snapshotBlock: uint256 = self.motions[motionId].snapshotBlock votingBalance: uint256 = min( - GovToken(self.token).getPriorVotes(msg.sender, motion.snapshotBlock), + GovToken(self.token).getPriorVotes(msg.sender, snapshotBlock), GovToken(self.token).getPriorVotes(msg.sender, block.number) ) assert votingBalance > 0, "!voting_balance" - totalSupply: uint256 = GovToken(self.token).totalSupplyAt(motion.snapshotBlock) - newObjectionsAmount: uint256 = motion.objections + votingBalance + totalSupply: uint256 = GovToken(self.token).totalSupplyAt(snapshotBlock) + newObjectionsAmount: uint256 = self.motions[motionId].objections + votingBalance newObjectionsAmountPct: uint256 = (newObjectionsAmount * HUNDRED_PERCENT) / totalSupply log MotionObjected(motionId, msg.sender, votingBalance, newObjectionsAmount, newObjectionsAmountPct) # update motion objections or delete motion if objections threshold is reached - if newObjectionsAmountPct >= motion.objectionsThreshold: + if newObjectionsAmountPct >= self.motions[motionId].objectionsThreshold: self.motions[motionId] = empty(Motion) log MotionRejected(motionId) else: @@ -365,23 +372,28 @@ def cancelMotion(motionId: uint256): The sender must be the proposer of the motion or the guardian role. @param motionId: The id of the motion """ - motion: Motion = self.motions[motionId] - assert motion.id != 0, "!motion_exists" + # motion: Motion = self.motions[motionId] + assert self.motions[motionId].id != 0, "!motion_exists" # only guardian or proposer can cancel motion - assert msg.sender == self.knight or msg.sender == motion.proposer, "!access" + assert msg.sender == self.knight or msg.sender == self.motions[motionId].proposer, "!access" # if motion is queued, cancel it in timelock - if motion.isQueued: - numOperations: uint256 = len(motion.targets) + if self.motions[motionId].isQueued: + numOperations: uint256 = len(self.motions[motionId].targets) + targets: DynArray[address, MAX_POSSIBLE_OPERATIONS] = self.motions[motionId].targets + values: DynArray[uint256, MAX_POSSIBLE_OPERATIONS] = self.motions[motionId].values + signatures: DynArray[String[METHOD_SIG_SIZE], MAX_POSSIBLE_OPERATIONS] = self.motions[motionId].signatures + calldatas: DynArray[Bytes[CALL_DATA_LEN], MAX_POSSIBLE_OPERATIONS] = self.motions[motionId].calldatas + eta: uint256 = self.motions[motionId].eta for i in range(MAX_POSSIBLE_OPERATIONS): if i >= numOperations: break DualTimelock(self.timelock).cancelRapidTransaction( - motion.targets[i], - motion.values[i], - motion.signatures[i], - motion.calldatas[i], - motion.eta + targets[i], + values[i], + signatures[i], + calldatas[i], + eta ) # delete motion @@ -518,18 +530,17 @@ def canObjectToMotion(motionId: uint256, objector: address) -> bool: @param objector: The address of the objector @return bool: True if the objector can object to the motion """ - motion: Motion = self.motions[motionId] - if motion.id == 0: + if self.motions[motionId].id == 0: return False - if motion.isQueued: # motion is queued + if self.motions[motionId].isQueued: # motion is queued return False - if motion.timeForQueue <= block.timestamp: # motion is expired + if self.motions[motionId].timeForQueue <= block.timestamp: # motion is expired return False if self.objections[motionId][objector]: # objector already objected return False # check voting balance at motion snapshot block and compare to current block number and use the lower one votingBalance: uint256 = min( - GovToken(self.token).getPriorVotes(objector, motion.snapshotBlock), + GovToken(self.token).getPriorVotes(objector, self.motions[motionId].snapshotBlock), GovToken(self.token).getPriorVotes(objector, block.number) ) if votingBalance == 0: # objector has no voting balance From 10172f008b9ea1e71c2cac3d4756d8f456a076d3 Mon Sep 17 00:00:00 2001 From: poolpitako <78830419+poolpitako@users.noreply.github.com> Date: Fri, 28 Apr 2023 17:57:47 -0500 Subject: [PATCH 6/9] typo (#33) --- ape-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ape-config.yaml b/ape-config.yaml index a355466..a61a22f 100644 --- a/ape-config.yaml +++ b/ape-config.yaml @@ -5,7 +5,7 @@ plugins: - name: infura - name: tokens -# require OpenZepplin Contracts +# require OpenZeppelin Contracts dependencies: - name: openzeppelin github: OpenZeppelin/openzeppelin-contracts From ee4345fe491a9c7e6f471660452d1fe1c15c260e Mon Sep 17 00:00:00 2001 From: Storming0x <6074987+storming0x@users.noreply.github.com> Date: Mon, 22 May 2023 11:08:36 -0600 Subject: [PATCH 7/9] Fix: quorum initial value (#34) Co-authored-by: storming0x <storm0x@storm0x.com> --- src/SerpentorBravo.vy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SerpentorBravo.vy b/src/SerpentorBravo.vy index 46d3d37..75b3c5c 100644 --- a/src/SerpentorBravo.vy +++ b/src/SerpentorBravo.vy @@ -271,7 +271,7 @@ def __init__( @param votingPeriod The initial voting period @param votingDelay The initial voting delay @param proposalThreshold The initial proposal threshold - @param quorumVotes The initial quorum voting setting + @param quorumVotes The initial quorum voting setting, recommended to be higher than proposalThreshold @param initialProposalId The initialProposalId to start the counter """ assert timelockAddr != empty(address), "!timelock" @@ -280,6 +280,7 @@ def __init__( assert votingPeriod >= MIN_VOTING_PERIOD and votingPeriod <= MAX_VOTING_PERIOD, "!votingPeriod" assert votingDelay >= MIN_VOTING_DELAY and votingDelay <= MAX_VOTING_DELAY, "!votingDelay" assert proposalThreshold >= MIN_PROPOSAL_THRESHOLD and proposalThreshold <= MAX_PROPOSAL_THRESHOLD, "!proposalThreshold" + assert quorumVotes > proposalThreshold, "!quorumVotes" self.admin = admin self.votingPeriod = votingPeriod self.votingDelay = votingDelay From ac2cd7dd6696df9206caa11175a5059343f56be1 Mon Sep 17 00:00:00 2001 From: Storming0x <6074987+storming0x@users.noreply.github.com> Date: Fri, 26 May 2023 14:17:33 -0600 Subject: [PATCH 8/9] Feat/example motion factory (#35) * Chore: commit baseMotionFactory * Feat: example motion factories * Fix failing test --------- Co-authored-by: storming0x <storm0x@storm0x.com> --- foundry_test/BaseMotionFactory.t.sol | 163 +++++++++++++++ .../BribesToSplitterMotionFactory.t.sol | 114 +++++++++++ foundry_test/LeanTrack.t.sol | 7 +- foundry_test/TransferMotionFactory.t.sol | 185 ++++++++++++++++++ .../VaultOperationsMotionFactory.t.sol | 157 +++++++++++++++ foundry_test/utils/MockLeanTrack.sol | 66 +++++++ foundry_test/utils/MockVault.sol | 36 ++++ src/LeanTrack.vy | 4 +- src/SerpentorBravo.vy | 2 +- src/factories/BaseMotionFactory.sol | 92 +++++++++ .../BribesToSplitterMotionFactory.sol | 54 +++++ .../examples/TransferMotionFactory.sol | 46 +++++ .../examples/VaultOperationsMotionFactory.sol | 50 +++++ 13 files changed, 972 insertions(+), 4 deletions(-) create mode 100644 foundry_test/BaseMotionFactory.t.sol create mode 100644 foundry_test/BribesToSplitterMotionFactory.t.sol create mode 100644 foundry_test/TransferMotionFactory.t.sol create mode 100644 foundry_test/VaultOperationsMotionFactory.t.sol create mode 100644 foundry_test/utils/MockLeanTrack.sol create mode 100644 foundry_test/utils/MockVault.sol create mode 100644 src/factories/BaseMotionFactory.sol create mode 100644 src/factories/examples/BribesToSplitterMotionFactory.sol create mode 100644 src/factories/examples/TransferMotionFactory.sol create mode 100644 src/factories/examples/VaultOperationsMotionFactory.sol diff --git a/foundry_test/BaseMotionFactory.t.sol b/foundry_test/BaseMotionFactory.t.sol new file mode 100644 index 0000000..bf6bdde --- /dev/null +++ b/foundry_test/BaseMotionFactory.t.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: AGPL +pragma solidity ^0.8.16; + +import "@openzeppelin/token/ERC20/ERC20.sol"; +import {console} from "forge-std/console.sol"; + +import {ExtendedTest} from "./utils/ExtendedTest.sol"; +import {VyperDeployer} from "../lib/utils/VyperDeployer.sol"; +import {DualTimelock} from "./interfaces/DualTimelock.sol"; +import { + LeanTrack, + Factory, + Motion +} from "./interfaces/LeanTrack.sol"; +import {GovToken} from "./utils/GovToken.sol"; +import {TransferMotionFactory} from "../src/factories/examples/TransferMotionFactory.sol"; + +import {MockLeanTrack, MotionArgs} from "./utils/MockLeanTrack.sol"; + +// these tests covers both the example TransferMotionFactory and the BaseMotionFactory contracts +contract BaseMotionFactoryTest is ExtendedTest { + uint256 public constant DEFAULT_LIMIT = 1000; + VyperDeployer private vyperDeployer = new VyperDeployer(); + DualTimelock private timelock; + ERC20 private token; + TransferMotionFactory private transferfactory; + MockLeanTrack private leanTrack; + GovToken private govToken; + + uint256 public delay = 2 days; + uint256 public leanTrackDelay = 1 days; + uint256 public factoryMotionDuration = 1 days; + + address public admin = address(1); + address public authorized = address(2); + address public objectoor = address(3); + address public mediumVoter = address(4); + address public whaleVoter1 = address(5); + address public whaleVoter2 = address(6); + address public knight = address(7); + address public smallVoter = address(8); + address public grantee = address(0xABCD); + address public executor = address(7); + + event MotionCreated( + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas + ); + + function setUp() public { + // deploy token + govToken = new GovToken(18); + token = ERC20(govToken); + + // deploy mock lean track + leanTrack = new MockLeanTrack(); + + transferfactory = new TransferMotionFactory(address(leanTrack), address(admin)); + + // set transfer limit + hoax(admin); + transferfactory.setTransferLimit(address(token), DEFAULT_LIMIT); + // set authorized transfer motion creator + hoax(admin); + transferfactory.setAuthorized(authorized, true); + + // vm traces + vm.label(address(transferfactory), "transferfactory"); + vm.label(address(leanTrack), "leanTrack"); + vm.label(address(govToken), "govToken"); + vm.label(address(token), "token"); + } + function testSetup() public { + assertEq(address(transferfactory.gov()), admin); + assertEq(address(transferfactory.leanTrack()), address(leanTrack)); + + assertEq(transferfactory.transferLimits(address(token)), DEFAULT_LIMIT); + assertEq(transferfactory.authorized(authorized), true); + } + + function testCreateTransferMotion() public { + // create transfer motion + hoax(authorized); + uint256 motionId = transferfactory.createTransferMotion(address(token), grantee, 100); + + assertEq(motionId, 1); + address[] memory targets = new address[](1); + targets[0] = address(token); + uint256[] memory values = new uint256[](1); + values[0] = 0; + string[] memory signatures = new string[](1); + signatures[0] = "transfer(address,uint256)"; + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = abi.encode(grantee, 100); + + // check lean track was called with expected params + MotionArgs memory motionArgs = leanTrack.getMotionArgs(motionId); + assertEq(motionArgs.id, motionId); + assertEq(motionArgs.targets, targets); + assertEq(motionArgs.calldatas[0], calldatas[0]); + assertEq(motionArgs.signatures[0], signatures[0]); + assertEq(motionArgs.values[0], values[0]); + } + + function testCannotCreateMotionWithZeroAmount() public { + vm.expectRevert("!amount"); + + // create transfer motion + hoax(authorized); + transferfactory.createTransferMotion(address(token), grantee, 0); + } + + function testOnlyAuthorizedCanCreateMotion() public { + vm.expectRevert("!auth"); + + // create transfer motion + hoax(objectoor); + transferfactory.createTransferMotion(address(token), grantee, 100); + } + + function testRandomCanotCallSetAuthorized() public { + vm.expectRevert(); + + // set authorized + hoax(objectoor); + transferfactory.setAuthorized(objectoor, true); + } + + + function testCancelTransferMotion() public { + // create transfer motion + hoax(authorized); + uint256 motionId = transferfactory.createTransferMotion(address(token), grantee, 100); + + assertEq(motionId, 1); + address[] memory targets = new address[](1); + targets[0] = address(token); + uint256[] memory values = new uint256[](1); + values[0] = 0; + string[] memory signatures = new string[](1); + signatures[0] = "transfer(address,uint256)"; + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = abi.encode(grantee, 100); + + // check lean track was called with expected params + assertEq(leanTrack.motions(motionId).id, motionId); + MotionArgs memory motionArgs = leanTrack.getMotionArgs(motionId); + assertEq(motionArgs.targets, targets); + assertEq(motionArgs.calldatas[0], calldatas[0]); + assertEq(motionArgs.signatures[0], signatures[0]); + assertEq(motionArgs.values[0], values[0]); + + // cancel transfer motion + hoax(authorized); + transferfactory.cancelMotion(motionId); + + // check lean track motion id was deleted + assertEq(leanTrack.motions(motionId).id, 0); + } + +} diff --git a/foundry_test/BribesToSplitterMotionFactory.t.sol b/foundry_test/BribesToSplitterMotionFactory.t.sol new file mode 100644 index 0000000..50661ea --- /dev/null +++ b/foundry_test/BribesToSplitterMotionFactory.t.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: AGPL +pragma solidity ^0.8.16; + +import "@openzeppelin/token/ERC20/ERC20.sol"; +import {console} from "forge-std/console.sol"; + +import {ExtendedTest} from "./utils/ExtendedTest.sol"; +import {VyperDeployer} from "../lib/utils/VyperDeployer.sol"; +import {DualTimelock} from "./interfaces/DualTimelock.sol"; +import { + LeanTrack, + Factory, + Motion +} from "./interfaces/LeanTrack.sol"; +import {GovToken} from "./utils/GovToken.sol"; +import {BribesToSplitterMotionFactory} from "../src/factories/examples/BribesToSplitterMotionFactory.sol"; + +import {MockLeanTrack, MotionArgs} from "./utils/MockLeanTrack.sol"; + +// these tests covers both the example BribesToSplitter and the BaseMotionFactory contracts +contract BribesToSplitterMotionFactoryTest is ExtendedTest { + address public immutable VOTER = 0xF147b8125d2ef93FB6965Db97D6746952a133934; + address public immutable SPLITTER = 0x527e80008D212E2891C737Ba8a2768a7337D7Fd2; + uint256 public constant DEFAULT_LIMIT = 1000; + VyperDeployer private vyperDeployer = new VyperDeployer(); + DualTimelock private timelock; + ERC20 private token; + BribesToSplitterMotionFactory private factory; + MockLeanTrack private leanTrack; + GovToken private govToken; + + uint256 public delay = 2 days; + uint256 public leanTrackDelay = 1 days; + uint256 public factoryMotionDuration = 1 days; + + address public admin = address(1); + address public authorized = address(2); + address public objectoor = address(3); + address public mediumVoter = address(4); + address public whaleVoter1 = address(5); + address public whaleVoter2 = address(6); + address public knight = address(7); + address public smallVoter = address(8); + address public grantee = address(0xABCD); + address public executor = address(7); + + event MotionCreated( + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas + ); + + function setUp() public { + // deploy token + govToken = new GovToken(18); + token = ERC20(govToken); + + // deploy mock lean track + leanTrack = new MockLeanTrack(); + + factory = new BribesToSplitterMotionFactory(address(leanTrack), address(admin)); + + // set transfer limit + hoax(admin); + factory.setTransferLimit(address(token), DEFAULT_LIMIT); + // set authorized transfer motion creator + hoax(admin); + factory.setAuthorized(authorized, true); + + // vm traces + vm.label(address(factory), "factory"); + vm.label(address(leanTrack), "leanTrack"); + vm.label(address(govToken), "govToken"); + vm.label(address(token), "token"); + } + function testSetup() public { + assertEq(address(factory.gov()), admin); + assertEq(address(factory.leanTrack()), address(leanTrack)); + + assertEq(factory.transferLimits(address(token)), DEFAULT_LIMIT); + assertEq(factory.authorized(authorized), true); + } + + function testCreateTransferMotion() public { + address[] memory tokens = new address[](1); + tokens[0] = address(token); + uint256[] memory amounts = new uint256[](1); + amounts[0] = 100; + + // create transfer motion + hoax(authorized); + uint256 motionId = factory.createBribesTransferMotion(tokens, amounts); + + assertEq(motionId, 1); + address[] memory targets = new address[](1); + targets[0] = VOTER; + uint256[] memory values = new uint256[](1); + values[0] = 0; + string[] memory signatures = new string[](1); + bytes memory calldataForTransfer = abi.encodeWithSignature("transfer(address,uint256)", SPLITTER, 100); + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = abi.encodeWithSignature("execute(address,uint256,bytes)", address(token), 0, calldataForTransfer); + + // check lean track was called with expected params + MotionArgs memory motionArgs = leanTrack.getMotionArgs(motionId); + assertEq(motionArgs.id, motionId); + assertEq(motionArgs.targets, targets); + assertEq(motionArgs.calldatas[0], calldatas[0]); + assertEq(motionArgs.signatures[0], signatures[0]); + assertEq(motionArgs.values[0], values[0]); + } + +} diff --git a/foundry_test/LeanTrack.t.sol b/foundry_test/LeanTrack.t.sol index a5474ee..9b523e6 100644 --- a/foundry_test/LeanTrack.t.sol +++ b/foundry_test/LeanTrack.t.sol @@ -32,7 +32,7 @@ contract LeanTrackTest is ExtendedTest { uint256 public constant MAX_OPERATIONS = 10; uint256 public constant MIN_OBJECTIONS_THRESHOLD = 100; // 1% uint256 public constant MAX_OBJECTIONS_THRESHOLD = 3000; // 30% - uint256 public constant MIN_MOTION_DURATION = 16 hours; + uint256 public constant MIN_MOTION_DURATION = 1; // 1 second address public admin = address(1); address public factory = address(2); @@ -192,7 +192,8 @@ contract LeanTrackTest is ExtendedTest { address(0), address(timelock), address(leanTrack), - address(token) + address(token), + address(this) ]; for (uint i = 0; i < reservedList.length; i++) reserved[reservedList[i]] = true; @@ -1011,6 +1012,8 @@ contract LeanTrackTest is ExtendedTest { function testCannotObjectToMotionWithZeroVotingBalance(address random) public { vm.assume(!reserved[random]); + uint256 votingBalanceForObjector = token.balanceOf(random); + vm.assume(votingBalanceForObjector == 0); // setup uint256 motionId; (motionId,) = _createMotion(1); diff --git a/foundry_test/TransferMotionFactory.t.sol b/foundry_test/TransferMotionFactory.t.sol new file mode 100644 index 0000000..d0a32d8 --- /dev/null +++ b/foundry_test/TransferMotionFactory.t.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: AGPL +pragma solidity ^0.8.16; + +import "@openzeppelin/token/ERC20/ERC20.sol"; +import {console} from "forge-std/console.sol"; + +import {ExtendedTest} from "./utils/ExtendedTest.sol"; +import {VyperDeployer} from "../lib/utils/VyperDeployer.sol"; +import {DualTimelock} from "./interfaces/DualTimelock.sol"; +import { + LeanTrack, + Factory, + Motion +} from "./interfaces/LeanTrack.sol"; +import {GovToken} from "./utils/GovToken.sol"; +import {TransferMotionFactory} from "../src/factories/examples/TransferMotionFactory.sol"; + +import {MockLeanTrack, MotionArgs} from "./utils/MockLeanTrack.sol"; + +// these tests covers both the example TransferMotionFactory and the BaseMotionFactory contracts +contract TransferMotionFactoryTest is ExtendedTest { + uint256 public constant DEFAULT_LIMIT = 1000; + VyperDeployer private vyperDeployer = new VyperDeployer(); + DualTimelock private timelock; + ERC20 private token; + TransferMotionFactory private transferfactory; + MockLeanTrack private leanTrack; + GovToken private govToken; + + uint256 public delay = 2 days; + uint256 public leanTrackDelay = 1 days; + uint256 public factoryMotionDuration = 1 days; + + address public admin = address(1); + address public authorized = address(2); + address public objectoor = address(3); + address public mediumVoter = address(4); + address public whaleVoter1 = address(5); + address public whaleVoter2 = address(6); + address public knight = address(7); + address public smallVoter = address(8); + address public grantee = address(0xABCD); + address public executor = address(7); + + event MotionCreated( + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas + ); + + function setUp() public { + // deploy token + govToken = new GovToken(18); + token = ERC20(govToken); + + // deploy mock lean track + leanTrack = new MockLeanTrack(); + + transferfactory = new TransferMotionFactory(address(leanTrack), address(admin)); + + // set transfer limit + hoax(admin); + transferfactory.setTransferLimit(address(token), DEFAULT_LIMIT); + // set authorized transfer motion creator + hoax(admin); + transferfactory.setAuthorized(authorized, true); + + // vm traces + vm.label(address(transferfactory), "transferfactory"); + vm.label(address(leanTrack), "leanTrack"); + vm.label(address(govToken), "govToken"); + vm.label(address(token), "token"); + } + function testSetup() public { + assertEq(address(transferfactory.gov()), admin); + assertEq(address(transferfactory.leanTrack()), address(leanTrack)); + + assertEq(transferfactory.transferLimits(address(token)), DEFAULT_LIMIT); + assertEq(transferfactory.authorized(authorized), true); + } + + function testCreateTransferMotion() public { + // create transfer motion + hoax(authorized); + uint256 motionId = transferfactory.createTransferMotion(address(token), grantee, 100); + + assertEq(motionId, 1); + address[] memory targets = new address[](1); + targets[0] = address(token); + uint256[] memory values = new uint256[](1); + values[0] = 0; + string[] memory signatures = new string[](1); + signatures[0] = "transfer(address,uint256)"; + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = abi.encode(grantee, 100); + + // check lean track was called with expected params + MotionArgs memory motionArgs = leanTrack.getMotionArgs(motionId); + assertEq(motionArgs.id, motionId); + assertEq(motionArgs.targets, targets); + assertEq(motionArgs.calldatas[0], calldatas[0]); + assertEq(motionArgs.signatures[0], signatures[0]); + assertEq(motionArgs.values[0], values[0]); + } + + function testCannotCreateMotionWithZeroAmount() public { + vm.expectRevert("!amount"); + + // create transfer motion + hoax(authorized); + transferfactory.createTransferMotion(address(token), grantee, 0); + } + + function testOnlyAuthorizedCanCreateMotion() public { + vm.expectRevert("!auth"); + + // create transfer motion + hoax(objectoor); + transferfactory.createTransferMotion(address(token), grantee, 100); + } + + function testCannotTransferPassedLimit() public { + vm.expectRevert("amount > limit"); + + // create transfer motion + hoax(authorized); + transferfactory.createTransferMotion(address(token), grantee, DEFAULT_LIMIT + 1); + } + + function testCannotTransferUnidentifiedToken() public { + vm.expectRevert("amount > limit"); + + // create transfer motion for random token not in setup + hoax(authorized); + transferfactory.createTransferMotion(address(grantee), grantee, 100); + } + + function testDisallowTokenTransfer() public { + // create transfer motion + hoax(authorized); + transferfactory.createTransferMotion(address(token), grantee, 100); + + // disallow token transfer + hoax(admin); + transferfactory.disallowTokenTransfer(address(token)); + + // create transfer motion + vm.expectRevert("amount > limit"); + hoax(authorized); + transferfactory.createTransferMotion(address(token), grantee, 100); + } + + function testCancelTransferMotion() public { + // create transfer motion + hoax(authorized); + uint256 motionId = transferfactory.createTransferMotion(address(token), grantee, 100); + + assertEq(motionId, 1); + address[] memory targets = new address[](1); + targets[0] = address(token); + uint256[] memory values = new uint256[](1); + values[0] = 0; + string[] memory signatures = new string[](1); + signatures[0] = "transfer(address,uint256)"; + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = abi.encode(grantee, 100); + + // check lean track was called with expected params + assertEq(leanTrack.motions(motionId).id, motionId); + MotionArgs memory motionArgs = leanTrack.getMotionArgs(motionId); + assertEq(motionArgs.targets, targets); + assertEq(motionArgs.calldatas[0], calldatas[0]); + assertEq(motionArgs.signatures[0], signatures[0]); + assertEq(motionArgs.values[0], values[0]); + + // cancel transfer motion + hoax(authorized); + transferfactory.cancelMotion(motionId); + + // check lean track motion id was deleted + assertEq(leanTrack.motions(motionId).id, 0); + } + +} diff --git a/foundry_test/VaultOperationsMotionFactory.t.sol b/foundry_test/VaultOperationsMotionFactory.t.sol new file mode 100644 index 0000000..41f4183 --- /dev/null +++ b/foundry_test/VaultOperationsMotionFactory.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: AGPL +pragma solidity ^0.8.16; + +import "@openzeppelin/token/ERC20/ERC20.sol"; +import {console} from "forge-std/console.sol"; + +import {ExtendedTest} from "./utils/ExtendedTest.sol"; +import {VyperDeployer} from "../lib/utils/VyperDeployer.sol"; +import {DualTimelock} from "./interfaces/DualTimelock.sol"; +import { + LeanTrack, + Factory, + Motion +} from "./interfaces/LeanTrack.sol"; +import {GovToken} from "./utils/GovToken.sol"; +import {VaultOperationsMotionFactory} from "../src/factories/examples/VaultOperationsMotionFactory.sol"; +import {MockVault as Vault} from "./utils/MockVault.sol"; + +import { + LeanTrack, + Factory, + Motion +} from "./interfaces/LeanTrack.sol"; + +// these tests covers VaultsOperationFactory and the BaseMotionFactory contracts +contract VaultOperationsMotionFactoryTest is ExtendedTest { + uint256 public constant QUORUM = 2000; // 20% + VyperDeployer private vyperDeployer = new VyperDeployer(); + DualTimelock private timelock; + ERC20 private token; + VaultOperationsMotionFactory private factory; + LeanTrack private leanTrack; + GovToken private govToken; + Vault private vault; + + uint256 public delay = 2 days; + uint256 public leanTrackDelay = 60 seconds; + uint256 public factoryMotionDuration = 2 minutes; + + address public admin = address(1); + address public authorized = address(2); + address public objectoor = address(3); + address public mediumVoter = address(4); + address public whaleVoter1 = address(5); + address public whaleVoter2 = address(6); + address public knight = address(7); + address public smallVoter = address(8); + address public grantee = address(0xABCD); + address public executor = address(7); + + event MotionCreated( + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas + ); + + function setUp() public { + // deploy token + govToken = new GovToken(18); + token = ERC20(govToken); + + bytes memory args = abi.encode(admin, address(0), delay, leanTrackDelay); + timelock = DualTimelock(vyperDeployer.deployContract("src/", "DualTimelock", args)); + console.log("address for DualTimelock: ", address(timelock)); + + bytes memory argsLeanTrack = abi.encode(address(token), admin, address(timelock), knight); + leanTrack = LeanTrack(vyperDeployer.deployContract("src/", "LeanTrack", argsLeanTrack)); + console.log("address for LeanTrack: ", address(leanTrack)); + + hoax(address(timelock)); + timelock.setPendingLeanTrack(address(leanTrack)); + + hoax(address(knight)); + leanTrack.acceptTimelockAccess(); + + factory = new VaultOperationsMotionFactory(address(leanTrack), address(admin)); + + // set authorized vault operations motion creator + hoax(admin); + factory.setAuthorized(authorized, true); + + // setup factory + hoax(admin); + // short time duration for this factory since its only emergerncy functions + leanTrack.addMotionFactory(address(factory), QUORUM, factoryMotionDuration); + + // add executor + hoax(admin); + leanTrack.addExecutor(address(knight)); + + // setup vault + vault = new Vault(address(timelock)); + + // vm traces + vm.label(address(timelock), "DualTimelock"); + vm.label(address(factory), "factory"); + vm.label(address(leanTrack), "leanTrack"); + vm.label(address(govToken), "govToken"); + vm.label(address(token), "token"); + vm.label(address(vault), "vault"); + } + function testSetup() public { + assertNeq(address(timelock), address(0)); + assertNeq(address(leanTrack), address(0)); + assertNeq(address(vault), address(0)); + + assertEq(address(timelock.admin()), admin); + assertEq(timelock.delay(), delay); + assertEq(timelock.leanTrackDelay(), leanTrackDelay); + assertEq(leanTrack.admin(), admin); + assertEq(leanTrack.token(), address(token)); + assertTrue(leanTrack.executors(address(knight))); + assertTrue(leanTrack.factories(address(factory)).isFactory); + assertEq(leanTrack.factories(address(factory)).objectionsThreshold, QUORUM); + assertEq(leanTrack.factories(address(factory)).motionDuration, factoryMotionDuration); + assertEq(address(factory.gov()), admin); + assertEq(address(factory.leanTrack()), address(leanTrack)); + + assertEq(factory.authorized(authorized), true); + } + + function testDisableDepositLimitInVault() public { + // create motion to disable deposit limit in vault + hoax(authorized); + address[] memory vaults = new address[](1); + vaults[0] = address(vault); + uint256 motionId = factory.disableDepositLimit(vaults); + + assertEq(motionId, 1); + + Motion memory motion = leanTrack.motions(motionId); + assertEq(motion.id, motionId); + + vm.warp(motion.timeForQueue); //skip to eta + + // setup queue motion + hoax(objectoor); + leanTrack.queueMotion(motionId); + + motion = leanTrack.motions(motionId); + assertTrue(motion.eta != 0); + vm.warp(motion.eta); //skip to eta + + //execute + hoax(knight); + leanTrack.enactMotion(motionId); + + //assert motion has been enacted + motion = leanTrack.motions(motionId); + assertEq(motion.id, 0); + // check transaction was executed + assertEq(vault.depositLimit(), 0); + + } + +} diff --git a/foundry_test/utils/MockLeanTrack.sol b/foundry_test/utils/MockLeanTrack.sol new file mode 100644 index 0000000..759b83d --- /dev/null +++ b/foundry_test/utils/MockLeanTrack.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: AGPL +pragma solidity ^0.8.16; + +struct MotionArgs { + uint256 id; + address[] targets; + uint256[] values; + string[] signatures; + bytes[] calldatas; + } + +contract MockLeanTrack { + event MotionCreated( + uint256 indexed motionId, + address indexed proposer, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, + uint256 timeForQueue, + uint256 snapshotBlock, + uint256 objectionsThreshold + ); + + uint256 public motionCount = 0; + + // mapping with motion args + mapping(uint256 => MotionArgs) private _motions; + + function createMotion( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas + ) external returns (uint256) { + motionCount++; + uint256 motionId = motionCount; + _motions[motionId] = MotionArgs(motionId, targets, values, signatures, calldatas); + + emit MotionCreated( + motionId, + msg.sender, + targets, + values, + signatures, + calldatas, + 0, + 0, + 0 + ); + + return motionId; + } + + function getMotionArgs(uint256 motionId) external view returns (MotionArgs memory) { + return _motions[motionId]; + } + + function cancelMotion(uint256 motionId) external { + delete _motions[motionId]; + } + + function motions(uint256 motionId) external view returns (MotionArgs memory) { + return _motions[motionId]; + } +} \ No newline at end of file diff --git a/foundry_test/utils/MockVault.sol b/foundry_test/utils/MockVault.sol new file mode 100644 index 0000000..191ee65 --- /dev/null +++ b/foundry_test/utils/MockVault.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: AGPL +pragma solidity ^0.8.16; + +// mock vault for testing purposes +contract MockVault { + + address public immutable gov; + + mapping(address => uint256) public performanceFee; + // deposit limit set to MAX_UINT256 by default + uint256 public depositLimit = type(uint256).max; + + // modifier for onlyGov + modifier onlyGov() { + _onlyGov(); + _; + } + + constructor(address _gov) { + gov = _gov; + } + + // check if the caller is the gov + function _onlyGov() internal view { + require(msg.sender == gov, "!gov"); + } + + // method that management cannot call + function updateStrategyPerformanceFee(address _strategy, uint256 _performanceFee) external onlyGov { + performanceFee[_strategy] = _performanceFee; + } + // method that management cannot call + function setDepositLimit(uint256 _depositLimit) external onlyGov { + depositLimit = _depositLimit; + } +} \ No newline at end of file diff --git a/src/LeanTrack.vy b/src/LeanTrack.vy index 715f963..e435613 100644 --- a/src/LeanTrack.vy +++ b/src/LeanTrack.vy @@ -23,7 +23,9 @@ MIN_OBJECTIONS_THRESHOLD: constant(uint256) = 100 # @notice upper bound for objections threshold settings # @dev represented in basis points (30% = 3000) MAX_OBJECTIONS_THRESHOLD: constant(uint256) = 3000 -MIN_MOTION_DURATION: constant(uint256) = 57600 # 16 hours +# @dev minimum time in seconds for queueing motion allows for 1 hour of objections +# @dev left low for emergency situations, factories can set higher values for non emergency operations +MIN_MOTION_DURATION: constant(uint256) = 1 # 1 second HUNDRED_PERCENT: constant(uint256) = 10000 # 100% ### interfaces diff --git a/src/SerpentorBravo.vy b/src/SerpentorBravo.vy index 75b3c5c..f9b8979 100644 --- a/src/SerpentorBravo.vy +++ b/src/SerpentorBravo.vy @@ -271,7 +271,7 @@ def __init__( @param votingPeriod The initial voting period @param votingDelay The initial voting delay @param proposalThreshold The initial proposal threshold - @param quorumVotes The initial quorum voting setting, recommended to be higher than proposalThreshold + @param quorumVotes The initial quorum voting setting, recommended to be higher than proposalThreshold, should be higher than proposalThreshold @param initialProposalId The initialProposalId to start the counter """ assert timelockAddr != empty(address), "!timelock" diff --git a/src/factories/BaseMotionFactory.sol b/src/factories/BaseMotionFactory.sol new file mode 100644 index 0000000..9ddef96 --- /dev/null +++ b/src/factories/BaseMotionFactory.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: AGPL +pragma solidity ^0.8.16; + +interface LeanTrack { + /** + * @dev view functions + */ + function createMotion(address[] memory targets, uint256[] memory values, string[] memory signatures, bytes[] memory calldatas) external returns (uint256); + function queueMotion(uint256 motionId) external returns (bytes32[] memory); + function enactMotion(uint256 motionId) external; + function cancelMotion(uint256 motionId) external; + function objectToMotion(uint256 motionId) external; +} + +abstract contract BaseMotionFactory { + + address public immutable leanTrack; + address public immutable gov; + // authorized roles for this factory to execute restricted calls + mapping(address => bool) public authorized; + + modifier onlyAuthorized() { + _onlyAuthorized(); + _; + } + + modifier onlyGov() { + _onlyGov(); + _; + } + + constructor(address _leanTrack, address _gov) { + leanTrack = _leanTrack; + gov = _gov; + } + + /** + * @dev internal view function to check if the caller is the gov + */ + function _onlyGov() internal view { + require(msg.sender == gov, "!gov"); + } + + /** + * @dev internal view function to check if the caller is authorized + */ + function _onlyAuthorized() internal view { + require(authorized[msg.sender], "!auth"); + } + + /** + * @dev internal function create a motion + * @param targets array of addresses of the contracts to call + * @param values array of values to send to each contract + * @param signatures array of function signatures to call + * @param calldatas array of calldata to send + * @return motionId id of the created motion + */ + function _createMotion( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas + ) internal virtual returns (uint256) { + return LeanTrack(leanTrack).createMotion( + targets, + values, + signatures, + calldatas + ); + } + + function cancelMotion(uint256 motionId) external virtual onlyAuthorized { + _cancelMotion(motionId); + } + + // create an internal function for canceling a motion + function _cancelMotion(uint256 motionId) internal virtual { + LeanTrack(leanTrack).cancelMotion(motionId); + } + + /** + * @dev set authorized roles for this factory to execute restricted calls + * @param _authorized address of the authorized role + * @param _status status of the authorized role + * @notice only gov can call this function + * @notice doesnt handle separate granular roles, implementations should handle usage of these authorized addresses + */ + function setAuthorized(address _authorized, bool _status) external onlyGov { + authorized[_authorized] = _status; + } +} \ No newline at end of file diff --git a/src/factories/examples/BribesToSplitterMotionFactory.sol b/src/factories/examples/BribesToSplitterMotionFactory.sol new file mode 100644 index 0000000..67fd606 --- /dev/null +++ b/src/factories/examples/BribesToSplitterMotionFactory.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: AGPL +pragma solidity ^0.8.16; + +import "../BaseMotionFactory.sol"; + +// This contract is used as an example implementation for testing purposes only +// It is not meant to be used in production and lacks more security checks +/** + * @dev Example contract for creating motions that transfer tokens + */ +contract BribesToSplitterMotionFactory is BaseMotionFactory { + + //voter = safe.contract('curve-voter.ychad.eth') + //splitter = safe.contract('bribe-splitter.ychad.eth') + address public immutable VOTER = 0xF147b8125d2ef93FB6965Db97D6746952a133934; + address public immutable SPLITTER = 0x527e80008D212E2891C737Ba8a2768a7337D7Fd2; + + // mapping that handle transfer limits for each token + mapping(address => uint256) public transferLimits; + + constructor(address _leanTrack, address _gov) BaseMotionFactory(_leanTrack, _gov) {} + + // function to set transfer limits for a token + function setTransferLimit(address token, uint256 limit) external onlyGov { + require(limit > 0, "> 0"); + transferLimits[token] = limit; + } + + function disallowTokenTransfer(address token) external onlyGov { + transferLimits[token] = 0; + } + + // function to create a motion that transfers tokens from bribes to splitter + function createBribesTransferMotion( + address[] calldata token, + uint256[] calldata amount + ) external onlyAuthorized returns (uint256) { + // iterate over tokens and amounts + require(token.length == amount.length, "token.length != amount.length"); + address[] memory targets = new address[](token.length); + uint256[] memory values = new uint256[](token.length); + string[] memory signatures = new string[](token.length); + bytes[] memory calldatas = new bytes[](token.length); + for (uint256 i = 0; i < token.length; i++) { + require (amount[i] > 0, "!amount"); + require(amount[i] <= transferLimits[token[i]], "amount > limit"); + targets[i] = VOTER; + values[i] = 0; + bytes memory calldataForTransfer = abi.encodeWithSignature("transfer(address,uint256)", SPLITTER, amount[i]); + calldatas[i] = abi.encodeWithSignature("execute(address,uint256,bytes)", token[i], 0, calldataForTransfer); + } + return _createMotion(targets, values, signatures, calldatas); + } +} \ No newline at end of file diff --git a/src/factories/examples/TransferMotionFactory.sol b/src/factories/examples/TransferMotionFactory.sol new file mode 100644 index 0000000..c5db724 --- /dev/null +++ b/src/factories/examples/TransferMotionFactory.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: AGPL +pragma solidity ^0.8.16; + +import "../BaseMotionFactory.sol"; + +// This contract is used as an example implementation for testing purposes only +// It is not meant to be used in production and lacks more security checks +/** + * @dev Example contract for creating motions that transfer tokens + */ +contract TransferMotionFactory is BaseMotionFactory { + + // mapping that handle transfer limits for each token + mapping(address => uint256) public transferLimits; + + constructor(address _leanTrack, address _gov) BaseMotionFactory(_leanTrack, _gov) {} + + // function to set transfer limits for a token + function setTransferLimit(address token, uint256 limit) external onlyGov { + require(limit > 0, "> 0"); + transferLimits[token] = limit; + } + + function disallowTokenTransfer(address token) external onlyGov { + transferLimits[token] = 0; + } + + // function to create a motion that transfers tokens + function createTransferMotion( + address token, + address to, + uint256 amount + ) external onlyAuthorized returns (uint256) { + require (amount > 0, "!amount"); + require(amount <= transferLimits[token], "amount > limit"); + address[] memory targets = new address[](1); + targets[0] = token; + uint256[] memory values = new uint256[](1); + values[0] = 0; + string[] memory signatures = new string[](1); + signatures[0] = "transfer(address,uint256)"; + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = abi.encode(to, amount); + return _createMotion(targets, values, signatures, calldatas); + } +} \ No newline at end of file diff --git a/src/factories/examples/VaultOperationsMotionFactory.sol b/src/factories/examples/VaultOperationsMotionFactory.sol new file mode 100644 index 0000000..15bce64 --- /dev/null +++ b/src/factories/examples/VaultOperationsMotionFactory.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: AGPL +pragma solidity ^0.8.16; + +import "../BaseMotionFactory.sol"; + +interface Vault { + function updateStrategyDebtRatio(address _strategy, uint256 _debtRatio) external; +} + +// This contract is used as an example implementation for testing purposes only +// It is not meant to be used in production and lacks more security checks +/** + * @dev Example contract for creating motions that manage emergency operations for yearn vaults + */ +contract VaultOperationsMotionFactory is BaseMotionFactory { + + constructor(address _leanTrack, address _gov) BaseMotionFactory(_leanTrack, _gov) {} + + // NOTE: this function could also be implemented with batch vaults and limits + function setDepositLimit(address _vault, uint256 _limit) external onlyAuthorized { + // WARNING: this is a simplified example, in production you should check if the vault is a valid vault + address[] memory targets = new address[](1); + targets[0] = _vault; + uint256[] memory values = new uint256[](1); + values[0] = 0; + string[] memory signatures = new string[](1); + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = abi.encodeWithSignature("setDepositLimit(uint256)", _limit); + _createMotion(targets, values, signatures, calldatas); + } + + // emergency function to disable deposit into multiple vaults + function disableDepositLimit(address[] calldata _vaults) external onlyAuthorized returns (uint256) { + // iterate vaults and create motion + address[] memory targets = new address[](_vaults.length); + uint256[] memory values = new uint256[](_vaults.length); + string[] memory signatures = new string[](_vaults.length); + bytes[] memory calldatas = new bytes[](_vaults.length); + // WARNING: this is a simplified example, in production you should check if the vault is a valid vault + for (uint256 i = 0; i < _vaults.length; i++) { + targets[i] = _vaults[i]; + values[i] = 0; + calldatas[i] = abi.encodeWithSignature("setDepositLimit(uint256)", 0); + } + return _createMotion(targets, values, signatures, calldatas); + } + + + +} \ No newline at end of file From 5574d312e2198445c520fd9c0268a2c585481221 Mon Sep 17 00:00:00 2001 From: Storming0x <6074987+storming0x@users.noreply.github.com> Date: Thu, 7 Dec 2023 17:35:43 -0600 Subject: [PATCH 9/9] Feat/example motion factories (#36) * Chore: commit baseMotionFactory * Feat: example motion factories * Fix failing test * chore: add example --------- Co-authored-by: storming0x <storm0x@storm0x.com> --- src/factories/examples/TransferMotionFactory.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/factories/examples/TransferMotionFactory.sol b/src/factories/examples/TransferMotionFactory.sol index c5db724..518c6aa 100644 --- a/src/factories/examples/TransferMotionFactory.sol +++ b/src/factories/examples/TransferMotionFactory.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: AGPL -pragma solidity ^0.8.16; + +pragma solidity ^0.8.17; import "../BaseMotionFactory.sol";