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";