Repository: pcaversaccio/snekmate (language: python)

Issue:
💥 `TimelockController` Contract
Build a contract module that acts as a time-locked controller. In a governance system, the `TimelockController` contract is in charge of introducing a delay between a proposal and its execution. It can be used with or without a `Governor` (see https://github.com/pcaversaccio/snekmate/issues/177).

## References

- https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/governance/TimelockController.sol
- https://github.com/storming0x/serpentor/blob/master/src/Timelock.vy


Discussion / hints:
After a bilateral discussion, this issue is assigned to @cairoeth.

Fix patch:
diff --git a/.gas-snapshot b/.gas-snapshot
index 9f91dc85..994774a5 100644
--- a/.gas-snapshot
+++ b/.gas-snapshot
@@ -4,12 +4,12 @@ AccessControlTest:testFuzzGrantRoleAdminRoleSuccess(address) (runs: 256, μ: 437
 AccessControlTest:testFuzzGrantRoleMultipleTimesSuccess(address) (runs: 256, μ: 48930, ~: 48930)
 AccessControlTest:testFuzzGrantRoleNonAdmin(address,address) (runs: 256, μ: 16544, ~: 16544)
 AccessControlTest:testFuzzGrantRoleSuccess(address) (runs: 256, μ: 43797, ~: 43797)
-AccessControlTest:testFuzzRenounceRoleMultipleTimesSuccess(address) (runs: 256, μ: 44372, ~: 44356)
+AccessControlTest:testFuzzRenounceRoleMultipleTimesSuccess(address) (runs: 256, μ: 44371, ~: 44356)
 AccessControlTest:testFuzzRenounceRoleNonMsgSender(address) (runs: 256, μ: 9556, ~: 9556)
-AccessControlTest:testFuzzRenounceRoleSuccess(address) (runs: 256, μ: 40867, ~: 40851)
-AccessControlTest:testFuzzRevokeRoleMultipleTimesSuccess(address) (runs: 256, μ: 40637, ~: 40620)
+AccessControlTest:testFuzzRenounceRoleSuccess(address) (runs: 256, μ: 40866, ~: 40851)
+AccessControlTest:testFuzzRevokeRoleMultipleTimesSuccess(address) (runs: 256, μ: 40636, ~: 40620)
 AccessControlTest:testFuzzRevokeRoleNonAdmin(address,address) (runs: 256, μ: 16498, ~: 16498)
-AccessControlTest:testFuzzRevokeRoleSuccess(address) (runs: 256, μ: 39481, ~: 39464)
+AccessControlTest:testFuzzRevokeRoleSuccess(address) (runs: 256, μ: 39480, ~: 39464)
 AccessControlTest:testFuzzSetRoleAdminPreviousAdminCallsGrantRole(address,address) (runs: 256, μ: 75747, ~: 75747)
 AccessControlTest:testFuzzSetRoleAdminPreviousAdminCallsRevokeRole(address,address) (runs: 256, μ: 101276, ~: 101276)
 AccessControlTest:testFuzzSetRoleAdminSuccess(address,address) (runs: 256, μ: 89648, ~: 89648)
@@ -60,8 +60,8 @@ BatchDistributorTest:testDistributeTokenMultipleAddressesSuccess() (gas: 616202)
 BatchDistributorTest:testDistributeTokenOneAddressSuccess() (gas: 578527)
 BatchDistributorTest:testDistributeTokenRevertWithInsufficientAllowance() (gas: 574268)
 BatchDistributorTest:testDistributeTokenRevertWithInsufficientBalance() (gas: 575054)
-BatchDistributorTest:testFuzzDistributeEtherMultipleAddressesSuccess(((address,uint256)[]),uint256) (runs: 256, μ: 1812214, ~: 1793268)
-BatchDistributorTest:testFuzzDistributeTokenMultipleAddressesSuccess(((address,uint256)[]),address,uint256) (runs: 256, μ: 1337340, ~: 1321378)
+BatchDistributorTest:testFuzzDistributeEtherMultipleAddressesSuccess(((address,uint256)[]),uint256) (runs: 256, μ: 1840920, ~: 1861320)
+BatchDistributorTest:testFuzzDistributeTokenMultipleAddressesSuccess(((address,uint256)[]),address,uint256) (runs: 256, μ: 1337465, ~: 1321455)
 Create2AddressTest:testComputeAddress() (gas: 550599)
 Create2AddressTest:testComputeAddressSelf() (gas: 558922)
 Create2AddressTest:testFuzzComputeAddress(bytes32,address) (runs: 256, μ: 551194, ~: 551194)
@@ -87,26 +87,26 @@ CreateAddressTest:testComputeAddressSelfNonceUint56() (gas: 539228)
 CreateAddressTest:testComputeAddressSelfNonceUint64() (gas: 539345)
 CreateAddressTest:testComputeAddressSelfNonceUint8() (gas: 538951)
 CreateAddressTest:testComputeAddressSelfRevertTooHighNonce() (gas: 8836)
-CreateAddressTest:testFuzzComputeAddressNonce0x7f(uint64,address) (runs: 256, μ: 537963, ~: 538066)
-CreateAddressTest:testFuzzComputeAddressNonceUint16(uint64,address) (runs: 256, μ: 537412, ~: 537681)
-CreateAddressTest:testFuzzComputeAddressNonceUint24(uint64,address) (runs: 256, μ: 537536, ~: 537657)
-CreateAddressTest:testFuzzComputeAddressNonceUint32(uint64,address) (runs: 256, μ: 537551, ~: 537664)
-CreateAddressTest:testFuzzComputeAddressNonceUint40(uint64,address) (runs: 256, μ: 537662, ~: 537756)
-CreateAddressTest:testFuzzComputeAddressNonceUint48(uint64,address) (runs: 256, μ: 537641, ~: 537740)
-CreateAddressTest:testFuzzComputeAddressNonceUint56(uint64,address) (runs: 256, μ: 537687, ~: 537769)
-CreateAddressTest:testFuzzComputeAddressNonceUint64(uint64,address) (runs: 256, μ: 537668, ~: 537873)
-CreateAddressTest:testFuzzComputeAddressNonceUint8(uint64,address) (runs: 256, μ: 537454, ~: 537531)
-CreateAddressTest:testFuzzComputeAddressRevertTooHighNonce(uint256,address) (runs: 256, μ: 12926, ~: 12857)
-CreateAddressTest:testFuzzComputeAddressSelfNonce0x7f(uint64) (runs: 256, μ: 543802, ~: 543905)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint16(uint64) (runs: 256, μ: 543016, ~: 542941)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint24(uint64) (runs: 256, μ: 543292, ~: 543395)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint32(uint64) (runs: 256, μ: 543372, ~: 543491)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint40(uint64) (runs: 256, μ: 543390, ~: 543475)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint48(uint64) (runs: 256, μ: 543438, ~: 543549)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint56(uint64) (runs: 256, μ: 543527, ~: 543619)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint64(uint64) (runs: 256, μ: 543599, ~: 543749)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint8(uint64) (runs: 256, μ: 543108, ~: 543186)
-CreateAddressTest:testFuzzComputeAddressSelfRevertTooHighNonce(uint256) (runs: 256, μ: 12722, ~: 12696)
+CreateAddressTest:testFuzzComputeAddressNonce0x7f(uint64,address) (runs: 256, μ: 537964, ~: 538066)
+CreateAddressTest:testFuzzComputeAddressNonceUint16(uint64,address) (runs: 256, μ: 537407, ~: 537680)
+CreateAddressTest:testFuzzComputeAddressNonceUint24(uint64,address) (runs: 256, μ: 537539, ~: 537657)
+CreateAddressTest:testFuzzComputeAddressNonceUint32(uint64,address) (runs: 256, μ: 537541, ~: 537664)
+CreateAddressTest:testFuzzComputeAddressNonceUint40(uint64,address) (runs: 256, μ: 537665, ~: 537756)
+CreateAddressTest:testFuzzComputeAddressNonceUint48(uint64,address) (runs: 256, μ: 537642, ~: 537740)
+CreateAddressTest:testFuzzComputeAddressNonceUint56(uint64,address) (runs: 256, μ: 537685, ~: 537769)
+CreateAddressTest:testFuzzComputeAddressNonceUint64(uint64,address) (runs: 256, μ: 537670, ~: 537873)
+CreateAddressTest:testFuzzComputeAddressNonceUint8(uint64,address) (runs: 256, μ: 537453, ~: 537531)
+CreateAddressTest:testFuzzComputeAddressRevertTooHighNonce(uint256,address) (runs: 256, μ: 12920, ~: 12857)
+CreateAddressTest:testFuzzComputeAddressSelfNonce0x7f(uint64) (runs: 256, μ: 543801, ~: 543905)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint16(uint64) (runs: 256, μ: 543043, ~: 542941)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint24(uint64) (runs: 256, μ: 543302, ~: 543395)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint32(uint64) (runs: 256, μ: 543380, ~: 543491)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint40(uint64) (runs: 256, μ: 543394, ~: 543475)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint48(uint64) (runs: 256, μ: 543437, ~: 543549)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint56(uint64) (runs: 256, μ: 543525, ~: 543619)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint64(uint64) (runs: 256, μ: 543574, ~: 543749)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint8(uint64) (runs: 256, μ: 543109, ~: 543186)
+CreateAddressTest:testFuzzComputeAddressSelfRevertTooHighNonce(uint256) (runs: 256, μ: 12735, ~: 12696)
 ECDSATest:testEthSignedMessageHash() (gas: 5846)
 ECDSATest:testFuzzEthSignedMessageHash(string) (runs: 256, μ: 6432, ~: 6426)
 ECDSATest:testFuzzRecoverWithInvalidSignature(bytes,string) (runs: 256, μ: 15159, ~: 15161)
@@ -133,7 +133,7 @@ EIP712DomainSeparatorTest:testCachedDomainSeparatorV4() (gas: 7562)
 EIP712DomainSeparatorTest:testDomainSeparatorV4() (gas: 11305)
 EIP712DomainSeparatorTest:testEIP712Domain() (gas: 13629)
 EIP712DomainSeparatorTest:testFuzzDomainSeparatorV4(uint8) (runs: 256, μ: 11460, ~: 11480)
-EIP712DomainSeparatorTest:testFuzzEIP712Domain(bytes1,uint8,bytes32,uint256[]) (runs: 256, μ: 19159, ~: 19122)
+EIP712DomainSeparatorTest:testFuzzEIP712Domain(bytes1,uint8,bytes32,uint256[]) (runs: 256, μ: 19161, ~: 19122)
 EIP712DomainSeparatorTest:testFuzzHashTypedDataV4(address,address,uint256,uint256,uint64) (runs: 256, μ: 7698, ~: 7698)
 EIP712DomainSeparatorTest:testHashTypedDataV4() (gas: 13017)
 ERC1155Invariants:invariantOwner() (runs: 256, calls: 3840, reverts: 3437)
@@ -163,35 +163,35 @@ ERC1155Test:testExistsAfterBatchMint() (gas: 143320)
 ERC1155Test:testExistsAfterSingleBurn() (gas: 137816)
 ERC1155Test:testExistsAfterSingleMint() (gas: 119400)
 ERC1155Test:testExistsBeforeMint() (gas: 7642)
-ERC1155Test:testFuzzBurnBatchSuccess(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 132323, ~: 132328)
-ERC1155Test:testFuzzBurnSuccess(address,address,uint256) (runs: 256, μ: 140104, ~: 140134)
+ERC1155Test:testFuzzBurnBatchSuccess(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 132334, ~: 132343)
+ERC1155Test:testFuzzBurnSuccess(address,address,uint256) (runs: 256, μ: 140114, ~: 140134)
 ERC1155Test:testFuzzRenounceOwnershipNonOwner(address) (runs: 256, μ: 13977, ~: 13977)
-ERC1155Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 48433, ~: 48416)
-ERC1155Test:testFuzzSafeBatchTransferFromByApprovedOperator(address,address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 225211, ~: 225191)
+ERC1155Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 48432, ~: 48416)
+ERC1155Test:testFuzzSafeBatchTransferFromByApprovedOperator(address,address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 225230, ~: 225191)
 ERC1155Test:testFuzzSafeBatchTransferFromEOAReceiver(address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 190347, ~: 190304)
-ERC1155Test:testFuzzSafeBatchTransferFromNoData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 564164, ~: 564164)
-ERC1155Test:testFuzzSafeBatchTransferFromWithData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 564718, ~: 564321)
+ERC1155Test:testFuzzSafeBatchTransferFromNoData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 564154, ~: 564164)
+ERC1155Test:testFuzzSafeBatchTransferFromWithData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 564729, ~: 564321)
 ERC1155Test:testFuzzSafeMintBatchEOAReceiver(address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 152243, ~: 152222)
 ERC1155Test:testFuzzSafeMintBatchNoData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 519951, ~: 519951)
 ERC1155Test:testFuzzSafeMintBatchNonMinter(address) (runs: 256, μ: 40926, ~: 40926)
-ERC1155Test:testFuzzSafeMintBatchWithData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 520300, ~: 520066)
+ERC1155Test:testFuzzSafeMintBatchWithData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 520295, ~: 520066)
 ERC1155Test:testFuzzSafeMintEOAReceiver(address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 151294, ~: 151259)
 ERC1155Test:testFuzzSafeMintNoData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 520118, ~: 520118)
 ERC1155Test:testFuzzSafeMintNonMinter(address) (runs: 256, μ: 33132, ~: 33132)
 ERC1155Test:testFuzzSafeMintWithData(address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 521416, ~: 521009)
-ERC1155Test:testFuzzSafeTransferFromByApprovedOperator(address,address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 204219, ~: 204191)
+ERC1155Test:testFuzzSafeTransferFromByApprovedOperator(address,address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 204239, ~: 204191)
 ERC1155Test:testFuzzSafeTransferFromEOAReceiver(address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 169443, ~: 169391)
-ERC1155Test:testFuzzSafeTransferFromNoData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 606373, ~: 606383)
-ERC1155Test:testFuzzSafeTransferFromWithData(address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 608365, ~: 607645)
+ERC1155Test:testFuzzSafeTransferFromNoData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 606383, ~: 606383)
+ERC1155Test:testFuzzSafeTransferFromWithData(address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 608346, ~: 607645)
 ERC1155Test:testFuzzSetApprovalForAllRevoke(address,address) (runs: 256, μ: 31053, ~: 31022)
 ERC1155Test:testFuzzSetApprovalForAllSuccess(address,address) (runs: 256, μ: 44181, ~: 44181)
 ERC1155Test:testFuzzSetMinterNonOwner(address,string) (runs: 256, μ: 15772, ~: 15699)
 ERC1155Test:testFuzzSetMinterSuccess(string) (runs: 256, μ: 33178, ~: 33178)
 ERC1155Test:testFuzzSetUriNonMinter(address) (runs: 256, μ: 14452, ~: 14452)
-ERC1155Test:testFuzzTotalSupplyAfterBatchBurn(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 126363, ~: 126368)
-ERC1155Test:testFuzzTotalSupplyAfterBatchMint(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 140987, ~: 140922)
+ERC1155Test:testFuzzTotalSupplyAfterBatchBurn(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 126374, ~: 126383)
+ERC1155Test:testFuzzTotalSupplyAfterBatchMint(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 140991, ~: 141055)
 ERC1155Test:testFuzzTotalSupplyAfterSingleBurn(address,uint256,bytes) (runs: 256, μ: 138036, ~: 138007)
-ERC1155Test:testFuzzTotalSupplyAfterSingleMint(uint256,uint256,bytes) (runs: 256, μ: 91653, ~: 119836)
+ERC1155Test:testFuzzTotalSupplyAfterSingleMint(uint256,uint256,bytes) (runs: 256, μ: 93285, ~: 119839)
 ERC1155Test:testFuzzTransferOwnershipNonOwner(address,address) (runs: 256, μ: 14086, ~: 14086)
 ERC1155Test:testFuzzTransferOwnershipSuccess(address,address) (runs: 256, μ: 73957, ~: 73928)
 ERC1155Test:testHasOwner() (gas: 9714)
@@ -266,8 +266,8 @@ ERC1155Test:testUriBaseAndTokenUriNotSet() (gas: 2768301)
 ERC1155Test:testUriBaseAndTokenUriSet() (gas: 65517)
 ERC1155Test:testUriNoBaseURI() (gas: 2818373)
 ERC1155Test:testUriNoTokenUri() (gas: 17407)
-ERC20Invariants:invariantOwner() (runs: 256, calls: 3840, reverts: 3392)
-ERC20Invariants:invariantTotalSupply() (runs: 256, calls: 3840, reverts: 3392)
+ERC20Invariants:invariantOwner() (runs: 256, calls: 3840, reverts: 3390)
+ERC20Invariants:invariantTotalSupply() (runs: 256, calls: 3840, reverts: 3390)
 ERC20Test:testApproveExceedingBalanceCase1() (gas: 40223)
 ERC20Test:testApproveExceedingBalanceCase2() (gas: 46478)
 ERC20Test:testApproveFromZeroAddress() (gas: 13220)
@@ -290,26 +290,26 @@ ERC20Test:testCachedDomainSeparator() (gas: 7699)
 ERC20Test:testDomainSeparator() (gas: 11449)
 ERC20Test:testEIP712Domain() (gas: 13739)
 ERC20Test:testFuzzApproveSuccess(address,uint256) (runs: 256, μ: 39193, ~: 40437)
-ERC20Test:testFuzzBurnFromInsufficientAllowance(address,uint256,uint8) (runs: 256, μ: 175220, ~: 175283)
-ERC20Test:testFuzzBurnFromSuccess(address,uint256) (runs: 256, μ: 279935, ~: 280986)
+ERC20Test:testFuzzBurnFromInsufficientAllowance(address,uint256,uint8) (runs: 256, μ: 175155, ~: 175280)
+ERC20Test:testFuzzBurnFromSuccess(address,uint256) (runs: 256, μ: 280125, ~: 281154)
 ERC20Test:testFuzzBurnInvalidAmount(address,uint256) (runs: 256, μ: 16454, ~: 16454)
-ERC20Test:testFuzzBurnSuccessCase(uint256) (runs: 256, μ: 256007, ~: 255800)
+ERC20Test:testFuzzBurnSuccessCase(uint256) (runs: 256, μ: 256021, ~: 255800)
 ERC20Test:testFuzzDomainSeparator(uint8) (runs: 256, μ: 11665, ~: 11685)
-ERC20Test:testFuzzEIP712Domain(bytes1,uint8,bytes32,uint256[]) (runs: 256, μ: 19449, ~: 19409)
+ERC20Test:testFuzzEIP712Domain(bytes1,uint8,bytes32,uint256[]) (runs: 256, μ: 19444, ~: 19409)
 ERC20Test:testFuzzMintNonMinter(string,uint256) (runs: 256, μ: 13152, ~: 13080)
-ERC20Test:testFuzzMintSuccess(string,uint256) (runs: 256, μ: 54130, ~: 55192)
+ERC20Test:testFuzzMintSuccess(string,uint256) (runs: 256, μ: 54206, ~: 55192)
 ERC20Test:testFuzzPermitInvalid(string,string,uint16) (runs: 256, μ: 45279, ~: 45277)
 ERC20Test:testFuzzPermitSuccess(string,string,uint16) (runs: 256, μ: 70299, ~: 70295)
 ERC20Test:testFuzzRenounceOwnershipNonOwner(address) (runs: 256, μ: 13979, ~: 13979)
-ERC20Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 48360, ~: 48345)
+ERC20Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 48361, ~: 48345)
 ERC20Test:testFuzzSetMinterNonOwner(address,string) (runs: 256, μ: 15777, ~: 15704)
 ERC20Test:testFuzzSetMinterSuccess(string) (runs: 256, μ: 33196, ~: 33196)
-ERC20Test:testFuzzTransferFromInsufficientAllowance(address,address,uint256,uint8) (runs: 256, μ: 173301, ~: 173761)
-ERC20Test:testFuzzTransferFromSuccess(address,address,uint256) (runs: 256, μ: 200334, ~: 202446)
+ERC20Test:testFuzzTransferFromInsufficientAllowance(address,address,uint256,uint8) (runs: 256, μ: 173482, ~: 173761)
+ERC20Test:testFuzzTransferFromSuccess(address,address,uint256) (runs: 256, μ: 200080, ~: 202152)
 ERC20Test:testFuzzTransferInvalidAmount(address,address,uint256) (runs: 256, μ: 16849, ~: 16849)
 ERC20Test:testFuzzTransferOwnershipNonOwner(address,address) (runs: 256, μ: 14068, ~: 14068)
-ERC20Test:testFuzzTransferOwnershipSuccess(address,address) (runs: 256, μ: 73902, ~: 73874)
-ERC20Test:testFuzzTransferSuccess(address,uint256) (runs: 256, μ: 172886, ~: 173875)
+ERC20Test:testFuzzTransferOwnershipSuccess(address,address) (runs: 256, μ: 73901, ~: 73874)
+ERC20Test:testFuzzTransferSuccess(address,uint256) (runs: 256, μ: 172825, ~: 173876)
 ERC20Test:testHasOwner() (gas: 9758)
 ERC20Test:testInitialSetup() (gas: 1467701)
 ERC20Test:testMintNonMinter() (gas: 12639)
@@ -349,20 +349,20 @@ ERC2981Test:testDeleteDefaultRoyaltyNonOwner() (gas: 10537)
 ERC2981Test:testFuzzDeleteDefaultRoyaltyNonOwner(address) (runs: 256, μ: 13271, ~: 13271)
 ERC2981Test:testFuzzRenounceOwnershipNonOwner(address) (runs: 256, μ: 13972, ~: 13972)
 ERC2981Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 24598, ~: 24598)
-ERC2981Test:testFuzzResetTokenRoyalty(address,address,uint256,uint256,uint96,uint256) (runs: 256, μ: 93597, ~: 94717)
+ERC2981Test:testFuzzResetTokenRoyalty(address,address,uint256,uint256,uint96,uint256) (runs: 256, μ: 93338, ~: 94699)
 ERC2981Test:testFuzzResetTokenRoyaltyNonOwner(address) (runs: 256, μ: 13246, ~: 13246)
