commit 9d6c18fbdcdc52617c107572c28e8100b650bbe0 Author: Theo Date: Wed May 3 20:15:19 2023 +0300 Init diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7cc88f0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sol linguist-language=Solidity \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf0b572 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +out +cache \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e977ca2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std + branch = v1.5.5 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a0c6d4f --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +## Requirements + +- 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 + +``` +git clone --recurse-submodules https://git.tornado.ws/Theo/proposal-19-contract.git +``` + +## Testing + +``` +npm run test +``` + +## Contract + +https://etherscan.io/address/0xfd533220d9f030a166ca0e126202d17fa4818d89#code \ No newline at end of file diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..7b03414 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,8 @@ +[profile.default] +solc-version = "0.8.19" +src = 'src' +out = 'out' +libs = ["node_modules", "lib"] +chain_id = 1 +optimizer = true +optimizer-runs = 10_000_000 \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..73d44ec --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 73d44ec7d124e3831bc5f832267889ffb6f9bc3f diff --git a/package.json b/package.json new file mode 100644 index 0000000..fa3b98f --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "proposal-19-contract", + "version": "0.1.0", + "description": "Tornado proposal #19 smart contract code & tests", + "main": "index.js", + "directories": { + "lib": "lib", + "test": "test" + }, + "scripts": { + "test": "forge test -vvvvv --fork-url https://rpc.mevblocker.io --block-number 17179667", + "build": "forge build --optimize" + }, + "repository": { + "type": "git", + "url": "https://git.tornado.ws/Theo/proposal-19-contract" + }, + "author": "Theo", + "license": "MIT" +} diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..71409b8 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,6 @@ +@proprietary/=src/proprietary/ +@interfaces/=src/interfaces/ +@root/=src/ + +@forge-std/=lib/forge-std/src/ + diff --git a/src/Proposal.sol b/src/Proposal.sol new file mode 100644 index 0000000..76e062f --- /dev/null +++ b/src/Proposal.sol @@ -0,0 +1,38 @@ +pragma solidity 0.8.19; + +import "@interfaces/ISablier.sol"; +import "@interfaces/IERC20.sol"; + +contract Proposal { + function executeProposal() external { + uint256 FISCAL_QUARTER_DURATION = 91 days; + uint256 REMUNERATION_START_TS = block.timestamp; + uint256 REMUNERATION_AMOUNT = 4172 ether; + uint256 REMUNERATION_NORMALISED_AMOUNT = REMUNERATION_AMOUNT - + (REMUNERATION_AMOUNT % FISCAL_QUARTER_DURATION); + + uint256 SERVICES_COST_REIMBURSEMENT_AMOUNT = 834 ether; + + address _tokenAddress = 0x77777FeDdddFfC19Ff86DB637967013e6C6A116C; + address _sablierAddress = 0xCD18eAa163733Da39c232722cBC4E8940b1D8888; + address REMUNERATION_ADDRESS = 0x9Ff3C1Bea9ffB56a78824FE29f457F066257DD58; + + IERC20(_tokenAddress).transfer( + REMUNERATION_ADDRESS, + SERVICES_COST_REIMBURSEMENT_AMOUNT + ); + + IERC20(_tokenAddress).approve( + _sablierAddress, + REMUNERATION_NORMALISED_AMOUNT + ); + + ISablier(_sablierAddress).createStream( + REMUNERATION_ADDRESS, + REMUNERATION_NORMALISED_AMOUNT, + _tokenAddress, + REMUNERATION_START_TS, + REMUNERATION_START_TS + FISCAL_QUARTER_DURATION + ); + } +} diff --git a/src/interfaces/IERC20.sol b/src/interfaces/IERC20.sol new file mode 100644 index 0000000..84ccf0f --- /dev/null +++ b/src/interfaces/IERC20.sol @@ -0,0 +1,15 @@ +pragma solidity 0.8.19; + +interface IERC20 { + function transfer(address to, uint256 amount) external returns (bool); + + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); + + function balanceOf(address owner) external returns (uint256); + + function approve(address spender, uint256 amount) external; +} diff --git a/src/interfaces/IGovernance.sol b/src/interfaces/IGovernance.sol new file mode 100644 index 0000000..a14e52b --- /dev/null +++ b/src/interfaces/IGovernance.sol @@ -0,0 +1,21 @@ +pragma solidity 0.8.19; + +interface IGovernance { + function propose( + address target, + string memory description + ) external returns (uint256); + + function castVote(uint256 proposalId, bool support) external; + + function execute(uint256 proposalId) external payable; + + function lock( + address owner, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} diff --git a/src/interfaces/ISablier.sol b/src/interfaces/ISablier.sol new file mode 100644 index 0000000..962813e --- /dev/null +++ b/src/interfaces/ISablier.sol @@ -0,0 +1,11 @@ +pragma solidity 0.8.19; + +interface ISablier { + function createStream( + address recipent, + uint256 deposit, + address tokenAddress, + uint256 startTime, + uint256 stopTime + ) external returns (uint256); +} diff --git a/src/proprietary/Mock.sol b/src/proprietary/Mock.sol new file mode 100644 index 0000000..5b3c7b8 --- /dev/null +++ b/src/proprietary/Mock.sol @@ -0,0 +1,40 @@ +pragma solidity 0.8.19; + +contract Mock { + uint256 constant TEST_PRIVATE_KEY_ONE = + 0x66ddbd7cbe4a566df405f6ded0b908c669f88cdb1656380c050e3a457bd21df0; + uint256 constant TEST_PRIVATE_KEY_TWO = + 0xa4c8c98120e77741a87a116074a2df4ddb20d1149069290fd4a3d7ee65c55064; + address constant TEST_ADDRESS_ONE = + 0x118251976c65AFAf291f5255450ddb5b6A4d8B88; + address constant TEST_ADDRESS_TWO = + 0x63aE7d90Eb37ca39FC62dD9991DbEfeE70673a20; + + uint256 constant PROPOSAL_DURATION = 7 days; + uint256 constant PROPOSAL_THRESHOLD = 25000 ether; + string constant PROPOSAL_DESCRIPTION = + "{title:'Proposal #19: New developer and remuneration',description:''}"; + + address constant VERIFIER_ADDRESS = + 0x77777FeDdddFfC19Ff86DB637967013e6C6A116C; + + bytes32 constant PERMIT_TYPEHASH = + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + + bytes32 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 constant PERMIT_FUNC_SELECTOR = uint16(0x1901); +} diff --git a/src/proprietary/Parameters.sol b/src/proprietary/Parameters.sol new file mode 100644 index 0000000..97a57a0 --- /dev/null +++ b/src/proprietary/Parameters.sol @@ -0,0 +1,18 @@ +pragma solidity 0.8.19; + +contract Parameters { + uint256 FISCAL_QUARTER_DURATION = 91 days; + uint256 REMUNERATION_START_TS = block.timestamp; + uint256 REMUNERATION_AMOUNT = 4172 ether; + uint256 REMUNERATION_NORMALISED_AMOUNT = + REMUNERATION_AMOUNT - (REMUNERATION_AMOUNT % FISCAL_QUARTER_DURATION); + uint256 SERVICES_COST_REIMBURSEMENT_AMOUNT = 834 ether; + + address REMUNERATION_ADDRESS = 0x9Ff3C1Bea9ffB56a78824FE29f457F066257DD58; + + // Beneficary addresses + address public _governanceAddress = + 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce; + address public _tokenAddress = 0x77777FeDdddFfC19Ff86DB637967013e6C6A116C; + address public _sablierAddress = 0xCD18eAa163733Da39c232722cBC4E8940b1D8888; +} diff --git a/test/Proposal.t.sol b/test/Proposal.t.sol new file mode 100644 index 0000000..d678a31 --- /dev/null +++ b/test/Proposal.t.sol @@ -0,0 +1,117 @@ +pragma solidity ^0.8.1; + +import "@interfaces/IGovernance.sol"; +import "@interfaces/IERC20.sol"; + +import "@proprietary/Parameters.sol"; +import "@proprietary/Mock.sol"; + +import "@root/Proposal.sol"; +import "@forge-std/Test.sol"; + +contract ProposalTest is Test, Parameters, Mock { + modifier conditionStateChecks() { + checkParameters(); + _; + checkResults(); + } + + function testProposal() public conditionStateChecks { + uint256 proposalId = voteAndCreateProposal(address(new Proposal())); + + IGovernance(_governanceAddress).execute(proposalId); + } + + function voteAndCreateProposal( + address proposalAddress + ) public returns (uint256) { + retrieveAndLockBalance( + TEST_PRIVATE_KEY_ONE, + TEST_ADDRESS_ONE, + PROPOSAL_THRESHOLD + ); + retrieveAndLockBalance(TEST_PRIVATE_KEY_TWO, TEST_ADDRESS_TWO, 1 ether); + + /* ----------PROPOSER------------ */ + vm.startPrank(TEST_ADDRESS_ONE); + + uint256 proposalId = IGovernance(_governanceAddress).propose( + proposalAddress, + PROPOSAL_DESCRIPTION + ); + + // TIME-TRAVEL + vm.warp(block.timestamp + 6 hours); + + IGovernance(_governanceAddress).castVote(proposalId, true); + + vm.stopPrank(); + /* ------------------------------ */ + + /* -------------VOTER-------------*/ + vm.startPrank(TEST_ADDRESS_TWO); + IGovernance(_governanceAddress).castVote(proposalId, true); + vm.stopPrank(); + /* ------------------------------ */ + + // TIME-TRAVEL + vm.warp(block.timestamp + PROPOSAL_DURATION); + + return proposalId; + } + + function retrieveAndLockBalance( + uint256 privateKey, + address voter, + uint256 amount + ) internal { + uint256 lockTimestamp = block.timestamp + PROPOSAL_DURATION; + bytes32 messageHash = keccak256( + abi.encodePacked( + PERMIT_FUNC_SELECTOR, + EIP712_DOMAIN, + keccak256( + abi.encode( + PERMIT_TYPEHASH, + voter, + _governanceAddress, + amount, + 0, + lockTimestamp + ) + ) + ) + ); + + /* ----------GOVERNANCE------- */ + vm.startPrank(_governanceAddress); + IERC20(_tokenAddress).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(); + /* ----------------------------*/ + } + + function checkParameters() internal { + require(REMUNERATION_NORMALISED_AMOUNT > 4100 ether); + } + + function checkResults() internal { + require( + IERC20(_tokenAddress).balanceOf(REMUNERATION_ADDRESS) == 834 ether + ); + } +}