From d32346fba0c361ee2b913c1027261fdc6e9728a0 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 7 Jun 2023 12:07:18 +0200 Subject: [PATCH] Initial commit --- .env.example | 3 + .gitignore | 20 +++++++ .gitmodules | 3 + README.md | 27 +++++++++ foundry.toml | 26 +++++++++ package.json | 20 +++++++ remappings.txt | 11 ++++ src/ExampleProposal.sol | 11 ++++ src/interfaces/IGnosisSafe.sol | 84 ++++++++++++++++++++++++++++ src/interfaces/IGovernance.sol | 56 +++++++++++++++++++ src/proprietary/TornadoAddresses.sol | 14 +++++ test/ExampleProposal.t.sol | 24 ++++++++ test/utils/Mock.sol | 18 ++++++ test/utils/MockProposal.sol | 36 ++++++++++++ test/utils/ProposalUtils.sol | 59 +++++++++++++++++++ test/utils/Utils.sol | 57 +++++++++++++++++++ 16 files changed, 469 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 README.md create mode 100644 foundry.toml create mode 100644 package.json create mode 100644 remappings.txt create mode 100644 src/ExampleProposal.sol create mode 100644 src/interfaces/IGnosisSafe.sol create mode 100644 src/interfaces/IGovernance.sol create mode 100644 src/proprietary/TornadoAddresses.sol create mode 100644 test/ExampleProposal.t.sol create mode 100644 test/utils/Mock.sol create mode 100644 test/utils/MockProposal.sol create mode 100644 test/utils/ProposalUtils.sol create mode 100644 test/utils/Utils.sol diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ecd9cc1 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +MAINNET_RPC_URL= +PRIVATE_KEY= +ETHERSCAN_KEY= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e72d065 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env + +# Node modules +node_modules/ + +# yarn +yarn.lock \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..888d42d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/README.md b/README.md new file mode 100644 index 0000000..76d531f --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Proposal title + +### Changes / effect description + +- 1 +- 2 +- ... + +### Requirements + +- Rust ([Need only for Windows](https://doc.rust-lang.org/cargo/getting-started/installation.html)) +- Foundryup ([Windows](https://github.com/altugbakan/foundryup-windows), [Linux](https://book.getfoundry.sh/getting-started/installation)) +- Node 14 or higher ([Windows](https://github.com/coreybutler/nvm-windows), [Linux](https://github.com/nvm-sh/nvm)) + +### Installation + +```text +git clone --recurse-submodules +cd +npm install +``` + +### Testing + +```text +npm run test +``` \ No newline at end of file diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..28a09c4 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,26 @@ +[profile.default] +# General +src = 'src' +out = 'out' +libs = ["node_modules", "lib"] + +# Compiler +auto_detect_solc = true +via_ir = true +optimizer = true +optimizer_runs = 200 +auto_detect_remappings = true + +# Network +chain_id = 1 + +# Tests +verbosity = 3 + +[rpc_endpoints] +mainnet = "${MAINNET_RPC_URL}" + +[fmt] +line_length = 140 +number_underscore = 'thousands' +bracket_spacing = true \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..b4e9b16 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "forge-proposal-template", + "version": "1.0.0", + "repository": "https://git.tornado.ws/Theo/forge-proposal-template", + "author": "Theo", + "license": "MIT", + "private": false, + "scripts": { + "test": "forge test" + }, + "dependencies": { + "@ensdomains/ens-contracts": "^0.0.21", + "@openzeppelin/contracts": "^4.9.0", + "@openzeppelin/upgrades-core": "^1.26.2" + }, + "optionalDependencies": { + "@gnosis.pm/ido-contracts": "^0.5.0", + "@gnosis.pm/safe-contracts": "1.3.0" + } +} diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..91d487a --- /dev/null +++ b/remappings.txt @@ -0,0 +1,11 @@ +@root/=src/ +@interfaces/=src/interfaces +@proprietary/=src/proprietary +@forge-std/=lib/forge-std/src/ + +@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ +@openzeppelin/upgrades/=node_modules/@openzeppelin/upgrades-core/ +@ens/contracts/=node_modules/@ensdomains/ens-contracts/contracts/ +@gnosis/contracts/=node_modules/@gnosis.pm/safe-contracts/contracts/ +@gnosis/ido-contracts/=node_modules/@gnosis.pm/ido-contracts/contracts/ +@torn-token/=node_modules/torn-token/ diff --git a/src/ExampleProposal.sol b/src/ExampleProposal.sol new file mode 100644 index 0000000..68fd33d --- /dev/null +++ b/src/ExampleProposal.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import { IGovernance } from "@interfaces/IGovernance.sol"; + +contract ExampleProposal { + function executeProposal() public { + /* ... */ + } +} diff --git a/src/interfaces/IGnosisSafe.sol b/src/interfaces/IGnosisSafe.sol new file mode 100644 index 0000000..5a96277 --- /dev/null +++ b/src/interfaces/IGnosisSafe.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +interface IGnosisSafe { + enum Operation { + Call, + DelegateCall + } + + function NAME() external view returns (string memory); + + function VERSION() external view returns (string memory); + + function nonce() external view returns (uint256); + + function domainSeparator() external view returns (bytes32); + + function signedMessages(bytes32) external view returns (uint256); + + function approvedHashes(address, bytes32) external view returns (uint256); + + function setup( + address[] calldata _owners, + uint256 _threshold, + address to, + bytes calldata data, + address fallbackHandler, + address paymentToken, + uint256 payment, + address payable paymentReceiver + ) external; + + function execTransaction( + address to, + uint256 value, + bytes calldata data, + Operation operation, + uint256 safeTxGas, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address payable refundReceiver, + bytes calldata signatures + ) external returns (bool success); + + function requiredTxGas(address to, uint256 value, bytes calldata data, Operation operation) + external + returns (uint256); + + function approveHash(bytes32 hashToApprove) external; + + function signMessage(bytes calldata _data) external; + + function isValidSignature(bytes calldata _data, bytes calldata _signature) external returns (bytes4); + + function getMessageHash(bytes memory message) external view returns (bytes32); + + function encodeTransactionData( + address to, + uint256 value, + bytes memory data, + Operation operation, + uint256 safeTxGas, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address refundReceiver, + uint256 _nonce + ) external view returns (bytes memory); + + function getTransactionHash( + address to, + uint256 value, + bytes memory data, + Operation operation, + uint256 safeTxGas, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address refundReceiver, + uint256 _nonce + ) external view returns (bytes32); +} diff --git a/src/interfaces/IGovernance.sol b/src/interfaces/IGovernance.sol new file mode 100644 index 0000000..8d86702 --- /dev/null +++ b/src/interfaces/IGovernance.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +enum ProposalState { + Pending, + Active, + Defeated, + Timelocked, + AwaitingExecution, + Executed, + Expired +} + +struct Proposal { + // Creator of the proposal + address proposer; + // target addresses for the call to be made + address target; + // The block at which voting begins + uint256 startTime; + // The block at which voting ends: votes must be cast prior to this block + uint256 endTime; + // Current number of votes in favor of this proposal + uint256 forVotes; + // Current number of votes in opposition to this proposal + uint256 againstVotes; + // Flag marking whether the proposal has been executed + bool executed; + // Flag marking whether the proposal voting time has been extended + // Voting time can be extended once, if the proposal outcome has changed during CLOSING_PERIOD + bool extended; +} + +interface IGovernance { + function initialized() external view returns (bool); + function initializing() external view returns (bool); + function EXECUTION_DELAY() external view returns (uint256); + function EXECUTION_EXPIRATION() external view returns (uint256); + function QUORUM_VOTES() external view returns (uint256); + function PROPOSAL_THRESHOLD() external view returns (uint256); + function VOTING_DELAY() external view returns (uint256); + function VOTING_PERIOD() external view returns (uint256); + function CLOSING_PERIOD() external view returns (uint256); + function VOTE_EXTEND_TIME() external view returns (uint256); + function torn() external view returns (address); + function proposals(uint256 index) external view returns (Proposal memory); + function proposalCount() external view returns (uint256); + function lockedBalance(address account) external view returns (uint256); + function propose(address target, string memory description) external returns (uint256); + function castVote(uint256 proposalId, bool support) external; + function lock(address owner, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; + function lockWithApproval(uint256 amount) external; + function execute(uint256 proposalId) external payable; + function state(uint256 proposalId) external view returns (ProposalState); +} diff --git a/src/proprietary/TornadoAddresses.sol b/src/proprietary/TornadoAddresses.sol new file mode 100644 index 0000000..7d9e98d --- /dev/null +++ b/src/proprietary/TornadoAddresses.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +contract TornadoAddresses { + address public constant ENSAddress = 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e; + address public constant tornTokenAddress = 0x77777FeDdddFfC19Ff86DB637967013e6C6A116C; + address public constant multisigAddress = 0xb04E030140b30C27bcdfaafFFA98C57d80eDa7B4; + address public constant governanceAddress = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce; + address public constant stakingAddress = 0x5B3f656C80E8ddb9ec01Dd9018815576E9238c29; + address public constant relayerRegistryAddress = 0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2; + address public constant feeManagerAddress = 0x5f6c97C6AD7bdd0AE7E0Dd4ca33A4ED3fDabD4D7; + address public constant governanceVaultAddress = 0x2F50508a8a3D323B91336FA3eA6ae50E55f32185; +} diff --git a/test/ExampleProposal.t.sol b/test/ExampleProposal.t.sol new file mode 100644 index 0000000..9efd7bc --- /dev/null +++ b/test/ExampleProposal.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { ProposalUtils } from "./utils/ProposalUtils.sol"; +import { ExampleProposal } from "@root/ExampleProposal.sol"; + +import { console2 } from "@forge-std/console2.sol"; + +contract TestExampleProposal is ProposalUtils { + modifier executeCurrentProposalBefore() { + createAndExecuteProposal(); + _; + } + + function createAndExecuteProposal() public { + address proposalAddress = address(new ExampleProposal()); /* your proposal initialization */ + + proposeAndExecute(proposalAddress); + } + + /* your tests */ + + function testProposal() public executeCurrentProposalBefore { } +} diff --git a/test/utils/Mock.sol b/test/utils/Mock.sol new file mode 100644 index 0000000..f3d0c00 --- /dev/null +++ b/test/utils/Mock.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { IGovernance } from "@interfaces/IGovernance.sol"; + +contract Mock { + // Developer address with 22 staked TORN + address public constant TEST_REAL_ADDRESS_WITH_BALANCE = 0x9Ff3C1Bea9ffB56a78824FE29f457F066257DD58; + address public constant TEST_RELAYER_ADDRESS = 0x30F96AEF199B399B722F8819c9b0723016CEAe6C; // moon-relayer.eth (just for testing) + + // Address and private key to test staking, Governance lock and rewards accruals + address public constant TEST_STAKER_ADDRESS = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + uint256 public constant TEST_STAKER_PRIVATE_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + + // Test account to create proposal and vote for it + address public constant TEST_ADDRESS_ONE = 0x118251976c65AFAf291f5255450ddb5b6A4d8B88; + uint256 public constant TEST_PRIVATE_KEY_ONE = 0x66ddbd7cbe4a566df405f6ded0b908c669f88cdb1656380c050e3a457bd21df0; +} diff --git a/test/utils/MockProposal.sol b/test/utils/MockProposal.sol new file mode 100644 index 0000000..66c06dd --- /dev/null +++ b/test/utils/MockProposal.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { IGovernance } from "@interfaces/IGovernance.sol"; +import { TornadoAddresses } from "@proprietary/TornadoAddresses.sol"; + +import { Test } from "@forge-std/Test.sol"; +import { console2 } from "@forge-std/console2.sol"; + +contract MockProposal is TornadoAddresses, Test { + uint256 public PROPOSAL_VOTING_DURATION; + uint256 public PROPOSAL_LOCKED_DURATION; + uint256 public PROPOSAL_DURATION; + uint256 public PROPOSAL_EXECUTION_MAX_DURATION; + uint256 public PROPOSAL_QOURUM_THRESHOLD; + string public constant PROPOSAL_DESCRIPTION = "{title:'Some proposal',description:''}"; + + function _fetchConfiguration() internal { + IGovernance governance = IGovernance(governanceAddress); + + PROPOSAL_LOCKED_DURATION = governance.EXECUTION_DELAY(); + PROPOSAL_EXECUTION_MAX_DURATION = governance.EXECUTION_EXPIRATION(); + PROPOSAL_QOURUM_THRESHOLD = governance.QUORUM_VOTES(); + PROPOSAL_VOTING_DURATION = governance.VOTING_DELAY(); + PROPOSAL_VOTING_DURATION = governance.VOTING_PERIOD(); + PROPOSAL_DURATION = PROPOSAL_VOTING_DURATION + PROPOSAL_LOCKED_DURATION; + } + + function setUp() public virtual { + // If fork block number unitialized, then set latest + if (block.number == 1) vm.createSelectFork(vm.rpcUrl("mainnet")); + else vm.createSelectFork(vm.rpcUrl("mainnet"), block.number); + + _fetchConfiguration(); + } +} diff --git a/test/utils/ProposalUtils.sol b/test/utils/ProposalUtils.sol new file mode 100644 index 0000000..9828915 --- /dev/null +++ b/test/utils/ProposalUtils.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { Utils } from "./Utils.sol"; +import { Proposal, IGovernance } from "@interfaces/IGovernance.sol"; + +contract ProposalUtils is Utils { + IGovernance internal governance = IGovernance(governanceAddress); + + function getProposalExecutableTime(uint256 proposalId) internal view returns (uint256) { + Proposal memory proposal = getProposal(proposalId); + return proposal.endTime + PROPOSAL_LOCKED_DURATION + 1 seconds; + } + + function getProposal(uint256 proposalId) internal view returns (Proposal memory) { + return governance.proposals(proposalId); + } + + function hasProposal(uint256 proposalId) internal view returns (bool) { + return governance.proposalCount() >= proposalId; + } + + function waitUntilExecutable(uint256 proposalId) internal { + uint256 proposalExecutableTime = getProposalExecutableTime(proposalId); + require(block.timestamp < proposalExecutableTime + PROPOSAL_EXECUTION_MAX_DURATION, "Too late to execute proposal"); + + vm.warp(proposalExecutableTime); + } + + function proposeAndVote(address proposalAddress) public returns (uint256) { + retrieveAndLockBalance(TEST_PRIVATE_KEY_ONE, TEST_ADDRESS_ONE, PROPOSAL_QOURUM_THRESHOLD + 1 ether); + + /* ----------PROPOSE------------ */ + vm.startPrank(TEST_ADDRESS_ONE); + + uint256 proposalId = governance.propose(proposalAddress, PROPOSAL_DESCRIPTION); + + /* ------------------------------ */ + + // TIME-TRAVEL + vm.warp(block.timestamp + 6 hours); + + /* ------------VOTE-------------- */ + + governance.castVote(proposalId, true); + + vm.stopPrank(); + /* ------------------------------ */ + + return proposalId; + } + + function proposeAndExecute(address proposalAddress) public { + uint256 proposalId = proposeAndVote(proposalAddress); + + waitUntilExecutable(proposalId); + governance.execute(proposalId); + } +} diff --git a/test/utils/Utils.sol b/test/utils/Utils.sol new file mode 100644 index 0000000..51b1acd --- /dev/null +++ b/test/utils/Utils.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import { Test } from "@forge-std/Test.sol"; + +import { TornadoAddresses } from "@proprietary/TornadoAddresses.sol"; +import { Mock } from "./Mock.sol"; +import { MockProposal } from "./MockProposal.sol"; +import { IGovernance } from "@interfaces/IGovernance.sol"; + +contract Utils is TornadoAddresses, Test, Mock, MockProposal { + address public constant VERIFIER_ADDRESS = tornTokenAddress; + + bytes32 public constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + bytes32 public constant EIP712_DOMAIN = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes("TornadoCash")), + keccak256(bytes("1")), + 1, + VERIFIER_ADDRESS + ) + ); + + uint16 public constant PERMIT_FUNC_SELECTOR = uint16(0x1901); + + function retrieveAndLockBalance(uint256 privateKey, address voter, uint256 amount) public { + uint256 lockTimestamp = block.timestamp + PROPOSAL_DURATION; + uint256 accountNonce = ERC20Permit(tornTokenAddress).nonces(voter); + + bytes32 messageHash = keccak256( + abi.encodePacked( + PERMIT_FUNC_SELECTOR, + EIP712_DOMAIN, + keccak256(abi.encode(PERMIT_TYPEHASH, voter, governanceAddress, amount, accountNonce, lockTimestamp)) + ) + ); + + /* ----------GOVERNANCE------- */ + vm.startPrank(governanceAddress); + IERC20(tornTokenAddress).transfer(voter, amount); + vm.stopPrank(); + /* ----------------------------*/ + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, messageHash); + + /* ----------VOTER------------ */ + vm.startPrank(voter); + IGovernance(governanceAddress).lock(voter, amount, lockTimestamp, v, r, s); + vm.stopPrank(); + /* ----------------------------*/ + } +}