-ERC2981Test:testFuzzRoyaltyInfoDefaultRoyalty(address,uint256,uint256,uint96,uint256) (runs: 256, μ: 77988, ~: 79183)
-ERC2981Test:testFuzzRoyaltyInfoDeleteDefaultRoyalty(address,uint256,uint256,uint96,uint256) (runs: 256, μ: 61712, ~: 62571)
-ERC2981Test:testFuzzRoyaltyInfoSetTokenRoyalty(address,address,uint256,uint256,uint96,uint256) (runs: 256, μ: 119876, ~: 121030)
-ERC2981Test:testFuzzRoyaltyInfoSetTokenRoyaltyUpdate(address,address,uint256,uint256,uint96,uint256) (runs: 256, μ: 164296, ~: 165531)
-ERC2981Test:testFuzzRoyaltyInfoUpdateDefaultRoyalty(address,address,uint256,uint256,uint96,uint256) (runs: 256, μ: 84143, ~: 84678)
+ERC2981Test:testFuzzRoyaltyInfoDefaultRoyalty(address,uint256,uint256,uint96,uint256) (runs: 256, μ: 77900, ~: 79183)
+ERC2981Test:testFuzzRoyaltyInfoDeleteDefaultRoyalty(address,uint256,uint256,uint96,uint256) (runs: 256, μ: 61643, ~: 62573)
+ERC2981Test:testFuzzRoyaltyInfoSetTokenRoyalty(address,address,uint256,uint256,uint96,uint256) (runs: 256, μ: 118900, ~: 121030)
+ERC2981Test:testFuzzRoyaltyInfoSetTokenRoyaltyUpdate(address,address,uint256,uint256,uint96,uint256) (runs: 256, μ: 162152, ~: 165531)
+ERC2981Test:testFuzzRoyaltyInfoUpdateDefaultRoyalty(address,address,uint256,uint256,uint96,uint256) (runs: 256, μ: 83741, ~: 84678)
 ERC2981Test:testFuzzSetDefaultRoyaltyNonOwner(address) (runs: 256, μ: 15525, ~: 15525)
-ERC2981Test:testFuzzSetDefaultRoyaltyTooHighFeeNumerator(uint96) (runs: 256, μ: 21342, ~: 21125)
+ERC2981Test:testFuzzSetDefaultRoyaltyTooHighFeeNumerator(uint96) (runs: 256, μ: 21316, ~: 21125)
 ERC2981Test:testFuzzSetTokenRoyaltyInvalidReceiver(address) (runs: 256, μ: 18626, ~: 18626)
 ERC2981Test:testFuzzSetTokenRoyaltyNonOwner(address) (runs: 256, μ: 15565, ~: 15565)
-ERC2981Test:testFuzzSetTokenRoyaltyTooHighFeeNumerator(uint96) (runs: 256, μ: 21383, ~: 21166)
+ERC2981Test:testFuzzSetTokenRoyaltyTooHighFeeNumerator(uint96) (runs: 256, μ: 21357, ~: 21166)
 ERC2981Test:testFuzzTransferOwnershipNonOwner(address,address) (runs: 256, μ: 14039, ~: 14039)
-ERC2981Test:testFuzzTransferOwnershipSuccess(address,address) (runs: 256, μ: 29566, ~: 29577)
+ERC2981Test:testFuzzTransferOwnershipSuccess(address,address) (runs: 256, μ: 29577, ~: 29577)
 ERC2981Test:testHasOwner() (gas: 9688)
 ERC2981Test:testInitialSetup() (gas: 468824)
 ERC2981Test:testRenounceOwnershipNonOwner() (gas: 10871)
@@ -388,8 +388,8 @@ ERC2981Test:testSupportsInterfaceSuccessGasCost() (gas: 6345)
 ERC2981Test:testTransferOwnershipNonOwner() (gas: 12649)
 ERC2981Test:testTransferOwnershipSuccess() (gas: 22091)
 ERC2981Test:testTransferOwnershipToZeroAddress() (gas: 15557)
-ERC4626VaultInvariants:invariantTotalAssets() (runs: 256, calls: 3840, reverts: 3244)
-ERC4626VaultInvariants:invariantTotalSupply() (runs: 256, calls: 3840, reverts: 3244)
+ERC4626VaultInvariants:invariantTotalAssets() (runs: 256, calls: 3840, reverts: 3242)
+ERC4626VaultInvariants:invariantTotalSupply() (runs: 256, calls: 3840, reverts: 3242)
 ERC4626VaultTest:testCachedDomainSeparator() (gas: 7676)
 ERC4626VaultTest:testDepositInsufficientAllowance() (gas: 82425)
 ERC4626VaultTest:testDepositWithNoApproval() (gas: 24740)
@@ -400,10 +400,10 @@ ERC4626VaultTest:testEmptyVaultDeposit() (gas: 556789)
 ERC4626VaultTest:testEmptyVaultMint() (gas: 557600)
 ERC4626VaultTest:testEmptyVaultRedeem() (gas: 191988)
 ERC4626VaultTest:testEmptyVaultwithdraw() (gas: 204833)
-ERC4626VaultTest:testFail_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 554348, ~: 557113)
-ERC4626VaultTest:testFail_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 556261, ~: 560088)
+ERC4626VaultTest:testFail_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 554223, ~: 558247)
+ERC4626VaultTest:testFail_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 556033, ~: 560088)
 ERC4626VaultTest:testFuzzDomainSeparator(uint8) (runs: 256, μ: 11557, ~: 11577)
-ERC4626VaultTest:testFuzzEIP712Domain(bytes1,uint8,bytes32,uint256[]) (runs: 256, μ: 19366, ~: 19328)
+ERC4626VaultTest:testFuzzEIP712Domain(bytes1,uint8,bytes32,uint256[]) (runs: 256, μ: 19367, ~: 19328)
 ERC4626VaultTest:testFuzzPermitInvalid(string,string,uint16) (runs: 256, μ: 45211, ~: 45209)
 ERC4626VaultTest:testFuzzPermitSuccess(string,string,uint16) (runs: 256, μ: 70194, ~: 70190)
 ERC4626VaultTest:testInitialSetup() (gas: 5784605)
@@ -424,30 +424,30 @@ ERC4626VaultTest:testVaultInteractionsForSomeoneElse() (gas: 221714)
 ERC4626VaultTest:testWithdrawInsufficientAllowance() (gas: 122543)
 ERC4626VaultTest:testWithdrawInsufficientAssets() (gas: 117830)
 ERC4626VaultTest:testWithdrawWithNoAssets() (gas: 21327)
