├── .envrc ├── .gitattributes ├── .gitignore ├── .gitmodules ├── Makefile ├── README.md ├── env ├── nix ├── sources.json └── sources.nix ├── remappings.txt ├── shell.nix └── src ├── Governor.t.sol ├── Registrar.t.sol ├── Utils.sol └── VestingToken.t.sol /.envrc: -------------------------------------------------------------------------------- 1 | # vim: set ft=sh: 2 | 3 | if has lorri; then 4 | eval "$(lorri direnv)" 5 | else 6 | use nix 7 | fi 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /out 2 | hevm.cache.*/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "radicle-contracts"] 2 | path = lib/radicle-contracts 3 | url = https://github.com/radicle-dev/radicle-contracts 4 | [submodule "ens"] 5 | path = lib/ens 6 | url = https://github.com/ensdomains/ens 7 | [submodule "openzeppelin-contracts"] 8 | path = lib/openzeppelin-contracts 9 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 10 | [submodule "uniswap-lib"] 11 | path = lib/uniswap-lib 12 | url = https://github.com/Uniswap/uniswap-lib/ 13 | [submodule "uniswap-v2-core"] 14 | path = lib/uniswap-v2-core 15 | url = https://github.com/Uniswap/uniswap-v2-core 16 | [submodule "lib/ds-test"] 17 | path = lib/ds-test 18 | url = https://github.com/dapphub/ds-test 19 | [submodule "lib/ds-math"] 20 | path = lib/ds-math 21 | url = https://github.com/dapphub/ds-math 22 | [submodule "lib/ethregistrar"] 23 | path = lib/ethregistrar 24 | url = https://github.com/ensdomains/ethregistrar 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all :; dapp build 2 | clean :; dapp clean 3 | test :; dapp test 4 | deploy :; dapp create RadicleContractsTests 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ```sh 2 | git clone git@github.com:dapp-org/radicle-contract-tests.git --recursive 3 | nix-shell 4 | dapp test --rpc-url 5 | ``` 6 | 7 | ### Deployment Checklist 8 | - [ ] balanceOf tokensHolder == rad.totalSupply 9 | - [ ] timelock.admin == address(governor) 10 | - [ ] ens == 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e 11 | -------------------------------------------------------------------------------- /env: -------------------------------------------------------------------------------- 1 | export DAPP_REMAPPINGS=$(cat remappings.txt) 2 | export DAPP_SOLC_VERSION=0.7.5 -------------------------------------------------------------------------------- /nix/sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "dapptools": { 3 | "branch": "hevm-sign", 4 | "description": "Dapp, Seth, Hevm, and more", 5 | "homepage": "https://dapp.tools", 6 | "owner": "dapphub", 7 | "repo": "dapptools", 8 | "rev": "25bf67c265bb7c20bdd35d4b10585283d4223c19", 9 | "sha256": "1r116c58zawnmld7xpf4k0aphz3k056bjhznh0v803mkhy4ky68n", 10 | "type": "tarball", 11 | "url": "https://github.com/dapphub/dapptools/archive/25bf67c265bb7c20bdd35d4b10585283d4223c19.tar.gz", 12 | "url_template": "https://github.com///archive/.tar.gz" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /nix/sources.nix: -------------------------------------------------------------------------------- 1 | # This file has been generated by Niv. 2 | 3 | let 4 | 5 | # 6 | # The fetchers. fetch_ fetches specs of type . 7 | # 8 | 9 | fetch_file = pkgs: name: spec: 10 | let 11 | name' = sanitizeName name + "-src"; 12 | in 13 | if spec.builtin or true then 14 | builtins_fetchurl { inherit (spec) url sha256; name = name'; } 15 | else 16 | pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; 17 | 18 | fetch_tarball = pkgs: name: spec: 19 | let 20 | name' = sanitizeName name + "-src"; 21 | in 22 | if spec.builtin or true then 23 | builtins_fetchTarball { name = name'; inherit (spec) url sha256; } 24 | else 25 | pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; 26 | 27 | fetch_git = name: spec: 28 | let 29 | ref = 30 | if spec ? ref then spec.ref else 31 | if spec ? branch then "refs/heads/${spec.branch}" else 32 | if spec ? tag then "refs/tags/${spec.tag}" else 33 | abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!"; 34 | in 35 | builtins.fetchGit { url = spec.repo; inherit (spec) rev; inherit ref; }; 36 | 37 | fetch_local = spec: spec.path; 38 | 39 | fetch_builtin-tarball = name: throw 40 | ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. 41 | $ niv modify ${name} -a type=tarball -a builtin=true''; 42 | 43 | fetch_builtin-url = name: throw 44 | ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. 45 | $ niv modify ${name} -a type=file -a builtin=true''; 46 | 47 | # 48 | # Various helpers 49 | # 50 | 51 | # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 52 | sanitizeName = name: 53 | ( 54 | concatMapStrings (s: if builtins.isList s then "-" else s) 55 | ( 56 | builtins.split "[^[:alnum:]+._?=-]+" 57 | ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) 58 | ) 59 | ); 60 | 61 | # The set of packages used when specs are fetched using non-builtins. 62 | mkPkgs = sources: system: 63 | let 64 | sourcesNixpkgs = 65 | import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; 66 | hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; 67 | hasThisAsNixpkgsPath = == ./.; 68 | in 69 | if builtins.hasAttr "nixpkgs" sources 70 | then sourcesNixpkgs 71 | else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then 72 | import {} 73 | else 74 | abort 75 | '' 76 | Please specify either (through -I or NIX_PATH=nixpkgs=...) or 77 | add a package called "nixpkgs" to your sources.json. 78 | ''; 79 | 80 | # The actual fetching function. 81 | fetch = pkgs: name: spec: 82 | 83 | if ! builtins.hasAttr "type" spec then 84 | abort "ERROR: niv spec ${name} does not have a 'type' attribute" 85 | else if spec.type == "file" then fetch_file pkgs name spec 86 | else if spec.type == "tarball" then fetch_tarball pkgs name spec 87 | else if spec.type == "git" then fetch_git name spec 88 | else if spec.type == "local" then fetch_local spec 89 | else if spec.type == "builtin-tarball" then fetch_builtin-tarball name 90 | else if spec.type == "builtin-url" then fetch_builtin-url name 91 | else 92 | abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; 93 | 94 | # If the environment variable NIV_OVERRIDE_${name} is set, then use 95 | # the path directly as opposed to the fetched source. 96 | replace = name: drv: 97 | let 98 | saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name; 99 | ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; 100 | in 101 | if ersatz == "" then drv else ersatz; 102 | 103 | # Ports of functions for older nix versions 104 | 105 | # a Nix version of mapAttrs if the built-in doesn't exist 106 | mapAttrs = builtins.mapAttrs or ( 107 | f: set: with builtins; 108 | listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) 109 | ); 110 | 111 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 112 | range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1); 113 | 114 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 115 | stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); 116 | 117 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 118 | stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); 119 | concatMapStrings = f: list: concatStrings (map f list); 120 | concatStrings = builtins.concatStringsSep ""; 121 | 122 | # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 123 | optionalAttrs = cond: as: if cond then as else {}; 124 | 125 | # fetchTarball version that is compatible between all the versions of Nix 126 | builtins_fetchTarball = { url, name ? null, sha256 }@attrs: 127 | let 128 | inherit (builtins) lessThan nixVersion fetchTarball; 129 | in 130 | if lessThan nixVersion "1.12" then 131 | fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) 132 | else 133 | fetchTarball attrs; 134 | 135 | # fetchurl version that is compatible between all the versions of Nix 136 | builtins_fetchurl = { url, name ? null, sha256 }@attrs: 137 | let 138 | inherit (builtins) lessThan nixVersion fetchurl; 139 | in 140 | if lessThan nixVersion "1.12" then 141 | fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) 142 | else 143 | fetchurl attrs; 144 | 145 | # Create the final "sources" from the config 146 | mkSources = config: 147 | mapAttrs ( 148 | name: spec: 149 | if builtins.hasAttr "outPath" spec 150 | then abort 151 | "The values in sources.json should not have an 'outPath' attribute" 152 | else 153 | spec // { outPath = replace name (fetch config.pkgs name spec); } 154 | ) config.sources; 155 | 156 | # The "config" used by the fetchers 157 | mkConfig = 158 | { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null 159 | , sources ? if isNull sourcesFile then {} else builtins.fromJSON (builtins.readFile sourcesFile) 160 | , system ? builtins.currentSystem 161 | , pkgs ? mkPkgs sources system 162 | }: rec { 163 | # The sources, i.e. the attribute set of spec name to spec 164 | inherit sources; 165 | 166 | # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers 167 | inherit pkgs; 168 | }; 169 | 170 | in 171 | mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); } -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @ensdomains/=./lib/ 2 | @openzeppelin/=lib/openzeppelin-contracts/ 3 | @uniswap/v2-core=lib/uniswap-v2-core/ 4 | @uniswap/lib/=lib/uniswap-lib/ 5 | radicle-contracts/=lib/radicle-contracts/contracts/ 6 | ds-test/=lib/ds-test/src/ 7 | ds-test=lib/ds-test/src/index.sol 8 | ds-math/=lib/ds-math/src/ 9 | ds-math=lib/ds-math/src/index.sol 10 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | sources = import ./nix/sources.nix; 3 | pkgs = import sources.dapptools {}; 4 | in 5 | pkgs.mkShell { 6 | buildInputs = with pkgs; [ 7 | dapp 8 | seth 9 | hevm 10 | niv 11 | ethsign 12 | solc-static-versions.solc_0_7_5 13 | ]; 14 | DAPP_SOLC="solc-0.7.5"; 15 | DAPP_REMAPPINGS=pkgs.lib.strings.fileContents ./remappings.txt; 16 | DAPP_LINK_TEST_LIBRARIES=0; 17 | DAPP_BUILD_OPTIMIZE=1; 18 | } 19 | -------------------------------------------------------------------------------- /src/Governor.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.7.5; 2 | pragma abicoder v2; 3 | 4 | import {Phase0} from "radicle-contracts/deploy/phase0.sol"; 5 | import {RadicleToken} from "radicle-contracts/Governance/RadicleToken.sol"; 6 | import {Governor} from "radicle-contracts/Governance/Governor.sol"; 7 | import {Timelock} from "radicle-contracts/Governance/Timelock.sol"; 8 | 9 | import {ENS} from "@ensdomains/ens/contracts/ENS.sol"; 10 | import {ENSRegistry} from "@ensdomains/ens/contracts/ENSRegistry.sol"; 11 | import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 12 | 13 | import {DSTest} from "ds-test/test.sol"; 14 | import {Hevm, Utils} from "./Utils.sol"; 15 | 16 | contract RadUser { 17 | RadicleToken rad; 18 | Governor gov; 19 | Timelock timelock; 20 | constructor (RadicleToken rad_, Governor gov_, Timelock timelock_) { 21 | rad = rad_; 22 | gov = gov_; 23 | timelock = timelock_; 24 | } 25 | function delegate(address to) public { 26 | rad.delegate(to); 27 | } 28 | function transfer(address to, uint amt) public { 29 | rad.transfer(to, amt); 30 | } 31 | function burn(uint amt) public { 32 | rad.burnFrom(address(this), amt); 33 | } 34 | 35 | function propose(address target, string memory sig, bytes memory cd) public returns (uint) { 36 | address[] memory targets = new address[](1); 37 | uint[] memory values = new uint[](1); 38 | string[] memory sigs = new string[](1); 39 | bytes[] memory calldatas = new bytes[](1); 40 | targets[0] = target; 41 | values[0] = 0; 42 | sigs[0] = sig; 43 | calldatas[0] = cd; 44 | return gov.propose(targets, values, sigs, calldatas, ""); 45 | } 46 | function queue(uint proposalId) public { 47 | gov.queue(proposalId); 48 | } 49 | function castVote(uint proposalId, bool support) public { 50 | gov.castVote(proposalId, support); 51 | } 52 | 53 | function queueTimelock(address target, string memory sig, bytes memory cd, uint256 eta) public { 54 | timelock.queueTransaction(target, 0, sig, cd, eta); 55 | } 56 | function executeTimelock(address target, string memory sig, bytes memory cd, uint256 eta) public { 57 | timelock.executeTransaction(target, 0, sig, cd, eta); 58 | } 59 | 60 | function accept() public { 61 | timelock.acceptAdmin(); 62 | } 63 | } 64 | 65 | contract GovernanceTest is DSTest { 66 | Governor gov; 67 | RadicleToken rad; 68 | RadUser usr; 69 | RadUser ali; 70 | RadUser bob; 71 | RadUser cal; 72 | Timelock timelock; 73 | 74 | uint x; // only writeable by timelock 75 | 76 | Hevm hevm = Hevm(HEVM_ADDRESS); 77 | 78 | function setUp() public { 79 | Phase0 phase0 = new Phase0( address(this) 80 | , 2 days 81 | , address(0) 82 | , ENS(address(this)) 83 | , "namehash" 84 | , "label" 85 | ); 86 | 87 | rad = phase0.token(); 88 | timelock = phase0.timelock(); 89 | gov = phase0.governor(); 90 | 91 | usr = new RadUser(rad, gov, timelock); 92 | ali = new RadUser(rad, gov, timelock); 93 | bob = new RadUser(rad, gov, timelock); 94 | cal = new RadUser(rad, gov, timelock); 95 | // proposal threshold is 1% 96 | rad.transfer(address(ali), 500_000 ether); 97 | rad.transfer(address(bob), 500_001 ether); 98 | // quorum is 4% 99 | rad.transfer(address(cal), 5_000_000 ether); 100 | } 101 | 102 | function test_deploy() public { 103 | uint gas_before = gasleft(); 104 | Phase0 phase0 = new Phase0( address(this) 105 | , 2 days 106 | , address(0) 107 | , ENS(address(this)) 108 | , "namehash" 109 | , "label" 110 | ); 111 | uint gas_after = gasleft(); 112 | log_named_uint("deployment gas", gas_before - gas_after); 113 | } 114 | 115 | function test_radAddress() public { 116 | assertEq(address(rad), address(0x25E827B40a7D04de0D177BB228A99F69b83fA7FC)); 117 | } 118 | 119 | function test_domainSeparator() public { 120 | uint256 chainId; 121 | assembly { 122 | chainId := chainid() 123 | } 124 | bytes32 DOMAIN = rad.DOMAIN_SEPARATOR(); 125 | assertEq(DOMAIN, 126 | keccak256( 127 | abi.encode( 128 | keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"), 129 | keccak256(bytes(rad.NAME())), 130 | chainId, 131 | address(rad)))); 132 | log_named_bytes32("DOMAIN_SEPARATOR()", DOMAIN); 133 | } 134 | 135 | // generated with 136 | // export NONCE=0; export ETH_KEYSTORE=./secrets; export ETH_PASSWORD=./secrets/radical; export ETH_FROM=0xd521c744831cfa3ffe472d9f5f9398c9ac806203 137 | // ./bin/permit 0x25E827B40a7D04de0D177BB228A99F69b83fA7FC 100 -1 138 | function test_permit() public { 139 | address owner = 0xD521C744831cFa3ffe472d9F5F9398c9Ac806203; 140 | assertEq(rad.nonces(owner), 0); 141 | assertEq(rad.allowance(owner, address(rad)), 0); 142 | rad.permit(owner, address(rad), 100, uint(-1), 143 | 28, 144 | 0xb1b88cc9bdd69831879b406e560b29fc6938d556f8f7be5c580ce11cfd3d354e, 145 | 0x4ab00b718c09a9f9fb2dd35c2555659bb2b45509b402845bd37b8ef82eb97661); 146 | assertEq(rad.allowance(owner, address(rad)), 100); 147 | assertEq(rad.nonces(owner), 1); 148 | } 149 | 150 | function test_permit_typehash() public { 151 | assertEq(rad.PERMIT_TYPEHASH(), 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9); // seth keccak $(seth --from-ascii "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") 152 | } 153 | 154 | function testFail_permit_replay() public { 155 | address owner = 0xD521C744831cFa3ffe472d9F5F9398c9Ac806203; 156 | rad.permit(owner, address(rad), 100, uint(-1), 157 | 28, 158 | 0xb1b88cc9bdd69831879b406e560b29fc6938d556f8f7be5c580ce11cfd3d354e, 159 | 0x4ab00b718c09a9f9fb2dd35c2555659bb2b45509b402845bd37b8ef82eb97661); 160 | rad.permit(owner, address(rad), 100, uint(-1), 161 | 28, 162 | 0xb1b88cc9bdd69831879b406e560b29fc6938d556f8f7be5c580ce11cfd3d354e, 163 | 0x4ab00b718c09a9f9fb2dd35c2555659bb2b45509b402845bd37b8ef82eb97661); 164 | } 165 | 166 | function nextBlock() internal { 167 | hevm.roll(block.number + 1); 168 | } 169 | 170 | function set_x(uint _x) public { 171 | require(msg.sender == address(timelock)); 172 | x = _x; 173 | } 174 | 175 | function test_Delegate(uint96 a, uint96 b, uint96 c, address d, address e) public { 176 | if (a > 100000000 ether) return; 177 | if (uint(b) + uint(c) > uint(a)) return; 178 | if (d == address(0) || e == address(0)) return; 179 | rad.transfer(address(usr), a); 180 | usr.delegate(address(usr)); // delegating to self should be a noop 181 | usr.delegate(d); 182 | nextBlock(); 183 | assertEq(uint(rad.getCurrentVotes(address(d))), a); 184 | usr.transfer(e, b); 185 | nextBlock(); 186 | assertEq(uint(rad.getCurrentVotes(address(d))), a - b); 187 | usr.burn(c); 188 | nextBlock(); 189 | assertEq(uint(rad.getPriorVotes(address(d), block.number - 3)), a); 190 | assertEq(uint(rad.getPriorVotes(address(d), block.number - 2)), a - b); 191 | assertEq(uint(rad.getPriorVotes(address(d), block.number - 1)), a - b - c); 192 | assertEq(uint(rad.getCurrentVotes(address(d))), a - b - c); 193 | } 194 | 195 | function test_propose() public { 196 | uint proposals = gov.proposalCount(); 197 | ali.delegate(address(bob)); 198 | bob.delegate(address(bob)); 199 | nextBlock(); 200 | bob.propose(address(this), "set_x(uint256)", abi.encode(uint(1))); 201 | assertEq(gov.proposalCount(), proposals + 1); 202 | } 203 | 204 | // governance follows the flow: 205 | // - propose 206 | // - queue 207 | // - execute OR cancel 208 | function test_vote_to_execution() public { 209 | ali.delegate(address(bob)); 210 | bob.delegate(address(bob)); 211 | cal.delegate(address(cal)); 212 | nextBlock(); 213 | uint id = bob.propose(address(this), "set_x(uint256)", abi.encode(uint(1))); 214 | assertEq(uint(gov.state(id)), 0 , "proposal is pending"); 215 | 216 | // proposal is Pending until block.number + votingDelay + 1 217 | hevm.roll(block.number + gov.votingDelay() + 1); 218 | assertEq(uint(gov.state(id)), 1, "proposal is active"); 219 | 220 | // votes cast must have been checkpointed by delegation, and 221 | // exceed the quorum and votes against 222 | cal.castVote(id, true); 223 | hevm.roll(block.number + gov.votingPeriod()); 224 | assertEq(uint(gov.state(id)), 4, "proposal is successful"); 225 | 226 | // queueing succeeds unless already queued 227 | // (N.B. cannot queue multiple calls to same signature as-is) 228 | bob.queue(id); 229 | assertEq(uint(gov.state(id)), 5, "proposal is queued"); 230 | 231 | // can only execute following time delay 232 | assertEq(x, 0, "x is unmodified"); 233 | hevm.warp(block.timestamp + 2 days); 234 | gov.execute(id); 235 | assertEq(uint(gov.state(id)), 7, "proposal is executed"); 236 | assertEq(x, 1, "x is modified"); 237 | } 238 | 239 | function test_change_timelock_admin() public { 240 | ali.delegate(address(bob)); 241 | bob.delegate(address(bob)); 242 | cal.delegate(address(cal)); 243 | nextBlock(); 244 | 245 | // set timelock's pendingAdmin to bob 246 | uint id = bob.propose(address(timelock), "setPendingAdmin(address)", abi.encode(address(bob))); 247 | assertEq(uint(gov.state(id)), 0 , "proposal is pending"); 248 | 249 | // proposal is Pending until block.number + votingDelay + 1 250 | hevm.roll(block.number + gov.votingDelay() + 1); 251 | assertEq(uint(gov.state(id)), 1, "proposal is active"); 252 | 253 | // votes cast must have been checkpointed by delegation, and 254 | // exceed the quorum and votes against 255 | cal.castVote(id, true); 256 | hevm.roll(block.number + gov.votingPeriod()); 257 | assertEq(uint(gov.state(id)), 4, "proposal is successful"); 258 | 259 | // queueing succeeds unless already queued 260 | // (N.B. cannot queue multiple calls to same signature as-is) 261 | bob.queue(id); 262 | assertEq(uint(gov.state(id)), 5, "proposal is queued"); 263 | 264 | // can only execute following time delay 265 | hevm.warp(block.timestamp + 2 days); 266 | gov.execute(id); 267 | assertEq(uint(gov.state(id)), 7, "proposal is executed"); 268 | 269 | bob.accept(); 270 | assertEq(timelock.admin(), address(bob)); 271 | 272 | assertEq(x, 0, "x is unmodified"); 273 | uint eta = block.timestamp + timelock.delay(); 274 | bob.queueTimelock(address(this), "set_x(uint256)", abi.encode(uint(1)), eta); 275 | hevm.warp(block.timestamp + 2 days); 276 | bob.executeTimelock(address(this), "set_x(uint256)", abi.encode(uint(1)), eta); 277 | assertEq(x, 1, "x is modified"); 278 | } 279 | 280 | function test_transfer_rad_from_timelock() public { 281 | uint originalBobBalance = rad.balanceOf(address(bob)); 282 | uint transferAmount = 9 ether; 283 | rad.transfer(address(timelock), 10 ether); 284 | assertEq(rad.balanceOf(address(timelock)), 10 ether); 285 | 286 | ali.delegate(address(bob)); 287 | bob.delegate(address(bob)); 288 | cal.delegate(address(cal)); 289 | nextBlock(); 290 | 291 | uint id = bob.propose(address(rad), "transfer(address,uint256)", abi.encode(address(bob), uint(transferAmount))); 292 | assertEq(uint(gov.state(id)), 0 , "proposal is pending"); 293 | 294 | // proposal is Pending until block.number + votingDelay + 1 295 | hevm.roll(block.number + gov.votingDelay() + 1); 296 | assertEq(uint(gov.state(id)), 1, "proposal is active"); 297 | 298 | // votes cast must have been checkpointed by delegation, and 299 | // exceed the quorum and votes against 300 | cal.castVote(id, true); 301 | hevm.roll(block.number + gov.votingPeriod()); 302 | assertEq(uint(gov.state(id)), 4, "proposal is successful"); 303 | 304 | // queueing succeeds unless already queued 305 | // (N.B. cannot queue multiple calls to same signature as-is) 306 | bob.queue(id); 307 | assertEq(uint(gov.state(id)), 5, "proposal is queued"); 308 | 309 | hevm.warp(block.timestamp + 2 days); 310 | gov.execute(id); 311 | 312 | assertEq(rad.balanceOf(address(timelock)), uint(1 ether)); 313 | assertEq(rad.balanceOf(address(bob)), originalBobBalance + transferAmount); 314 | } 315 | 316 | /* function testAbiEncode() public { */ 317 | /* address[] memory targets = new address[](1); */ 318 | /* uint256[] memory values = new uint256[](1); */ 319 | /* string[] memory sigs = new string[](1); */ 320 | /* bytes[] memory datas = new bytes[](1); */ 321 | /* targets[0] = 0xFCBcd8C32305228F205c841c03f59D2491f92Cb4; */ 322 | /* values[0] = 0; */ 323 | /* sigs[0] = "setDomainOwner(address)"; */ 324 | /* datas[0] = abi.encode(address(0xEcEDFd8BA8ae39a6Bd346Fe9E5e0aBeA687fFF31)); */ 325 | /* bytes memory encoded = abi.encodeWithSignature("propose(address[],uint256[],string[],bytes[],string)", */ 326 | /* targets, */ 327 | /* values, */ 328 | /* sigs, */ 329 | /* datas, */ 330 | /* "This proposal migrates the radicle.eth domain and token to a new Registrar."); */ 331 | /* assertEq0(encoded, hex"da95691a00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000fcbcd8c32305228f205c841c03f59d2491f92cb400000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000017736574446f6d61696e4f776e6572286164647265737329000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000ecedfd8ba8ae39a6bd346fe9e5e0abea687fff31000000000000000000000000000000000000000000000000000000000000004b546869732070726f706f73616c206d69677261746573207468652072616469636c652e65746820646f6d61696e20616e6420746f6b656e20746f2061206e6577205265676973747261722e000000000000000000000000000000000000000000"); */ 332 | /* } */ 333 | 334 | /* function testAbiEncode2() public { */ 335 | /* string[] memory sigs = new string[](1); */ 336 | /* sigs[0] = "setDomainOwner(address)"; */ 337 | /* bytes memory encoded = abi.encodeWithSignature("f(string[],string)", */ 338 | /* sigs, */ 339 | /* "This proposal migrates the radicle.eth domain and token to a new Registrar."); */ 340 | /* assertEq0(encoded, hex"07ac501f0000000 action "callas(address,address,bytes,uint)" [AbiAddressType, AbiAddressType, AbiBytesDynamicType, AbiUIntType 256] $ 341 | \sig tps outOffset outSize input -> case decodeBuffer tps input of 342 | CAbi [AbiAddress caller', AbiAddress target, AbiBytesDynamic calldata, AbiUInt 256 val] -> 343 | let 344 | target' = litAddr target 345 | value' = num val 346 | stk = lookup (state . stack) 347 | in 348 | delegateCall caller' xGas target' target' value' xInOffset xInSize outOffset outSize stk $ 349 | \callee -> do 350 | zoom state $ do 351 | assign callvalue (litWord value') 352 | assign caller (litAddr caller') 353 | assign contract callee 354 | transfer caller' callee value' 355 | touchAccount caller' 356 | touchAccount callee 357 | _ -> vmError (BadCheatCode sig)00000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000017736574446f6d61696e4f776e6572286164647265737329000000000000000000000000000000000000000000000000000000000000000000000000000000004b546869732070726f706f73616c206d69677261746573207468652072616469636c652e65746820646f6d61696e20616e6420746f6b656e20746f2061206e6577205265676973747261722e000000000000000000000000000000000000000000"); */ 358 | /* } */ 359 | 360 | /* function testAbiEncode3() public { */ 361 | /* string[] memory sigs = new string[](1); */ 362 | /* sigs[0] = "setDomainOwner(address)"; */ 363 | /* bytes memory encoded = abi.encodeWithSignature("f(string[])", */ 364 | /* sigs); */ 365 | /* assertEq0(encoded, hex"e9cc87800000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000017736574446f6d61696e4f776e6572286164647265737329000000000000000000"); */ 366 | /* } */ 367 | } 368 | -------------------------------------------------------------------------------- /src/Registrar.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.7.5; 2 | pragma abicoder v2; 3 | 4 | import {Phase0} from "radicle-contracts/deploy/phase0.sol"; 5 | import {RadicleToken} from "radicle-contracts/Governance/RadicleToken.sol"; 6 | import {Governor} from "radicle-contracts/Governance/Governor.sol"; 7 | import {Timelock} from "radicle-contracts/Governance/Timelock.sol"; 8 | import {VestingToken} from "radicle-contracts/Governance/VestingToken.sol"; 9 | import {Registrar, Commitments} from "radicle-contracts/Registrar.sol"; 10 | 11 | import {ENS} from "@ensdomains/ens/contracts/ENS.sol"; 12 | import {ENSRegistry} from "@ensdomains/ens/contracts/ENSRegistry.sol"; 13 | import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 14 | 15 | import {DSTest} from "ds-test/test.sol"; 16 | import {DSMath} from "ds-math/math.sol"; 17 | 18 | import {Hevm, Utils} from "./Utils.sol"; 19 | 20 | contract RegistrarRPCTests is DSTest { 21 | ENS ens; 22 | RadicleToken rad; 23 | Registrar registrar; 24 | bytes32 domain; 25 | uint tokenId; 26 | Hevm hevm = Hevm(HEVM_ADDRESS); 27 | Governor gov; 28 | 29 | function setUp() public { 30 | domain = Utils.namehash(["radicle", "eth"]); 31 | ens = ENS(0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e); 32 | tokenId = uint(keccak256(abi.encodePacked("radicle"))); // seth keccak radicle 33 | Phase0 phase0 = new Phase0( address(this) 34 | , 2 days 35 | , address(0) 36 | , ens 37 | , domain 38 | , "radicle" 39 | ); 40 | registrar = phase0.registrar(); 41 | rad = phase0.token(); 42 | gov = phase0.governor(); 43 | log_named_address("Registrar", address(registrar)); 44 | 45 | // make this contract the owner of the radicle.eth domain 46 | hevm.store( 47 | address(ens), 48 | keccak256(abi.encodePacked(domain, uint(0))), 49 | Utils.asBytes32(address(this)) 50 | ); 51 | 52 | // make this contract the owner of the radicle.eth 721 token 53 | address ethRegistrarAddr = ens.owner(Utils.namehash(["eth"])); 54 | 55 | // set owner["radicle"] = address(registrar) 56 | // TODO: make this less inscrutible 57 | hevm.store( 58 | ethRegistrarAddr, 59 | 0x7906724a382e1baec969d07da2f219928e717131ddfd68dbe3d678f62fa3065b, 60 | Utils.asBytes32(address(this)) 61 | ); 62 | 63 | // ownedTokensCount[address(this)] 64 | // TODO: make this less inscrutible 65 | hevm.store( 66 | ethRegistrarAddr, 67 | bytes32(uint(99769381792979770997497849739242275106480790460331428765085642759382986339262)), 68 | bytes32(uint(1)) 69 | ); 70 | 71 | // transfer ownership of the ENS record to the registrar 72 | ens.setOwner(domain, address(registrar)); 73 | 74 | // transfer ownership of the 721 token to the registrar 75 | IERC721(ethRegistrarAddr).transferFrom(address(this), address(registrar), tokenId); 76 | } 77 | 78 | // --- tests --- 79 | 80 | // the ownership of the correct node in ens changes after domain registration 81 | function test_register(string memory name) public { 82 | if (bytes(name).length < 2) return; 83 | if (bytes(name).length > 128) return; 84 | bytes32 node = Utils.namehash([name, "radicle", "eth"]); 85 | 86 | assertEq(ens.owner(node), address(0)); 87 | registerWith(registrar, name); 88 | assertEq(ens.owner(node), address(this)); 89 | } 90 | 91 | // BUG: the resolver is address(0x0) for radicle subdomains 92 | function test_resolverUnset() public { 93 | bytes32 node = Utils.namehash(["microsoft", "radicle", "eth"]); 94 | 95 | assertEq(ens.owner(node), address(0)); 96 | registerWith(registrar, "microsoft"); 97 | assertEq(ens.owner(node), address(this)); 98 | assertEq(ens.resolver(node), ens.resolver(Utils.namehash(["radicle", "eth"]))); 99 | } 100 | 101 | // BUG: names transfered to the zero address can never be reregistered 102 | function test_reregistration(string memory name) public { 103 | if (bytes(name).length < 2) return; 104 | if (bytes(name).length > 128) return; 105 | bytes32 node = Utils.namehash([name, "radicle", "eth"]); 106 | registerWith(registrar, name, 666); 107 | 108 | ens.setOwner(node, address(0)); 109 | assertEq(ens.owner(node), address(0)); 110 | assertTrue(ens.recordExists(node)); 111 | 112 | registerWith(registrar, name, 667); 113 | assertEq(ens.owner(node), address(this)); 114 | } 115 | 116 | // domain registration still works after transfering ownership of the 117 | // "radicle.eth" domain to a new registrar 118 | // TODO: the Timelock is the admin of the Registrar, 119 | // so we will need to make a proposal to perform this transition. 120 | function test_register_with_new_owner() public { 121 | string memory name = "mrchico"; 122 | Registrar registrar2 = new Registrar( 123 | ens, 124 | rad, 125 | address(this), 126 | 50, 127 | domain, 128 | tokenId 129 | ); 130 | log_named_address("registrar2", address(registrar2)); 131 | rad.delegate(address(this)); 132 | hevm.roll(block.number + 1); 133 | // This proposal was generated with `radgov propose newRegistrar`! 134 | // where newRegistrar is: 135 | // 136 | // This proposal migrates the radicle.eth domain and token 137 | // to a new Registrar. 138 | // 139 | // ## PROPOSAL ## 140 | // 141 | // ``` 142 | // 0xFCBcd8C32305228F205c841c03f59D2491f92Cb4 0 "setDomainOwner(address)" 0xEcEDFd8BA8ae39a6Bd346Fe9E5e0aBeA687fFF31 143 | // ``` 144 | (bool success, bytes memory retdata) = address(gov).call(hex"da95691a00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000fcbcd8c32305228f205c841c03f59d2491f92cb400000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000017736574446f6d61696e4f776e6572286164647265737329000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000ecedfd8ba8ae39a6bd346fe9e5e0abea687fff31000000000000000000000000000000000000000000000000000000000000004b546869732070726f706f73616c206d69677261746573207468652072616469636c652e65746820646f6d61696e20616e6420746f6b656e20746f2061206e6577205265676973747261722e000000000000000000000000000000000000000000"); 145 | assertTrue(success); 146 | uint id = abi.decode(retdata, (uint)); 147 | assertEq(id, 1); 148 | (address[] memory targets, uint256[] memory values, string[] memory signatures, bytes[] memory calldatas) = gov.getActions(1); 149 | assertEq(targets[0], address(registrar)); 150 | assertEq(values[0], 0); 151 | assertEq(signatures[0], "setDomainOwner(address)"); 152 | assertEq0(calldatas[0], abi.encode(address(registrar2))); 153 | 154 | // advance the time and vote the proposal through 155 | hevm.roll(block.number + gov.votingDelay() + 1); 156 | assertEq(uint(gov.state(id)), 1, "proposal is active"); 157 | 158 | // votes cast must have been checkpointed by delegation, and 159 | // exceed the quorum and votes against 160 | gov.castVote(id, true); 161 | hevm.roll(block.number + gov.votingPeriod()); 162 | assertEq(uint(gov.state(id)), 4, "proposal is successful"); 163 | 164 | // queueing succeeds unless already queued 165 | // (N.B. cannot queue multiple calls to same signature as-is) 166 | gov.queue(id); 167 | assertEq(uint(gov.state(id)), 5, "proposal is queued"); 168 | 169 | // can only execute following time delay 170 | hevm.warp(block.timestamp + 2 days); 171 | gov.execute(id); 172 | assertEq(uint(gov.state(id)), 7, "proposal is executed"); 173 | 174 | // the new registrar is now the owner of the domain 175 | assertEq(ens.owner(domain), address(registrar2)); 176 | 177 | // and so we can register with it 178 | registerWith(registrar2, name); 179 | 180 | assertEq(ens.owner(Utils.namehash([name, "radicle", "eth"])), address(this)); 181 | } 182 | 183 | // a domain that has already been registered cannot be registered again 184 | function testFail_double_register(string memory name) public { 185 | require(bytes(name).length > 0); 186 | require(bytes(name).length <= 32); 187 | 188 | registerWith(registrar, name); 189 | registerWith(registrar, name); 190 | } 191 | 192 | // unfortunately we need something like `hevm.callFrom` to test this properly 193 | // this test is really just testing the scenario where the call is frontrun by an attacker 194 | // we are still able to validate that: 195 | // - the permit is correct 196 | // - the attacker still pays 197 | function test_commit_with_permit(uint sk, string memory name, uint salt) public { 198 | if (sk == 0) return; 199 | if (!registrar.valid(name)) return; 200 | address owner = hevm.addr(sk); 201 | 202 | { 203 | uint preBal = rad.balanceOf(address(this)); 204 | uint preSupply = rad.totalSupply(); 205 | rad.approve(address(registrar), registrar.registrationFeeRad()); 206 | 207 | // sign the `permit` message 208 | uint value = registrar.registrationFeeRad(); 209 | uint deadline = type(uint).max; 210 | bytes32 structHash = keccak256(abi.encode( 211 | rad.PERMIT_TYPEHASH(), owner, address(registrar), value, rad.nonces(owner), deadline 212 | )); 213 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", rad.DOMAIN_SEPARATOR(), structHash)); 214 | (uint8 v, bytes32 r, bytes32 s) = hevm.sign(sk, digest); 215 | 216 | // generate the name commitment 217 | bytes32 commitment = keccak256(abi.encodePacked(name, owner, salt)); 218 | 219 | // commit to the name using permit 220 | registrar.commitWithPermit(commitment, owner, value, deadline, v, r, s); 221 | 222 | assertEq( 223 | preBal - rad.balanceOf(address(this)), registrar.registrationFeeRad(), 224 | "frontrunner had to pay" 225 | ); 226 | assertEq( 227 | rad.allowance(owner, address(registrar)), registrar.registrationFeeRad(), 228 | "owner approved registrar for registrationFeeRad" 229 | ); 230 | assertEq( 231 | preSupply - rad.totalSupply(), registrar.registrationFeeRad(), 232 | "rad totalSupply has decreased by registrationFeeRad rad" 233 | ); 234 | assertEq( 235 | registrar.commitments().commited(commitment), block.number, 236 | "name was commited to" 237 | ); 238 | } 239 | 240 | { 241 | // jump forward until name can be registered 242 | hevm.roll(block.number + registrar.minCommitmentAge() + 1); 243 | registrar.register(name, owner, salt); 244 | 245 | assertEq( 246 | ens.owner(Utils.namehash([name, "radicle", "eth"])), owner, 247 | "owner controls the name in ens" 248 | ); 249 | } 250 | } 251 | 252 | /* 253 | here we test the full end to end flow commitBySig flow, validating that: 254 | 255 | - the relayer is compensated 256 | - the registration fee is burned 257 | - the name is commited to 258 | - the owner paid for both the registration and the relaying fee 259 | - the name can subsequently be registered 260 | */ 261 | function test_commit_by_sig( 262 | uint sk, string memory name, uint salt, uint expiry, uint64 submissionFee 263 | ) public { 264 | if (sk == 0) return; 265 | if (!registrar.valid(name)) return; 266 | if (expiry < block.timestamp) return; 267 | 268 | address owner = hevm.addr(sk); 269 | uint totalFee = registrar.registrationFeeRad() + submissionFee; 270 | 271 | // commit 272 | { 273 | // give `owner` some rad and approve the registrar for them 274 | rad.transfer(address(owner), totalFee); 275 | hevm.store( 276 | address(rad), 277 | keccak256(abi.encodePacked( 278 | uint(address(registrar)), 279 | keccak256(abi.encodePacked(uint(owner), uint(1))) 280 | )), 281 | bytes32(totalFee) 282 | ); 283 | require(rad.allowance(owner, address(registrar)) == totalFee, "incorrect allowance"); 284 | 285 | 286 | // generate the commitment 287 | bytes32 commitment = keccak256(abi.encodePacked(name, owner, salt)); 288 | 289 | // produce the signed commit message 290 | bytes32 digest; 291 | { // stack too deep... 292 | bytes32 domainSeparator = 293 | keccak256( 294 | abi.encode( 295 | registrar.DOMAIN_TYPEHASH(), 296 | keccak256(bytes(registrar.NAME())), 297 | Utils.getChainId(), 298 | address(registrar) 299 | ) 300 | ); 301 | 302 | bytes32 structHash = 303 | keccak256( 304 | abi.encode( 305 | registrar.COMMIT_TYPEHASH(), 306 | commitment, 307 | registrar.nonces(owner), 308 | expiry, 309 | submissionFee 310 | ) 311 | ); 312 | 313 | digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); 314 | } 315 | (uint8 v, bytes32 r, bytes32 s) = hevm.sign(sk, digest); 316 | 317 | // cache some values 318 | uint preBalThis = rad.balanceOf(address(this)); 319 | uint preBalOwner = rad.balanceOf(owner); 320 | uint preSupply = rad.totalSupply(); 321 | 322 | // submit the signed commitment 323 | registrar.commitBySig(commitment, registrar.nonces(owner), expiry, submissionFee, v, r, s); 324 | 325 | // assertions 326 | assertEq( 327 | registrar.commitments().commited(commitment), block.number, 328 | "commitment was made at the current block" 329 | ); 330 | assertEq( 331 | rad.balanceOf(address(this)) - preBalThis, submissionFee, 332 | "relayer was paid submissionFee" 333 | ); 334 | assertEq( 335 | preSupply - rad.totalSupply(), registrar.registrationFeeRad(), 336 | "registration fee was burned" 337 | ); 338 | assertEq( 339 | preBalOwner - rad.balanceOf(owner), totalFee, 340 | "owner paid submissionFee + registrationFeeRad" 341 | ); 342 | } 343 | 344 | // register 345 | { 346 | // jump forward until name can be registered 347 | hevm.roll(block.number + registrar.minCommitmentAge() + 1); 348 | registrar.register(name, owner, salt); 349 | 350 | assertEq( 351 | ens.owner(Utils.namehash([name, "radicle", "eth"])), owner, 352 | "owner controls the name in ens" 353 | ); 354 | 355 | } 356 | } 357 | 358 | struct Signature { 359 | uint8 v; 360 | bytes32 r; 361 | bytes32 s; 362 | } 363 | 364 | struct PermitParams { 365 | address owner; 366 | address spender; 367 | uint value; 368 | uint expiry; 369 | } 370 | 371 | struct CommitParams { 372 | bytes32 commitment; 373 | uint nonce; 374 | uint expiry; 375 | uint submissionFee; 376 | } 377 | 378 | struct TestParams { 379 | uint sk; 380 | string name; 381 | uint salt; 382 | uint expiry; 383 | uint112 submissionFee; 384 | } 385 | 386 | /* 387 | function test_commit_by_sig_with_permit(TestParams memory args) public { 388 | if (args.sk == 0) return; 389 | if (!registrar.valid(args.name)) return; 390 | if (args.expiry < block.timestamp) return; 391 | 392 | PermitParams memory permitParams; 393 | { 394 | permitParams = PermitParams( 395 | hevm.addr(args.sk), 396 | address(registrar), 397 | registrar.registrationFeeRad() + args.submissionFee, 398 | args.expiry 399 | ); 400 | } 401 | 402 | CommitParams memory commitParams; 403 | { 404 | commitParams = CommitParams( 405 | keccak256(abi.encodePacked(args.name, hevm.addr(args.sk), args.salt)), 406 | registrar.nonces(hevm.addr(args.sk)), 407 | args.expiry, 408 | args.submissionFee 409 | ); 410 | } 411 | 412 | Signature memory permitSig; 413 | { permitSig = signPermit(args.sk, permitParams); } 414 | 415 | Signature memory commitSig; 416 | { commitSig = signCommit(args.sk, commitParams); } 417 | 418 | // make the commitment 419 | registrar.commitBySigWithPermit( 420 | commitParams.commitment, 421 | commitParams.nonce, 422 | commitParams.expiry, 423 | commitParams.submissionFee, 424 | commitSig.v, commitSig.r, commitSig.s, 425 | permitParams.owner, 426 | permitParams.value, 427 | permitParams.expiry, 428 | permitSig.v, permitSig.r, permitSig.s 429 | ); 430 | } 431 | */ 432 | 433 | // Utils.nameshash does the right thing for radicle.eth subdomains 434 | function test_namehash(string memory name) public { 435 | bytes32 node = Utils.namehash([name, "radicle", "eth"]); 436 | assertEq(node, keccak256(abi.encodePacked( 437 | keccak256(abi.encodePacked( 438 | keccak256(abi.encodePacked( 439 | bytes32(uint(0)), 440 | keccak256("eth") 441 | )), 442 | keccak256("radicle") 443 | )), 444 | keccak256(bytes(name)) 445 | ))); 446 | } 447 | 448 | // --- helpers --- 449 | 450 | function registerWith(Registrar reg, string memory name) internal { 451 | registerWith(reg, name, 42069); 452 | } 453 | 454 | /* function registerFor(Registrar reg, address owner, string memory name, bytes32 r, bytes32 s, uint8 v, bytes32 permit_r, bytes32 permit_s, uint8 permit_v) internal { */ 455 | /* uint salt = 150987; */ 456 | /* bytes32 commitment = keccak256(abi.encodePacked(name, owner, salt)); */ 457 | /* reg.commitBySigWithPermit(commitment, 0, uint(-1), 1 ether, owner, uint(-1), uint(-1), permit_v, permit_r, permit_s); */ 458 | /* hevm.roll(block.number + 100); */ 459 | /* reg.register(name, owner, salt); */ 460 | /* } */ 461 | 462 | function registerWith(Registrar reg, string memory name, uint salt) internal { 463 | uint preBal = rad.balanceOf(address(this)); 464 | 465 | bytes32 commitment = keccak256(abi.encodePacked(name, address(this), salt)); 466 | rad.approve(address(reg), uint(-1)); 467 | 468 | reg.commit(commitment); 469 | hevm.roll(block.number + 100); 470 | reg.register(name, address(this), salt); 471 | 472 | assertEq(rad.balanceOf(address(this)), preBal - 10 ether); 473 | } 474 | 475 | function signPermit(uint sk, PermitParams memory args) internal returns (Signature memory) { 476 | require(args.owner == hevm.addr(sk), "signPermit: signing from wrong address"); 477 | bytes32 structHash = 478 | keccak256( 479 | abi.encode( 480 | rad.PERMIT_TYPEHASH(), 481 | args.owner, 482 | args.spender, 483 | args.value, 484 | rad.nonces(args.owner), 485 | args.expiry 486 | ) 487 | ); 488 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", rad.DOMAIN_SEPARATOR(), structHash)); 489 | (uint8 v, bytes32 r, bytes32 s) = hevm.sign(sk, digest); 490 | return Signature(v, r, s); 491 | } 492 | 493 | function signCommit(uint sk, CommitParams memory args) internal returns (Signature memory) { 494 | address signer = hevm.addr(sk); 495 | bytes32 domainSeparator = 496 | keccak256( 497 | abi.encode( 498 | registrar.DOMAIN_TYPEHASH(), 499 | keccak256(bytes(registrar.NAME())), 500 | Utils.getChainId(), 501 | address(registrar) 502 | ) 503 | ); 504 | 505 | bytes32 structHash = 506 | keccak256( 507 | abi.encode( 508 | registrar.COMMIT_TYPEHASH(), 509 | args.commitment, 510 | args.nonce, 511 | args.expiry, 512 | args.submissionFee 513 | ) 514 | ); 515 | 516 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); 517 | (uint8 v, bytes32 r, bytes32 s) = hevm.sign(sk, digest); 518 | return Signature(v, r, s); 519 | } 520 | } 521 | -------------------------------------------------------------------------------- /src/Utils.sol: -------------------------------------------------------------------------------- 1 | import {RadicleToken} from "radicle-contracts/Governance/RadicleToken.sol"; 2 | import {VestingToken} from "radicle-contracts/Governance/VestingToken.sol"; 3 | 4 | interface Hevm { 5 | function warp(uint256) external; 6 | function roll(uint256) external; 7 | function store(address,bytes32,bytes32) external; 8 | function sign(uint,bytes32) external returns (uint8,bytes32,bytes32); 9 | function addr(uint) external returns (address); 10 | } 11 | 12 | library Utils { 13 | function create2Address( 14 | bytes32 salt, address creator, bytes memory creationCode, bytes memory args 15 | ) internal pure returns (address) { 16 | return address(uint(keccak256(abi.encodePacked( 17 | bytes1(0xff), 18 | creator, 19 | salt, 20 | keccak256(abi.encodePacked(creationCode, args)) 21 | )))); 22 | } 23 | 24 | function mkVestingToken( 25 | address token, 26 | address owner, 27 | address beneficiary, 28 | uint amount, 29 | uint vestingStartTime, 30 | uint vestingPeriod, 31 | uint cliffPeriod 32 | ) internal returns (VestingToken) { 33 | bytes32 salt = bytes32("0xacab"); 34 | 35 | address vestAddress = Utils.create2Address( 36 | salt, 37 | address(this), 38 | type(VestingToken).creationCode, 39 | abi.encode( 40 | token, owner, beneficiary, amount, vestingStartTime, vestingPeriod, cliffPeriod 41 | ) 42 | ); 43 | 44 | RadicleToken(token).approve(vestAddress, uint(-1)); 45 | VestingToken vest = new VestingToken{salt: salt}( 46 | token, owner, beneficiary, amount, vestingStartTime, vestingPeriod, cliffPeriod 47 | ); 48 | 49 | return vest; 50 | } 51 | 52 | function asBytes32(address addr) internal pure returns (bytes32) { 53 | return bytes32(uint256(uint160(addr))); 54 | } 55 | 56 | function namehash(string[] memory domain) internal pure returns (bytes32) { 57 | if (domain.length == 0) { 58 | return bytes32(uint(0)); 59 | } 60 | if (domain.length == 1) { 61 | return keccak256(abi.encodePacked(bytes32(0), keccak256(bytes(domain[0])))); 62 | } 63 | else { 64 | bytes memory label = bytes(domain[0]); 65 | string[] memory remainder = new string[](domain.length - 1); 66 | for (uint i = 1; i < domain.length; i++) { 67 | remainder[i - 1] = domain[i]; 68 | } 69 | return keccak256(abi.encodePacked(namehash(remainder), keccak256(label))); 70 | } 71 | } 72 | function namehash(string[1] memory domain) internal pure returns (bytes32) { 73 | string[] memory dyn = new string[](1); 74 | dyn[0] = domain[0]; 75 | return namehash(dyn); 76 | } 77 | function namehash(string[2] memory domain) internal pure returns (bytes32) { 78 | string[] memory dyn = new string[](domain.length); 79 | for (uint i; i < domain.length; i++) { 80 | dyn[i] = domain[i]; 81 | } 82 | return namehash(dyn); 83 | } 84 | function namehash(string[3] memory domain) internal pure returns (bytes32) { 85 | string[] memory dyn = new string[](domain.length); 86 | for (uint i; i < domain.length; i++) { 87 | dyn[i] = domain[i]; 88 | } 89 | return namehash(dyn); 90 | } 91 | function namehash(string[4] memory domain) internal pure returns (bytes32) { 92 | string[] memory dyn = new string[](domain.length); 93 | for (uint i; i < domain.length; i++) { 94 | dyn[i] = domain[i]; 95 | } 96 | return namehash(dyn); 97 | } 98 | function namehash(string[5] memory domain) internal pure returns (bytes32) { 99 | string[] memory dyn = new string[](domain.length); 100 | for (uint i; i < domain.length; i++) { 101 | dyn[i] = domain[i]; 102 | } 103 | return namehash(dyn); 104 | } 105 | 106 | function getChainId() internal pure returns (uint chainId) { 107 | assembly { chainId := chainid() } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/VestingToken.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.7.5; 2 | pragma abicoder v2; 3 | 4 | import {RadicleToken} from "radicle-contracts/Governance/RadicleToken.sol"; 5 | import {VestingToken} from "radicle-contracts/Governance/VestingToken.sol"; 6 | 7 | import {DSTest} from "ds-test/test.sol"; 8 | import {DSMath} from "ds-math/math.sol"; 9 | 10 | import {Hevm, Utils} from "./Utils.sol"; 11 | 12 | contract VestingUser { 13 | function withdrawVested(VestingToken vest) public { 14 | vest.withdrawVested(); 15 | } 16 | } 17 | 18 | contract VestingOwner { 19 | function terminateVesting(VestingToken vest) public { 20 | vest.terminateVesting(); 21 | } 22 | } 23 | 24 | contract VestingTokenTests is DSTest, DSMath { 25 | RadicleToken rad; 26 | VestingUser user; 27 | VestingOwner owner; 28 | Hevm hevm = Hevm(HEVM_ADDRESS); 29 | 30 | function setUp() public { 31 | hevm.warp(12345678); 32 | 33 | rad = new RadicleToken(address(this)); 34 | user = new VestingUser(); 35 | owner = new VestingOwner(); 36 | } 37 | 38 | // Demonstrates a bug where withdrawableBalance() could revert after 39 | // vesting has been interrupted. 40 | function test_vesting_failure() public { 41 | VestingToken vest = Utils.mkVestingToken( 42 | address(rad), 43 | address(this), 44 | address(user), 45 | 10000000 ether, 46 | block.timestamp - 1, 47 | 2 weeks, 48 | 1 days 49 | ); 50 | 51 | hevm.warp(block.timestamp + 2 days); 52 | vest.terminateVesting(); 53 | hevm.warp(block.timestamp + 1 days); 54 | 55 | // withdrawableBalance reverts if vesting was interrupted 56 | vest.withdrawableBalance(); 57 | } 58 | 59 | // `withdrawableBalance()` should always return the actual amount that will 60 | // be withdrawan when calling `withdrawVested()` 61 | function test_withdrawal_amount( 62 | uint24 jump, uint24 amount, uint8 startOffset, uint24 vestingPeriod, uint24 cliffPeriod 63 | ) public { 64 | if (vestingPeriod == 0) return; 65 | if (startOffset == 0) return; 66 | if (amount == 0) return; 67 | if (amount > 10000000 ether) return; 68 | 69 | VestingToken vest = Utils.mkVestingToken( 70 | address(rad), 71 | address(this), 72 | address(user), 73 | amount, 74 | block.timestamp - startOffset, 75 | vestingPeriod, 76 | cliffPeriod 77 | ); 78 | 79 | hevm.warp(block.timestamp + jump); 80 | 81 | uint amt = vest.withdrawableBalance(); 82 | uint prebal = rad.balanceOf(address(user)); 83 | 84 | user.withdrawVested(vest); 85 | uint postbal = rad.balanceOf(address(user)); 86 | 87 | assertEq(postbal - prebal, amt, "withdrawn amount matches withdrawableBalance"); 88 | } 89 | 90 | // The VestingToken should be empty after `terminateVesting()` has been called 91 | // The beneficiary should have received all vested tokens 92 | // The owner should have received all unvested tokens 93 | function test_empty_after_termination( 94 | uint24 jump, uint24 amount, uint8 startOffset, uint24 vestingPeriod, uint24 cliffPeriod 95 | ) public { 96 | if (vestingPeriod == 0) return; 97 | if (startOffset == 0) return; 98 | if (amount == 0) return; 99 | if (amount > 10000000 ether) return; 100 | 101 | VestingToken vest = Utils.mkVestingToken( 102 | address(rad), 103 | address(owner), 104 | address(user), 105 | amount, 106 | block.timestamp - startOffset, 107 | vestingPeriod, 108 | cliffPeriod 109 | ); 110 | 111 | hevm.warp(block.timestamp + jump); 112 | 113 | assertEq(rad.balanceOf(address(vest)), amount); 114 | uint vested = vest.withdrawableBalance(); 115 | log_named_uint("vested", vested); 116 | log_named_uint("amount", amount); 117 | uint unvested = sub(amount, vest.withdrawableBalance()); 118 | 119 | owner.terminateVesting(vest); 120 | 121 | assertEq( 122 | rad.balanceOf(address(vest)), 0, 123 | "vesting token is empty" 124 | ); 125 | assertEq( 126 | rad.balanceOf(address(user)), vested, 127 | "beneficiary has received all vested tokens" 128 | ); 129 | assertEq( 130 | rad.balanceOf(address(owner)), unvested, 131 | "owner has received all unvested tokens" 132 | ); 133 | } 134 | 135 | // The `withdrawn` attribute should always accurately reflect the actual amount withdrawn 136 | // Demonstrates a bug where the withdrawn attribute is set to a misleading value after termination 137 | function test_withdrawn_accounting( 138 | uint8 jump, uint24 amount, uint8 startOffset, uint24 vestingPeriod, uint24 cliffPeriod 139 | ) public { 140 | if (vestingPeriod == 0) return; 141 | if (startOffset == 0) return; 142 | if (amount == 0) return; 143 | if (amount > 10000000 ether) return; 144 | 145 | VestingToken vest = Utils.mkVestingToken( 146 | address(rad), 147 | address(owner), 148 | address(user), 149 | amount, 150 | block.timestamp - startOffset, 151 | vestingPeriod, 152 | cliffPeriod 153 | ); 154 | 155 | uint withdrawn = 0; 156 | 157 | for (uint i; i < 10; i++) { 158 | hevm.warp(block.timestamp + jump); 159 | uint prebal = rad.balanceOf(address(user)); 160 | user.withdrawVested(vest); 161 | 162 | uint postbal = rad.balanceOf(address(user)); 163 | withdrawn = add(withdrawn, postbal - prebal); 164 | } 165 | 166 | assertEq(withdrawn, vest.withdrawn(), "pre-termination"); 167 | 168 | hevm.warp(block.timestamp + jump); 169 | uint withdrawable = vest.withdrawableBalance(); 170 | owner.terminateVesting(vest); 171 | 172 | assertEq(vest.withdrawn(), add(withdrawn, withdrawable), "post-termination"); 173 | } 174 | } 175 | --------------------------------------------------------------------------------