-ERC4626VaultTest:test_RT_deposit_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 473656, ~: 476219)
-ERC4626VaultTest:test_RT_deposit_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 475496, ~: 477934)
-ERC4626VaultTest:test_RT_mint_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 474716, ~: 476410)
-ERC4626VaultTest:test_RT_mint_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 476458, ~: 478182)
-ERC4626VaultTest:test_RT_redeem_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 473392, ~: 476462)
-ERC4626VaultTest:test_RT_redeem_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 473988, ~: 476655)
-ERC4626VaultTest:test_RT_withdraw_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 475158, ~: 478033)
-ERC4626VaultTest:test_RT_withdraw_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 476278, ~: 478681)
-ERC4626VaultTest:test_asset((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 423325, ~: 427727)
-ERC4626VaultTest:test_convertToAssets((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 429198, ~: 432743)
-ERC4626VaultTest:test_convertToShares((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 428579, ~: 432510)
-ERC4626VaultTest:test_deposit((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 468470, ~: 472341)
-ERC4626VaultTest:test_maxDeposit((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 423337, ~: 427740)
-ERC4626VaultTest:test_maxMint((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 423362, ~: 427765)
-ERC4626VaultTest:test_maxRedeem((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 423477, ~: 427879)
-ERC4626VaultTest:test_maxWithdraw((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 425030, ~: 429549)
-ERC4626VaultTest:test_mint((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 469577, ~: 472557)
-ERC4626VaultTest:test_previewDeposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 466374, ~: 468752)
-ERC4626VaultTest:test_previewMint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 467591, ~: 469131)
-ERC4626VaultTest:test_previewRedeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 467897, ~: 470574)
-ERC4626VaultTest:test_previewWithdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 470745, ~: 472090)
-ERC4626VaultTest:test_redeem((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 472309, ~: 475022)
-ERC4626VaultTest:test_totalAssets((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 423927, ~: 428329)
-ERC4626VaultTest:test_withdraw((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 473454, ~: 476196)
+ERC4626VaultTest:test_RT_deposit_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 473608, ~: 475944)
+ERC4626VaultTest:test_RT_deposit_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 475389, ~: 477933)
+ERC4626VaultTest:test_RT_mint_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 474274, ~: 476284)
+ERC4626VaultTest:test_RT_mint_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 476630, ~: 478151)
+ERC4626VaultTest:test_RT_redeem_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 473670, ~: 476737)
+ERC4626VaultTest:test_RT_redeem_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 473963, ~: 476539)
+ERC4626VaultTest:test_RT_withdraw_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 475427, ~: 478372)
+ERC4626VaultTest:test_RT_withdraw_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 476380, ~: 478560)
+ERC4626VaultTest:test_asset((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 422368, ~: 427654)
+ERC4626VaultTest:test_convertToAssets((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 429078, ~: 432395)
+ERC4626VaultTest:test_convertToShares((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 428584, ~: 432529)
+ERC4626VaultTest:test_deposit((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 468672, ~: 472487)
+ERC4626VaultTest:test_maxDeposit((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 422381, ~: 427667)
+ERC4626VaultTest:test_maxMint((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 422406, ~: 427692)
+ERC4626VaultTest:test_maxRedeem((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 422521, ~: 427806)
+ERC4626VaultTest:test_maxWithdraw((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 424074, ~: 429427)
+ERC4626VaultTest:test_mint((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 469793, ~: 472627)
+ERC4626VaultTest:test_previewDeposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 465930, ~: 468818)
+ERC4626VaultTest:test_previewMint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 467656, ~: 469248)
+ERC4626VaultTest:test_previewRedeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 468133, ~: 470638)
+ERC4626VaultTest:test_previewWithdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 470392, ~: 471970)
+ERC4626VaultTest:test_redeem((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 472120, ~: 474925)
+ERC4626VaultTest:test_totalAssets((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 422971, ~: 428256)
+ERC4626VaultTest:test_withdraw((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 473584, ~: 476530)
 ERC721Invariants:invariantOwner() (runs: 256, calls: 3840, reverts: 3469)
 ERC721Invariants:invariantTotalSupply() (runs: 256, calls: 3840, reverts: 3469)
 ERC721Test:testApproveClearingApprovalWithNoPriorApproval() (gas: 177294)
@@ -469,31 +469,31 @@ ERC721Test:testBurnSuccessViaApproveAndSetApprovalForAll() (gas: 371421)
 ERC721Test:testCachedDomainSeparator() (gas: 7699)
 ERC721Test:testDomainSeparator() (gas: 11450)
 ERC721Test:testEIP712Domain() (gas: 13877)
-ERC721Test:testFuzzApproveClearingApprovalWithNoPriorApproval(address,address) (runs: 256, μ: 196799, ~: 196799)
+ERC721Test:testFuzzApproveClearingApprovalWithNoPriorApproval(address,address) (runs: 256, μ: 196789, ~: 196799)
 ERC721Test:testFuzzApproveClearingApprovalWithPriorApproval(address,address) (runs: 256, μ: 184072, ~: 184072)
 ERC721Test:testFuzzApproveFromNonOwner(address) (runs: 256, μ: 172895, ~: 172895)
 ERC721Test:testFuzzApproveFromOperatorAddress(address,address,address) (runs: 256, μ: 222958, ~: 222958)
-ERC721Test:testFuzzApproveWithNoPriorApproval(address,address) (runs: 256, μ: 196754, ~: 196754)
-ERC721Test:testFuzzApproveWithPriorApproval(address,address) (runs: 256, μ: 203884, ~: 203884)
+ERC721Test:testFuzzApproveWithNoPriorApproval(address,address) (runs: 256, μ: 196744, ~: 196754)
+ERC721Test:testFuzzApproveWithPriorApproval(address,address) (runs: 256, μ: 203874, ~: 203884)
 ERC721Test:testFuzzBurnSuccess(address) (runs: 256, μ: 305615, ~: 305600)
-ERC721Test:testFuzzDomainSeparator(uint8) (runs: 256, μ: 11641, ~: 11663)
+ERC721Test:testFuzzDomainSeparator(uint8) (runs: 256, μ: 11643, ~: 11663)
 ERC721Test:testFuzzEIP712Domain(bytes1,uint8,bytes32,uint256[]) (runs: 256, μ: 19581, ~: 19542)
-ERC721Test:testFuzzGetApprovedApprovedTokenId(address,address) (runs: 256, μ: 194385, ~: 194385)
+ERC721Test:testFuzzGetApprovedApprovedTokenId(address,address) (runs: 256, μ: 194375, ~: 194385)
 ERC721Test:testFuzzPermitInvalid(string,string,uint16) (runs: 256, μ: 201667, ~: 201665)
 ERC721Test:testFuzzPermitSuccess(string,string,uint16) (runs: 256, μ: 227137, ~: 227133)
 ERC721Test:testFuzzRenounceOwnershipNonOwner(address) (runs: 256, μ: 13938, ~: 13938)
-ERC721Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 48380, ~: 48364)
+ERC721Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 48379, ~: 48364)
 ERC721Test:testFuzzSafeMintNonMinter(address) (runs: 256, μ: 15894, ~: 15894)
-ERC721Test:testFuzzSafeMintSuccess(address[]) (runs: 256, μ: 21015630, ~: 20441661)
-ERC721Test:testFuzzSafeTransferFromWithData(address,address,address,bytes) (runs: 256, μ: 1428908, ~: 1425307)
+ERC721Test:testFuzzSafeMintSuccess(address[]) (runs: 256, μ: 20855146, ~: 20004472)
+ERC721Test:testFuzzSafeTransferFromWithData(address,address,address,bytes) (runs: 256, μ: 1429054, ~: 1429893)
 ERC721Test:testFuzzSetApprovalForAllSuccess(address,address) (runs: 256, μ: 188481, ~: 188481)
 ERC721Test:testFuzzSetMinterNonOwner(address,string) (runs: 256, μ: 15822, ~: 15749)
 ERC721Test:testFuzzSetMinterSuccess(string) (runs: 256, μ: 33217, ~: 33216)
-ERC721Test:testFuzzTokenByIndex(address,string[]) (runs: 256, μ: 21390285, ~: 21253069)
-ERC721Test:testFuzzTotalSupply(address,string[]) (runs: 256, μ: 21261017, ~: 21123504)
-ERC721Test:testFuzzTransferFrom(address,address,address) (runs: 256, μ: 560100, ~: 560080)
+ERC721Test:testFuzzTokenByIndex(address,string[]) (runs: 256, μ: 21399845, ~: 21253069)
+ERC721Test:testFuzzTotalSupply(address,string[]) (runs: 256, μ: 21270507, ~: 21123504)
+ERC721Test:testFuzzTransferFrom(address,address,address) (runs: 256, μ: 560097, ~: 560080)
 ERC721Test:testFuzzTransferOwnershipNonOwner(address,address) (runs: 256, μ: 14090, ~: 14090)
-ERC721Test:testFuzzTransferOwnershipSuccess(address,address) (runs: 256, μ: 73920, ~: 73892)
+ERC721Test:testFuzzTransferOwnershipSuccess(address,address) (runs: 256, μ: 73922, ~: 73892)
 ERC721Test:testGetApprovedApprovedTokenId() (gas: 194287)
 ERC721Test:testGetApprovedInvalidTokenId() (gas: 11097)
 ERC721Test:testGetApprovedNotApprovedTokenId() (gas: 170525)
@@ -556,19 +556,19 @@ ERC721Test:testTransferOwnershipToZeroAddress() (gas: 15608)
 MathTest:testCbrtRoundDown() (gas: 41159)
 MathTest:testCbrtRoundUp() (gas: 41806)
 MathTest:testCeilDiv() (gas: 12481)
-MathTest:testFuzzCbrt(uint256,bool) (runs: 256, μ: 19431, ~: 19249)
+MathTest:testFuzzCbrt(uint256,bool) (runs: 256, μ: 19443, ~: 19288)
 MathTest:testFuzzCeilDiv(uint256,uint256) (runs: 256, μ: 8962, ~: 8979)
 MathTest:testFuzzInt256Average(int256,int256) (runs: 256, μ: 5683, ~: 5683)
 MathTest:testFuzzLog10(uint256,bool) (runs: 256, μ: 6971, ~: 6961)
-MathTest:testFuzzLog2(uint256,bool) (runs: 256, μ: 6825, ~: 6803)
-MathTest:testFuzzLog256(uint256,bool) (runs: 256, μ: 6853, ~: 6831)
+MathTest:testFuzzLog2(uint256,bool) (runs: 256, μ: 6826, ~: 6803)
+MathTest:testFuzzLog256(uint256,bool) (runs: 256, μ: 6849, ~: 6827)
 MathTest:testFuzzMulDiv(uint256,uint256,uint256) (runs: 256, μ: 13038, ~: 12781)
-MathTest:testFuzzMulDivDomain(uint256,uint256,uint256) (runs: 256, μ: 10485, ~: 10570)
+MathTest:testFuzzMulDivDomain(uint256,uint256,uint256) (runs: 256, μ: 10488, ~: 10570)
 MathTest:testFuzzSignum(int256) (runs: 256, μ: 5578, ~: 5570)
 MathTest:testFuzzUint256Average(uint256,uint256) (runs: 256, μ: 5763, ~: 5763)
-MathTest:testFuzzWadCbrt(uint256) (runs: 256, μ: 18973, ~: 18650)
-MathTest:testFuzzWadExp(int256) (runs: 256, μ: 14492, ~: 14597)
-MathTest:testFuzzWadLn(int256) (runs: 256, μ: 15990, ~: 15780)
+MathTest:testFuzzWadCbrt(uint256) (runs: 256, μ: 18982, ~: 18667)
+MathTest:testFuzzWadExp(int256) (runs: 256, μ: 14484, ~: 14597)
+MathTest:testFuzzWadLn(int256) (runs: 256, μ: 15982, ~: 15766)
 MathTest:testInt256Average() (gas: 12010)
 MathTest:testLog10RoundDown() (gas: 17685)
 MathTest:testLog10RoundUp() (gas: 18866)
@@ -589,8 +589,8 @@ MathTest:testWadExp() (gas: 24809)
 MathTest:testWadExpOverflow() (gas: 11127)
 MathTest:testWadLn() (gas: 24196)
 MathTest:testWadLnNegativeValues() (gas: 11130)
-MerkleProofVerificationTest:testFuzzMultiProofVerifySingleLeaf(bytes32[],uint256) (runs: 256, μ: 1649976154, ~: 1649972494)
-MerkleProofVerificationTest:testFuzzVerify(bytes32[],uint256) (runs: 256, μ: 135974866, ~: 135971282)
+MerkleProofVerificationTest:testFuzzMultiProofVerifySingleLeaf(bytes32[],uint256) (runs: 256, μ: 1649976152, ~: 1649972613)
+MerkleProofVerificationTest:testFuzzVerify(bytes32[],uint256) (runs: 256, μ: 135974864, ~: 135971401)
 MerkleProofVerificationTest:testFuzzVerifyMultiProofMultipleLeaves(bool,bool,bool) (runs: 256, μ: 412472467, ~: 412472462)
 MerkleProofVerificationTest:testInvalidMerkleMultiProof() (gas: 412478415)
 MerkleProofVerificationTest:testInvalidMerkleProof() (gas: 33970390)
@@ -615,7 +615,7 @@ Ownable2StepInvariants:invariantPendingOwner() (runs: 256, calls: 3840, reverts:
 Ownable2StepTest:testAcceptOwnershipNonPendingOwner() (gas: 46604)
 Ownable2StepTest:testAcceptOwnershipSuccess() (gas: 39404)
 Ownable2StepTest:testFuzzAcceptOwnershipNonPendingOwner(address) (runs: 256, μ: 45814, ~: 45814)
-Ownable2StepTest:testFuzzAcceptOwnershipSuccess(address,address) (runs: 256, μ: 65112, ~: 65084)
+Ownable2StepTest:testFuzzAcceptOwnershipSuccess(address,address) (runs: 256, μ: 65113, ~: 65084)
 Ownable2StepTest:testFuzzPendingOwnerResetAfterRenounceOwnership(address) (runs: 256, μ: 38751, ~: 38735)
 Ownable2StepTest:testFuzzRenounceOwnershipNonOwner(address) (runs: 256, μ: 13954, ~: 13954)
 Ownable2StepTest:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 43684, ~: 43668)
@@ -657,4 +657,96 @@ SignatureCheckerTest:testFuzzEIP1271WithValidSignature(string) (runs: 256, μ: 3
 SignatureCheckerTest:testFuzzEOAWithInvalidSignature(bytes,string) (runs: 256, μ: 16837, ~: 16836)
 SignatureCheckerTest:testFuzzEOAWithInvalidSigner(string,string) (runs: 256, μ: 21466, ~: 21527)
 SignatureCheckerTest:testFuzzEOAWithValidSignature(string,string) (runs: 256, μ: 20585, ~: 20646)
-SignatureCheckerTest:testInitialSetup() (gas: 5415)
\ No newline at end of file
+SignatureCheckerTest:testInitialSetup() (gas: 5415)
+TimelockControllerInvariants:invariantExecutedLessThanOrEqualToScheduled() (runs: 256, calls: 3840, reverts: 1528)
+TimelockControllerInvariants:invariantExecutedProposalCancellation() (runs: 256, calls: 3840, reverts: 1531)
+TimelockControllerInvariants:invariantExecutingCancelledProposal() (runs: 256, calls: 3840, reverts: 1528)
+TimelockControllerInvariants:invariantExecutingNotReadyProposal() (runs: 256, calls: 3840, reverts: 1535)
+TimelockControllerInvariants:invariantOnceProposalExecution() (runs: 256, calls: 3840, reverts: 1485)
+TimelockControllerInvariants:invariantProposalsExecutedMatchCount() (runs: 256, calls: 3840, reverts: 1528)
+TimelockControllerInvariants:invariantSumOfProposals() (runs: 256, calls: 3840, reverts: 1528)
+TimelockControllerTest:testAdminCannotBatchExecute() (gas: 750573)
+TimelockControllerTest:testAdminCannotBatchSchedule() (gas: 748359)
+TimelockControllerTest:testAdminCannotCancel() (gas: 13280)
+TimelockControllerTest:testAdminCannotExecute() (gas: 18397)
+TimelockControllerTest:testAdminCannotSchedule() (gas: 16069)
+TimelockControllerTest:testBatchCancelFinished() (gas: 4641370)
+TimelockControllerTest:testBatchEqualAndGreaterMinimumDelay() (gas: 6144529)
+TimelockControllerTest:testBatchHasBeenExecuted() (gas: 4638874)
+TimelockControllerTest:testBatchHasNotBeenExecuted() (gas: 3078509)
+TimelockControllerTest:testBatchInsufficientDelay() (gas: 1532717)
+TimelockControllerTest:testBatchMinimumDelayUpdate() (gas: 3085797)
+TimelockControllerTest:testBatchOperationAlreadyScheduled() (gas: 4593338)
+TimelockControllerTest:testBatchOperationIsNotReady() (gas: 4598655)
+TimelockControllerTest:testBatchPendingIfExecuted() (gas: 4637848)
+TimelockControllerTest:testBatchPendingIfNotYetExecuted() (gas: 3078517)
+TimelockControllerTest:testBatchPredecessorInvalid() (gas: 4601007)
+TimelockControllerTest:testBatchPredecessorMultipleNotExecuted() (gas: 6141737)
+TimelockControllerTest:testBatchPredecessorNotExecuted() (gas: 7662956)
+TimelockControllerTest:testBatchPredecessorNotScheduled() (gas: 6116865)
+TimelockControllerTest:testBatchReadyAfterTheExecutionTime() (gas: 3079082)
+TimelockControllerTest:testBatchReadyBeforeTheExecutionTime() (gas: 3079099)
+TimelockControllerTest:testBatchReadyOnTheExecutionTime() (gas: 3078985)
+TimelockControllerTest:testBatchScheduleAndExecuteWithEmptySalt() (gas: 4642907)
+TimelockControllerTest:testBatchScheduleAndExecuteWithNonEmptySalt() (gas: 4646352)
+TimelockControllerTest:testBatchTargetRevert() (gas: 9186500)
+TimelockControllerTest:testBatchTimestampHasBeenExecuted() (gas: 4637665)
+TimelockControllerTest:testBatchTimestampHasNotBeenExecuted() (gas: 3078316)
+TimelockControllerTest:testCanReceiveEther() (gas: 12124)
+TimelockControllerTest:testCancellerCanCancelOperation() (gas: 3064799)
+TimelockControllerTest:testCompleteOperationWithAssignExecutorRoleToZeroAddress() (gas: 124673)
+TimelockControllerTest:testCompletePipelineOperationMinimumDelayUpdate() (gas: 73442)
+TimelockControllerTest:testCompletePipelineOperationSetRoleAdmin() (gas: 100466)
+TimelockControllerTest:testExecutorCanBatchExecute() (gas: 3049249)
+TimelockControllerTest:testExecutorCanExecute() (gas: 29920)
+TimelockControllerTest:testExecutorCannotBatchSchedule() (gas: 1485375)
+TimelockControllerTest:testExecutorCannotCancel() (gas: 15281)
+TimelockControllerTest:testExecutorCannotSchedule() (gas: 19073)
+TimelockControllerTest:testFuzzBatchValue(uint256) (runs: 256, μ: 4652693, ~: 4652826)
+TimelockControllerTest:testFuzzHashOperation(address,uint256,bytes,bytes32,bytes32) (runs: 256, μ: 8261, ~: 8159)
+TimelockControllerTest:testFuzzHashOperationBatch(address[],uint256[],bytes[],bytes32,bytes32) (runs: 256, μ: 1881454, ~: 1889945)
+TimelockControllerTest:testFuzzOperationValue(uint256) (runs: 256, μ: 113151, ~: 113263)
+TimelockControllerTest:testHandleERC1155() (gas: 41505969)
+TimelockControllerTest:testHandleERC721() (gas: 7056837)
+TimelockControllerTest:testHashOperation() (gas: 10233)
+TimelockControllerTest:testHashOperationBatch() (gas: 1523421)
+TimelockControllerTest:testInitialSetup() (gas: 4245443)
+TimelockControllerTest:testInvalidOperation() (gas: 7856)
+TimelockControllerTest:testOperationAlreadyScheduled() (gas: 52465)
+TimelockControllerTest:testOperationCancelFinished() (gas: 101892)
+TimelockControllerTest:testOperationEqualAndGreaterMinimumDelay() (gas: 90244)
+TimelockControllerTest:testOperationHasBeenExecuted() (gas: 99429)
+TimelockControllerTest:testOperationHasNotBeenExecuted() (gas: 52365)
+TimelockControllerTest:testOperationInsufficientDelay() (gas: 19490)
+TimelockControllerTest:testOperationMinimumDelayUpdate() (gas: 61101)
+TimelockControllerTest:testOperationOperationIsNotReady() (gas: 57820)
+TimelockControllerTest:testOperationPendingIfExecuted() (gas: 98359)
+TimelockControllerTest:testOperationPendingIfNotYetExecuted() (gas: 52429)
+TimelockControllerTest:testOperationPredecessorInvalid() (gas: 62855)
+TimelockControllerTest:testOperationPredecessorMultipleNotExecuted() (gas: 92559)
+TimelockControllerTest:testOperationPredecessorNotExecuted() (gas: 99291)
+TimelockControllerTest:testOperationPredecessorNotScheduled() (gas: 66755)
+TimelockControllerTest:testOperationReadyAfterTheExecutionTime() (gas: 52984)
+TimelockControllerTest:testOperationReadyBeforeTheExecutionTime() (gas: 52927)
+TimelockControllerTest:testOperationReadyOnTheExecutionTime() (gas: 52821)
+TimelockControllerTest:testOperationTargetRevert() (gas: 110365)
+TimelockControllerTest:testOperationTimestampHasBeenExecuted() (gas: 98112)
+TimelockControllerTest:testOperationTimestampHasNotBeenExecuted() (gas: 52195)
+TimelockControllerTest:testProposerCanBatchSchedule() (gas: 3088645)
+TimelockControllerTest:testProposerCanCancel() (gas: 20179)
+TimelockControllerTest:testProposerCanSchedule() (gas: 75659)
+TimelockControllerTest:testProposerCannotBatchExecute() (gas: 1489786)
+TimelockControllerTest:testProposerCannotExecute() (gas: 23599)
+TimelockControllerTest:testReturnsLaterMinimumDelayForCalls() (gas: 20365)
+TimelockControllerTest:testRevertWhenNotTimelock() (gas: 9053)
+TimelockControllerTest:testScheduleAndExecuteWithEmptySalt() (gas: 103471)
+TimelockControllerTest:testScheduleAndExecuteWithNonEmptySalt() (gas: 106828)
+TimelockControllerTest:testStrangerCannotBatchExecute() (gas: 748592)
+TimelockControllerTest:testStrangerCannotBatchSchedule() (gas: 746399)
+TimelockControllerTest:testStrangerCannotCancel() (gas: 11276)
+TimelockControllerTest:testStrangerCannotExecute() (gas: 16414)
+TimelockControllerTest:testStrangerCannotSchedule() (gas: 14198)
+TimelockControllerTest:testSupportsInterfaceInvalidInterfaceId() (gas: 5628)
+TimelockControllerTest:testSupportsInterfaceInvalidInterfaceIdGasCost() (gas: 6418)
+TimelockControllerTest:testSupportsInterfaceSuccess() (gas: 7234)
+TimelockControllerTest:testSupportsInterfaceSuccessGasCost() (gas: 6525)
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 67e04804..7a633eeb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,3 +57,6 @@ venv
 # Python build files
 dist
 *.egg-info
+
+# Ape build files
+.build
diff --git a/.solhint.json b/.solhint.json
index 127600a2..aeadcf84 100644
--- a/.solhint.json
+++ b/.solhint.json
@@ -4,6 +4,7 @@
     "func-visibility": ["warn", { "ignoreConstructors": true }],
     "func-name-mixedcase": "off",
     "private-vars-leading-underscore": "off",
-    "one-contract-per-file": "off"
+    "one-contract-per-file": "off",
+    "max-states-count": "off"
   }
 }
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aac8a0d0..be8ef13f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
 
 ## [`0.0.5`](https://github.com/pcaversaccio/snekmate/releases/tag/v0.0.5) (Unreleased)
 
+### 💥 New Features
+
+- **Governance**
+  - [`TimelockController`](https://github.com/pcaversaccio/snekmate/blob/v0.0.5/src/governance/TimelockController.vy): A multi-role-based timelock controller reference implementation. ([#195](https://github.com/pcaversaccio/snekmate/pull/195))
+
 ### ♻️ Refactoring
 
 - **Utility Functions**
diff --git a/README.md b/README.md
index e0c77f0b..72d02b0b 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,8 @@ src
 │   ├── ERC4626 — "Modern and Gas-Efficient ERC-4626 Tokenised Vault Implementation"
 │   └── interfaces
 │       └── IERC2981 — "EIP-2981 Interface Definition"
+├── governance
+│   └── TimelockController — "Multi-Role-Based Timelock Controller Reference Implementation"
 ├── tokens
 │   ├── ERC20 — "Modern and Gas-Efficient ERC-20 + EIP-2612 Implementation"
 │   ├── ERC721 — "Modern and Gas-Efficient ERC-721 + EIP-4494 Implementation"
@@ -113,6 +115,7 @@ This repository contains [Foundry](https://github.com/foundry-rs/foundry)-based
 | `AccessControl`           |       ✅       |            ✅            |         ✅          |
 | `ERC2981`                 |       ✅       |            ✅            |         ✅          |
 | `ERC4626`                 |       ✅       |            ✅            |         ✅          |
+| `TimelockController`      |       ✅       |            ✅            |         ✅          |
 | `ERC20`                   |       ✅       |            ✅            |         ✅          |
 | `ERC721`                  |       ✅       |            ✅            |         ✅          |
 | `ERC1155`                 |       ✅       |            ✅            |         ✅          |
diff --git a/package.json b/package.json
index 51f36c25..b6e8fe9a 100644
--- a/package.json
+++ b/package.json
@@ -22,10 +22,10 @@
     "url": "https://github.com/pcaversaccio/snekmate/issues"
   },
   "scripts": {
-    "prettier:check": "npx prettier -c **/*.{md,sol,js,json,yml,yaml}",
-    "prettier:fix": "npx prettier -w **/*.{md,sol,js,json,yml,yaml}",
-    "solhint:check": "npx solhint '**/*.sol'",
-    "solhint:fix": "npx solhint '**/*.sol' --fix",
+    "prettier:check": "npx prettier -c \"**/*.{md,sol,js,json,yml,yaml}\"",
+    "prettier:fix": "npx prettier -w \"**/*.{md,sol,js,json,yml,yaml}\"",
+    "solhint:check": "npx solhint \"**/*.sol\"",
+    "solhint:fix": "npx solhint \"**/*.sol\" --fix",
     "lint:check": "pnpm prettier:check && pnpm solhint:check && npx eslint --ext .js .",
     "lint:fix": "pnpm prettier:fix && pnpm solhint:fix && npx eslint --ext .js . --fix"
   },
diff --git a/src/auth/AccessControl.vy b/src/auth/AccessControl.vy
index 0f6eec57..e4a5b14f 100644
--- a/src/auth/AccessControl.vy
+++ b/src/auth/AccessControl.vy
@@ -63,6 +63,9 @@ implements: IAccessControl
 
 
 # @dev The default 32-byte admin role.
+# @notice If you declare a variable as `public`,
+# Vyper automatically generates an `external`
+# getter function for the variable.
 DEFAULT_ADMIN_ROLE: public(constant(bytes32)) = empty(bytes32)
 
 
@@ -93,9 +96,6 @@ _SUPPORTED_INTERFACES: constant(bytes4[2]) = [
 
 
 # @dev Returns `True` if `account` has been granted `role`.
-# @notice If you declare a variable as `public`,
-# Vyper automatically generates an `external`
-# getter function for the variable.
 hasRole: public(HashMap[bytes32, HashMap[address, bool]])
 
 
@@ -226,6 +226,7 @@ def set_role_admin(role: bytes32, admin_role: bytes32):
 
 
 @internal
+@view
 def _check_role(role: bytes32, account: address):
     """
     @dev Reverts with a standard message if `account`
diff --git a/src/auth/Ownable.vy b/src/auth/Ownable.vy
index b5f07671..256b2cb3 100644
--- a/src/auth/Ownable.vy
+++ b/src/auth/Ownable.vy
@@ -17,6 +17,9 @@
 
 
 # @dev Returns the address of the current owner.
+# @notice If you declare a variable as `public`,
+# Vyper automatically generates an `external`
+# getter function for the variable.
 owner: public(address)
 
 
diff --git a/src/auth/Ownable2Step.vy b/src/auth/Ownable2Step.vy
index 9a09bbb5..d4aa02c5 100644
--- a/src/auth/Ownable2Step.vy
+++ b/src/auth/Ownable2Step.vy
@@ -16,6 +16,9 @@
 
 
 # @dev Returns the address of the current owner.
+# @notice If you declare a variable as `public`,
+# Vyper automatically generates an `external`
+# getter function for the variable.
 owner: public(address)
 
 
diff --git a/src/extensions/ERC2981.vy b/src/extensions/ERC2981.vy
index 390c510d..32e6cf65 100644
--- a/src/extensions/ERC2981.vy
+++ b/src/extensions/ERC2981.vy
@@ -67,6 +67,9 @@ _SUPPORTED_INTERFACES: constant(bytes4[2]) = [
 
 
 # @dev Returns the address of the current owner.
+# @notice If you declare a variable as `public`,
+# Vyper automatically generates an `external`
+# getter function for the variable.
 owner: public(address)
 
 
diff --git a/src/governance/TimelockController.vy b/src/governance/TimelockController.vy
new file mode 100644
index 00000000..0e904e45
--- /dev/null
+++ b/src/governance/TimelockController.vy
@@ -0,0 +1,932 @@
+# pragma version ^0.3.10
+"""
+@title Multi-Role-Based Timelock Controller Reference Implementation
+@custom:contract-name TimelockController
+@license GNU Affero General Public License v3.0 only
+@author pcaversaccio
+@custom:coauthor cairoeth
+@notice This module enables the timelocking of operations by scheduling
+        and executing transactions. By leveraging `AccessControl`, the
+        `TimelockController` contract introduces three roles:
+          1. proposer (`PROPOSER_ROLE`),
+          2. executor (`EXECUTOR_ROLE`), and
+          3. canceller (`CANCELLER_ROLE`).
+        The proposer role is responsible for proposing operations, the
+        executor role is responsible for executing scheduled proposal(s),
+        and the canceller is responsible for cancelling proposal(s). This
+        contract is self-administered by default (unless an optional admin
+        account is granted at construction), meaning administration tasks
+        (e.g. grant or revoke roles) have to go through the timelock process.
+        At contract creation time, proposers are granted the proposer and
+        canceller roles.
+
+        The proposal(s) must be scheduled with a delay that is greater
+        than or equal to the minimum delay `get_minimum_delay`, which can
+        be updated via a proposal to itself and is measured in seconds.
+        Additionally, proposal(s) can be linked to preceding proposal(s)
+        that must be executed before the proposal can be executed.
+
+        Ready proposal(s) can be executed by the executor, who is solely
+        responsible for calling the `execute` or `execute_batch` functions.
+        Eventually, the proposal(s) can be batched individually or in batches.
+        The latter is useful for processes that have to be executed in the
+        same block.
+
+        Please note that the `TimelockController` contract is able to receive
+        and transfer ERC-721 and ERC-1155 tokens.
+
+        The implementation is inspired by OpenZeppelin's implementation here:
+        https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/governance/TimelockController.sol.
+"""
+
+
+# @dev We import and implement the `ERC165` interface,
+# which is a built-in interface of the Vyper compiler.
+from vyper.interfaces import ERC165
+implements: ERC165
+
+
+# @dev We import and implement the `IAccessControl`
+# interface, which is written using standard Vyper
+# syntax.
+from ..auth.interfaces import IAccessControl
+implements: IAccessControl
+
+
+# @dev We import and implement the `IERC721Receiver`
+# interface, which is written using standard Vyper
+# syntax.
+from ..tokens.interfaces import IERC721Receiver
+implements: IERC721Receiver
+
+
+# @dev We import and implement the `IERC1155Receiver`
+# interface, which is written using standard Vyper
+# syntax.
+from ..tokens.interfaces import IERC1155Receiver
+implements: IERC1155Receiver
+
+
+# @dev The default 32-byte admin role.
+# @notice If you declare a variable as `public`,
+# Vyper automatically generates an `external`
+# getter function for the variable.
+DEFAULT_ADMIN_ROLE: public(constant(bytes32)) = empty(bytes32)
+
+
+# @dev The 32-byte proposer role.
+# @notice Responsible for proposing operations.
+PROPOSER_ROLE: public(constant(bytes32)) = keccak256("PROPOSER_ROLE")
+
+
+# @dev The 32-byte executor role.
+# @notice Responsible for executing scheduled proposals.
+EXECUTOR_ROLE: public(constant(bytes32)) = keccak256("EXECUTOR_ROLE")
+
+
+# @dev The 32-byte canceller role.
+# @notice Responsible for cancelling proposals.
+CANCELLER_ROLE: public(constant(bytes32)) = keccak256("CANCELLER_ROLE")
+
+
+# @dev The 4-byte function selector of `onERC721Received(address,address,uint256,bytes)`.
+IERC721_TOKENRECEIVER_SELECTOR: public(constant(bytes4)) = 0x150B7A02
+
+
+# @dev The 4-byte function selector of `onERC1155Received(address,address,uint256,uint256,bytes)`.
+IERC1155_TOKENRECEIVER_SINGLE_SELECTOR: public(constant(bytes4)) = 0xF23A6E61
+
+
+# @dev The 4-byte function selector of `onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)`.
+IERC1155_TOKENRECEIVER_BATCH_SELECTOR:  public(constant(bytes4)) = 0xBC197C81
+
+
+# @dev Stores the ERC-165 interface identifier for each
+# imported interface. The ERC-165 interface identifier
+# is defined as the XOR of all function selectors in the
+# interface.
+# @notice Note that the ERC-165 interface identifier for
+# the `ERC721TokenReceiver` interface is not included as
+# it is not required by the EIP:
+# https://eips.ethereum.org/EIPS/eip-721#specification.
+_SUPPORTED_INTERFACES: constant(bytes4[3]) = [
+    0x01FFC9A7, # The ERC-165 identifier for ERC-165.
+    0x7965DB0B, # The ERC-165 identifier for `IAccessControl`.
+    0x4E2312E0, # The ERC-165 identifier for `IERC1155Receiver`.
+]
+
+
+# @dev The 32-byte timestamp that specifies whether a
+# proposal has been executed.
+_DONE_TIMESTAMP: constant(uint256) = 1
+
+
+# @dev Stores the 1-byte upper bound for the dynamic arrays.
+_DYNARRAY_BOUND: constant(uint8) = max_value(uint8)
+
+
+# @dev The possible states of a proposal.
+# @notice Enums are treated differently in Vyper and
+# Solidity. The members are represented by `uint256`
+# values (in Solidity the values are of type `uint8`)
+# in the form `2**n`, where `n` is the index of the
+# member in the range `0 <= n <= 255` (i.e. the first
+# index value is `1`). For further insights also, see
+# the following Twitter thread:
+# https://twitter.com/pcaversaccio/status/1626514029094047747.
+enum OperationState:
+    UNSET
+    WAITING
+    READY
+    DONE
+
+
+# @dev Returns the timestamp at which an operation
+# becomes ready (`0` for `UNSET` operations, `1` for
+# `DONE` operations).
+get_timestamp: public(HashMap[bytes32, uint256])
+
+
+# @dev Returns the minimum delay in seconds for an
+# operation to become valid. This value can be changed
+# by executing an operation that invokes `update_delay`.
+get_minimum_delay: public(uint256)
+
+
+# @dev Returns `True` if `account` has been granted `role`.
+hasRole: public(HashMap[bytes32, HashMap[address, bool]])
+
+
+# @dev Returns the admin role that controls `role`.
+getRoleAdmin: public(HashMap[bytes32, bytes32])
+
+
+# @dev Emitted when a call is scheduled as part of
+# operation `id`. Note that `index` is the index
+# position of the proposal. If the proposal is
+# individual, the `index` is `0`.
+event CallScheduled:
+    id: indexed(bytes32)
+    index: indexed(uint256)
+    target: address
+    amount: uint256
+    payload: Bytes[1_024]
+    predecessor: bytes32
+    delay: uint256
+
+
+# @dev Emitted when a call is performed as part of
+# operation `id`. Note that `index` is the index
+# position of the proposal. If the proposal is
+# individual, the `index` is `0`.
+event CallExecuted:
+    id: indexed(bytes32)
+    index: indexed(uint256)
+    target: address
+    amount: uint256
+    payload: Bytes[1_024]
+
+
+# @dev Emitted when new proposal is scheduled with
+# non-zero salt.
+event CallSalt:
+    id: indexed(bytes32)
+    salt: bytes32
+
+
+# @dev Emitted when operation `id` is cancelled.
+event Cancelled:
+    id: indexed(bytes32)
+
+
+# @dev Emitted when the minimum delay for future
+# operations is modified.
+event MinimumDelayChange:
+    old_duration: uint256
+    new_duration: uint256
+
+
+# @dev Emitted when `new_admin_role` is set as
+# `role`'s admin role, replacing `previous_admin_role`.
+# Note that `DEFAULT_ADMIN_ROLE` is the starting
+# admin for all roles, despite `RoleAdminChanged`
+# not being emitted signaling this.
+event RoleAdminChanged:
+    role: indexed(bytes32)
+    previous_admin_role: indexed(bytes32)
+    new_admin_role: indexed(bytes32)
+
+
+# @dev Emitted when `account` is granted `role`.
+# Note that `sender` is the account (an admin
+# role bearer) that originated the contract call.
+event RoleGranted:
+    role: indexed(bytes32)
+    account: indexed(address)
+    sender: indexed(address)
+
+
+# @dev Emitted when `account` is revoked `role`.
+# Note that `sender` is the account that originated
+# the contract call:
+#   - if using `revokeRole`, it is the admin role
+#     bearer,
+#   - if using `renounceRole`, it is the role bearer
+#     (i.e. `account`).
+event RoleRevoked:
+    role: indexed(bytes32)
+    account: indexed(address)
+    sender: indexed(address)
+
+
+@external
+@payable
+def __init__(minimum_delay_: uint256, proposers_: DynArray[address, _DYNARRAY_BOUND], executors_: DynArray[address, _DYNARRAY_BOUND], admin_: address):
+    """
+    @dev Initialises the contract with the following parameters:
+           - `minimum_delay_`: The initial minimum delay in seconds
+              for operations,
+           - `proposers_`: The accounts to be granted proposer and
+              canceller roles,
+           - `executors_`: The accounts to be granted executor role,
+           - `admin_`: The optional account to be granted admin role
+              (disable with the zero address).
+
+         IMPORTANT: The optional admin can aid with initial
+         configuration of roles after deployment without being
+         subject to delay, but this role should be subsequently
+         renounced in favor of administration through timelocked
+         proposals.
+
+         To omit the opcodes for checking the `msg.value`
+         in the creation-time EVM bytecode, the constructor
+         is declared as `payable`.
+    @param minimum_delay_ The 32-byte minimum delay in seconds
+           for operations.
+    @param proposers_ The 20-byte array of accounts to be granted
+           proposer and canceller roles.
+    @param executors_ The 20-byte array of accounts to be granted
+           executor role.
+    @param admin_ The 20-byte (optional) account to be granted admin
+           role.
+    """
+    # Configure the contract to be self-administered.
+    self._grant_role(DEFAULT_ADMIN_ROLE, self)
+
+    # Set the optional admin.
+    if (admin_ != empty(address)):
+        self._grant_role(DEFAULT_ADMIN_ROLE, admin_)
+
+    # Register the proposers and cancellers.
+    for proposer in proposers_:
+        self._grant_role(PROPOSER_ROLE, proposer)
+        self._grant_role(CANCELLER_ROLE, proposer)
+
+    # Register the executors.
+    for executor in executors_:
+        self._grant_role(EXECUTOR_ROLE, executor)
+
+    # Set the minimum delay.
+    self.get_minimum_delay = minimum_delay_
+    log MinimumDelayChange(empty(uint256), minimum_delay_)
+
+
+@external
+@payable
+def __default__():
+    """
+    @dev This contract might receive/hold ETH as part
+         of the maintenance process.
+    """
+    pass
+
+
+@external
+@view
+def supportsInterface(interface_id: bytes4) -> bool:
+    """
+    @dev Returns `True` if this contract implements the
+         interface defined by `interface_id`.
+    @param interface_id The 4-byte interface identifier.
+    @return bool The verification whether the contract
+            implements the interface or not.
+    """
+    return interface_id in _SUPPORTED_INTERFACES
+
+
+@external
+@view
+def is_operation(id: bytes32) -> bool:
+    """
+    @dev Returns whether an `id` corresponds to a registered
+         operation. This includes both `WAITING`, `READY`, and
+         `DONE` operations.
+    @param id The 32-byte operation identifier.
+    @return bool The verification whether the `id`
+            corresponds to a registered operation or not.
+    """
+    return self._is_operation(id)
+
+
+@external
+@view
+def is_operation_pending(id: bytes32) -> bool:
+    """
+    @dev Returns whether an operation is pending or not.
+         Note that a "pending" operation may also be
+         "ready".
+    @param id The 32-byte operation identifier.
+    @return bool The verification whether the operation
+            is pending or not.
+    """
+    return self._is_operation_pending(id)
+
+
+@external
+@view
+def is_operation_ready(id: bytes32) -> bool:
+    """
+    @dev Returns whether an operation is ready for
+         execution. Note that a "ready" operation is
+         also "pending".
+    @param id The 32-byte operation identifier.
+    @return bool The verification whether the operation
+            is ready or not.
+    """
+    return self._is_operation_ready(id)
+
+
+@external
+@view
+def is_operation_done(id: bytes32) -> bool:
+    """
+    @dev Returns whether an operation is done or not.
+    @param id The 32-byte operation identifier.
+    @return bool The verification whether the operation
+            is done or not.
+    """
+    return self._is_operation_done(id)
+
+
+@external
+@view
+def get_operation_state(id: bytes32) -> OperationState:
+    """
+    @dev Returns the state of an operation.
+    @param id The 32-byte operation identifier.
+    @return OperationState The 32-byte state of the
+            operation.
+    """
+    return self._get_operation_state(id)
+
+
+@external
+@pure
+def hash_operation(target: address, amount: uint256, payload: Bytes[1_024], predecessor: bytes32, salt: bytes32) -> bytes32:
+    """
+    @dev Returns the identifier of an operation containing
+         a single transaction.
+    @param target The 20-bytes address of the target contract.
+    @param amount The 32-byte amount of native tokens to transfer
+           with the call.
+    @param payload The maximum 1,024-byte ABI-encoded calldata.
+    @param predecessor The 32-byte hash of the preceding operation
+           (optional with empty bytes).
+    @param salt The 32-byte salt of the operation.
+    @return bytes32 The 32-byte hash of the operation.
+    """
+    return self._hash_operation(target, amount, payload, predecessor, salt)
+
+
+@external
+@pure
+def hash_operation_batch(targets: DynArray[address, _DYNARRAY_BOUND], amounts: DynArray[uint256, _DYNARRAY_BOUND], payloads: DynArray[Bytes[1_024], _DYNARRAY_BOUND],
+                         predecessor: bytes32, salt: bytes32) -> bytes32:
+    """
+    @dev Returns the identifier of an operation containing
+         a batch of transactions.
+    @param targets The 20-byte array of the target contracts.
+    @param amounts The 32-byte array of native tokens amounts to
+           transfer with each call.
+    @param payloads The maximum 1,024-byte byte array of ABI-encoded
+           calldata.
+    @param predecessor The 32-byte hash of the preceding operation
+           (optional with empty bytes).
+    @param salt The 32-byte salt of the operation.
+    @return bytes32 The 32-byte hash of the operation.
+    """
+    return self._hash_operation_batch(targets, amounts, payloads, predecessor, salt)
+
+
+@external
+def schedule(target: address, amount: uint256, payload: Bytes[1_024], predecessor: bytes32, salt: bytes32, delay: uint256):
+    """
+    @dev Schedules an operation containing a single transaction.
+         Emits `CallScheduled` and `CallSalt` if the salt is non-zero.
+    @notice Note that the caller must have the `PROPOSER_ROLE` role.
+    @param target The 20-byte address of the target contract.
+    @param amount The 32-byte amount of native tokens to transfer
+           with the call.
+    @param payload The maximum 1,024-byte ABI-encoded calldata.
+    @param predecessor The 32-byte hash of the preceding operation
+           (optional with empty bytes).
+    @param salt The 32-byte salt of the operation.
+    @param delay The 32-byte delay before the operation becomes valid.
+           Must be greater than or equal to the minimum delay.
+    """
+    self._check_role(PROPOSER_ROLE, msg.sender)
+    id: bytes32 = self._hash_operation(target, amount, payload, predecessor, salt)
+
+    self._schedule(id, delay)
+    log CallScheduled(id, empty(uint256), target, amount, payload, predecessor, delay)
+    if (salt != empty(bytes32)):
+        log CallSalt(id, salt)
+
+
+@external
+def schedule_batch(targets: DynArray[address, _DYNARRAY_BOUND], amounts: DynArray[uint256, _DYNARRAY_BOUND], payloads: DynArray[Bytes[1_024], _DYNARRAY_BOUND],
+                   predecessor: bytes32, salt: bytes32, delay: uint256):
+    """
+    @dev Schedules an operation containing a batch of transactions.
+         Emits one `CallScheduled` event per transaction in the
+         batch and `CallSalt` if the salt is non-zero.
+    @notice Note that the caller must have the `PROPOSER_ROLE` role.
+    @param targets The 20-byte array of the target contracts.
+    @param amounts The 32-byte array of native tokens amounts to
+           transfer with each call.
+    @param payloads The maximum 1,024-byte byte array of ABI-encoded
+           calldata.
+    @param predecessor The 32-byte hash of the preceding operation
+           (optional with empty bytes).
+    @param salt The 32-byte salt of the operation.
+    @param delay The 32-byte delay before the operation becomes valid.
+           Must be greater than or equal to the minimum delay.
+    """
+    self._check_role(PROPOSER_ROLE, msg.sender)
+    assert len(targets) == len(amounts) and len(targets) == len(payloads), "TimelockController: length mismatch"
+    id: bytes32 = self._hash_operation_batch(targets, amounts, payloads, predecessor, salt)
+
+    self._schedule(id, delay)
+    idx: uint256 = empty(uint256)
+    for target in targets:
+        log CallScheduled(id, idx, target, amounts[idx], payloads[idx], predecessor, delay)
+        # The following line cannot overflow because we have
+        # limited the dynamic array `targets` by the `constant`
+        # parameter `_DYNARRAY_BOUND`, which is bounded by the
+        # maximum value of `uint8`.
+        idx = unsafe_add(idx, 1)
+    if (salt != empty(bytes32)):
+        log CallSalt(id, salt)
+
+
+@external
+def cancel(id: bytes32):
+    """
+    @dev Cancels an operation.
+    @notice Note that the caller must have the `CANCELLER_ROLE` role.
+    @param id The 32-byte operation identifier.
+    """
+    self._check_role(CANCELLER_ROLE, msg.sender)
+    assert self._is_operation_pending(id), "TimelockController: operation cannot be cancelled"
+    self.get_timestamp[id] = empty(uint256)
+    log Cancelled(id)
+
+
+@external
+@payable
+def execute(target: address, amount: uint256, payload: Bytes[1_024], predecessor: bytes32, salt: bytes32):
+    """
+    @dev Executes a ready operation containing a single transaction.
+         Emits a `CallExecuted` event.
+    @notice Note that the caller must have the `EXECUTOR_ROLE` role.
+    @param target The 20-byte address of the target contract.
+    @param amount The 32-byte amount of native tokens to transfer
+           with the call.
+    @param payload The maximum 1,024-byte ABI-encoded calldata.
+    @param predecessor The 32-byte hash of the preceding operation
+           (optional with empty bytes).
+    @param salt The 32-byte salt of the operation.
+    @custom:security This function can reenter, but it doesn't pose
+                     a risk because `_after_call` checks that the
+                     proposal is pending, thus any modifications to
+                     the operation during reentrancy are caught.
+    """
+    self._only_role_or_open_role(EXECUTOR_ROLE)
+    id: bytes32 = self._hash_operation(target, amount, payload, predecessor, salt)
+
+    self._before_call(id, predecessor)
+    self._execute(target, amount, payload)
+    log CallExecuted(id, empty(uint256), target, amount, payload)
+    self._after_call(id)
+
+
+@external
+@payable
+def execute_batch(targets: DynArray[address, _DYNARRAY_BOUND], amounts: DynArray[uint256, _DYNARRAY_BOUND], payloads: DynArray[Bytes[1_024], _DYNARRAY_BOUND],
+                  predecessor: bytes32, salt: bytes32):
+    """
+    @dev Executes a ready operation containing a batch of transactions.
+         Emits one `CallExecuted` event per transaction in the batch.
+    @notice Note that the caller must have the `EXECUTOR_ROLE` role.
+    @param targets The 20-byte array of the target contracts.
+    @param amounts The 32-byte array of native tokens amounts to
+           transfer with each call.
+    @param payloads The maximum 1,024-byte byte array of ABI-encoded
+           calldata.
+    @param predecessor The 32-byte hash of the preceding operation
+           (optional with empty bytes).
+    @param salt The 32-byte salt of the operation.
+    @custom:security This function can reenter, but it doesn't pose
+                     a risk because `_after_call` checks that the
+                     proposal is pending, thus any modifications to
+                     the operation during reentrancy are caught.
+    """
+    self._only_role_or_open_role(EXECUTOR_ROLE)
+    assert len(targets) == len(amounts) and len(targets) == len(payloads), "TimelockController: length mismatch"
+    id: bytes32 = self._hash_operation_batch(targets, amounts, payloads, predecessor, salt)
+
+    self._before_call(id, predecessor)
+    idx: uint256 = empty(uint256)
+    for target in targets:
+        self._execute(target, amounts[idx], payloads[idx])
+        log CallExecuted(id, idx, target, amounts[idx], payloads[idx])
+        # The following line cannot overflow because we have
+        # limited the dynamic array `targets` by the `constant`
+        # parameter `_DYNARRAY_BOUND`, which is bounded by the
+        # maximum value of `uint8`.
+        idx = unsafe_add(idx, 1)
+    self._after_call(id)
+
+
+@external
+def update_delay(new_delay: uint256):
+    """
+    @dev Changes the minimum timelock duration for future
+         operations. Emits a `MinimumDelayChange` event.
+    @notice Note that the caller must be the `TimelockController`
+            contract itself. This can only be achieved by scheduling
+            and later executing an operation where the `TimelockController`
+            contract is the target and the payload is the ABI-encoded
+            call to this function.
+    @param new_delay The new 32-byte minimum delay in seconds.
+    """
+    assert msg.sender == self, "TimelockController: caller must be timelock"
+    log MinimumDelayChange(self.get_minimum_delay, new_delay)
+    self.get_minimum_delay = new_delay
+
+
+@external
+def grantRole(role: bytes32, account: address):
+    """
+    @dev Sourced from {AccessControl-grantRole}.
+    @notice See {AccessControl-grantRole} for the
+            function docstring.
+    """
+    self._check_role(self.getRoleAdmin[role], msg.sender)
+    self._grant_role(role, account)
+
+
+@external
+def revokeRole(role: bytes32, account: address):
+    """
+    @dev Sourced from {AccessControl-revokeRole}.
+    @notice See {AccessControl-revokeRole} for the
+            function docstring.
+    """
+    self._check_role(self.getRoleAdmin[role], msg.sender)
+    self._revoke_role(role, account)
+
+
+@external
+def renounceRole(role: bytes32, account: address):
+    """
+    @dev Sourced from {AccessControl-renounceRole}.
+    @notice See {AccessControl-renounceRole} for the
+            function docstring.
+    """
+    assert account == msg.sender, "AccessControl: can only renounce roles for itself"
+    self._revoke_role(role, account)
+
+
+@external
+def set_role_admin(role: bytes32, admin_role: bytes32):
+    """
+    @dev Sourced from {AccessControl-set_role_admin}.
+    @notice See {AccessControl-set_role_admin} for the
+            function docstring.
+    """
+    self._check_role(self.getRoleAdmin[role], msg.sender)
+    self._set_role_admin(role, admin_role)
+
+
+@external
+def onERC721Received(operator: address, owner: address, token_id: uint256, data: Bytes[1_024]) -> bytes4:
+    """
+    @dev Whenever a `token_id` token is transferred to
+         this contract via ERC-721 `safeTransferFrom` by
+         `operator` from `owner`, this function is called.
+    @notice It must return its function selector to
+            confirm the token transfer. If any other value
+            is returned or the interface is not implemented
+            by the recipient, the transfer will be reverted.
+    @param operator The 20-byte address which called
+           the `safeTransferFrom` function.
+    @param owner The 20-byte address which previously
+           owned the token.
+    @param token_id The 32-byte identifier of the token.
+    @param data The maximum 1,024-byte additional data
+           with no specified format.
+    @return bytes4 The 4-byte function selector of `onERC721Received`.
+    """
+    return IERC721_TOKENRECEIVER_SELECTOR
+
+
+@external
+def onERC1155Received(operator: address, owner: address, id: uint256, amount: uint256, data: Bytes[1_024]) -> bytes4:
+    """
+    @dev Handles the receipt of a single ERC-1155 token type.
+         This function is called at the end of a `safeTransferFrom`
+         after the balance has been updated.
+    @notice It must return its function selector to
+            confirm the token transfer. If any other value
+            is returned or the interface is not implemented
+            by the recipient, the transfer will be reverted.
+    @param operator The 20-byte address which called
+           the `safeTransferFrom` function.
+    @param owner The 20-byte address which previously
+           owned the token.
+    @param id The 32-byte identifier of the token.
+    @param amount The 32-byte token amount that is
+           being transferred.
+    @param data The maximum 1,024-byte additional data
+           with no specified format.
+    @return bytes4 The 4-byte function selector of `onERC1155Received`.
+    """
+    return IERC1155_TOKENRECEIVER_SINGLE_SELECTOR
+
+
+@external
+def onERC1155BatchReceived(operator: address, owner: address, ids: DynArray[uint256, 65_535], amounts: DynArray[uint256, 65_535],
+                           data: Bytes[1_024]) -> bytes4:
+    """
+    @dev Handles the receipt of multiple ERC-1155 token types.
+         This function is called at the end of a `safeBatchTransferFrom`
+         after the balances have been updated.
+    @notice It must return its function selector to
+            confirm the token transfers. If any other value
+            is returned or the interface is not implemented
+            by the recipient, the transfers will be reverted.
+    @param operator The 20-byte address which called
+           the `safeBatchTransferFrom` function.
+    @param owner The 20-byte address which previously
+           owned the tokens.
+    @param ids The 32-byte array of token identifiers. Note
+           that the order and length must match the 32-byte
+           `amounts` array.
+    @param amounts The 32-byte array of token amounts that are
+           being transferred. Note that the order and length must
+           match the 32-byte `ids` array.
+    @param data The maximum 1,024-byte additional data
+           with no specified format.
+    @return bytes4 The 4-byte function selector of `onERC1155BatchReceived`.
+    """
+    return IERC1155_TOKENRECEIVER_BATCH_SELECTOR
+
+
+@internal
+@view
+def _is_operation(id: bytes32) -> bool:
+    """
+    @dev Returns whether an `id` corresponds to a registered
+         operation. This includes both `WAITING`, `READY`, and
+         `DONE` operations.
+    @param id The 32-byte operation identifier.
+    @return bool The verification whether the `id`
+            corresponds to a registered operation or not.
+    """
+    return self._get_operation_state(id) != OperationState.UNSET
+
+
+@internal
+@view
+def _is_operation_pending(id: bytes32) -> bool:
+    """
+    @dev Returns whether an operation is pending or not.
+         Note that a "pending" operation may also be
+         "ready".
+    @param id The 32-byte operation identifier.
+    @return bool The verification whether the operation
+            is pending or not.
+    """
+    state: OperationState = self._get_operation_state(id)
+    return state == OperationState.WAITING or state == OperationState.READY
+
+
+@internal
+@view
+def _is_operation_ready(id: bytes32) -> bool:
+    """
+    @dev Returns whether an operation is ready for
+         execution. Note that a "ready" operation is
+         also "pending".
+    @param id The 32-byte operation identifier.
+    @return bool The verification whether the operation
+            is ready or not.
+    """
+    return self._get_operation_state(id) == OperationState.READY
+
+
+@internal
+@view
+def _is_operation_done(id: bytes32) -> bool:
+    """
+    @dev Returns whether an operation is done or not.
+    @param id The 32-byte operation identifier.
+    @return bool The verification whether the operation
+            is done or not.
+    """
+    return self._get_operation_state(id) == OperationState.DONE
+
+
+@internal
+@view
+def _get_operation_state(id: bytes32) -> OperationState:
+    """
+    @dev Returns the state of an operation.
+    @param id The 32-byte operation identifier.
+    @return OperationState The 32-byte state of the
+            operation.
+    """
+    timestamp: uint256 = self.get_timestamp[id]
+    if (timestamp == empty(uint256)):
+        return OperationState.UNSET
+    elif (timestamp == _DONE_TIMESTAMP):
+        return OperationState.DONE
+    elif (timestamp > block.timestamp):
+        return OperationState.WAITING
+    else:
+        return OperationState.READY
+
+
+@internal
+@pure
+def _hash_operation(target: address, amount: uint256, payload: Bytes[1_024], predecessor: bytes32, salt: bytes32) -> bytes32:
+    """
+    @dev Returns the identifier of an operation containing
+         a single transaction.
+    @param target The 20-bytes address of the target contract.
+    @param amount The 32-byte amount of native tokens to transfer
+           with the call.
+    @param payload The maximum 1,024-byte ABI-encoded calldata.
+    @param predecessor The 32-byte hash of the preceding operation
+           (optional with empty bytes).
+    @param salt The 32-byte salt of the operation.
+    @return bytes32 The 32-byte hash of the operation.
+    """
+    return keccak256(_abi_encode(target, amount, payload, predecessor, salt))
+
+
+@internal
+@pure
+def _hash_operation_batch(targets: DynArray[address, _DYNARRAY_BOUND], amounts: DynArray[uint256, _DYNARRAY_BOUND], payloads: DynArray[Bytes[1_024], _DYNARRAY_BOUND],
+                          predecessor: bytes32, salt: bytes32) -> bytes32:
+    """
+    @dev Returns the identifier of an operation containing
+         a batch of transactions.
+    @param targets The 20-byte array of the target contracts.
+    @param amounts The 32-byte array of native tokens amounts to
+           transfer with each call.
+    @param payloads The maximum 1,024-byte byte array of ABI-encoded
+           calldata.
+    @param predecessor The 32-byte hash of the preceding operation
+           (optional with empty bytes).
+    @param salt The 32-byte salt of the operation.
+    @return bytes32 The 32-byte hash of the operation.
+    """
+    return keccak256(_abi_encode(targets, amounts, payloads, predecessor, salt))
+
+
+@internal
+def _schedule(id: bytes32, delay: uint256):
+    """
+    @dev Schedules an operation that is to become valid
+         after a given delay.
+    @notice This is an `internal` function without access
+            restriction.
+    @param id The 32-byte operation identifier.
+    @param delay The 32-byte delay before the operation
+           becomes valid. Must be greater than or equal
+           to the minimum delay.
+    """
+    assert not(self._is_operation(id)), "TimelockController: operation already scheduled"
+    assert delay >= self.get_minimum_delay, "TimelockController: insufficient delay"
+    self.get_timestamp[id] = block.timestamp + delay
+
+
+@internal
+def _execute(target: address, amount: uint256, payload: Bytes[1_024]):
+    """
+    @dev Executes an operation call.
+    @notice This is an `internal` function without access
+            restriction.
+    @param target The 20-byte address of the target contract.
+    @param amount The 32-byte amount of native tokens to transfer
+           with the call.
+    @param payload The maximum 1,024-byte ABI-encoded calldata.
+    """
+    return_data: Bytes[max_value(uint8)] = b""
+    success: bool = empty(bool)
+    success, return_data = raw_call(target, payload, max_outsize=255, value=amount, revert_on_failure=False)
+    if (not(success)):
+        if len(return_data) != empty(uint256):
+            # Bubble up the revert reason.
+            raw_revert(return_data)
+        else:
+            raise "TimelockController: underlying transaction reverted"
+
+
+@internal
+@view
+def _before_call(id: bytes32, predecessor: bytes32):
+    """
+    @dev Implements safety checks that must succeed before
+         executing (an) operation call(s).
+    @param id The 32-byte operation identifier.
+    @param predecessor The 32-byte hash of the preceding
+           operation.
+    """
+    assert self._is_operation_ready(id), "TimelockController: operation is not ready"
+    assert predecessor == empty(bytes32) or self._is_operation_done(predecessor), "TimelockController: missing dependency"
+
+
+@internal
+def _after_call(id: bytes32):
+    """
+    @dev Implements safety checks that must succeed after
+         executing (an) operation call(s).
+    @param id The 32-byte operation identifier.
+    """
+    assert self._is_operation_ready(id), "TimelockController: operation is not ready"
+    self.get_timestamp[id] = _DONE_TIMESTAMP
+
+
+@internal
+@view
+def _only_role_or_open_role(role: bytes32):
+    """
+    @dev Limits a function to be callable only by a certain
+         role. In addition to checking the sender's role,
+         the zero address `empty(address)` is also considered.
+         Granting a role to `empty(address)` is equivalent to
+         enabling this role for everyone.
+    @param role The 32-byte role definition.
+    """
+    if (not(self.hasRole[role][empty(address)])):
+        self._check_role(role, msg.sender)
+
+
+@internal
+@view
+def _check_role(role: bytes32, account: address):
+    """
+    @dev Sourced from {AccessControl-_check_role}.
+    @notice See {AccessControl-_check_role} for the
+            function docstring.
+    """
+    assert self.hasRole[role][account], "AccessControl: account is missing role"
+
+
+@internal
+def _set_role_admin(role: bytes32, admin_role: bytes32):
+    """
+    @dev Sourced from {AccessControl-_set_role_admin}.
+    @notice See {AccessControl-_set_role_admin} for the
+            function docstring.
+    """
+    previous_admin_role: bytes32 = self.getRoleAdmin[role]
+    self.getRoleAdmin[role] = admin_role
+    log RoleAdminChanged(role, previous_admin_role, admin_role)
+
+
+@internal
+def _grant_role(role: bytes32, account: address):
+    """
+    @dev Sourced from {AccessControl-_grant_role}.
+    @notice See {AccessControl-_grant_role} for the
+            function docstring.
+    """
+    if (not(self.hasRole[role][account])):
+        self.hasRole[role][account] = True
+        log RoleGranted(role, account, msg.sender)
+
+
+@internal
+def _revoke_role(role: bytes32, account: address):
+    """
+    @dev Sourced from {AccessControl-_revoke_role}.
+    @notice See {AccessControl-_revoke_role} for the
+            function docstring.
+    """
+    if (self.hasRole[role][account]):
+        self.hasRole[role][account] = False
+        log RoleRevoked(role, account, msg.sender)
diff --git a/src/tokens/ERC1155.vy b/src/tokens/ERC1155.vy
index 4b96fbfc..1741e467 100644
--- a/src/tokens/ERC1155.vy
+++ b/src/tokens/ERC1155.vy
@@ -76,6 +76,9 @@ _BASE_URI: immutable(String[80])
 
 
 # @dev Mapping from owner to operator approvals.
+# @notice If you declare a variable as `public`,
+# Vyper automatically generates an `external`
+# getter function for the variable.
 isApprovedForAll: public(HashMap[address, HashMap[address, bool]])
 
 
diff --git a/src/utils/Create2Address.vy b/src/utils/Create2Address.vy
index 70b11833..4effe643 100644
--- a/src/utils/Create2Address.vy
+++ b/src/utils/Create2Address.vy
@@ -12,6 +12,9 @@
 """
 
 
+# @dev The 1-byte `CREATE2` offset constant used to prevent
+# collisions with addresses created using the traditional
+# `keccak256(rlp([sender, nonce]))` formula.
 _COLLISION_OFFSET: constant(bytes1) = 0xFF
 
 
diff --git a/src/utils/ECDSA.vy b/src/utils/ECDSA.vy
index e5d85aae..78f1f9d1 100644
--- a/src/utils/ECDSA.vy
+++ b/src/utils/ECDSA.vy
@@ -18,6 +18,7 @@
 """
 
 
+# @dev Constants used as part of the ECDSA recovery function.
 _MALLEABILITY_THRESHOLD: constant(bytes32) = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
 _SIGNATURE_INCREMENT: constant(bytes32) = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
 
diff --git a/src/utils/SignatureChecker.vy b/src/utils/SignatureChecker.vy
index 71e6c859..68371c15 100644
--- a/src/utils/SignatureChecker.vy
+++ b/src/utils/SignatureChecker.vy
@@ -22,7 +22,14 @@
 """
 
 
+# @dev The 4-byte function selector of `isValidSignature(bytes32,bytes)`.
+# @notice If you declare a variable as `public`,
+# Vyper automatically generates an `external`
+# getter function for the variable.
 IERC1271_ISVALIDSIGNATURE_SELECTOR: public(constant(bytes4)) = 0x1626BA7E
+
+
+# @dev Constants used as part of the ECDSA recovery function.
 _MALLEABILITY_THRESHOLD: constant(bytes32) = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
 _SIGNATURE_INCREMENT: constant(bytes32) = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
 


Test patch:
diff --git a/test/extensions/ERC4626.t.sol b/test/extensions/ERC4626.t.sol
index 7c526658..e1e8e3d1 100644
--- a/test/extensions/ERC4626.t.sol
+++ b/test/extensions/ERC4626.t.sol
@@ -1596,7 +1596,6 @@ contract ERC4626VaultTest is ERC4626Test {
         address spender = makeAddr("spender");
         uint256 amount = 100;
         uint256 nonce = ERC4626ExtendedDecimalsOffset0.nonces(owner);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 domainSeparator = ERC4626ExtendedDecimalsOffset0
             .DOMAIN_SEPARATOR();
@@ -1642,7 +1641,6 @@ contract ERC4626VaultTest is ERC4626Test {
         address spender = makeAddr("spender");
         uint256 amount = 100;
         uint256 nonce = ERC4626ExtendedDecimalsOffset0.nonces(owner);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 domainSeparator = ERC4626ExtendedDecimalsOffset0
             .DOMAIN_SEPARATOR();
@@ -1693,7 +1691,6 @@ contract ERC4626VaultTest is ERC4626Test {
         address spender = makeAddr("spender");
         uint256 amount = 100;
         uint256 nonce = ERC4626ExtendedDecimalsOffset0.nonces(owner);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 domainSeparator = ERC4626ExtendedDecimalsOffset0
             .DOMAIN_SEPARATOR();
@@ -1733,7 +1730,6 @@ contract ERC4626VaultTest is ERC4626Test {
         address spender = makeAddr("spender");
         uint256 amount = 100;
         uint256 nonce = ERC4626ExtendedDecimalsOffset0.nonces(owner);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 domainSeparator = keccak256(
             abi.encode(
@@ -1780,7 +1776,6 @@ contract ERC4626VaultTest is ERC4626Test {
         address spender = makeAddr("spender");
         uint256 amount = 100;
         uint256 nonce = 1;
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 domainSeparator = ERC4626ExtendedDecimalsOffset0
             .DOMAIN_SEPARATOR();
@@ -1820,7 +1815,6 @@ contract ERC4626VaultTest is ERC4626Test {
         address spender = makeAddr("spender");
         uint256 amount = 100;
         uint256 nonce = ERC4626ExtendedDecimalsOffset0.nonces(owner);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp - 1;
         bytes32 domainSeparator = ERC4626ExtendedDecimalsOffset0
             .DOMAIN_SEPARATOR();
@@ -1915,7 +1909,6 @@ contract ERC4626VaultTest is ERC4626Test {
         address spenderAddr = makeAddr(spender);
         uint256 amount = block.number;
         uint256 nonce = ERC4626ExtendedDecimalsOffset0.nonces(ownerAddr);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + increment;
         bytes32 domainSeparator = ERC4626ExtendedDecimalsOffset0
             .DOMAIN_SEPARATOR();
@@ -1969,7 +1962,6 @@ contract ERC4626VaultTest is ERC4626Test {
         address spenderAddr = makeAddr(spender);
         uint256 amount = block.number;
         uint256 nonce = ERC4626ExtendedDecimalsOffset0.nonces(ownerAddr);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + increment;
         bytes32 domainSeparator = ERC4626ExtendedDecimalsOffset0
             .DOMAIN_SEPARATOR();
diff --git a/test/governance/TimelockController.t.sol b/test/governance/TimelockController.t.sol
new file mode 100644
index 00000000..21ab477e
--- /dev/null
+++ b/test/governance/TimelockController.t.sol
@@ -0,0 +1,4454 @@
+// SPDX-License-Identifier: WTFPL
+pragma solidity ^0.8.23;
+
+import {Test} from "forge-std/Test.sol";
+import {VyperDeployer} from "utils/VyperDeployer.sol";
+
+import {IERC165} from "openzeppelin/utils/introspection/IERC165.sol";
+import {IAccessControl} from "openzeppelin/access/IAccessControl.sol";
+import {IERC1155Receiver} from "openzeppelin/token/ERC1155/IERC1155Receiver.sol";
+
+import {ERC721ReceiverMock} from "../tokens/mocks/ERC721ReceiverMock.sol";
+import {ERC1155ReceiverMock} from "../tokens/mocks/ERC1155ReceiverMock.sol";
+import {CallReceiverMock} from "./mocks/CallReceiverMock.sol";
+
+import {IERC721Extended} from "../tokens/interfaces/IERC721Extended.sol";
+import {IERC1155Extended} from "../tokens/interfaces/IERC1155Extended.sol";
+import {ITimelockController} from "./interfaces/ITimelockController.sol";
+
+/**
+ * @dev The standard access control functionalities are not tested as they
+ * were taken 1:1 from `AccessControl.vy`. See `AccessControl.t.sol` for the
+ * corresponding tests. However, please integrate these tests into your own
+ * test suite before deploying `TimelockController` into production!
+ */
+contract TimelockControllerTest is Test {
+    bytes32 private constant DEFAULT_ADMIN_ROLE = bytes32(0);
+    bytes32 private constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE");
+    bytes32 private constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");
+    bytes32 private constant CANCELLER_ROLE = keccak256("CANCELLER_ROLE");
+
+    bytes4 private constant IERC721_TOKENRECEIVER_SELECTOR =
+        ERC721ReceiverMock.onERC721Received.selector;
+    bytes4 private constant IERC1155_TOKENRECEIVER_SINGLE_SELECTOR =
+        ERC1155ReceiverMock.onERC1155Received.selector;
+    bytes4 private constant IERC1155_TOKENRECEIVER_BATCH_SELECTOR =
+        ERC1155ReceiverMock.onERC1155BatchReceived.selector;
+
+    uint256 private constant MIN_DELAY = 2 days;
+    uint256 private constant DONE_TIMESTAMP = 1;
+
+    address private constant PROPOSER_ONE =
+        address(uint160(uint256(keccak256(abi.encodePacked("PROPOSER_ONE")))));
+    address private constant PROPOSER_TWO =
+        address(uint160(uint256(keccak256(abi.encodePacked("PROPOSER_TWO")))));
+    address private constant EXECUTOR_ONE =
+        address(uint160(uint256(keccak256(abi.encodePacked("EXECUTOR_ONE")))));
+    address private constant EXECUTOR_TWO =
+        address(uint160(uint256(keccak256(abi.encodePacked("EXECUTOR_TWO")))));
+    address private constant STRANGER =
+        address(uint160(uint256(keccak256(abi.encodePacked("STRANGER")))));
+
+    bytes32 private constant NO_PREDECESSOR = bytes32("");
+    bytes32 private constant EMPTY_SALT = bytes32("");
+    bytes32 private constant SALT = keccak256("WAGMI");
+
+    VyperDeployer private vyperDeployer = new VyperDeployer();
+    CallReceiverMock private callReceiverMock = new CallReceiverMock();
+
+    ITimelockController private timelockController;
+    ITimelockController private timelockControllerInitialEventEmptyAdmin;
+    ITimelockController private timelockControllerInitialEventNonEmptyAdmin;
+    IERC721Extended private erc721Mock;
+    IERC1155Extended private erc1155Mock;
+
+    address private deployer = address(vyperDeployer);
+    address private self = address(this);
+    address private zeroAddress = zeroAddress;
+    address private target = address(callReceiverMock);
+    address private timelockControllerAddr;
+    address private timelockControllerInitialEventEmptyAdminAddr;
+    address private timelockControllerInitialEventNonEmptyAdminAddr;
+
+    address[2] private proposers = [PROPOSER_ONE, PROPOSER_TWO];
+    address[2] private executors = [EXECUTOR_ONE, EXECUTOR_TWO];
+
+    /**
+     * @dev An `internal` helper function to check whether a specific role `role`
+     * is not assigned to an array of addresses with the length 2.
+     * @param accessControl The contract that implements the `IAccessControl` interface.
+     * @param role The 32-byte role definition.
+     * @param addresses The 20-byte array with the length 2 of accounts to be checked.
+     */
+    function checkRoleNotSetForAddresses(
+        IAccessControl accessControl,
+        bytes32 role,
+        address[2] storage addresses
+    ) internal {
+        assertTrue(
+            !accessControl.hasRole(role, addresses[0]) &&
+                !accessControl.hasRole(role, addresses[1])
+        );
+    }
+
+    function setUp() public {
+        address[] memory proposers_ = new address[](2);
+        proposers_[0] = proposers[0];
+        proposers_[1] = proposers[1];
+        address[] memory executors_ = new address[](2);
+        executors_[0] = executors[0];
+        executors_[1] = executors[1];
+
+        bytes memory args = abi.encode(MIN_DELAY, proposers_, executors_, self);
+        timelockController = ITimelockController(
+            vyperDeployer.deployContract(
+                "src/governance/",
+                "TimelockController",
+                args
+            )
+        );
+        timelockControllerAddr = address(timelockController);
+    }
+
+    function testInitialSetup() public {
+        assertEq(timelockController.DEFAULT_ADMIN_ROLE(), DEFAULT_ADMIN_ROLE);
+        assertEq(timelockController.PROPOSER_ROLE(), PROPOSER_ROLE);
+        assertEq(timelockController.EXECUTOR_ROLE(), EXECUTOR_ROLE);
+        assertEq(timelockController.CANCELLER_ROLE(), CANCELLER_ROLE);
+        assertEq(
+            timelockController.IERC721_TOKENRECEIVER_SELECTOR(),
+            IERC721_TOKENRECEIVER_SELECTOR
+        );
+        assertEq(
+            timelockController.IERC1155_TOKENRECEIVER_SINGLE_SELECTOR(),
+            IERC1155_TOKENRECEIVER_SINGLE_SELECTOR
+        );
+        assertEq(
+            timelockController.IERC1155_TOKENRECEIVER_BATCH_SELECTOR(),
+            IERC1155_TOKENRECEIVER_BATCH_SELECTOR
+        );
+        assertTrue(
+            timelockController.hasRole(
+                timelockController.DEFAULT_ADMIN_ROLE(),
+                timelockControllerAddr
+            )
+        );
+        assertTrue(
+            timelockController.hasRole(
+                timelockController.DEFAULT_ADMIN_ROLE(),
+                self
+            )
+        );
+        assertTrue(
+            timelockController.hasRole(
+                timelockController.PROPOSER_ROLE(),
+                PROPOSER_ONE
+            )
+        );
+        assertTrue(
+            timelockController.hasRole(
+                timelockController.PROPOSER_ROLE(),
+                PROPOSER_TWO
+            )
+        );
+        assertTrue(
+            timelockController.hasRole(
+                timelockController.CANCELLER_ROLE(),
+                PROPOSER_ONE
+            )
+        );
+        assertTrue(
+            timelockController.hasRole(
+                timelockController.CANCELLER_ROLE(),
+                PROPOSER_TWO
+            )
+        );
+        assertTrue(
+            timelockController.hasRole(
+                timelockController.EXECUTOR_ROLE(),
+                EXECUTOR_ONE
+            )
+        );
+        assertTrue(
+            timelockController.hasRole(
+                timelockController.EXECUTOR_ROLE(),
+                EXECUTOR_TWO
+            )
+        );
+        assertTrue(
+            !timelockController.hasRole(
+                timelockController.PROPOSER_ROLE(),
+                self
+            )
+        );
+        assertTrue(
+            !timelockController.hasRole(
+                timelockController.CANCELLER_ROLE(),
+                self
+            )
+        );
+        assertTrue(
+            !timelockController.hasRole(
+                timelockController.EXECUTOR_ROLE(),
+                self
+            )
+        );
+        assertTrue(
+            !timelockController.hasRole(
+                timelockController.PROPOSER_ROLE(),
+                timelockControllerAddr
+            )
+        );
+        assertTrue(
+            !timelockController.hasRole(
+                timelockController.CANCELLER_ROLE(),
+                timelockControllerAddr
+            )
+        );
+        assertTrue(
+            !timelockController.hasRole(
+                timelockController.EXECUTOR_ROLE(),
+                timelockControllerAddr
+            )
+        );
+        checkRoleNotSetForAddresses(
+            timelockController,
+            timelockController.DEFAULT_ADMIN_ROLE(),
+            proposers
+        );
+        checkRoleNotSetForAddresses(
+            timelockController,
+            timelockController.EXECUTOR_ROLE(),
+            proposers
+        );
+        checkRoleNotSetForAddresses(
+            timelockController,
+            timelockController.DEFAULT_ADMIN_ROLE(),
+            executors
+        );
+        checkRoleNotSetForAddresses(
+            timelockController,
+            timelockController.PROPOSER_ROLE(),
+            executors
+        );
+        checkRoleNotSetForAddresses(
+            timelockController,
+            timelockController.CANCELLER_ROLE(),
+            executors
+        );
+        assertEq(timelockController.get_minimum_delay(), MIN_DELAY);
+
+        address[] memory proposers_ = new address[](2);
+        proposers_[0] = proposers[0];
+        proposers_[1] = proposers[1];
+        address[] memory executors_ = new address[](2);
+        executors_[0] = executors[0];
+        executors_[1] = executors[1];
+        bytes memory argsEmptyAdmin = abi.encode(
+            MIN_DELAY,
+            proposers_,
+            executors_,
+            zeroAddress
+        );
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleGranted(
+            DEFAULT_ADMIN_ROLE,
+            vm.computeCreateAddress(deployer, vm.getNonce(deployer)),
+            deployer
+        );
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleGranted(PROPOSER_ROLE, proposers[0], deployer);
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleGranted(CANCELLER_ROLE, proposers[0], deployer);
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleGranted(PROPOSER_ROLE, proposers[1], deployer);
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleGranted(CANCELLER_ROLE, proposers[1], deployer);
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleGranted(EXECUTOR_ROLE, executors[0], deployer);
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleGranted(EXECUTOR_ROLE, executors[1], deployer);
+        vm.expectEmit(true, true, false, false);
+        emit ITimelockController.MinimumDelayChange(0, MIN_DELAY);
+        timelockControllerInitialEventEmptyAdmin = ITimelockController(
+            vyperDeployer.deployContract(
+                "src/governance/",
+                "TimelockController",
+                argsEmptyAdmin
+            )
+        );
+        timelockControllerInitialEventEmptyAdminAddr = address(
+            timelockControllerInitialEventEmptyAdmin
+        );
+        assertEq(
+            timelockControllerInitialEventEmptyAdmin.DEFAULT_ADMIN_ROLE(),
+            DEFAULT_ADMIN_ROLE
+        );
+        assertEq(
+            timelockControllerInitialEventEmptyAdmin.PROPOSER_ROLE(),
+            PROPOSER_ROLE
+        );
+        assertEq(
+            timelockControllerInitialEventEmptyAdmin.EXECUTOR_ROLE(),
+            EXECUTOR_ROLE
+        );
+        assertEq(
+            timelockControllerInitialEventEmptyAdmin.CANCELLER_ROLE(),
+            CANCELLER_ROLE
+        );
+        assertEq(
+            timelockControllerInitialEventEmptyAdmin
+                .IERC721_TOKENRECEIVER_SELECTOR(),
+            IERC721_TOKENRECEIVER_SELECTOR
+        );
+        assertEq(
+            timelockControllerInitialEventEmptyAdmin
+                .IERC1155_TOKENRECEIVER_SINGLE_SELECTOR(),
+            IERC1155_TOKENRECEIVER_SINGLE_SELECTOR
+        );
+        assertEq(
+            timelockControllerInitialEventEmptyAdmin
+                .IERC1155_TOKENRECEIVER_BATCH_SELECTOR(),
+            IERC1155_TOKENRECEIVER_BATCH_SELECTOR
+        );
+        assertTrue(
+            timelockControllerInitialEventEmptyAdmin.hasRole(
+                timelockControllerInitialEventEmptyAdmin.DEFAULT_ADMIN_ROLE(),
+                timelockControllerInitialEventEmptyAdminAddr
+            )
+        );
+        assertTrue(
+            !timelockControllerInitialEventEmptyAdmin.hasRole(
+                timelockControllerInitialEventEmptyAdmin.DEFAULT_ADMIN_ROLE(),
+                self
+            )
+        );
+        assertTrue(
+            timelockControllerInitialEventEmptyAdmin.hasRole(
+                timelockControllerInitialEventEmptyAdmin.PROPOSER_ROLE(),
+                PROPOSER_ONE
+            )
+        );
+        assertTrue(
+            timelockControllerInitialEventEmptyAdmin.hasRole(
+                timelockControllerInitialEventEmptyAdmin.PROPOSER_ROLE(),
+                PROPOSER_TWO
+            )
+        );
+        assertTrue(
+            timelockControllerInitialEventEmptyAdmin.hasRole(
+                timelockControllerInitialEventEmptyAdmin.CANCELLER_ROLE(),
+                PROPOSER_ONE
+            )
+        );
+        assertTrue(
+            timelockControllerInitialEventEmptyAdmin.hasRole(
+                timelockControllerInitialEventEmptyAdmin.CANCELLER_ROLE(),
+                PROPOSER_TWO
+            )
+        );
+        assertTrue(
+            timelockControllerInitialEventEmptyAdmin.hasRole(
+                timelockControllerInitialEventEmptyAdmin.EXECUTOR_ROLE(),
+                EXECUTOR_ONE
+            )
+        );
+        assertTrue(
+            timelockControllerInitialEventEmptyAdmin.hasRole(
+                timelockControllerInitialEventEmptyAdmin.EXECUTOR_ROLE(),
+                EXECUTOR_TWO
+            )
+        );
+        assertTrue(
+            !timelockControllerInitialEventEmptyAdmin.hasRole(
+                timelockControllerInitialEventEmptyAdmin.PROPOSER_ROLE(),
+                self
+            )
+        );
+        assertTrue(
+            !timelockControllerInitialEventEmptyAdmin.hasRole(
+                timelockControllerInitialEventEmptyAdmin.CANCELLER_ROLE(),
+                self
+            )
+        );
+        assertTrue(
+            !timelockControllerInitialEventEmptyAdmin.hasRole(
+                timelockControllerInitialEventEmptyAdmin.EXECUTOR_ROLE(),
+                self
+            )
+        );
+        assertTrue(
+            !timelockControllerInitialEventEmptyAdmin.hasRole(
+                timelockControllerInitialEventEmptyAdmin.PROPOSER_ROLE(),
+                timelockControllerInitialEventEmptyAdminAddr
+            )
+        );
+        assertTrue(
+            !timelockControllerInitialEventEmptyAdmin.hasRole(
+                timelockControllerInitialEventEmptyAdmin.CANCELLER_ROLE(),
+                timelockControllerInitialEventEmptyAdminAddr
+            )
+        );
+        assertTrue(
+            !timelockControllerInitialEventEmptyAdmin.hasRole(
+                timelockControllerInitialEventEmptyAdmin.EXECUTOR_ROLE(),
+                timelockControllerInitialEventEmptyAdminAddr
+            )
+        );
+        checkRoleNotSetForAddresses(
+            timelockControllerInitialEventEmptyAdmin,
+            timelockControllerInitialEventEmptyAdmin.DEFAULT_ADMIN_ROLE(),
+            proposers
+        );
+        checkRoleNotSetForAddresses(
+            timelockControllerInitialEventEmptyAdmin,
+            timelockControllerInitialEventEmptyAdmin.EXECUTOR_ROLE(),
+            proposers
+        );
+        checkRoleNotSetForAddresses(
+            timelockControllerInitialEventEmptyAdmin,
+            timelockControllerInitialEventEmptyAdmin.DEFAULT_ADMIN_ROLE(),
+            executors
+        );
+        checkRoleNotSetForAddresses(
+            timelockControllerInitialEventEmptyAdmin,
+            timelockControllerInitialEventEmptyAdmin.PROPOSER_ROLE(),
+            executors
+        );
+        checkRoleNotSetForAddresses(
+            timelockControllerInitialEventEmptyAdmin,
+            timelockControllerInitialEventEmptyAdmin.CANCELLER_ROLE(),
+            executors
+        );
+        assertEq(
+            timelockControllerInitialEventEmptyAdmin.get_minimum_delay(),
+            MIN_DELAY
+        );
+
+        bytes memory argsNonEmptyAdmin = abi.encode(
+            MIN_DELAY,
+            proposers_,
+            executors_,
+            self
+        );
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleGranted(
+            DEFAULT_ADMIN_ROLE,
+            vm.computeCreateAddress(deployer, vm.getNonce(deployer)),
+            deployer
+        );
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleGranted(DEFAULT_ADMIN_ROLE, self, deployer);
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleGranted(PROPOSER_ROLE, proposers[0], deployer);
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleGranted(CANCELLER_ROLE, proposers[0], deployer);
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleGranted(PROPOSER_ROLE, proposers[1], deployer);
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleGranted(CANCELLER_ROLE, proposers[1], deployer);
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleGranted(EXECUTOR_ROLE, executors[0], deployer);
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleGranted(EXECUTOR_ROLE, executors[1], deployer);
+        vm.expectEmit(true, true, false, false);
+        emit ITimelockController.MinimumDelayChange(0, MIN_DELAY);
+        timelockControllerInitialEventNonEmptyAdmin = ITimelockController(
+            vyperDeployer.deployContract(
+                "src/governance/",
+                "TimelockController",
+                argsNonEmptyAdmin
+            )
+        );
+        timelockControllerInitialEventNonEmptyAdminAddr = address(
+            timelockControllerInitialEventNonEmptyAdmin
+        );
+        assertEq(
+            timelockControllerInitialEventNonEmptyAdmin.DEFAULT_ADMIN_ROLE(),
+            DEFAULT_ADMIN_ROLE
+        );
+        assertEq(
+            timelockControllerInitialEventNonEmptyAdmin.PROPOSER_ROLE(),
+            PROPOSER_ROLE
+        );
+        assertEq(
+            timelockControllerInitialEventNonEmptyAdmin.EXECUTOR_ROLE(),
+            EXECUTOR_ROLE
+        );
+        assertEq(
+            timelockControllerInitialEventNonEmptyAdmin.CANCELLER_ROLE(),
+            CANCELLER_ROLE
+        );
+        assertEq(
+            timelockControllerInitialEventNonEmptyAdmin
+                .IERC721_TOKENRECEIVER_SELECTOR(),
+            IERC721_TOKENRECEIVER_SELECTOR
+        );
+        assertEq(
+            timelockControllerInitialEventNonEmptyAdmin
+                .IERC1155_TOKENRECEIVER_SINGLE_SELECTOR(),
+            IERC1155_TOKENRECEIVER_SINGLE_SELECTOR
+        );
+        assertEq(
+            timelockControllerInitialEventNonEmptyAdmin
+                .IERC1155_TOKENRECEIVER_BATCH_SELECTOR(),
+            IERC1155_TOKENRECEIVER_BATCH_SELECTOR
+        );
+        assertTrue(
+            timelockControllerInitialEventNonEmptyAdmin.hasRole(
+                timelockControllerInitialEventNonEmptyAdmin
+                    .DEFAULT_ADMIN_ROLE(),
+                timelockControllerInitialEventNonEmptyAdminAddr
+            )
+        );
+        assertTrue(
+            timelockControllerInitialEventNonEmptyAdmin.hasRole(
+                timelockControllerInitialEventNonEmptyAdmin
+                    .DEFAULT_ADMIN_ROLE(),
+                self
+            )
+        );
+        assertTrue(
+            timelockControllerInitialEventNonEmptyAdmin.hasRole(
+                timelockControllerInitialEventNonEmptyAdmin.PROPOSER_ROLE(),
+                PROPOSER_ONE
+            )
+        );
+        assertTrue(
+            timelockControllerInitialEventNonEmptyAdmin.hasRole(
+                timelockControllerInitialEventNonEmptyAdmin.PROPOSER_ROLE(),
+                PROPOSER_TWO
+            )
+        );
+        assertTrue(
+            timelockControllerInitialEventNonEmptyAdmin.hasRole(
+                timelockControllerInitialEventNonEmptyAdmin.CANCELLER_ROLE(),
+                PROPOSER_ONE
+            )
+        );
+        assertTrue(
+            timelockControllerInitialEventNonEmptyAdmin.hasRole(
+                timelockControllerInitialEventNonEmptyAdmin.CANCELLER_ROLE(),
+                PROPOSER_TWO
+            )
+        );
+        assertTrue(
+            timelockControllerInitialEventNonEmptyAdmin.hasRole(
+                timelockControllerInitialEventNonEmptyAdmin.EXECUTOR_ROLE(),
+                EXECUTOR_ONE
+            )
+        );
+        assertTrue(
+            timelockControllerInitialEventNonEmptyAdmin.hasRole(
+                timelockControllerInitialEventNonEmptyAdmin.EXECUTOR_ROLE(),
+                EXECUTOR_TWO
+            )
+        );
+        assertTrue(
+            !timelockControllerInitialEventNonEmptyAdmin.hasRole(
+                timelockControllerInitialEventNonEmptyAdmin.PROPOSER_ROLE(),
+                self
+            )
+        );
+        assertTrue(
+            !timelockControllerInitialEventNonEmptyAdmin.hasRole(
+                timelockControllerInitialEventNonEmptyAdmin.CANCELLER_ROLE(),
+                self
+            )
+        );
+        assertTrue(
+            !timelockControllerInitialEventNonEmptyAdmin.hasRole(
+                timelockControllerInitialEventNonEmptyAdmin.EXECUTOR_ROLE(),
+                self
+            )
+        );
+        assertTrue(
+            !timelockControllerInitialEventNonEmptyAdmin.hasRole(
+                timelockControllerInitialEventNonEmptyAdmin.PROPOSER_ROLE(),
+                timelockControllerInitialEventNonEmptyAdminAddr
+            )
+        );
+        assertTrue(
+            !timelockControllerInitialEventNonEmptyAdmin.hasRole(
+                timelockControllerInitialEventNonEmptyAdmin.CANCELLER_ROLE(),
+                timelockControllerInitialEventNonEmptyAdminAddr
+            )
+        );
+        assertTrue(
+            !timelockControllerInitialEventNonEmptyAdmin.hasRole(
+                timelockControllerInitialEventNonEmptyAdmin.EXECUTOR_ROLE(),
+                timelockControllerInitialEventNonEmptyAdminAddr
+            )
+        );
+        checkRoleNotSetForAddresses(
+            timelockControllerInitialEventNonEmptyAdmin,
+            timelockControllerInitialEventNonEmptyAdmin.DEFAULT_ADMIN_ROLE(),
+            proposers
+        );
+        checkRoleNotSetForAddresses(
+            timelockControllerInitialEventNonEmptyAdmin,
+            timelockControllerInitialEventNonEmptyAdmin.EXECUTOR_ROLE(),
+            proposers
+        );
+        checkRoleNotSetForAddresses(
+            timelockControllerInitialEventNonEmptyAdmin,
+            timelockControllerInitialEventNonEmptyAdmin.DEFAULT_ADMIN_ROLE(),
+            executors
+        );
+        checkRoleNotSetForAddresses(
+            timelockControllerInitialEventNonEmptyAdmin,
+            timelockControllerInitialEventNonEmptyAdmin.PROPOSER_ROLE(),
+            executors
+        );
+        checkRoleNotSetForAddresses(
+            timelockControllerInitialEventNonEmptyAdmin,
+            timelockControllerInitialEventNonEmptyAdmin.CANCELLER_ROLE(),
+            executors
+        );
+        assertEq(
+            timelockControllerInitialEventNonEmptyAdmin.get_minimum_delay(),
+            MIN_DELAY
+        );
+    }
+
+    function testCanReceiveEther() public {
+        payable(timelockControllerAddr).transfer(0.5 ether);
+        assertEq(timelockControllerAddr.balance, 0.5 ether);
+    }
+
+    function testSupportsInterfaceSuccess() public {
+        assertTrue(
+            timelockController.supportsInterface(type(IERC165).interfaceId)
+        );
+        assertTrue(
+            timelockController.supportsInterface(
+                type(IAccessControl).interfaceId
+            )
+        );
+        assertTrue(
+            timelockController.supportsInterface(
+                type(IERC1155Receiver).interfaceId
+            )
+        );
+    }
+
+    function testSupportsInterfaceSuccessGasCost() public {
+        uint256 startGas = gasleft();
+        timelockController.supportsInterface(type(IERC165).interfaceId);
+        uint256 gasUsed = startGas - gasleft();
+        assertTrue(
+            gasUsed <= 30_000 &&
+                timelockController.supportsInterface(type(IERC165).interfaceId)
+        );
+    }
+
+    function testSupportsInterfaceInvalidInterfaceId() public {
+        assertTrue(!timelockController.supportsInterface(0x0011bbff));
+    }
+
+    function testSupportsInterfaceInvalidInterfaceIdGasCost() public {
+        uint256 startGas = gasleft();
+        timelockController.supportsInterface(0x0011bbff);
+        uint256 gasUsed = startGas - gasleft();
+        assertTrue(
+            gasUsed <= 30_000 &&
+                !timelockController.supportsInterface(0x0011bbff)
+        );
+    }
+
+    function testHashOperation() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertEq(
+            operationId,
+            keccak256(
+                abi.encode(target, amount, payload, NO_PREDECESSOR, EMPTY_SALT)
+            )
+        );
+    }
+
+    function testScheduleAndExecuteWithEmptySalt() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertEq(vm.load(target, slot), bytes32(uint256(0)));
+        assertTrue(!timelockController.is_operation(operationId));
+        assertEq(timelockController.get_operation_state(operationId), 1);
+        assertEq(timelockController.get_timestamp(operationId), 0);
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        assertTrue(timelockController.is_operation(operationId));
+        assertEq(
+            timelockController.get_timestamp(operationId),
+            block.timestamp + MIN_DELAY
+        );
+        vm.stopPrank();
+
+        assertEq(timelockController.get_operation_state(operationId), 2);
+        vm.warp(block.timestamp + MIN_DELAY);
+        assertEq(timelockController.get_operation_state(operationId), 4);
+        vm.startPrank(EXECUTOR_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallExecuted(
+            operationId,
+            0,
+            target,
+            amount,
+            payload
+        );
+        timelockController.execute(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertEq(vm.load(target, slot), value);
+        assertTrue(timelockController.is_operation(operationId));
+        assertEq(timelockController.get_timestamp(operationId), DONE_TIMESTAMP);
+        assertEq(timelockController.get_operation_state(operationId), 8);
+        vm.stopPrank();
+    }
+
+    function testScheduleAndExecuteWithNonEmptySalt() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            SALT
+        );
+        assertEq(vm.load(target, slot), bytes32(uint256(0)));
+        assertTrue(!timelockController.is_operation(operationId));
+        assertEq(timelockController.get_operation_state(operationId), 1);
+        assertEq(timelockController.get_timestamp(operationId), 0);
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        vm.expectEmit(true, false, false, true);
+        emit ITimelockController.CallSalt(operationId, SALT);
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            SALT,
+            MIN_DELAY
+        );
+        assertTrue(timelockController.is_operation(operationId));
+        assertEq(
+            timelockController.get_timestamp(operationId),
+            block.timestamp + MIN_DELAY
+        );
+        vm.stopPrank();
+
+        assertEq(timelockController.get_operation_state(operationId), 2);
+        vm.warp(block.timestamp + MIN_DELAY);
+        assertEq(timelockController.get_operation_state(operationId), 4);
+        vm.startPrank(EXECUTOR_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallExecuted(
+            operationId,
+            0,
+            target,
+            amount,
+            payload
+        );
+        timelockController.execute(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            SALT
+        );
+        assertEq(vm.load(target, slot), value);
+        assertTrue(timelockController.is_operation(operationId));
+        assertEq(timelockController.get_timestamp(operationId), DONE_TIMESTAMP);
+        assertEq(timelockController.get_operation_state(operationId), 8);
+        vm.stopPrank();
+    }
+
+    function testOperationAlreadyScheduled() public {
+        uint256 amount = 0;
+        bytes memory payload = new bytes(0);
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.expectRevert("TimelockController: operation already scheduled");
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+    }
+
+    function testOperationInsufficientDelay() public {
+        vm.expectRevert("TimelockController: insufficient delay");
+        vm.prank(PROPOSER_ONE);
+        timelockController.schedule(
+            target,
+            0,
+            new bytes(0),
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY - 1
+        );
+    }
+
+    function testOperationEqualAndGreaterMinimumDelay() public {
+        uint256 amount = 0;
+        bytes memory payload = new bytes(0);
+        bytes32 operationId = timelockController.hash_operation(
+            zeroAddress,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            zeroAddress,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            zeroAddress,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        assertTrue(timelockController.is_operation(operationId));
+        vm.stopPrank();
+
+        operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            SALT
+        );
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY + 1
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            SALT,
+            MIN_DELAY + 1
+        );
+        assertTrue(timelockController.is_operation(operationId));
+        vm.stopPrank();
+    }
+
+    function testOperationMinimumDelayUpdate() public {
+        uint256 amount = 0;
+        bytes memory payload = new bytes(0);
+        bytes32 operationId = timelockController.hash_operation(
+            zeroAddress,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            zeroAddress,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            zeroAddress,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        uint256 operationTimestampBefore = timelockController.get_timestamp(
+            operationId
+        );
+        vm.stopPrank();
+
+        vm.startPrank(timelockControllerAddr);
+        vm.expectEmit(true, true, false, false);
+        emit ITimelockController.MinimumDelayChange(
+            MIN_DELAY,
+            MIN_DELAY + 31 days
+        );
+        timelockController.update_delay(MIN_DELAY + 31 days);
+        uint256 operationTimestampAfter = timelockController.get_timestamp(
+            operationId
+        );
+        assertEq(timelockController.get_minimum_delay(), MIN_DELAY + 31 days);
+        assertEq(operationTimestampAfter, operationTimestampBefore);
+        vm.stopPrank();
+    }
+
+    function testCompletePipelineOperationMinimumDelayUpdate() public {
+        uint256 amount = 0;
+        bytes memory payload = abi.encodeWithSelector(
+            timelockController.update_delay.selector,
+            MIN_DELAY + 31 days
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            timelockControllerAddr,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            timelockControllerAddr,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            timelockControllerAddr,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY);
+        vm.startPrank(EXECUTOR_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallExecuted(
+            operationId,
+            0,
+            timelockControllerAddr,
+            amount,
+            payload
+        );
+        timelockController.execute(
+            timelockControllerAddr,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertEq(timelockController.get_minimum_delay(), MIN_DELAY + 31 days);
+        vm.stopPrank();
+    }
+
+    function testOperationOperationIsNotReady() public {
+        uint256 amount = 0;
+        bytes memory payload = new bytes(0);
+        bytes32 operationId = timelockController.hash_operation(
+            zeroAddress,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            zeroAddress,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            zeroAddress,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY - 2 days);
+        vm.expectRevert("TimelockController: operation is not ready");
+        vm.prank(EXECUTOR_ONE);
+        timelockController.execute(
+            zeroAddress,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+    }
+
+    function testOperationPredecessorNotExecuted() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId1 = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        bytes32 operationId2 = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            operationId1,
+            SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId1,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId2,
+            0,
+            target,
+            amount,
+            payload,
+            operationId1,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            operationId1,
+            SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY + 2 days);
+        vm.expectRevert("TimelockController: missing dependency");
+        vm.prank(EXECUTOR_ONE);
+        timelockController.execute(target, amount, payload, operationId1, SALT);
+    }
+
+    function testOperationPredecessorNotScheduled() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId1 = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        bytes32 operationId2 = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            operationId1,
+            SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId2,
+            0,
+            target,
+            amount,
+            payload,
+            operationId1,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            operationId1,
+            SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY + 2 days);
+        vm.expectRevert("TimelockController: missing dependency");
+        vm.prank(EXECUTOR_ONE);
+        timelockController.execute(target, amount, payload, operationId1, SALT);
+    }
+
+    function testOperationPredecessorInvalid() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 invalidPredecessor = keccak256("Invalid Predecessor");
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            invalidPredecessor,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            invalidPredecessor,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            invalidPredecessor,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY + 2 days);
+        vm.expectRevert("TimelockController: missing dependency");
+        vm.prank(EXECUTOR_ONE);
+        timelockController.execute(
+            target,
+            amount,
+            payload,
+            invalidPredecessor,
+            EMPTY_SALT
+        );
+    }
+
+    function testOperationTargetRevert() public {
+        uint256 amount = 0;
+        bytes memory payload1 = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionRevertsWithReason.selector
+        );
+        bytes memory payload2 = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionRevertsWithEmptyReason.selector
+        );
+        bytes32 operationId1 = timelockController.hash_operation(
+            target,
+            amount,
+            payload1,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        bytes32 operationId2 = timelockController.hash_operation(
+            target,
+            amount,
+            payload2,
+            NO_PREDECESSOR,
+            SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId1,
+            0,
+            target,
+            amount,
+            payload1,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload1,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId2,
+            0,
+            target,
+            amount,
+            payload2,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload2,
+            NO_PREDECESSOR,
+            SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY + 2 days);
+        vm.expectRevert("CallReceiverMock: reverting");
+        vm.startPrank(EXECUTOR_ONE);
+        timelockController.execute(
+            target,
+            amount,
+            payload1,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        vm.expectRevert("TimelockController: underlying transaction reverted");
+        timelockController.execute(
+            target,
+            amount,
+            payload2,
+            NO_PREDECESSOR,
+            SALT
+        );
+        vm.stopPrank();
+    }
+
+    function testOperationPredecessorMultipleNotExecuted() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunction.selector
+        );
+        vm.startPrank(PROPOSER_TWO);
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            operationId,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY + 2 days);
+        vm.expectRevert("TimelockController: missing dependency");
+        vm.prank(EXECUTOR_ONE);
+        timelockController.execute(
+            target,
+            amount,
+            payload,
+            operationId,
+            EMPTY_SALT
+        );
+    }
+
+    function testOperationCancelFinished() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY + 1);
+        vm.startPrank(EXECUTOR_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallExecuted(
+            operationId,
+            0,
+            target,
+            amount,
+            payload
+        );
+        timelockController.execute(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        vm.stopPrank();
+
+        vm.prank(PROPOSER_ONE);
+        vm.expectRevert("TimelockController: operation cannot be cancelled");
+        timelockController.cancel(operationId);
+    }
+
+    function testOperationPendingIfNotYetExecuted() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        assertTrue(timelockController.is_operation_pending(operationId));
+        vm.stopPrank();
+    }
+
+    function testOperationPendingIfExecuted() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY);
+        vm.startPrank(EXECUTOR_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallExecuted(
+            operationId,
+            0,
+            target,
+            amount,
+            payload
+        );
+        timelockController.execute(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertTrue(!timelockController.is_operation_pending(operationId));
+        vm.stopPrank();
+    }
+
+    function testOperationReadyOnTheExecutionTime() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.warp(block.timestamp + MIN_DELAY);
+        assertTrue(timelockController.is_operation_ready(operationId));
+        vm.stopPrank();
+    }
+
+    function testOperationReadyAfterTheExecutionTime() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.warp(block.timestamp + MIN_DELAY + 1 days);
+        assertTrue(timelockController.is_operation_ready(operationId));
+        vm.stopPrank();
+    }
+
+    function testOperationReadyBeforeTheExecutionTime() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.warp(block.timestamp + MIN_DELAY - 1 days);
+        assertTrue(!timelockController.is_operation_ready(operationId));
+        vm.stopPrank();
+    }
+
+    function testOperationHasBeenExecuted() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY);
+        vm.startPrank(EXECUTOR_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallExecuted(
+            operationId,
+            0,
+            target,
+            amount,
+            payload
+        );
+        timelockController.execute(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertTrue(!timelockController.is_operation_ready(operationId));
+        assertTrue(timelockController.is_operation_done(operationId));
+        vm.stopPrank();
+    }
+
+    function testOperationHasNotBeenExecuted() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        assertTrue(!timelockController.is_operation_done(operationId));
+        vm.stopPrank();
+    }
+
+    function testOperationTimestampHasNotBeenExecuted() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        assertEq(
+            timelockController.get_timestamp(operationId),
+            block.timestamp + MIN_DELAY
+        );
+        vm.stopPrank();
+    }
+
+    function testOperationTimestampHasBeenExecuted() public {
+        uint256 amount = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY);
+        vm.startPrank(EXECUTOR_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallExecuted(
+            operationId,
+            0,
+            target,
+            amount,
+            payload
+        );
+        timelockController.execute(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertEq(timelockController.get_timestamp(operationId), DONE_TIMESTAMP);
+        vm.stopPrank();
+    }
+
+    function testHashOperationBatch() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertEq(
+            batchedOperationId,
+            keccak256(
+                abi.encode(
+                    targets,
+                    amounts,
+                    payloads,
+                    NO_PREDECESSOR,
+                    EMPTY_SALT
+                )
+            )
+        );
+    }
+
+    function testBatchScheduleAndExecuteWithEmptySalt() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertEq(vm.load(target, slot), bytes32(uint256(0)));
+        assertTrue(!timelockController.is_operation(batchedOperationId));
+        assertEq(timelockController.get_operation_state(batchedOperationId), 1);
+        assertEq(timelockController.get_timestamp(batchedOperationId), 0);
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        assertTrue(timelockController.is_operation(batchedOperationId));
+        assertEq(
+            timelockController.get_timestamp(batchedOperationId),
+            block.timestamp + MIN_DELAY
+        );
+        vm.stopPrank();
+
+        assertEq(timelockController.get_operation_state(batchedOperationId), 2);
+        vm.warp(block.timestamp + MIN_DELAY);
+        assertEq(timelockController.get_operation_state(batchedOperationId), 4);
+        vm.startPrank(EXECUTOR_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallExecuted(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i]
+            );
+        }
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertEq(vm.load(target, slot), value);
+        assertTrue(timelockController.is_operation(batchedOperationId));
+        assertEq(
+            timelockController.get_timestamp(batchedOperationId),
+            DONE_TIMESTAMP
+        );
+        assertEq(timelockController.get_operation_state(batchedOperationId), 8);
+        vm.stopPrank();
+    }
+
+    function testBatchScheduleAndExecuteWithNonEmptySalt() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            SALT
+        );
+        assertEq(vm.load(target, slot), bytes32(uint256(0)));
+        assertTrue(!timelockController.is_operation(batchedOperationId));
+        assertEq(timelockController.get_operation_state(batchedOperationId), 1);
+        assertEq(timelockController.get_timestamp(batchedOperationId), 0);
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+            vm.expectEmit(true, false, false, true);
+            emit ITimelockController.CallSalt(batchedOperationId, SALT);
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            SALT,
+            MIN_DELAY
+        );
+        assertTrue(timelockController.is_operation(batchedOperationId));
+        assertEq(
+            timelockController.get_timestamp(batchedOperationId),
+            block.timestamp + MIN_DELAY
+        );
+        vm.stopPrank();
+
+        assertEq(timelockController.get_operation_state(batchedOperationId), 2);
+        vm.warp(block.timestamp + MIN_DELAY);
+        assertEq(timelockController.get_operation_state(batchedOperationId), 4);
+        vm.startPrank(EXECUTOR_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallExecuted(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i]
+            );
+        }
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            SALT
+        );
+        assertEq(vm.load(target, slot), value);
+        assertTrue(timelockController.is_operation(batchedOperationId));
+        assertEq(
+            timelockController.get_timestamp(batchedOperationId),
+            DONE_TIMESTAMP
+        );
+        assertEq(timelockController.get_operation_state(batchedOperationId), 8);
+        vm.stopPrank();
+    }
+
+    function testBatchOperationAlreadyScheduled() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.expectRevert("TimelockController: operation already scheduled");
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+    }
+
+    function testBatchInsufficientDelay() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        vm.expectRevert("TimelockController: insufficient delay");
+        vm.prank(PROPOSER_ONE);
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY - 1
+        );
+    }
+
+    function testBatchEqualAndGreaterMinimumDelay() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        assertTrue(timelockController.is_operation(batchedOperationId));
+        vm.stopPrank();
+
+        batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            SALT
+        );
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY + 1
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            SALT,
+            MIN_DELAY + 1
+        );
+        assertTrue(timelockController.is_operation(batchedOperationId));
+        vm.stopPrank();
+    }
+
+    function testBatchMinimumDelayUpdate() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        uint256 operationTimestampBefore = timelockController.get_timestamp(
+            batchedOperationId
+        );
+        vm.stopPrank();
+
+        vm.startPrank(address(timelockController));
+        timelockController.update_delay(MIN_DELAY + 31 days);
+        uint256 operationTimestampAfter = timelockController.get_timestamp(
+            batchedOperationId
+        );
+        assertEq(timelockController.get_minimum_delay(), MIN_DELAY + 31 days);
+        assertEq(operationTimestampAfter, operationTimestampBefore);
+        vm.stopPrank();
+    }
+
+    function testBatchOperationIsNotReady() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY - 2 days);
+        vm.expectRevert("TimelockController: operation is not ready");
+        vm.prank(EXECUTOR_ONE);
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+    }
+
+    function testBatchPredecessorNotExecuted() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId1 = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        bytes32 batchedOperationId2 = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            batchedOperationId1,
+            SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId1,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId2,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                batchedOperationId1,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            batchedOperationId1,
+            SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY + 2 days);
+        vm.expectRevert("TimelockController: missing dependency");
+        vm.prank(EXECUTOR_ONE);
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            batchedOperationId1,
+            SALT
+        );
+    }
+
+    function testBatchPredecessorNotScheduled() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId1 = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        bytes32 batchedOperationId2 = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            batchedOperationId1,
+            SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId2,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                batchedOperationId1,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            batchedOperationId1,
+            SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY + 2 days);
+        vm.expectRevert("TimelockController: missing dependency");
+        vm.prank(EXECUTOR_ONE);
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            batchedOperationId1,
+            SALT
+        );
+    }
+
+    function testBatchPredecessorInvalid() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 invalidPredecessor = keccak256("Invalid Predecessor");
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            invalidPredecessor,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                invalidPredecessor,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            invalidPredecessor,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY + 2 days);
+        vm.expectRevert("TimelockController: missing dependency");
+        vm.prank(EXECUTOR_ONE);
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            invalidPredecessor,
+            EMPTY_SALT
+        );
+    }
+
+    function testBatchTargetRevert() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes[] memory payloads1 = new bytes[](1);
+        payloads1[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionRevertsWithReason.selector
+        );
+        bytes[] memory payloads2 = new bytes[](1);
+        payloads2[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionRevertsWithEmptyReason.selector
+        );
+        bytes32 batchedOperationId1 = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads1,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        bytes32 batchedOperationId2 = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads2,
+            NO_PREDECESSOR,
+            SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId1,
+                i,
+                targets[i],
+                amounts[i],
+                payloads1[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads1,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId2,
+                i,
+                targets[i],
+                amounts[i],
+                payloads2[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads2,
+            NO_PREDECESSOR,
+            SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY + 2 days);
+        vm.expectRevert("CallReceiverMock: reverting");
+        vm.startPrank(EXECUTOR_ONE);
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads1,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        vm.expectRevert("TimelockController: underlying transaction reverted");
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads2,
+            NO_PREDECESSOR,
+            SALT
+        );
+        vm.stopPrank();
+    }
+
+    function testBatchPredecessorMultipleNotExecuted() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunction.selector
+        );
+        vm.startPrank(PROPOSER_ONE);
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            batchedOperationId,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY + 2 days);
+        vm.expectRevert("TimelockController: missing dependency");
+        vm.prank(EXECUTOR_ONE);
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            batchedOperationId,
+            EMPTY_SALT
+        );
+    }
+
+    function testBatchCancelFinished() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY + 1);
+        vm.startPrank(EXECUTOR_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallExecuted(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i]
+            );
+        }
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        vm.stopPrank();
+
+        vm.prank(PROPOSER_ONE);
+        vm.expectRevert("TimelockController: operation cannot be cancelled");
+        timelockController.cancel(batchedOperationId);
+    }
+
+    function testBatchPendingIfNotYetExecuted() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        assertTrue(timelockController.is_operation_pending(batchedOperationId));
+        vm.stopPrank();
+    }
+
+    function testBatchPendingIfExecuted() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY);
+        vm.startPrank(EXECUTOR_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallExecuted(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i]
+            );
+        }
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertTrue(
+            !timelockController.is_operation_pending(batchedOperationId)
+        );
+        vm.stopPrank();
+    }
+
+    function testBatchReadyOnTheExecutionTime() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.warp(block.timestamp + MIN_DELAY);
+        assertTrue(timelockController.is_operation_ready(batchedOperationId));
+        vm.stopPrank();
+    }
+
+    function testBatchReadyAfterTheExecutionTime() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.warp(block.timestamp + MIN_DELAY + 1 days);
+        assertTrue(timelockController.is_operation_ready(batchedOperationId));
+        vm.stopPrank();
+    }
+
+    function testBatchReadyBeforeTheExecutionTime() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.warp(block.timestamp + MIN_DELAY - 1 days);
+        assertTrue(!timelockController.is_operation_ready(batchedOperationId));
+        vm.stopPrank();
+    }
+
+    function testBatchHasBeenExecuted() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY);
+        vm.startPrank(EXECUTOR_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallExecuted(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i]
+            );
+        }
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertTrue(!timelockController.is_operation_ready(batchedOperationId));
+        assertTrue(timelockController.is_operation_done(batchedOperationId));
+        vm.stopPrank();
+    }
+
+    function testBatchHasNotBeenExecuted() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        assertTrue(!timelockController.is_operation_done(batchedOperationId));
+        vm.stopPrank();
+    }
+
+    function testBatchTimestampHasNotBeenExecuted() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        uint256 operationTimestamp = timelockController.get_timestamp(
+            batchedOperationId
+        );
+        assertEq(operationTimestamp, block.timestamp + MIN_DELAY);
+        vm.stopPrank();
+    }
+
+    function testBatchTimestampHasBeenExecuted() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY);
+        vm.startPrank(EXECUTOR_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallExecuted(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i]
+            );
+        }
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertEq(
+            timelockController.get_timestamp(batchedOperationId),
+            DONE_TIMESTAMP
+        );
+        vm.stopPrank();
+    }
+
+    function testReturnsLaterMinimumDelayForCalls() public {
+        uint256 newMinDelay = 31 days;
+        vm.startPrank(timelockControllerAddr);
+        vm.expectEmit(true, true, false, false);
+        emit ITimelockController.MinimumDelayChange(
+            timelockController.get_minimum_delay(),
+            newMinDelay
+        );
+        timelockController.update_delay(newMinDelay);
+        vm.stopPrank();
+        assertEq(timelockController.get_minimum_delay(), newMinDelay);
+    }
+
+    function testInvalidOperation() public {
+        assertTrue(!timelockController.is_operation(keccak256("Invalid")));
+    }
+
+    function testRevertWhenNotTimelock() public {
+        vm.expectRevert("TimelockController: caller must be timelock");
+        vm.prank(STRANGER);
+        timelockController.update_delay(3 days);
+    }
+
+    function testAdminCannotBatchSchedule() public {
+        address[] memory targets = new address[](0);
+        uint256[] memory amounts = new uint256[](0);
+        bytes[] memory payloads = new bytes[](0);
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(self);
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+    }
+
+    function testAdminCannotSchedule() public {
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(self);
+        timelockController.schedule(
+            zeroAddress,
+            0,
+            new bytes(0),
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+    }
+
+    function testAdminCannotBatchExecute() public {
+        address[] memory targets = new address[](0);
+        uint256[] memory amounts = new uint256[](0);
+        bytes[] memory payloads = new bytes[](0);
+
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(self);
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+    }
+
+    function testAdminCannotExecute() public {
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(self);
+        timelockController.execute(
+            zeroAddress,
+            0,
+            new bytes(0),
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+    }
+
+    function testAdminCannotCancel() public {
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(self);
+        timelockController.cancel(EMPTY_SALT);
+    }
+
+    function testProposerCanBatchSchedule() public {
+        address[] memory targets = new address[](0);
+        uint256[] memory amounts = new uint256[](0);
+        bytes[] memory payloads = new bytes[](0);
+        vm.startPrank(PROPOSER_ONE);
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.prank(PROPOSER_TWO);
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+    }
+
+    function testProposerCanSchedule() public {
+        vm.startPrank(PROPOSER_ONE);
+        timelockController.schedule(
+            zeroAddress,
+            0,
+            new bytes(0),
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.startPrank(PROPOSER_TWO);
+        timelockController.schedule(
+            zeroAddress,
+            0,
+            new bytes(0),
+            NO_PREDECESSOR,
+            SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+    }
+
+    function testProposerCannotBatchExecute() public {
+        address[] memory targets = new address[](0);
+        uint256[] memory amounts = new uint256[](0);
+        bytes[] memory payloads = new bytes[](0);
+
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(PROPOSER_ONE);
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(PROPOSER_TWO);
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            SALT
+        );
+    }
+
+    function testProposerCannotExecute() public {
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(PROPOSER_ONE);
+        timelockController.execute(
+            zeroAddress,
+            0,
+            new bytes(0),
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(PROPOSER_TWO);
+        timelockController.execute(
+            zeroAddress,
+            0,
+            new bytes(0),
+            NO_PREDECESSOR,
+            SALT
+        );
+    }
+
+    function testProposerCanCancel() public {
+        vm.expectRevert("TimelockController: operation cannot be cancelled");
+        vm.prank(PROPOSER_ONE);
+        timelockController.cancel(EMPTY_SALT);
+
+        vm.expectRevert("TimelockController: operation cannot be cancelled");
+        vm.prank(PROPOSER_TWO);
+        timelockController.cancel(EMPTY_SALT);
+    }
+
+    function testExecutorCannotBatchSchedule() public {
+        address[] memory targets = new address[](0);
+        uint256[] memory amounts = new uint256[](0);
+        bytes[] memory payloads = new bytes[](0);
+
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(EXECUTOR_ONE);
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(EXECUTOR_TWO);
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            SALT,
+            MIN_DELAY
+        );
+    }
+
+    function testExecutorCannotSchedule() public {
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(EXECUTOR_ONE);
+        timelockController.schedule(
+            zeroAddress,
+            0,
+            new bytes(0),
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(EXECUTOR_TWO);
+        timelockController.schedule(
+            zeroAddress,
+            0,
+            new bytes(0),
+            NO_PREDECESSOR,
+            SALT,
+            MIN_DELAY
+        );
+    }
+
+    function testExecutorCanBatchExecute() public {
+        address[] memory targets = new address[](0);
+        uint256[] memory amounts = new uint256[](0);
+        bytes[] memory payloads = new bytes[](0);
+
+        vm.expectRevert("TimelockController: operation is not ready");
+        vm.prank(EXECUTOR_ONE);
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.expectRevert("TimelockController: operation is not ready");
+        vm.prank(EXECUTOR_TWO);
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            SALT
+        );
+    }
+
+    function testExecutorCanExecute() public {
+        vm.expectRevert("TimelockController: operation is not ready");
+        vm.prank(EXECUTOR_ONE);
+        timelockController.execute(
+            zeroAddress,
+            0,
+            new bytes(0),
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.expectRevert("TimelockController: operation is not ready");
+        vm.prank(EXECUTOR_TWO);
+        timelockController.execute(
+            zeroAddress,
+            0,
+            new bytes(0),
+            NO_PREDECESSOR,
+            SALT
+        );
+    }
+
+    function testExecutorCannotCancel() public {
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(EXECUTOR_ONE);
+        timelockController.cancel(EMPTY_SALT);
+
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(EXECUTOR_TWO);
+        timelockController.cancel(EMPTY_SALT);
+    }
+
+    function testStrangerCannotBatchSchedule() public {
+        address[] memory targets = new address[](0);
+        uint256[] memory amounts = new uint256[](0);
+        bytes[] memory payloads = new bytes[](0);
+
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(STRANGER);
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+    }
+
+    function testStrangerCannotSchedule() public {
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(STRANGER);
+        timelockController.schedule(
+            zeroAddress,
+            0,
+            new bytes(0),
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+    }
+
+    function testStrangerCannotBatchExecute() public {
+        address[] memory targets = new address[](0);
+        uint256[] memory amounts = new uint256[](0);
+        bytes[] memory payloads = new bytes[](0);
+
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(STRANGER);
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+    }
+
+    function testStrangerCannotExecute() public {
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(STRANGER);
+        timelockController.execute(
+            zeroAddress,
+            0,
+            new bytes(0),
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+    }
+
+    function testStrangerCannotCancel() public {
+        vm.expectRevert("AccessControl: account is missing role");
+        vm.prank(STRANGER);
+        timelockController.cancel(EMPTY_SALT);
+    }
+
+    function testCancellerCanCancelOperation() public {
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit();
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.expectEmit(true, false, false, false);
+        emit ITimelockController.Cancelled(batchedOperationId);
+        timelockController.cancel(batchedOperationId);
+        assertTrue(!timelockController.is_operation(batchedOperationId));
+        vm.stopPrank();
+    }
+
+    function testCompletePipelineOperationSetRoleAdmin() public {
+        uint256 amount = 0;
+        bytes memory payload = abi.encodeWithSelector(
+            timelockController.set_role_admin.selector,
+            EXECUTOR_ROLE,
+            PROPOSER_ROLE
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            timelockControllerAddr,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            timelockControllerAddr,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            timelockControllerAddr,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY);
+        vm.startPrank(EXECUTOR_ONE);
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleAdminChanged(
+            EXECUTOR_ROLE,
+            DEFAULT_ADMIN_ROLE,
+            PROPOSER_ROLE
+        );
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallExecuted(
+            operationId,
+            0,
+            timelockControllerAddr,
+            amount,
+            payload
+        );
+        timelockController.execute(
+            timelockControllerAddr,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertEq(timelockController.getRoleAdmin(EXECUTOR_ROLE), PROPOSER_ROLE);
+        assertTrue(
+            timelockController.getRoleAdmin(EXECUTOR_ROLE) != DEFAULT_ADMIN_ROLE
+        );
+        vm.stopPrank();
+    }
+
+    function testCompleteOperationWithAssignExecutorRoleToZeroAddress() public {
+        vm.startPrank(timelockControllerAddr);
+        timelockController.grantRole(EXECUTOR_ROLE, zeroAddress);
+        vm.stopPrank();
+
+        uint256 amount = 0;
+        bytes memory payload = abi.encodeWithSelector(
+            timelockController.set_role_admin.selector,
+            EXECUTOR_ROLE,
+            PROPOSER_ROLE
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            timelockControllerAddr,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            timelockControllerAddr,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            timelockControllerAddr,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY);
+        vm.startPrank(STRANGER);
+        vm.expectEmit(true, true, true, false);
+        emit IAccessControl.RoleAdminChanged(
+            EXECUTOR_ROLE,
+            DEFAULT_ADMIN_ROLE,
+            PROPOSER_ROLE
+        );
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallExecuted(
+            operationId,
+            0,
+            timelockControllerAddr,
+            amount,
+            payload
+        );
+        timelockController.execute(
+            timelockControllerAddr,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertEq(timelockController.getRoleAdmin(EXECUTOR_ROLE), PROPOSER_ROLE);
+        assertTrue(
+            timelockController.getRoleAdmin(EXECUTOR_ROLE) != DEFAULT_ADMIN_ROLE
+        );
+        vm.stopPrank();
+    }
+
+    function testHandleERC721() public {
+        bytes memory args = abi.encode(
+            "MyNFT",
+            "WAGMI",
+            "https://www.wagmi.xyz/",
+            "MyNFT",
+            "1"
+        );
+        erc721Mock = IERC721Extended(
+            vyperDeployer.deployContract("src/tokens/", "ERC721", args)
+        );
+        vm.startPrank(deployer);
+        erc721Mock.safe_mint(timelockControllerAddr, "my_awesome_nft_uri_1");
+        assertEq(erc721Mock.balanceOf(timelockControllerAddr), 1);
+        vm.stopPrank();
+
+        address[] memory targets = new address[](1);
+        targets[0] = address(erc721Mock);
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(erc721Mock.burn.selector, 0);
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY);
+        vm.startPrank(EXECUTOR_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallExecuted(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i]
+            );
+        }
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertEq(
+            timelockController.get_timestamp(batchedOperationId),
+            DONE_TIMESTAMP
+        );
+        assertEq(erc721Mock.balanceOf(timelockControllerAddr), 0);
+        vm.stopPrank();
+    }
+
+    function testHandleERC1155() public {
+        bytes memory args = abi.encode("https://www.wagmi.xyz/");
+        erc1155Mock = IERC1155Extended(
+            vyperDeployer.deployContract("src/tokens/", "ERC1155", args)
+        );
+        uint256[] memory ids = new uint256[](2);
+        uint256[] memory tokenAmounts = new uint256[](2);
+        ids[0] = 1;
+        ids[1] = 2;
+        tokenAmounts[0] = 1;
+        tokenAmounts[1] = 2;
+
+        vm.startPrank(deployer);
+        erc1155Mock.safe_mint(timelockControllerAddr, 0, 1, new bytes(0));
+        erc1155Mock.safe_mint_batch(
+            timelockControllerAddr,
+            ids,
+            tokenAmounts,
+            new bytes(0)
+        );
+        assertEq(erc1155Mock.balanceOf(timelockControllerAddr, 0), 1);
+        assertEq(erc1155Mock.balanceOf(timelockControllerAddr, 1), 1);
+        assertEq(erc1155Mock.balanceOf(timelockControllerAddr, 2), 2);
+        vm.stopPrank();
+
+        address[] memory targets = new address[](1);
+        targets[0] = address(erc1155Mock);
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = 0;
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            erc1155Mock.burn.selector,
+            timelockControllerAddr,
+            0,
+            1
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY);
+        vm.startPrank(EXECUTOR_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallExecuted(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i]
+            );
+        }
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertEq(
+            timelockController.get_timestamp(batchedOperationId),
+            DONE_TIMESTAMP
+        );
+        assertEq(erc1155Mock.balanceOf(timelockControllerAddr, 0), 0);
+        vm.stopPrank();
+    }
+
+    function testFuzzHashOperation(
+        address target_,
+        uint256 amount,
+        bytes memory payload,
+        bytes32 predecessor,
+        bytes32 salt
+    ) public {
+        bytes32 operationId = timelockController.hash_operation(
+            target_,
+            amount,
+            payload,
+            predecessor,
+            salt
+        );
+        assertEq(
+            operationId,
+            keccak256(abi.encode(target_, amount, payload, predecessor, salt))
+        );
+    }
+
+    function testFuzzOperationValue(uint256 amount) public {
+        amount = bound(amount, 0, type(uint64).max);
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes memory payload = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 operationId = timelockController.hash_operation(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        deal(timelockControllerAddr, amount);
+
+        vm.startPrank(PROPOSER_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallScheduled(
+            operationId,
+            0,
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            MIN_DELAY
+        );
+        timelockController.schedule(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY);
+        vm.startPrank(EXECUTOR_ONE);
+        vm.expectEmit(true, true, false, true);
+        emit ITimelockController.CallExecuted(
+            operationId,
+            0,
+            target,
+            amount,
+            payload
+        );
+        timelockController.execute(
+            target,
+            amount,
+            payload,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertEq(timelockController.get_timestamp(operationId), DONE_TIMESTAMP);
+        assertEq(address(callReceiverMock).balance, amount);
+        vm.stopPrank();
+    }
+
+    function testFuzzHashOperationBatch(
+        address[] memory targets_,
+        uint256[] memory amounts,
+        bytes[] memory payloads,
+        bytes32 predecessor,
+        bytes32 salt
+    ) public {
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets_,
+            amounts,
+            payloads,
+            predecessor,
+            salt
+        );
+        assertEq(
+            batchedOperationId,
+            keccak256(
+                abi.encode(targets_, amounts, payloads, predecessor, salt)
+            )
+        );
+    }
+
+    function testFuzzBatchValue(uint256 amount) public {
+        amount = bound(amount, 0, type(uint64).max);
+        address[] memory targets = new address[](1);
+        targets[0] = target;
+        uint256[] memory amounts = new uint256[](1);
+        amounts[0] = amount;
+        bytes32 slot = bytes32(uint256(1337));
+        bytes32 value = bytes32(uint256(6699));
+        bytes[] memory payloads = new bytes[](1);
+        payloads[0] = abi.encodeWithSelector(
+            callReceiverMock.mockFunctionWritesStorage.selector,
+            slot,
+            value
+        );
+        bytes32 batchedOperationId = timelockController.hash_operation_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        deal(timelockControllerAddr, amount);
+
+        vm.startPrank(PROPOSER_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallScheduled(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i],
+                NO_PREDECESSOR,
+                MIN_DELAY
+            );
+        }
+        timelockController.schedule_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT,
+            MIN_DELAY
+        );
+        vm.stopPrank();
+
+        vm.warp(block.timestamp + MIN_DELAY);
+        vm.startPrank(EXECUTOR_ONE);
+        for (uint256 i = 0; i < targets.length; ++i) {
+            vm.expectEmit(true, true, false, true);
+            emit ITimelockController.CallExecuted(
+                batchedOperationId,
+                i,
+                targets[i],
+                amounts[i],
+                payloads[i]
+            );
+        }
+        timelockController.execute_batch(
+            targets,
+            amounts,
+            payloads,
+            NO_PREDECESSOR,
+            EMPTY_SALT
+        );
+        assertEq(
+            timelockController.get_timestamp(batchedOperationId),
+            DONE_TIMESTAMP
+        );
+        assertEq(address(callReceiverMock).balance, amounts[0]);
+        vm.stopPrank();
+    }
+}
+
+contract TimelockControllerInvariants is Test {
+    VyperDeployer private vyperDeployer = new VyperDeployer();
+
+    ITimelockController private timelockController;
+    TimelockControllerHandler private timelockControllerHandler;
+
+    address private self = address(this);
+    address private timelockControllerAddr;
+
+    function setUp() public {
+        address[] memory proposers = new address[](1);
+        proposers[0] = self;
+        address[] memory executors = new address[](1);
+        executors[0] = self;
+        uint256 minDelay = 2 days;
+        bytes memory args = abi.encode(minDelay, proposers, executors, self);
+        timelockController = ITimelockController(
+            vyperDeployer.deployContract(
+                "src/governance/",
+                "TimelockController",
+                args
+            )
+        );
+        timelockControllerHandler = new TimelockControllerHandler(
+            timelockController,
+            minDelay,
+            proposers,
+            executors,
+            self
+        );
+        timelockControllerAddr = address(timelockControllerHandler);
+
+        bytes4[] memory selectors = new bytes4[](3);
+        selectors[0] = TimelockControllerHandler.schedule.selector;
+        selectors[1] = TimelockControllerHandler.execute.selector;
+        selectors[2] = TimelockControllerHandler.cancel.selector;
+        targetSelector(
+            FuzzSelector({addr: timelockControllerAddr, selectors: selectors})
+        );
+        targetContract(timelockControllerAddr);
+    }
+
+    /**
+     * @dev The number of scheduled transactions cannot exceed the number of
+     * executed transactions.
+     */
+    function invariantExecutedLessThanOrEqualToScheduled() public {
+        assertTrue(
+            timelockControllerHandler.executeCount() <=
+                timelockControllerHandler.scheduleCount()
+        );
+    }
+
+    /**
+     * @dev The number of proposals executed must match the count number.
+     */
+    function invariantProposalsExecutedMatchCount() public {
+        assertEq(
+            timelockControllerHandler.executeCount(),
+            timelockControllerHandler.counter()
+        );
+    }
+
+    /**
+     * @dev Proposals can only be scheduled and executed once.
+     */
+    function invariantOnceProposalExecution() public {
+        uint256[] memory executed = timelockControllerHandler.getExecuted();
+        for (uint256 i = 0; i < executed.length; ++i) {
+            // Ensure that the executed proposal cannot be executed again.
+            vm.expectRevert("TimelockController: operation is not ready");
+            timelockController.execute(
+                timelockControllerAddr,
+                0,
+                abi.encodeWithSelector(
+                    TimelockControllerHandler.increment.selector
+                ),
+                bytes32(""),
+                bytes32(executed[i])
+            );
+        }
+    }
+
+    /**
+     * @dev The sum of the executed proposals and the cancelled proposals must
+     * be less than or equal to the number of scheduled proposals.
+     */
+    function invariantSumOfProposals() public {
+        assertTrue(
+            (timelockControllerHandler.cancelCount() +
+                timelockControllerHandler.executeCount()) <=
+                timelockControllerHandler.scheduleCount()
+        );
+    }
+
+    /**
+     * @dev The executed proposals cannot be cancelled.
+     */
+    function invariantExecutedProposalCancellation() public {
+        uint256[] memory executed = timelockControllerHandler.getExecuted();
+        for (uint256 i = 0; i < executed.length; ++i) {
+            // Ensure that the executed proposal cannot be cancelled.
+            vm.expectRevert(
+                "TimelockController: operation cannot be cancelled"
+            );
+            timelockController.cancel(bytes32(executed[i]));
+        }
+    }
+
+    /**
+     * @dev The execution of a proposal that has been cancelled is not possible.
+     */
+    function invariantExecutingCancelledProposal() public {
+        uint256[] memory cancelled = timelockControllerHandler.getCancelled();
+        for (uint256 i = 0; i < cancelled.length; ++i) {
+            // Ensure that the cancelled proposal cannot be executed.
+            vm.expectRevert("TimelockController: operation is not ready");
+            timelockController.execute(
+                address(timelockControllerHandler),
+                0,
+                abi.encodeWithSelector(
+                    TimelockControllerHandler.increment.selector
+                ),
+                bytes32(""),
+                bytes32(cancelled[i])
+            );
+        }
+    }
+
+    /**
+     * @dev The execution of a proposal that is not ready is not possible.
+     */
+    function invariantExecutingNotReadyProposal() public {
+        uint256[] memory pending = timelockControllerHandler.getPending();
+        for (uint256 i = 0; i < pending.length; ++i) {
+            // Ensure that the pending proposal cannot be executed.
+            vm.expectRevert("TimelockController: operation is not ready");
+            timelockController.execute(
+                address(timelockControllerHandler),
+                0,
+                abi.encodeWithSelector(
+                    TimelockControllerHandler.increment.selector
+                ),
+                bytes32(""),
+                bytes32(pending[i])
+            );
+        }
+    }
+}
+
+contract TimelockControllerHandler is Test {
+    uint256 public counter;
+    uint256 public scheduleCount;
+    uint256 public executeCount;
+    uint256 public cancelCount;
+
+    ITimelockController private timelockController;
+
+    address private self = address(this);
+    uint256 private minDelay;
+    address private admin;
+    address private proposer;
+    address private executor;
+    uint256[] private pending;
+    uint256[] private executed;
+    uint256[] private cancelled;
+
+    constructor(
+        ITimelockController timelockController_,
+        uint256 minDelay_,
+        address[] memory proposer_,
+        address[] memory executor_,
+        address admin_
+    ) {
+        timelockController = timelockController_;
+        minDelay = minDelay_;
+        proposer = proposer_[0];
+        executor = executor_[0];
+        admin = admin_;
+    }
+
+    function schedule(uint256 random) external {
+        vm.startPrank(proposer);
+        timelockController.schedule(
+            self,
+            0,
+            abi.encodeWithSelector(this.increment.selector),
+            bytes32(""),
+            bytes32(random),
+            minDelay
+        );
+        pending.push(random);
+        scheduleCount++;
+        vm.stopPrank();
+    }
+
+    function execute(uint256 random) external {
+        if (pending.length == 0 || scheduleCount == 0) {
+            return;
+        }
+
+        uint256 identifier = random % pending.length;
+        uint256 operation = pending[identifier];
+
+        // Advance the time to make the proposal ready.
+        vm.warp(block.timestamp + minDelay);
+        vm.startPrank(executor);
+        timelockController.execute(
+            self,
+            0,
+            abi.encodeWithSelector(this.increment.selector),
+            bytes32(""),
+            bytes32(operation)
+        );
+        delete pending[identifier];
+        executed.push(operation);
+        executeCount++;
+        vm.stopPrank();
+    }
+
+    function cancel(uint256 random) external {
+        if (pending.length == 0 || scheduleCount == 0) {
+            return;
+        }
+
+        uint256 identifier = random % pending.length;
+        uint256 operation = pending[identifier];
+
+        vm.startPrank(proposer);
+        timelockController.cancel(bytes32(operation));
+        delete pending[identifier];
+        cancelled.push(operation);
+        cancelCount++;
+        vm.stopPrank();
+    }
+
+    function getExecuted() external view returns (uint256[] memory) {
+        return executed;
+    }
+
+    function getCancelled() external view returns (uint256[] memory) {
+        return cancelled;
+    }
+
+    function getPending() external view returns (uint256[] memory) {
+        return pending;
+    }
+
+    function increment() external {
+        counter++;
+    }
+}
diff --git a/test/governance/interfaces/ITimelockController.sol b/test/governance/interfaces/ITimelockController.sol
new file mode 100644
index 00000000..da07566b
--- /dev/null
+++ b/test/governance/interfaces/ITimelockController.sol
@@ -0,0 +1,131 @@
+// SPDX-License-Identifier: WTFPL
+pragma solidity ^0.8.23;
+
+import {IERC721Receiver} from "openzeppelin/token/ERC721/IERC721Receiver.sol";
+import {IERC1155Receiver} from "openzeppelin/token/ERC1155/IERC1155Receiver.sol";
+import {IAccessControl} from "openzeppelin/access/IAccessControl.sol";
+
+interface ITimelockController is
+    IERC721Receiver,
+    IERC1155Receiver,
+    IAccessControl
+{
+    event CallScheduled(
+        bytes32 indexed id,
+        uint256 indexed index,
+        address target,
+        uint256 amount,
+        bytes payload,
+        bytes32 predecessor,
+        uint256 delay
+    );
+
+    event CallExecuted(
+        bytes32 indexed id,
+        uint256 indexed index,
+        address target,
+        uint256 amount,
+        bytes payload
+    );
+
+    event CallSalt(bytes32 indexed id, bytes32 salt);
+
+    event Cancelled(bytes32 indexed id);
+
+    event MinimumDelayChange(uint256 oldDuration, uint256 newDuration);
+
+    function DEFAULT_ADMIN_ROLE() external pure returns (bytes32);
+
+    function PROPOSER_ROLE() external pure returns (bytes32);
+
+    function EXECUTOR_ROLE() external pure returns (bytes32);
+
+    function CANCELLER_ROLE() external pure returns (bytes32);
+
+    function IERC721_TOKENRECEIVER_SELECTOR() external pure returns (bytes4);
+
+    function IERC1155_TOKENRECEIVER_SINGLE_SELECTOR()
+        external
+        pure
+        returns (bytes4);
+
+    function IERC1155_TOKENRECEIVER_BATCH_SELECTOR()
+        external
+        pure
+        returns (bytes4);
+
+    function get_timestamp(bytes32 id) external view returns (uint256);
+
+    function get_minimum_delay() external view returns (uint256);
+
+    function is_operation(bytes32 id) external view returns (bool);
+
+    function is_operation_pending(bytes32 id) external view returns (bool);
+
+    function is_operation_ready(bytes32 id) external view returns (bool);
+
+    function is_operation_done(bytes32 id) external view returns (bool);
+
+    /**
+     * @dev As Enums are handled differently in Vyper and Solidity (see https://twitter.com/pcaversaccio/status/1626514029094047747),
+     * we directly return the underlying Vyper type `uint256` (instead of `OperationState`)
+     * for Enums for ease of testing.
+     */
+    function get_operation_state(bytes32 id) external view returns (uint256);
+
+    function hash_operation(
+        address target,
+        uint256 amount,
+        bytes calldata payload,
+        bytes32 predecessor,
+        bytes32 salt
+    ) external pure returns (bytes32 hash);
+
+    function hash_operation_batch(
+        address[] calldata targets,
+        uint256[] calldata amounts,
+        bytes[] calldata payloads,
+        bytes32 predecessor,
+        bytes32 salt
+    ) external pure returns (bytes32 hash);
+
+    function schedule(
+        address target,
+        uint256 amount,
+        bytes calldata payload,
+        bytes32 predecessor,
+        bytes32 salt,
+        uint256 delay
+    ) external;
+
+    function schedule_batch(
+        address[] calldata targets,
+        uint256[] calldata amounts,
+        bytes[] calldata payloads,
+        bytes32 predecessor,
+        bytes32 salt,
+        uint256 delay
+    ) external;
+
+    function cancel(bytes32 id) external;
+
+    function execute(
+        address target,
+        uint256 amount,
+        bytes calldata payload,
+        bytes32 predecessor,
+        bytes32 salt
+    ) external payable;
+
+    function execute_batch(
+        address[] calldata targets,
+        uint256[] calldata amounts,
+        bytes[] calldata payloads,
+        bytes32 predecessor,
+        bytes32 salt
+    ) external payable;
+
+    function update_delay(uint256 newDelay) external;
+
+    function set_role_admin(bytes32 role, bytes32 adminRole) external;
+}
diff --git a/test/governance/mocks/CallReceiverMock.sol b/test/governance/mocks/CallReceiverMock.sol
new file mode 100644
index 00000000..c6be49e3
--- /dev/null
+++ b/test/governance/mocks/CallReceiverMock.sol
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.23;
+
+/**
+ * @title CallReceiverMock
+ * @author pcaversaccio
+ * @notice Forked and adjusted accordingly from here:
+ * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/mocks/CallReceiverMock.sol.
+ * @dev Allows to test receiving state-changing and/or static external calls.
+ */
+contract CallReceiverMock {
+    event MockFunctionCalled();
+    event MockFunctionCalledWithArgs(uint256 a, uint256 b);
+
+    string private _retValue = "0xba5ed";
+
+    /**
+     * @dev Emits `MockFunctionCalled` and returns the string `0xba5ed`
+     * after function invocation.
+     * @return string The return string `0xba5ed`.
+     */
+    function mockFunction() public payable returns (string memory) {
+        emit MockFunctionCalled();
+        return _retValue;
+    }
+
+    /**
+     * @dev Emits `MockFunctionCalled` after function invocation.
+     */
+    function mockFunctionEmptyReturn() public payable {
+        emit MockFunctionCalled();
+    }
+
+    /**
+     * @dev Emits `MockFunctionCalledWithArgs` and returns the string `0xba5ed`
+     * after function invocation.
+     * @param a The first 32-byte function argument.
+     * @param b The second 32-byte function argument.
+     * @return string The return string `0xba5ed`.
+     */
+    function mockFunctionWithArgs(
+        uint256 a,
+        uint256 b
+    ) public payable returns (string memory) {
+        emit MockFunctionCalledWithArgs(a, b);
+        return _retValue;
+    }
+
+    /**
+     * @dev Emits `MockFunctionCalled` and returns the string `0xba5ed`
+     * after function invocation.
+     * @notice `payable` function calls will revert.
+     * @return string The return string `0xba5ed`.
+     */
+    function mockFunctionNonPayable() public returns (string memory) {
+        emit MockFunctionCalled();
+        return _retValue;
+    }
+
+    /**
+     * @dev Returns the string `0xba5ed` after function invocation.
+     * @notice Special function to mock `STATICCALL` calls.
+     * @return string The return string `0xba5ed`.
+     */
+    function mockStaticFunction() public view returns (string memory) {
+        return _retValue;
+    }
+
+    /**
+     * @dev Reverts with an empty reason.
+     */
+    function mockFunctionRevertsWithEmptyReason() public payable {
+        // solhint-disable-next-line reason-string, custom-errors
+        revert();
+    }
+
+    /**
+     * @dev Reverts with a non-empty reason.
+     */
+    function mockFunctionRevertsWithReason() public payable {
+        // solhint-disable-next-line custom-errors
+        revert("CallReceiverMock: reverting");
+    }
+
+    /**
+     * @dev Returns the string `0xba5ed` after function invocation.
+     * @param slot The 32-byte key in storage.
+     * @param value The 32-byte value to store at `slot`.
+     * @notice Writes to storage slot `slot` with value `value`.
+     * @return string The return string `0xba5ed`.
+     */
+    function mockFunctionWritesStorage(
+        bytes32 slot,
+        bytes32 value
+    ) public payable returns (string memory) {
+        // solhint-disable-next-line no-inline-assembly
+        assembly {
+            sstore(slot, value)
+        }
+        return _retValue;
+    }
+}
diff --git a/test/tokens/ERC20.t.sol b/test/tokens/ERC20.t.sol
index 56e02b2d..0c3f3a21 100644
--- a/test/tokens/ERC20.t.sol
+++ b/test/tokens/ERC20.t.sol
@@ -575,7 +575,6 @@ contract ERC20Test is Test {
         address spender = makeAddr("spender");
         uint256 amount = 100;
         uint256 nonce = ERC20Extended.nonces(owner);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 domainSeparator = ERC20Extended.DOMAIN_SEPARATOR();
         (uint8 v, bytes32 r, bytes32 s) = vm.sign(
@@ -609,7 +608,6 @@ contract ERC20Test is Test {
         address spender = makeAddr("spender");
         uint256 amount = 100;
         uint256 nonce = ERC20Extended.nonces(owner);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 domainSeparator = ERC20Extended.DOMAIN_SEPARATOR();
         (uint8 v, bytes32 r, bytes32 s) = vm.sign(
@@ -643,7 +641,6 @@ contract ERC20Test is Test {
         address spender = makeAddr("spender");
         uint256 amount = 100;
         uint256 nonce = ERC20Extended.nonces(owner);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 domainSeparator = ERC20Extended.DOMAIN_SEPARATOR();
         (uint8 v, bytes32 r, bytes32 s) = vm.sign(
@@ -674,7 +671,6 @@ contract ERC20Test is Test {
         address spender = makeAddr("spender");
         uint256 amount = 100;
         uint256 nonce = ERC20Extended.nonces(owner);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 domainSeparator = keccak256(
             abi.encode(
@@ -713,7 +709,6 @@ contract ERC20Test is Test {
         address spender = makeAddr("spender");
         uint256 amount = 100;
         uint256 nonce = 1;
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 domainSeparator = ERC20Extended.DOMAIN_SEPARATOR();
         (uint8 v, bytes32 r, bytes32 s) = vm.sign(
@@ -744,7 +739,6 @@ contract ERC20Test is Test {
         address spender = makeAddr("spender");
         uint256 amount = 100;
         uint256 nonce = ERC20Extended.nonces(owner);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp - 1;
         bytes32 domainSeparator = ERC20Extended.DOMAIN_SEPARATOR();
         (uint8 v, bytes32 r, bytes32 s) = vm.sign(
@@ -1076,7 +1070,6 @@ contract ERC20Test is Test {
         address spenderAddr = makeAddr(spender);
         uint256 amount = block.number;
         uint256 nonce = ERC20Extended.nonces(ownerAddr);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + increment;
         bytes32 domainSeparator = ERC20Extended.DOMAIN_SEPARATOR();
         (uint8 v, bytes32 r, bytes32 s) = vm.sign(
@@ -1118,7 +1111,6 @@ contract ERC20Test is Test {
         address spenderAddr = makeAddr(spender);
         uint256 amount = block.number;
         uint256 nonce = ERC20Extended.nonces(ownerAddr);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + increment;
         bytes32 domainSeparator = ERC20Extended.DOMAIN_SEPARATOR();
         (uint8 v, bytes32 r, bytes32 s) = vm.sign(
diff --git a/test/tokens/ERC721.t.sol b/test/tokens/ERC721.t.sol
index b1e69816..e47f9fa2 100644
--- a/test/tokens/ERC721.t.sol
+++ b/test/tokens/ERC721.t.sol
@@ -1629,7 +1629,6 @@ contract ERC721Test is Test {
         vm.stopPrank();
 
         uint256 nonce = ERC721Extended.nonces(tokenId);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 domainSeparator = ERC721Extended.DOMAIN_SEPARATOR();
         (uint8 v, bytes32 r, bytes32 s) = vm.sign(
@@ -1667,7 +1666,6 @@ contract ERC721Test is Test {
         vm.stopPrank();
 
         uint256 nonce = ERC721Extended.nonces(tokenId);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 domainSeparator = ERC721Extended.DOMAIN_SEPARATOR();
         (uint8 v, bytes32 r, bytes32 s) = vm.sign(
@@ -1705,7 +1703,6 @@ contract ERC721Test is Test {
         vm.stopPrank();
 
         uint256 nonce = ERC721Extended.nonces(tokenId);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 domainSeparator = ERC721Extended.DOMAIN_SEPARATOR();
         (uint8 v, bytes32 r, bytes32 s) = vm.sign(
@@ -1740,7 +1737,6 @@ contract ERC721Test is Test {
         vm.stopPrank();
 
         uint256 nonce = ERC721Extended.nonces(tokenId);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 domainSeparator = keccak256(
             abi.encode(
@@ -1783,7 +1779,6 @@ contract ERC721Test is Test {
         vm.stopPrank();
 
         uint256 nonce = 1;
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 domainSeparator = ERC721Extended.DOMAIN_SEPARATOR();
         (uint8 v, bytes32 r, bytes32 s) = vm.sign(
@@ -1818,7 +1813,6 @@ contract ERC721Test is Test {
         vm.stopPrank();
 
         uint256 nonce = 1;
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp - 1;
         bytes32 domainSeparator = ERC721Extended.DOMAIN_SEPARATOR();
         (uint8 v, bytes32 r, bytes32 s) = vm.sign(
@@ -2353,7 +2347,6 @@ contract ERC721Test is Test {
         vm.stopPrank();
 
         uint256 nonce = ERC721Extended.nonces(tokenId);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + increment;
         bytes32 domainSeparator = ERC721Extended.DOMAIN_SEPARATOR();
         (uint8 v, bytes32 r, bytes32 s) = vm.sign(
@@ -2399,7 +2392,6 @@ contract ERC721Test is Test {
         vm.stopPrank();
 
         uint256 nonce = ERC721Extended.nonces(tokenId);
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + increment;
         bytes32 domainSeparator = ERC721Extended.DOMAIN_SEPARATOR();
         (uint8 v, bytes32 r, bytes32 s) = vm.sign(
diff --git a/test/tokens/mocks/ERC721ReceiverMock.sol b/test/tokens/mocks/ERC721ReceiverMock.sol
index 03b46c89..6e327c20 100644
--- a/test/tokens/mocks/ERC721ReceiverMock.sol
+++ b/test/tokens/mocks/ERC721ReceiverMock.sol
@@ -11,16 +11,14 @@ import {IERC721Receiver} from "openzeppelin/token/ERC721/IERC721Receiver.sol";
  * @dev Allows to test receiving ERC-721 tokens as a smart contract.
  */
 contract ERC721ReceiverMock is IERC721Receiver {
-    // solhint-disable-next-line var-name-mixedcase
-    bytes4 private immutable _RETVAL;
-
     enum Error {
         None,
         RevertWithMessage,
         RevertWithoutMessage,
         Panic
     }
-    // solhint-disable-next-line var-name-mixedcase
+
+    bytes4 private immutable _RETVAL;
     Error private immutable _ERROR;
 
     event Received(address operator, address from, uint256 tokenId, bytes data);
diff --git a/test/utils/EIP712DomainSeparator.t.sol b/test/utils/EIP712DomainSeparator.t.sol
index 08c98f48..b09f66ad 100644
--- a/test/utils/EIP712DomainSeparator.t.sol
+++ b/test/utils/EIP712DomainSeparator.t.sol
@@ -83,7 +83,6 @@ contract EIP712DomainSeparatorTest is Test {
         address spender = makeAddr("spender");
         uint256 value = 100;
         uint256 nonce = 1;
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + 100_000;
         bytes32 structHash = keccak256(
             abi.encode(
@@ -161,7 +160,6 @@ contract EIP712DomainSeparatorTest is Test {
         uint256 nonce,
         uint64 increment
     ) public {
-        // solhint-disable-next-line not-rely-on-time
         uint256 deadline = block.timestamp + increment;
         bytes32 structHash = keccak256(
             abi.encode(