├── .gitignore ├── .gitmodules ├── LICENSE ├── README └── src ├── Attest.sol ├── Proposer.sol ├── Registry.sol ├── Runtime.sol ├── interfaces ├── IERC721Permit.sol ├── IProposer.sol └── IRuntime.sol ├── libraries ├── Cast.sol ├── Checkpoint.sol ├── ERC721Permit.sol ├── Firewall.sol ├── Header.sol ├── Pointer.sol ├── Revert.sol ├── Status.sol └── Transaction.sol └── test ├── Proposer.t.sol ├── Registry.t.sol ├── Runtime.t.sol └── mocks ├── Target.sol └── Vm.sol /.gitignore: -------------------------------------------------------------------------------- 1 | cache/ 2 | out/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/ds-test"] 2 | path = lib/ds-test 3 | url = https://github.com/dapphub/ds-test 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 7 | [submodule "lib/ut-0"] 8 | path = lib/ut-0 9 | url = https://github.com/automata-labs/ut-0 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Automata Labs Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | des 2 | 3 | `des` (DAO Execution System) is a DAO contract-set that tokenizes 4 | proposals into `ERC721` tokens and allows for arbitrary execution from 5 | an admin contract. 6 | 7 | The `Runtime` contract can execute both transcation batches and 8 | contracts deployments using the `create` and `create2` opcodes. Every 9 | transaction batch also includes a message that is emitted as an event 10 | when executed. 11 | 12 | By abstracting proposals into `ERC721` tokens, the logic is also 13 | factored out from the controller/governor/registry contract and enables 14 | upgradeability for more expressive proposal formats if desired (for 15 | example, a multi-choice proposal). 16 | 17 | Proposals start out as `draft`s and can then be staged by the owner or 18 | accounts with nft approval. When staged, anyone with enough token votes 19 | can `open` the proposal for voting. We believe the proposal lifecycle 20 | and staging/unstaging methodology should improve the feedback process 21 | between the proposer(s) and the DAO members/participants. 22 | 23 | `des` is released under "The MIT License (MIT)" license. 24 | Copyright (c) 2021 Automata Labs Inc. 25 | -------------------------------------------------------------------------------- /src/Attest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "ut-0/ERC20.sol"; 5 | 6 | import "./libraries/Pointer.sol"; 7 | import "./libraries/Checkpoint.sol"; 8 | 9 | contract Attest is ERC20 { 10 | using Checkpoint for Checkpoint.Data[]; 11 | using Checkpoint for mapping(address => Checkpoint.Data[]); 12 | using Pointer for Pointer.Data[]; 13 | using Pointer for mapping(address => Pointer.Data[]); 14 | 15 | error InvalidBlockNumber(uint32 blockNumber); 16 | 17 | /// @dev The delegation destination history. 18 | mapping(address => Pointer.Data[]) internal _pointerOf; 19 | /// @dev The delegation amount history. 20 | mapping(address => Checkpoint.Data[]) internal _checkpoints; 21 | 22 | constructor( 23 | string memory name_, 24 | string memory symbol_, 25 | uint8 decimals_ 26 | ) ERC20(name_, symbol_, decimals_) {} 27 | 28 | function magnitude(address account) external view returns (uint256) { 29 | return _pointerOf[account].length; 30 | } 31 | 32 | /// @dev Returns the length of pointers and checkpoints. 33 | function cardinality(address account) external view returns (uint256) { 34 | return _checkpoints[account].length; 35 | } 36 | 37 | function pointerOf(address account) external view returns (address) { 38 | return _pointerOf[account].latest(); 39 | } 40 | 41 | function pointerIn(address account, uint256 blockNumber) external view returns (address) { 42 | if (blockNumber <= block.number) 43 | return _pointerOf[account].lookup(blockNumber); 44 | else 45 | revert InvalidBlockNumber(uint32(blockNumber)); 46 | } 47 | 48 | function pointerAt( 49 | address account, 50 | uint256 index 51 | ) external view returns (address, uint32, uint32) { 52 | require(index < _pointerOf[account].length, "OutOfBounds"); 53 | Pointer.Data memory pointer = _pointerOf[account][index]; 54 | return (pointer.value, pointer.startBlock, pointer.endBlock); 55 | } 56 | 57 | function weightOf(address account) external view returns (uint256) { 58 | return _checkpoints[account].latest(); 59 | } 60 | 61 | function weightIn(address account, uint256 blockNumber) external view returns (uint256) { 62 | if (blockNumber <= block.number) 63 | return _checkpoints[account].lookup(blockNumber); 64 | else 65 | revert InvalidBlockNumber(uint32(blockNumber)); 66 | } 67 | 68 | function weightAt( 69 | address account, 70 | uint256 index 71 | ) external view returns (uint160, uint32, uint32) { 72 | require(index < _checkpoints[account].length, "OutOfBounds"); 73 | Checkpoint.Data memory checkpoint = _checkpoints[account][index]; 74 | return (checkpoint.amount, checkpoint.startBlock, checkpoint.endBlock); 75 | } 76 | 77 | function mint(address to, uint256 amount) external returns (bool) { 78 | return _mint(to, amount); 79 | } 80 | 81 | function burn(address from, uint256 amount) external returns (bool) { 82 | return _burn(from, amount); 83 | } 84 | 85 | /// @dev Delegate voting rights to another account. 86 | function delegate(address to) external { 87 | _delegate(msg.sender, to); 88 | } 89 | 90 | function _delegate(address from, address to) internal { 91 | _checkpoints.move(_pointerOf[from].latest(), to, _balanceOf[from]); 92 | _pointerOf[from].save(to); 93 | } 94 | 95 | /// @dev Called on {mint}, {burn}, {transfer} and {transferFrom}. 96 | /// @dev Should be used to update when transferring shares to a delegated account. 97 | function _after(address from, address to, uint256 amount) internal override virtual { 98 | _checkpoints.move(_pointerOf[from].latest(), _pointerOf[to].latest(), amount); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Proposer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./interfaces/IProposer.sol"; 5 | import "./libraries/Cast.sol"; 6 | import "./libraries/Checkpoint.sol"; 7 | import "./libraries/ERC721Permit.sol"; 8 | import "./libraries/Firewall.sol"; 9 | import "./libraries/Header.sol"; 10 | import "./libraries/Status.sol"; 11 | 12 | interface IAttest { 13 | function weightOf(address account) external view returns (uint256); 14 | 15 | function weightIn(address account, uint256 blockNumber) external view returns (uint256); 16 | 17 | function weightAt( 18 | address account, 19 | uint256 index 20 | ) external view returns (uint160, uint32, uint32); 21 | } 22 | 23 | /// @title Proposer 24 | /// @notice Tokenizes proposals as ERC721. 25 | contract Proposer is IProposer, ERC721Permit, Firewall { 26 | using Cast for uint256; 27 | using Header for Header.Data; 28 | 29 | error AttestOverflow(); 30 | error ContestationFailed(); 31 | error InvalidCheckpoint(uint256 index); 32 | error InvalidChoice(uint8 choice); 33 | error StatusError(Status status); 34 | error UndefinedId(uint256 tokenId); 35 | error UndefinedSelector(bytes4 selector); 36 | 37 | /// @inheritdoc IProposer 38 | address public immutable runtime; 39 | /// @inheritdoc IProposer 40 | address public immutable token; 41 | /// @inheritdoc IProposer 42 | uint128 public threshold; 43 | /// @inheritdoc IProposer 44 | uint128 public quorum; 45 | 46 | /// @inheritdoc IProposer 47 | uint32 public delay; 48 | /// @inheritdoc IProposer 49 | uint32 public period; 50 | /// @inheritdoc IProposer 51 | uint32 public window; 52 | /// @inheritdoc IProposer 53 | uint32 public extension; 54 | /// @inheritdoc IProposer 55 | uint32 public ttl; 56 | /// @inheritdoc IProposer 57 | uint32 public lifespan; 58 | 59 | /// @dev The next minted token id. 60 | uint256 private _nextId = 0; 61 | /// @inheritdoc IProposer 62 | mapping(uint256 => Proposal) public proposals; 63 | /// @inheritdoc IProposer 64 | mapping(uint256 => mapping(address => uint256)) public attests; 65 | 66 | constructor(address runtime_, address token_) ERC721Permit( 67 | "DAO Execution System Proposal NFT-V1", 68 | "DES-NFT-V1" 69 | ) { 70 | runtime = runtime_; 71 | token = token_; 72 | 73 | delay = 17280; // 3 days pending 74 | period = 40320; // 7 days to attest 75 | window = 17280; // 3 days to contest 76 | extension = 17280; // 3 days of contestation 77 | ttl = 17280; // 3 days queued 78 | lifespan = 80640; // 14 days until expiry (when accepted) 79 | } 80 | 81 | /// @inheritdoc IProposer 82 | function get(uint256 tokenId) external view returns (Proposal memory) { 83 | return proposals[tokenId]; 84 | } 85 | 86 | /// @inheritdoc IProposer 87 | function set(bytes4 selector, bytes memory data) external { 88 | if (selector == IProposer.threshold.selector) 89 | threshold = abi.decode(data, (uint128)); 90 | else if (selector == IProposer.threshold.selector) 91 | quorum = abi.decode(data, (uint128)); 92 | else if (selector == IProposer.delay.selector) 93 | delay = abi.decode(data, (uint32)); 94 | else if (selector == IProposer.period.selector) 95 | period = abi.decode(data, (uint32)); 96 | else if (selector == IProposer.window.selector) 97 | window = abi.decode(data, (uint32)); 98 | else if (selector == IProposer.extension.selector) 99 | extension = abi.decode(data, (uint32)); 100 | else if (selector == IProposer.ttl.selector) 101 | ttl = abi.decode(data, (uint32)); 102 | else if (selector == IProposer.lifespan.selector) 103 | lifespan = abi.decode(data, (uint32)); 104 | else 105 | revert UndefinedSelector(selector); 106 | } 107 | 108 | /// @inheritdoc IProposer 109 | function next() external view returns (uint256) { 110 | return _nextId; 111 | } 112 | 113 | /// @inheritdoc IProposer 114 | function hash(uint256 tokenId, uint256 index) external view returns (bytes32) { 115 | return proposals[tokenId].hash[index]; 116 | } 117 | 118 | /// @inheritdoc IProposer 119 | function hashes(uint256 tokenId) external view returns (bytes32[] memory) { 120 | return proposals[tokenId].hash; 121 | } 122 | 123 | /// @inheritdoc IProposer 124 | function maturity(uint256 tokenId) public view returns (uint32) { 125 | return proposals[tokenId].finality + ttl; 126 | } 127 | 128 | /// @inheritdoc IProposer 129 | function expiry(uint256 tokenId) public view returns (uint32) { 130 | return proposals[tokenId].finality + ttl + lifespan; 131 | } 132 | 133 | /// @inheritdoc IProposer 134 | function status(uint256 tokenId) public view returns (Status) { 135 | if (proposals[tokenId].merged) 136 | return Status.Merged; 137 | 138 | if (proposals[tokenId].closed) 139 | return Status.Closed; 140 | 141 | if (proposals[tokenId].start == 0) { 142 | if (proposals[tokenId].staged) { 143 | return Status.Staged; 144 | } else { 145 | return Status.Draft; 146 | } 147 | } 148 | 149 | if (_blockNumber() < proposals[tokenId].start) 150 | return Status.Pending; 151 | 152 | if (_blockNumber() < proposals[tokenId].end) 153 | return Status.Open; 154 | 155 | if (_blockNumber() < proposals[tokenId].trial) 156 | return Status.Contesting; 157 | 158 | if (_blockNumber() < proposals[tokenId].finality) 159 | return Status.Validation; 160 | 161 | if ( 162 | proposals[tokenId].ack > proposals[tokenId].nack && 163 | proposals[tokenId].ack > quorum 164 | ) { 165 | if (_blockNumber() < maturity(tokenId)) { 166 | return Status.Queued; 167 | } 168 | 169 | if (_blockNumber() < expiry(tokenId)) { 170 | return Status.Approved; 171 | } 172 | } 173 | 174 | return Status.Closed; 175 | } 176 | 177 | /// @inheritdoc IProposer 178 | function mint(address to, Header.Data calldata header) external auth returns (uint256 tokenId) { 179 | _mint(to, (tokenId = _nextId++)); 180 | _commit(tokenId, header); 181 | } 182 | 183 | /// @inheritdoc IProposer 184 | function stage(uint256 tokenId) external { 185 | require(_isApprovedOrOwner(msg.sender, tokenId), "Unauthorized"); 186 | require(status(tokenId) == Status.Draft, "NotDraft"); 187 | proposals[tokenId].staged = true; 188 | 189 | emit Stage(msg.sender, tokenId); 190 | } 191 | 192 | /// @inheritdoc IProposer 193 | function unstage(uint256 tokenId) external { 194 | require(_isApprovedOrOwner(msg.sender, tokenId), "Unauthorized"); 195 | require(status(tokenId) == Status.Staged, "NotStaged"); 196 | proposals[tokenId].staged = false; 197 | 198 | emit Unstage(msg.sender, tokenId); 199 | } 200 | 201 | /// @inheritdoc IProposer 202 | function open(uint256 tokenId) external { 203 | if (_isApprovedOrOwner(msg.sender, tokenId)) 204 | require(status(tokenId) == Status.Draft, "NotDraft"); 205 | else 206 | require(status(tokenId) == Status.Staged, "NotStaged"); 207 | 208 | require(IAttest(token).weightOf(msg.sender) >= threshold, "Insufficient"); 209 | proposals[tokenId].start = uint32(block.number) + delay; 210 | proposals[tokenId].end = proposals[tokenId].start + period; 211 | proposals[tokenId].trial = proposals[tokenId].end; 212 | proposals[tokenId].finality = proposals[tokenId].end + window; 213 | 214 | emit Open( 215 | msg.sender, 216 | tokenId, 217 | proposals[tokenId].start, 218 | proposals[tokenId].end, 219 | proposals[tokenId].finality 220 | ); 221 | } 222 | 223 | /// @inheritdoc IProposer 224 | function close(uint256 tokenId) external { 225 | require(_isApprovedOrOwner(msg.sender, tokenId), "Unauthorized"); 226 | require( 227 | status(tokenId) != Status.Closed || 228 | status(tokenId) != Status.Merged, 229 | "StatusMismatch" 230 | ); 231 | proposals[tokenId].closed = true; 232 | 233 | emit Close(msg.sender, tokenId); 234 | } 235 | 236 | /// @inheritdoc IProposer 237 | function done(uint256 tokenId) external auth { 238 | require(status(tokenId) == Status.Approved, "NotApproved"); 239 | proposals[tokenId].merged = true; 240 | 241 | emit Merge(tokenId); 242 | } 243 | 244 | /// @inheritdoc IProposer 245 | function attest(uint256 tokenId, uint8 support, uint96 amount, string memory comment) external { 246 | if (!_exists(tokenId)) 247 | revert UndefinedId(tokenId); 248 | 249 | Proposal storage proposal = proposals[tokenId]; 250 | Status status_ = status(tokenId); 251 | 252 | if (status_ == Status.Open) { 253 | if (support > 2) { 254 | revert InvalidChoice(support); 255 | } 256 | } else if (status_ == Status.Contesting) { 257 | if (proposal.side) { 258 | if (support != 1 && support != 2) { 259 | revert InvalidChoice(support); 260 | } 261 | } else { 262 | if (support != 0 && support != 2) { 263 | revert InvalidChoice(support); 264 | } 265 | } 266 | } else { 267 | revert StatusError(status_); 268 | } 269 | 270 | uint160 weight = IAttest(token).weightIn(msg.sender, proposal.start).u160(); 271 | 272 | if (amount > weight - attests[tokenId][msg.sender]) 273 | revert AttestOverflow(); 274 | 275 | if (support == 0) 276 | proposal.ack += amount; 277 | 278 | if (support == 1) 279 | proposal.nack += amount; 280 | 281 | attests[tokenId][msg.sender] += amount; 282 | 283 | emit Attest(msg.sender, tokenId, support, amount, comment); 284 | } 285 | 286 | /// @inheritdoc IProposer 287 | function contest(uint256 tokenId) external { 288 | if (status(tokenId) != Status.Validation) 289 | revert StatusError(status(tokenId)); 290 | 291 | Proposal storage proposal = proposals[tokenId]; 292 | 293 | if (proposal.ack < quorum) 294 | revert ContestationFailed(); 295 | 296 | if (proposal.side && proposal.ack > proposal.nack) 297 | revert ContestationFailed(); 298 | 299 | if (!proposal.side && proposal.nack > proposal.ack) 300 | revert ContestationFailed(); 301 | 302 | proposal.trial = _blockNumber() + extension; 303 | proposal.finality = proposal.trial + window; 304 | proposal.side = !proposal.side; 305 | 306 | emit Contest( 307 | msg.sender, 308 | tokenId, 309 | proposal.trial, 310 | proposal.finality, 311 | proposal.side 312 | ); 313 | } 314 | 315 | /// @inheritdoc IProposer 316 | function commit(uint256 tokenId, Header.Data calldata header) external { 317 | require(_isApprovedOrOwner(msg.sender, tokenId), "NotApprovedOrOwner"); 318 | _commit(tokenId, header); 319 | } 320 | 321 | /// @dev Internal commit function. 322 | function _commit(uint256 tokenId, Header.Data calldata header) internal { 323 | require(status(tokenId) == Status.Draft, "NotDraft"); 324 | proposals[tokenId].hash = header.hash(); 325 | 326 | emit Commit(msg.sender, tokenId, proposals[tokenId].hash, header); 327 | } 328 | 329 | /// @dev Increments a proposal nonce used for `ERC721Permit`. 330 | function _getAndIncrementNonce(uint256 tokenId) internal override returns (uint256) { 331 | return uint256(proposals[tokenId].nonce++); 332 | } 333 | 334 | /// @dev Returns a `uint32` casted `block.number`. 335 | function _blockNumber() internal view returns (uint32) { 336 | return uint32(block.number); 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/Registry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./interfaces/IProposer.sol"; 5 | import "./interfaces/IRuntime.sol"; 6 | import "./libraries/Cast.sol"; 7 | import "./libraries/Header.sol"; 8 | import "./libraries/Firewall.sol"; 9 | import "./libraries/Status.sol"; 10 | import "./libraries/Transaction.sol"; 11 | 12 | contract Registry is Firewall { 13 | using Cast for uint256; 14 | using Transaction for Transaction.Data; 15 | 16 | error UnauthorizedNFT(); 17 | error InvalidStatus(); 18 | error RunFailed(); 19 | error Premature(); 20 | 21 | struct Item { 22 | address nft; // erc721 token address 23 | uint96 id; // token id 24 | } 25 | 26 | /// @dev The immutable runtime. 27 | address public immutable runtime; 28 | /// @dev The next transaction id. 29 | uint256 private _rid; 30 | /// @dev The mapping of all approved nfts for the registry. 31 | mapping(address => bool) public approval; 32 | /// @dev The mapping of all proposals. 33 | mapping(uint256 => Item) public registry; 34 | /// @dev The mapping of all proposals. 35 | mapping(bytes32 => uint32) public instructions; 36 | 37 | constructor(address runtime_) { 38 | runtime = runtime_; 39 | } 40 | 41 | function approve(address nft, bool value) external auth { 42 | approval[nft] = value; 43 | } 44 | 45 | function create( 46 | address nft, 47 | address to, 48 | Header.Data memory header 49 | ) external returns (uint256 tid, uint256 rid) { 50 | if (approval[nft]) 51 | revert UnauthorizedNFT(); 52 | 53 | tid = IProposer(nft).mint(to, header); 54 | registry[(rid = _rid++)] = Item({ 55 | nft: nft, 56 | id: tid.u96() 57 | }); 58 | } 59 | 60 | function merge(uint256 rid) external { 61 | if (IProposer(registry[rid].nft).status(registry[rid].id) != Status.Approved) 62 | revert InvalidStatus(); 63 | 64 | bytes32[] memory hashes = IProposer(registry[rid].nft).hashes(registry[rid].id); 65 | uint32 maturity = IProposer(registry[rid].nft).maturity(registry[rid].id); 66 | 67 | for (uint256 i = 0; i < hashes.length; i++) { 68 | instructions[hashes[i]] = maturity; 69 | } 70 | 71 | IProposer(registry[rid].nft).done(registry[rid].id); 72 | } 73 | 74 | function run(Transaction.Data memory txn, bytes32 prevHash) external { 75 | if (prevHash != bytes32(0) && instructions[prevHash] != 1) 76 | revert RunFailed(); 77 | 78 | if (instructions[txn.hash(prevHash)] > _blockNumber()) 79 | revert Premature(); 80 | 81 | IRuntime(runtime).execute(txn.targets, txn.values, txn.calldatas(), txn.message); 82 | instructions[txn.hash(prevHash)] = 1; 83 | } 84 | 85 | function _blockNumber() internal view returns (uint32) { 86 | return uint32(block.number); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Runtime.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./interfaces/IRuntime.sol"; 5 | import "./libraries/Firewall.sol"; 6 | import "./libraries/Revert.sol"; 7 | 8 | /// @title Runtime 9 | /// @notice Executes transactions and deploys contracts for the DAO. 10 | contract Runtime is IRuntime, Firewall { 11 | /// @inheritdoc IRuntime 12 | function predict(bytes32 bytecodehash, bytes32 salt) external view returns (address) { 13 | return address( 14 | uint160( 15 | uint256( 16 | keccak256( 17 | abi.encodePacked( 18 | bytes1(0xff), 19 | address(this), 20 | salt, 21 | bytecodehash 22 | ) 23 | ) 24 | ) 25 | ) 26 | ); 27 | } 28 | 29 | /// @inheritdoc IRuntime 30 | function create(bytes memory bytecode) external returns (address deployment) { 31 | require(bytecode.length > 0, "BytecodeZero"); 32 | assembly { deployment := create(0, add(bytecode, 32), mload(bytecode)) } 33 | require(deployment != address(0), "DeployFailed"); 34 | 35 | emit Create(deployment); 36 | } 37 | 38 | /// @inheritdoc IRuntime 39 | function create2(bytes memory bytecode, bytes32 salt) external returns (address deployment) { 40 | require(bytecode.length > 0, "BytecodeZero"); 41 | assembly { deployment := create2(0, add(bytecode, 32), mload(bytecode), salt) } 42 | require(deployment != address(0), "DeployFailed"); 43 | 44 | emit Create2(deployment); 45 | } 46 | 47 | /// @inheritdoc IRuntime 48 | function call(address target, bytes calldata data) external { 49 | (bool success, bytes memory returndata) = target.call(data); 50 | 51 | if (!success) { 52 | revert(Revert.getRevertMsg(returndata)); 53 | } 54 | 55 | emit Call(target, data); 56 | } 57 | 58 | /// @inheritdoc IRuntime 59 | function execute( 60 | address[] calldata targets, 61 | uint256[] calldata values, 62 | bytes[] calldata datas, 63 | string calldata message 64 | ) external returns (bytes[] memory results) { 65 | require(targets.length == values.length, "Mismatch"); 66 | require(targets.length == datas.length, "Mismatch"); 67 | results = new bytes[](targets.length); 68 | 69 | for (uint256 i = 0; i < targets.length; i++) { 70 | bool success; 71 | 72 | if (targets[i] != address(this)) { 73 | (success, results[i]) = targets[i].call{value: values[i]}(datas[i]); 74 | } else if (targets[i] == address(this)) { 75 | (success, results[i]) = address(this).delegatecall(datas[i]); 76 | } 77 | 78 | if (!success) { 79 | revert(Revert.getRevertMsg(results[i])); 80 | } 81 | } 82 | 83 | emit Executed(keccak256(abi.encode(targets, values, datas, message)), message); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/interfaces/IERC721Permit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | 4 | import "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol"; 5 | 6 | /// @title ERC721 with permit 7 | /// @notice Extension to ERC721 that includes a permit function for signature based approvals 8 | interface IERC721Permit is IERC721 { 9 | /// @notice The permit typehash used in the permit signature 10 | /// @return The typehash for the permit 11 | function PERMIT_TYPEHASH() external pure returns (bytes32); 12 | 13 | /// @notice The domain separator used in the permit signature 14 | /// @return The domain seperator used in encoding of permit signature 15 | function DOMAIN_SEPARATOR() external view returns (bytes32); 16 | 17 | /// @notice Approve of a specific token ID for spending by spender via signature 18 | /// @param spender The account that is being approved 19 | /// @param tokenId The ID of the token that is being approved for spending 20 | /// @param deadline The deadline timestamp by which the call must be mined for the approve to work 21 | /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` 22 | /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` 23 | /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` 24 | function permit( 25 | address spender, 26 | uint256 tokenId, 27 | uint256 deadline, 28 | uint8 v, 29 | bytes32 r, 30 | bytes32 s 31 | ) external payable; 32 | } 33 | -------------------------------------------------------------------------------- /src/interfaces/IProposer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "../libraries/Header.sol"; 5 | import "../libraries/Status.sol"; 6 | 7 | struct Proposal { 8 | /// @dev The header hash. 9 | /// @dev Each hash is dependent on the previous hash for preserve sequencial execution. 10 | bytes32[] hash; 11 | 12 | /// @dev The nonce for permits 13 | uint96 nonce; 14 | /// @dev The block when voting starts. 15 | uint32 start; 16 | /// @dev The block when two-sided voting ends. 17 | uint32 end; 18 | /// @dev The block when one-sided voting ends. 19 | uint32 trial; 20 | /// @dev The block when voting ends. 21 | uint32 finality; 22 | /// @dev A boolean that controls the one-sided voting. 23 | /// - `true`: only `nack` or `pass` 24 | /// - `false`: only `ack` or `pass` 25 | bool side; 26 | /// @dev If the request is staged. 27 | bool staged; 28 | /// @dev The force close variable. 29 | bool closed; 30 | /// @dev The force close variable. 31 | bool merged; 32 | 33 | /// @dev Votes for merging the transaction. 34 | uint128 ack; 35 | /// @dev Votes against merging the transaction. 36 | uint128 nack; 37 | } 38 | 39 | interface IProposer { 40 | /// @notice Emitted when a proposal is attested. 41 | event Attest( 42 | address indexed sender, 43 | uint256 indexed tokenId, 44 | uint8 support, 45 | uint256 amount, 46 | string comment 47 | ); 48 | 49 | /// @notice Emitted when a proposal is closed. 50 | event Close(address indexed sender, uint256 indexed tokenId); 51 | 52 | /// @notice Emitted when a proposal hash is created/committed. 53 | event Commit( 54 | address indexed sender, 55 | uint256 indexed tokenId, 56 | bytes32[] hash, 57 | Header.Data header 58 | ); 59 | 60 | /// @notice Emitted when a proposal hash is contested. 61 | event Contest( 62 | address indexed sender, 63 | uint256 indexed tokenId, 64 | uint32 trial, 65 | uint32 finality, 66 | bool side 67 | ); 68 | 69 | /// @notice Emitted when a proposal is merged. 70 | event Merge(uint256 indexed tokenId); 71 | 72 | /// @notice Emitted when a proposal is opened. 73 | event Open( 74 | address indexed sender, 75 | uint256 indexed tokenId, 76 | uint32 start, 77 | uint32 end, 78 | uint32 finality 79 | ); 80 | 81 | /// @notice Emitted when a proposal is staged. 82 | event Stage(address indexed sender, uint256 indexed tokenId); 83 | 84 | /// @notice Emitted when a proposal is unstaged. 85 | event Unstage(address indexed sender, uint256 indexed tokenId); 86 | 87 | /// @notice Returns the immutable runtime. 88 | function runtime() external view returns (address); 89 | 90 | /// @notice Returns the immutable token. 91 | function token() external view returns (address); 92 | 93 | /// @notice The minimum amount of token delegations required to `open` a tx. 94 | function threshold() external view returns (uint128); 95 | 96 | /// @notice The minimum amount of `ack` required for a request to be valid. 97 | function quorum() external view returns (uint128); 98 | 99 | /// @notice The amount of blocks until a tx goes from draft to open. 100 | function delay() external view returns (uint32); 101 | 102 | /// @notice The amount of blocks that a tx is open. 103 | function period() external view returns (uint32); 104 | 105 | /// @notice The amount of blocks that a tx can be contested. 106 | function window() external view returns (uint32); 107 | 108 | /// @notice The amount of blocks that a tx is extended by when contested. 109 | function extension() external view returns (uint32); 110 | 111 | /// @notice The amount of blocks until a tx can be executed. 112 | function ttl() external view returns (uint32); 113 | 114 | /// @notice The amount of blocks until a tx goes stale. 115 | function lifespan() external view returns (uint32); 116 | 117 | /// @notice Returns a header hash. 118 | function hash(uint256 tokenId, uint256 index) external view returns (bytes32); 119 | 120 | /// @notice Returns a header hashes. 121 | function hashes(uint256 tokenId) external view returns (bytes32[] memory); 122 | 123 | /// @notice The mapping from token id to proposal. 124 | function proposals(uint256 tokenId) external view returns ( 125 | uint96 nonce, 126 | uint32 start, 127 | uint32 end, 128 | uint32 trial, 129 | uint32 finality, 130 | bool side, 131 | bool staged, 132 | bool closed, 133 | bool merged, 134 | uint128 ack, 135 | uint128 nack 136 | ); 137 | 138 | /// @notice The mapping of total amount of attests for each address. 139 | function attests(uint256 tokenId, address account) external view returns (uint256); 140 | 141 | /// @notice Returns the proposal as a struct. 142 | function get(uint256 tokenId) external view returns (Proposal memory); 143 | 144 | /// @notice The function to update contract parameters. 145 | function set(bytes4 selector, bytes memory data) external; 146 | 147 | /// @notice The next nft token id. 148 | function next() external view returns (uint256); 149 | 150 | /// @notice Returns the block number when a proposal is executable. 151 | /// @dev The block number is inclusive. 152 | function maturity(uint256 tokenId) external view returns (uint32); 153 | 154 | /// @notice Returns the block number when a proposal beocmes expired. 155 | /// @dev The block number is inclusive. 156 | function expiry(uint256 tokenId) external view returns (uint32); 157 | 158 | /// @notice Returns the status of the proposal. 159 | function status(uint256 tokenId) external view returns (Status); 160 | 161 | /// @notice Mints a new ERC721 draft proposal. 162 | function mint(address to, Header.Data calldata header) external returns (uint256 tokenId); 163 | 164 | /// @notice Stages a proposal to be opened by anyone with enough token weight. 165 | function stage(uint256 tokenId) external; 166 | 167 | /// @notice Unstage a propsal to draft status. 168 | function unstage(uint256 tokenId) external; 169 | 170 | /// @notice Opens a proposal. 171 | /// @dev Requires at least `threshold` amount token weight from the `msg.sender` to be callable. 172 | function open(uint256 tokenId) external; 173 | 174 | /// @notice Close a proposal. 175 | /// @dev A proposal can be closed except for when the status is `Merged` or `Closed`. 176 | function close(uint256 tokenId) external; 177 | 178 | /// @notice Mark a proposal as merged. 179 | /// @dev Expected to be called in the following order: registry -> runtime -> proposal. 180 | function done(uint256 tokenId) external; 181 | 182 | /// @notice Attest on a proposal with `ack`, `nack` or `abstain`. 183 | /// @dev The support mapping: 184 | /// - `ack` = `0` 185 | /// - `nack` = `1` 186 | /// - `abstain` = `2` 187 | function attest(uint256 tokenId, uint8 support, uint96 amount, string memory comment) external; 188 | 189 | /// @notice Contest a proposal after having passed/rejected. 190 | /// @dev Requires the first cycle to be `ack` majority for the contestation to begin. 191 | /// @dev The `contest` function can be continuously be called until one side wins. 192 | function contest(uint256 tokenId) external; 193 | 194 | /// @notice Updates the proposal `hash`es. 195 | function commit(uint256 tokenId, Header.Data calldata header) external; 196 | } 197 | -------------------------------------------------------------------------------- /src/interfaces/IRuntime.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IRuntime { 5 | /// @notice Emitted when a contract is deployed using `create`. 6 | event Create(address deployment); 7 | /// @notice Emitted when a contract is deployed using `create2`. 8 | event Create2(address deployment); 9 | /// @notice Emitted when a single call is executed. 10 | event Call(address target, bytes data); 11 | /// @notice Emitted when a transaction batch is executed. 12 | /// @param hash The hash is calculated similarly to the header hash. 13 | /// @param message The transaction batch message. Used for logging- and history purposes. 14 | event Executed(bytes32 indexed hash, string message); 15 | 16 | /// @notice Pre-compute the deterministic of the `create2` function. 17 | /// @param bytecodehash The hash of the bytecode used for `create2`. 18 | /// @param salt The salt used in the `create2`. 19 | /// @return The deterministic deployment address. 20 | function predict(bytes32 bytecodehash, bytes32 salt) external view returns (address); 21 | 22 | /// @notice Deploy a contract using the `create` opcode. 23 | /// @param bytecode The bytecode to be deployed as a contract. 24 | /// @return deployment The non-deterministic deployment address. 25 | function create(bytes memory bytecode) external returns (address deployment); 26 | 27 | /// @notice Deploy a contract using the `create2` opcode. 28 | /// @param bytecode The bytecode to be deployed as a contract. 29 | /// @param salt The salt used in the `create2` opcode. 30 | /// @return deployment The deterministic deployment address. 31 | function create2(bytes memory bytecode, bytes32 salt) external returns (address deployment); 32 | 33 | /// @notice Call a contract from the runtime. 34 | /// @param target The target address. 35 | /// @param data The calldata. 36 | function call(address target, bytes calldata data) external; 37 | 38 | /// @notice Execute a transaction batch (an array of transactions). 39 | /// @dev The message is used for emitting an event for what the transaction did. 40 | /// @dev The function allows for execution of empty transactions. 41 | /// @param targets The target addresses. 42 | /// @param values The ether values to be sent. 43 | /// @param datas The calldatas. 44 | /// @param message The transaction message. 45 | /// @return results The return data of the transaction batch. 46 | function execute( 47 | address[] calldata targets, 48 | uint256[] calldata values, 49 | bytes[] calldata datas, 50 | string calldata message 51 | ) external returns (bytes[] memory results); 52 | } 53 | -------------------------------------------------------------------------------- /src/libraries/Cast.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | library Cast { 5 | function u160(uint256 x) internal pure returns (uint160 y) { 6 | require (x <= type(uint160).max, "CastOverflow"); 7 | y = uint160(x); 8 | } 9 | 10 | function u96(uint256 x) internal pure returns (uint96 y) { 11 | require (x <= type(uint96).max, "CastOverflow"); 12 | y = uint96(x); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/libraries/Checkpoint.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | 5 | library Checkpoint { 6 | struct Data { 7 | uint160 amount; 8 | uint32 startBlock; 9 | uint32 endBlock; 10 | } 11 | 12 | function latest(Checkpoint.Data[] storage checkpoints) internal view returns (uint256) { 13 | uint256 len = checkpoints.length; 14 | return (len == 0) ? 0 : checkpoints[len - 1].amount; 15 | } 16 | 17 | /// @dev Look up an accounts total voting power. 18 | function lookup( 19 | Checkpoint.Data[] storage checkpoints, 20 | uint256 blockNumber 21 | ) internal view returns (uint256) { 22 | uint256 high = checkpoints.length; 23 | uint256 low = 0; 24 | while (low < high) { 25 | uint256 mid = (low & high) + (low ^ high) / 2; 26 | if (checkpoints[mid].startBlock > blockNumber) { 27 | high = mid; 28 | } else { 29 | low = mid + 1; 30 | } 31 | } 32 | 33 | return high == 0 ? 0 : checkpoints[high - 1].amount; 34 | } 35 | 36 | /// @dev Create a new checkpoint for a checkpoint array. 37 | function save( 38 | Checkpoint.Data[] storage checkpoints, 39 | function(uint256, uint256) view returns (uint256) op, 40 | uint256 delta 41 | ) internal returns (uint256 prev, uint256 next) { 42 | uint256 len = checkpoints.length; 43 | prev = len == 0 ? 0 : checkpoints[len - 1].amount; 44 | next = op(prev, delta); 45 | 46 | if (len > 0 && checkpoints[len - 1].startBlock == block.number) { 47 | checkpoints[len - 1].amount = uint160(next); 48 | } else { 49 | if (len > 0) { 50 | checkpoints[len - 1].endBlock = uint32(block.number) - uint32(1); 51 | } 52 | 53 | checkpoints.push( 54 | Checkpoint.Data({ 55 | amount: uint160(next), 56 | startBlock: uint32(block.number), 57 | endBlock: uint32(0) 58 | }) 59 | ); 60 | } 61 | } 62 | 63 | /// @dev Move the delegated votes from one account to another. 64 | function move( 65 | mapping(address => Checkpoint.Data[]) storage checkpoints, 66 | address from, 67 | address to, 68 | uint256 amount 69 | ) internal { 70 | if (from != to && amount > 0) { 71 | if (from != address(0)) { 72 | save(checkpoints[from], sub, amount); 73 | } 74 | 75 | if (to != address(0)) { 76 | save(checkpoints[to], add, amount); 77 | } 78 | } 79 | } 80 | 81 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 82 | return a + b; 83 | } 84 | 85 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 86 | return a - b; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/libraries/ERC721Permit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "openzeppelin-contracts/contracts/interfaces/IERC1271.sol"; 5 | import "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 6 | import "openzeppelin-contracts/contracts/utils/Address.sol"; 7 | 8 | import "../interfaces/IERC721Permit.sol"; 9 | 10 | /// @title ERC721 with permit 11 | /// @notice Nonfungible tokens that support an approve via signature, i.e. permit 12 | abstract contract ERC721Permit is ERC721, IERC721Permit { 13 | /// @dev Gets the current nonce for a token ID and then increments it, returning the original value 14 | function _getAndIncrementNonce(uint256 tokenId) internal virtual returns (uint256); 15 | 16 | /// @dev The hash of the name used in the permit signature verification 17 | bytes32 private immutable namehash; 18 | /// @dev The chain id that was set at deployment. 19 | uint256 internal immutable chainid_; 20 | /// @dev The domain separator that was set at deployment. 21 | bytes32 internal immutable domainseparator_; 22 | 23 | /// @notice Computes the namehash 24 | constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) { 25 | namehash = keccak256(bytes(name_)); 26 | chainid_ = block.chainid; 27 | domainseparator_ = _domainseparator(block.chainid); 28 | } 29 | 30 | /// @notice Returns the permit typehash. 31 | function PERMIT_TYPEHASH() public pure returns (bytes32) { 32 | return keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)"); 33 | } 34 | 35 | /// @inheritdoc IERC721Permit 36 | function DOMAIN_SEPARATOR() public view returns (bytes32) { 37 | return block.chainid == chainid_ ? domainseparator_ : _domainseparator(block.chainid); 38 | } 39 | 40 | /// @dev Override function to change version. 41 | function version() public pure virtual returns(string memory) { 42 | return "1"; 43 | } 44 | 45 | /// @inheritdoc IERC721Permit 46 | function permit( 47 | address spender, 48 | uint256 tokenId, 49 | uint256 deadline, 50 | uint8 v, 51 | bytes32 r, 52 | bytes32 s 53 | ) external payable override { 54 | require(block.timestamp <= deadline, 'Permit expired'); 55 | 56 | bytes32 digest = keccak256( 57 | abi.encodePacked( 58 | '\x19\x01', 59 | DOMAIN_SEPARATOR(), 60 | keccak256( 61 | abi.encode( 62 | PERMIT_TYPEHASH(), 63 | spender, 64 | tokenId, 65 | _getAndIncrementNonce(tokenId), 66 | deadline 67 | ) 68 | ) 69 | ) 70 | ); 71 | address owner = ownerOf(tokenId); 72 | require(spender != owner, 'ERC721Permit: approval to current owner'); 73 | 74 | if (Address.isContract(owner)) { 75 | require(IERC1271(owner).isValidSignature(digest, abi.encodePacked(r, s, v)) == 0x1626ba7e, 'Unauthorized'); 76 | } else { 77 | address recoveredAddress = ecrecover(digest, v, r, s); 78 | require(recoveredAddress != address(0), 'Invalid signature'); 79 | require(recoveredAddress == owner, 'Unauthorized'); 80 | } 81 | 82 | _approve(spender, tokenId); 83 | } 84 | 85 | /// @dev Compute the DOMAIN_SEPARATOR. 86 | function _domainseparator(uint256 chainid) internal view returns (bytes32) { 87 | return keccak256( 88 | abi.encode( 89 | // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') 90 | 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, 91 | namehash, 92 | keccak256(bytes(version())), 93 | chainid, 94 | address(this) 95 | ) 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/libraries/Firewall.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title Firewall 5 | /// @notice A minimal authorization contract. 6 | contract Firewall { 7 | /// @notice Emitted when an address is authorized. 8 | event Allowed(address addr); 9 | /// @notice Emitted when an address is unauthorized. 10 | event Denied(address addr); 11 | 12 | /// @dev The mapping of all authorized addresses. 13 | mapping(address => uint256) public rules; 14 | 15 | /// @dev Authorizes the `msg.sender` by default. 16 | constructor() { 17 | rules[msg.sender] = type(uint256).max; 18 | } 19 | 20 | /// @notice Authorize an address. 21 | /// @dev Can only be called by already authorized contracts. 22 | function allow(address addr) external virtual { 23 | require(rules[msg.sender] > 0, "Denied"); 24 | rules[addr] = 1; 25 | 26 | emit Allowed(addr); 27 | } 28 | 29 | /// @notice Unauthorize an address. 30 | /// @dev Can only be called by already authorized contracts. 31 | function deny(address addr) external virtual { 32 | require(rules[msg.sender] > 0, "Denied"); 33 | rules[addr] = 0; 34 | 35 | emit Denied(addr); 36 | } 37 | 38 | modifier auth { 39 | require(rules[msg.sender] > 0, "Denied"); 40 | _; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/libraries/Header.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./Transaction.sol"; 5 | 6 | library Header { 7 | using Transaction for Transaction.Data; 8 | 9 | struct Data { 10 | Transaction.Data[] data; 11 | string title; 12 | string description; 13 | } 14 | 15 | function hash(Header.Data memory self) internal pure returns (bytes32[] memory hashes) { 16 | hashes = new bytes32[](self.data.length); 17 | 18 | for (uint256 i = 0; i < self.data.length; i++) { 19 | if (i == 0) { 20 | hashes[i] = self.data[i].hash(bytes32(0)); 21 | } else { 22 | hashes[i] = self.data[i].hash(hashes[i - 1]); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/libraries/Pointer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | library Pointer { 5 | struct Data { 6 | address value; 7 | uint32 startBlock; 8 | uint32 endBlock; 9 | } 10 | 11 | function latest(Pointer.Data[] storage pointers) internal view returns (address) { 12 | uint256 len = pointers.length; 13 | return (len == 0) ? address(0) : pointers[len - 1].value; 14 | } 15 | 16 | /// @dev Look up an accounts total voting power. 17 | function lookup( 18 | Pointer.Data[] storage pointers, 19 | uint256 blockNumber 20 | ) internal view returns (address) { 21 | uint256 high = pointers.length; 22 | uint256 low = 0; 23 | 24 | while (low < high) { 25 | uint256 mid = (low & high) + (low ^ high) / 2; 26 | if (pointers[mid].startBlock > blockNumber) { 27 | high = mid; 28 | } else { 29 | low = mid + 1; 30 | } 31 | } 32 | 33 | if (high == 0) 34 | return address(0); 35 | else 36 | return pointers[high - 1].value; 37 | } 38 | 39 | /// @dev Create a new checkpoint for a checkpoint array. 40 | function save(Pointer.Data[] storage pointers, address value) internal { 41 | uint256 len = pointers.length; 42 | 43 | if (len > 0 && pointers[len - 1].startBlock == block.number) { 44 | pointers[len - 1].value = value; 45 | } else { 46 | if (len > 0) { 47 | pointers[len - 1].endBlock = uint32(block.number) - uint32(1); 48 | } 49 | 50 | pointers.push( 51 | Pointer.Data({ 52 | value: value, 53 | startBlock: uint32(block.number), 54 | endBlock: uint32(0) 55 | }) 56 | ); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/libraries/Revert.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Taken from https://github.com/sushiswap/BoringSolidity/blob/441e51c0544cf2451e6116fe00515e71d7c42e2c/contracts/BoringBatchable.sol 3 | pragma solidity >=0.6.0; 4 | 5 | library Revert { 6 | /// @dev Helper function to extract a useful revert message from a failed call. 7 | /// If the returned data is malformed or not correctly abi encoded then this call can fail itself. 8 | function getRevertMsg(bytes memory returnData) 9 | internal pure 10 | returns (string memory) 11 | { 12 | // If the _res length is less than 68, then the transaction failed silently (without a revert message) 13 | if (returnData.length < 68) return "Transaction reverted silently"; 14 | 15 | assembly { 16 | // Slice the sighash. 17 | returnData := add(returnData, 0x04) 18 | } 19 | return abi.decode(returnData, (string)); // All that remains is the revert string 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/libraries/Status.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | enum Status { 5 | Draft, 6 | Staged, 7 | Pending, 8 | Open, 9 | Validation, 10 | Contesting, 11 | Queued, 12 | Approved, 13 | Merged, 14 | Closed 15 | } 16 | -------------------------------------------------------------------------------- /src/libraries/Transaction.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | library Transaction { 5 | struct Data { 6 | address[] targets; 7 | uint256[] values; 8 | string[] signatures; 9 | bytes[] datas; // assumed to be `abi.encode`ed 10 | string message; 11 | } 12 | 13 | function calldatas(Transaction.Data memory self) internal pure returns (bytes[] memory) { 14 | bytes[] memory datas = new bytes[](self.signatures.length); 15 | 16 | for (uint256 i = 0; i < self.signatures.length; i++) { 17 | datas[i] = abi.encodePacked( 18 | // function selector 19 | bytes4(keccak256(bytes(self.signatures[i]))), 20 | // encoded arguments 21 | self.datas[i] 22 | ); 23 | } 24 | 25 | return datas; 26 | } 27 | 28 | function tree(Transaction.Data memory self) internal pure returns (bytes32) { 29 | bytes[] memory datas = new bytes[](self.signatures.length); 30 | 31 | for (uint256 i = 0; i < self.signatures.length; i++) { 32 | datas[i] = abi.encodePacked( 33 | // function selector 34 | bytes4(keccak256(bytes(self.signatures[i]))), 35 | // encoded arguments 36 | self.datas[i] 37 | ); 38 | } 39 | 40 | return keccak256( 41 | abi.encode( 42 | self.targets, 43 | self.values, 44 | datas, 45 | self.message 46 | ) 47 | ); 48 | } 49 | 50 | function hash(Transaction.Data memory self, bytes32 prev) internal pure returns (bytes32) { 51 | return keccak256(abi.encodePacked(tree(self), prev)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/Proposer.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | 6 | import "../interfaces/IProposer.sol"; 7 | import "../libraries/Status.sol"; 8 | import "../Attest.sol"; 9 | import "../Proposer.sol"; 10 | import "../Runtime.sol"; 11 | import "./mocks/Target.sol"; 12 | import "./mocks/Vm.sol"; 13 | 14 | contract User {} 15 | 16 | contract ProposerTest is DSTest, Vm { 17 | using Header for Header.Data; 18 | 19 | event Commit( 20 | address indexed sender, 21 | uint256 indexed tokenId, 22 | bytes32[] hash, 23 | Header.Data header 24 | ); 25 | 26 | User public user0; 27 | User public user1; 28 | 29 | Runtime public runtime; 30 | Attest public erc20; 31 | Proposer public proposer; 32 | 33 | function setUp() public { 34 | mine(100); 35 | 36 | user0 = new User(); 37 | user1 = new User(); 38 | 39 | runtime = new Runtime(); 40 | erc20 = new Attest("Token", "TOKEN", 18); 41 | proposer = new Proposer(address(runtime), address(erc20)); 42 | } 43 | 44 | function setUpEmptyHeader() public pure returns (Header.Data memory) { 45 | Transaction.Data[] memory data = new Transaction.Data[](1); 46 | Header.Data memory header = Header.Data({ 47 | data: data, 48 | title: "", 49 | description: "" 50 | }); 51 | 52 | return header; 53 | } 54 | 55 | function setUpEmptyProposal(address to) public returns (uint256) { 56 | Transaction.Data[] memory data = new Transaction.Data[](1); 57 | Header.Data memory header = Header.Data({ 58 | data: data, 59 | title: "", 60 | description: "" 61 | }); 62 | return proposer.mint(to, header); 63 | } 64 | 65 | /** 66 | * `mint` 67 | */ 68 | 69 | function testMint() public { 70 | Header.Data memory header = setUpEmptyHeader(); 71 | 72 | uint256 tokenIdExpected; 73 | uint256 tokenId; 74 | 75 | tokenIdExpected = proposer.next(); 76 | tokenId = proposer.mint(address(this), header); 77 | assertEq(tokenId, tokenIdExpected); 78 | assertEq(tokenId, 0); 79 | 80 | tokenIdExpected = proposer.next(); 81 | tokenId = proposer.mint(address(this), header); 82 | assertEq(tokenId, tokenIdExpected); 83 | assertEq(tokenId, 1); 84 | } 85 | 86 | /** 87 | * `stage` 88 | */ 89 | 90 | function testStage() public { 91 | uint256 tokenId = setUpEmptyProposal(address(this)); 92 | 93 | assertEq(uint(proposer.status(tokenId)), uint(Status.Draft)); 94 | proposer.stage(tokenId); 95 | assertEq(uint(proposer.status(tokenId)), uint(Status.Staged)); 96 | } 97 | 98 | function testStageUnauthorizedRevert() public { 99 | uint256 tokenId = setUpEmptyProposal(address(this)); 100 | 101 | startPrank(address(user0)); 102 | assertEq(uint(proposer.status(tokenId)), uint(Status.Draft)); 103 | expectRevert("Unauthorized"); 104 | proposer.stage(tokenId); 105 | } 106 | 107 | function testStageNotDraftRevert() public { 108 | uint256 tokenId = setUpEmptyProposal(address(this)); 109 | 110 | proposer.close(tokenId); 111 | assertEq(uint(proposer.status(tokenId)), uint(Status.Closed)); 112 | expectRevert("NotDraft"); 113 | proposer.stage(tokenId); 114 | } 115 | 116 | /** 117 | * `unstage` 118 | */ 119 | 120 | function testUnstage() public { 121 | uint256 tokenId = setUpEmptyProposal(address(this)); 122 | 123 | proposer.stage(tokenId); 124 | assertEq(uint(proposer.status(tokenId)), uint(Status.Staged)); 125 | proposer.unstage(tokenId); 126 | assertEq(uint(proposer.status(tokenId)), uint(Status.Draft)); 127 | 128 | proposer.stage(tokenId); 129 | assertEq(uint(proposer.status(tokenId)), uint(Status.Staged)); 130 | proposer.unstage(tokenId); 131 | assertEq(uint(proposer.status(tokenId)), uint(Status.Draft)); 132 | } 133 | 134 | function testUnstageUnauthorizedRevert() public { 135 | uint256 tokenId = setUpEmptyProposal(address(this)); 136 | 137 | proposer.stage(tokenId); 138 | assertEq(uint(proposer.status(tokenId)), uint(Status.Staged)); 139 | 140 | startPrank(address(user0)); 141 | expectRevert("Unauthorized"); 142 | proposer.unstage(tokenId); 143 | } 144 | 145 | function testUnstageNotStagedRevert() public { 146 | uint256 tokenId = setUpEmptyProposal(address(this)); 147 | 148 | expectRevert("NotStaged"); 149 | proposer.unstage(tokenId); 150 | } 151 | 152 | /** 153 | * `open` 154 | */ 155 | 156 | function testOpen() public { 157 | uint256 tokenId = setUpEmptyProposal(address(this)); 158 | 159 | proposer.open(tokenId); 160 | assertEq(uint(proposer.status(tokenId)), uint(Status.Pending)); 161 | mine(proposer.delay()); 162 | assertEq(uint(proposer.status(tokenId)), uint(Status.Open)); 163 | } 164 | 165 | function testOpenWithThreshold() public { 166 | uint256 tokenId = setUpEmptyProposal(address(this)); 167 | proposer.set(IProposer.threshold.selector, abi.encode(1e18)); 168 | erc20.mint(address(this), 1e18); 169 | erc20.delegate(address(this)); 170 | 171 | proposer.open(tokenId); 172 | assertEq(uint(proposer.status(tokenId)), uint(Status.Pending)); 173 | mine(proposer.delay()); 174 | assertEq(uint(proposer.status(tokenId)), uint(Status.Open)); 175 | } 176 | 177 | function testOpenStaged() public { 178 | uint256 tokenId = setUpEmptyProposal(address(this)); 179 | proposer.set(IProposer.threshold.selector, abi.encode(1e18)); 180 | proposer.stage(tokenId); 181 | erc20.mint(address(user0), 1e18); 182 | 183 | startPrank(address(user0)); 184 | erc20.delegate(address(user0)); 185 | assertEq(uint(proposer.status(tokenId)), uint(Status.Staged)); 186 | proposer.open(tokenId); 187 | assertEq(uint(proposer.status(tokenId)), uint(Status.Pending)); 188 | mine(proposer.delay()); 189 | assertEq(uint(proposer.status(tokenId)), uint(Status.Open)); 190 | } 191 | 192 | function testOpenNotStagedRevert() public { 193 | uint256 tokenId = setUpEmptyProposal(address(this)); 194 | 195 | startPrank(address(user0)); 196 | expectRevert("NotStaged"); 197 | proposer.open(tokenId); 198 | } 199 | 200 | function testOpenInsufficientTokensRevert() public { 201 | uint256 tokenId = setUpEmptyProposal(address(this)); 202 | proposer.set(IProposer.threshold.selector, abi.encode(1e18)); 203 | proposer.stage(tokenId); 204 | erc20.mint(address(user0), 1e18 - 1); 205 | 206 | startPrank(address(user0)); 207 | erc20.delegate(address(user0)); 208 | expectRevert("Insufficient"); 209 | proposer.open(tokenId); 210 | } 211 | 212 | /** 213 | * `attest` 214 | */ 215 | 216 | function testAttest() public { 217 | uint256 tokenId = setUpEmptyProposal(address(this)); 218 | erc20.mint(address(this), 1e18); 219 | erc20.mint(address(user0), 3e18); 220 | erc20.mint(address(user1), 3e18); 221 | proposer.open(tokenId); 222 | erc20.delegate(address(this)); 223 | prank(address(user0)); 224 | erc20.delegate(address(user0)); 225 | prank(address(user1)); 226 | erc20.delegate(address(this)); 227 | mine(proposer.delay()); 228 | 229 | proposer.attest(tokenId, 0, 5e17, ""); 230 | assertEq(proposer.get(tokenId).ack, 5e17); 231 | assertEq(proposer.get(tokenId).nack, 0); 232 | assertEq(proposer.attests(tokenId, address(this)), 5e17); 233 | 234 | proposer.attest(tokenId, 0, 3e18 + 5e17, ""); 235 | assertEq(proposer.get(tokenId).ack, 4e18); 236 | assertEq(proposer.get(tokenId).nack, 0); 237 | assertEq(proposer.attests(tokenId, address(this)), 4e18); 238 | 239 | prank(address(user0)); 240 | proposer.attest(tokenId, 0, 3e18, ""); 241 | assertEq(proposer.get(tokenId).ack, 7e18); 242 | assertEq(proposer.get(tokenId).nack, 0); 243 | assertEq(proposer.attests(tokenId, address(user0)), 3e18); 244 | } 245 | 246 | function testAttestWithContestation() public { 247 | uint256 tokenId = setUpEmptyProposal(address(this)); 248 | erc20.mint(address(this), 2e18); 249 | erc20.mint(address(user0), 5e18); 250 | erc20.mint(address(user1), 3e18); 251 | proposer.open(tokenId); 252 | erc20.delegate(address(this)); 253 | prank(address(user0)); 254 | erc20.delegate(address(user0)); 255 | prank(address(user1)); 256 | erc20.delegate(address(this)); 257 | roll(proposer.get(tokenId).start); 258 | 259 | // `attest` with 2e18 for ack 260 | proposer.attest(tokenId, 0, 1e18, ""); 261 | prank(address(user0)); 262 | proposer.attest(tokenId, 0, 1e18, ""); 263 | 264 | // roll to end and `contest` 265 | roll(proposer.get(tokenId).end); 266 | assertEq(uint(proposer.status(tokenId)), uint(Status.Validation)); 267 | proposer.contest(tokenId); 268 | assertEq(uint(proposer.status(tokenId)), uint(Status.Contesting)); 269 | 270 | // error on ack attest 271 | expectRevert(abi.encodeWithSignature("InvalidChoice(uint8)", uint8(0))); 272 | proposer.attest(tokenId, 0, 0, ""); 273 | // should pass on nack attest 274 | proposer.attest(tokenId, 1, 1e18, ""); 275 | 276 | roll(proposer.get(tokenId).trial); 277 | // should revert because the contestation failed (2e18 ack vs 1e18 nack) 278 | expectRevert(abi.encodeWithSignature("ContestationFailed()")); 279 | proposer.contest(tokenId); 280 | } 281 | 282 | function testAttestWithContiuousContestation() public { 283 | uint256 tokenId = setUpEmptyProposal(address(this)); 284 | erc20.mint(address(this), 2e18); 285 | erc20.mint(address(user0), 5e18); 286 | erc20.mint(address(user1), 3e18); 287 | proposer.open(tokenId); 288 | erc20.delegate(address(this)); 289 | prank(address(user0)); 290 | erc20.delegate(address(user0)); 291 | prank(address(user1)); 292 | erc20.delegate(address(user1)); 293 | roll(proposer.get(tokenId).start); 294 | 295 | // `attest` with 2e18 for ack 296 | proposer.attest(tokenId, 0, 1e18, ""); 297 | prank(address(user0)); 298 | proposer.attest(tokenId, 0, 1e18, ""); 299 | 300 | // roll to end and `contest` 301 | roll(proposer.get(tokenId).end); 302 | proposer.contest(tokenId); 303 | 304 | // attest for nack, so that nack > ack 305 | proposer.attest(tokenId, 1, 1e18, ""); 306 | prank(address(user0)); 307 | proposer.attest(tokenId, 1, 2e18, ""); 308 | 309 | // should be 2e18 ack and 3e18 nack, and we can contest again 310 | assertEq(proposer.get(tokenId).ack, 2e18); 311 | assertEq(proposer.get(tokenId).nack, 3e18); 312 | assert(proposer.get(tokenId).side == true); 313 | roll(proposer.get(tokenId).trial); 314 | proposer.contest(tokenId); 315 | assert(proposer.get(tokenId).side == false); 316 | 317 | // fail when trying to vote for nack during ack-time 318 | expectRevert(abi.encodeWithSignature("InvalidChoice(uint8)", uint8(1))); 319 | proposer.attest(tokenId, 1, 0, ""); 320 | 321 | prank(address(user0)); 322 | proposer.attest(tokenId, 0, 1e18, ""); 323 | prank(address(user1)); 324 | proposer.attest(tokenId, 0, 1e18, ""); 325 | assertEq(proposer.get(tokenId).ack, 4e18); 326 | assertEq(proposer.get(tokenId).nack, 3e18); 327 | 328 | // roll to finality and revert when contesting 329 | roll(proposer.get(tokenId).finality); 330 | assertEq(uint(proposer.status(tokenId)), uint(Status.Queued)); 331 | expectRevert(abi.encodeWithSignature("StatusError(uint8)", uint8(Status.Queued))); 332 | proposer.contest(tokenId); 333 | 334 | // roll to maturity and revert when contesting 335 | roll(proposer.maturity(tokenId)); 336 | assertEq(uint(proposer.status(tokenId)), uint(Status.Approved)); 337 | expectRevert(abi.encodeWithSignature("StatusError(uint8)", uint8(Status.Approved))); 338 | proposer.contest(tokenId); 339 | } 340 | 341 | function testAttestStatusErrorRevert() public { 342 | uint256 tokenId = setUpEmptyProposal(address(this)); 343 | 344 | // can't attest on draft 345 | expectRevert(abi.encodeWithSignature("StatusError(uint8)", uint8(Status.Draft))); 346 | proposer.attest(tokenId, 0, 0, ""); 347 | 348 | // can't attest on pending 349 | proposer.open(tokenId); 350 | expectRevert(abi.encodeWithSignature("StatusError(uint8)", uint8(Status.Pending))); 351 | proposer.attest(tokenId, 0, 0, ""); 352 | 353 | // still pending 354 | mine(proposer.delay() - 1); 355 | expectRevert(abi.encodeWithSignature("StatusError(uint8)", uint8(Status.Pending))); 356 | proposer.attest(tokenId, 0, 0, ""); 357 | 358 | // should pass, now open 359 | roll(proposer.get(tokenId).start); 360 | proposer.attest(tokenId, 0, 0, ""); 361 | } 362 | 363 | function testAttestNonExistentTokenRevert() public { 364 | expectRevert(abi.encodeWithSignature("UndefinedId(uint256)", 0)); 365 | proposer.attest(0, 0, 0, ""); 366 | 367 | expectRevert(abi.encodeWithSignature("UndefinedId(uint256)", 3)); 368 | proposer.attest(3, 0, 0, ""); 369 | } 370 | 371 | function testAttestOnClosedRevert() public { 372 | uint256 tokenId = setUpEmptyProposal(address(this)); 373 | proposer.open(tokenId); 374 | mine(proposer.delay()); 375 | 376 | // close and attest should revert 377 | proposer.close(tokenId); 378 | expectRevert(abi.encodeWithSignature("StatusError(uint8)", uint8(Status.Closed))); 379 | proposer.attest(tokenId, 0, 0, ""); 380 | } 381 | 382 | function testAttestOverflowRevert() public { 383 | uint256 tokenId = setUpEmptyProposal(address(this)); 384 | proposer.open(tokenId); 385 | mine(proposer.delay()); 386 | 387 | expectRevert(abi.encodeWithSignature("AttestOverflow()")); 388 | proposer.attest(tokenId, 0, 1, ""); 389 | } 390 | 391 | /** 392 | * `commit` 393 | */ 394 | 395 | function testCommit() public { 396 | Target target = new Target(); 397 | uint256 tokenId = setUpEmptyProposal(address(this)); 398 | 399 | Transaction.Data[] memory transactions = new Transaction.Data[](2); 400 | Header.Data memory header = Header.Data({ 401 | data: transactions, 402 | title: "Update the target value", 403 | description: "This proposal updates the target value." 404 | }); 405 | 406 | address[] memory targets_ = new address[](1); 407 | uint256[] memory values_ = new uint256[](1); 408 | string[] memory signatures_ = new string[](1); 409 | bytes[] memory datas_ = new bytes[](1); 410 | targets_[0] = address(target); 411 | values_[0] = 0; 412 | signatures_[0] = "update(uint256)"; 413 | datas_[0] = abi.encode(1337); 414 | transactions[0] = Transaction.Data({ 415 | targets: targets_, 416 | values: values_, 417 | signatures: signatures_, 418 | datas: datas_, 419 | message: "tweak: update target value 1337" 420 | }); 421 | 422 | address[] memory targets__ = new address[](1); 423 | uint256[] memory values__ = new uint256[](1); 424 | string[] memory signatures__ = new string[](1); 425 | bytes[] memory datas__ = new bytes[](1); 426 | targets__[0] = address(target); 427 | values__[0] = 0; 428 | signatures__[0] = "update(uint256)"; 429 | datas__[0] = abi.encode(42); 430 | transactions[1] = Transaction.Data({ 431 | targets: targets__, 432 | values: values__, 433 | signatures: signatures__, 434 | datas: datas__, 435 | message: "tweak: update target value to 42" 436 | }); 437 | 438 | expectEmit(true, true, true, true); 439 | emit Commit(address(this), tokenId, header.hash(), header); 440 | proposer.commit(tokenId, header); 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /src/test/Registry.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | 6 | import "../interfaces/IProposer.sol"; 7 | import "../libraries/Status.sol"; 8 | import "../Attest.sol"; 9 | import "../Proposer.sol"; 10 | import "../Registry.sol"; 11 | import "../Runtime.sol"; 12 | import "./mocks/Target.sol"; 13 | import "./mocks/Vm.sol"; 14 | 15 | contract RegistryTest is DSTest, Vm { 16 | Target public target = new Target(); 17 | 18 | Runtime public runtime; 19 | Attest public erc20; 20 | Registry public registry; 21 | Proposer public proposer; 22 | 23 | function setUp() public { 24 | runtime = new Runtime(); 25 | erc20 = new Attest("Token", "TOKEN", 18); 26 | registry = new Registry(address(runtime)); 27 | proposer = new Proposer(address(runtime), address(erc20)); 28 | 29 | runtime.allow(address(registry)); 30 | proposer.allow(address(registry)); 31 | } 32 | 33 | function setUpEmptyHeader() public pure returns (Header.Data memory) { 34 | Transaction.Data[] memory data = new Transaction.Data[](1); 35 | Header.Data memory header = Header.Data({ 36 | data: data, 37 | title: "", 38 | description: "" 39 | }); 40 | 41 | return header; 42 | } 43 | 44 | function setUpTargetHeader() public view returns (Header.Data memory) { 45 | Transaction.Data[] memory transactions = new Transaction.Data[](1); 46 | Header.Data memory header = Header.Data({ 47 | data: transactions, 48 | title: "Update the target value", 49 | description: "This proposal updates the target value." 50 | }); 51 | 52 | address[] memory targets = new address[](1); 53 | uint256[] memory values = new uint256[](1); 54 | string[] memory signatures = new string[](1); 55 | bytes[] memory datas = new bytes[](1); 56 | targets[0] = address(target); 57 | values[0] = 0; 58 | signatures[0] = "update(uint256)"; 59 | datas[0] = abi.encode(uint256(1337)); 60 | transactions[0] = Transaction.Data({ 61 | targets: targets, 62 | values: values, 63 | signatures: signatures, 64 | datas: datas, 65 | message: "tweak: update target value 1337" 66 | }); 67 | 68 | return header; 69 | } 70 | 71 | /** 72 | * `create` 73 | */ 74 | 75 | function testCreate() public { 76 | address nft; 77 | uint96 id; 78 | 79 | registry.create(address(proposer), address(this), setUpEmptyHeader()); 80 | (nft, id) = registry.registry(0); 81 | assertEq(nft, address(proposer)); 82 | assertEq(id, 0); 83 | 84 | registry.create(address(proposer), address(this), setUpEmptyHeader()); 85 | (nft, id) = registry.registry(1); 86 | assertEq(nft, address(proposer)); 87 | assertEq(id, 1); 88 | } 89 | 90 | /** 91 | * `merge` 92 | */ 93 | 94 | function testMerge() public { 95 | erc20.mint(address(this), 1e18); 96 | erc20.delegate(address(this)); 97 | 98 | (uint256 tokenId, uint256 registryId) = registry.create( 99 | address(proposer), 100 | address(this), 101 | setUpEmptyHeader() 102 | ); 103 | proposer.commit(tokenId, setUpTargetHeader()); 104 | proposer.open(tokenId); 105 | mine(proposer.delay()); 106 | assertEq(uint(proposer.status(tokenId)), uint(Status.Open)); 107 | proposer.attest(tokenId, 0, 1e18, ""); 108 | mine(proposer.get(tokenId).finality); 109 | assertEq(uint(proposer.status(tokenId)), uint(Status.Approved)); 110 | registry.merge(registryId); 111 | assertEq(uint(proposer.status(tokenId)), uint(Status.Merged)); 112 | } 113 | 114 | /** 115 | * `run` 116 | */ 117 | 118 | function testRun() public { 119 | erc20.mint(address(this), 1e18); 120 | erc20.delegate(address(this)); 121 | 122 | Header.Data memory header = setUpTargetHeader(); 123 | (uint256 tokenId, uint256 registryId) = registry.create( 124 | address(proposer), 125 | address(this), 126 | header 127 | ); 128 | proposer.open(tokenId); 129 | mine(proposer.delay()); 130 | proposer.attest(tokenId, 0, 1e18, ""); 131 | mine(proposer.get(tokenId).finality); 132 | registry.merge(registryId); 133 | 134 | registry.run(header.data[0], bytes32(0)); 135 | assertEq(target.value(), 1337); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/test/Runtime.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "ut-0/ERC20.sol"; 6 | 7 | import "../Runtime.sol"; 8 | import "./mocks/Vm.sol"; 9 | 10 | contract RuntimeTest is DSTest, Vm { 11 | Runtime public runtime; 12 | 13 | function setUp() public { 14 | runtime = new Runtime(); 15 | } 16 | 17 | function testCreate() public { 18 | bytes memory bytecode = abi.encodePacked( 19 | type(ERC20).creationCode, 20 | abi.encode("Token", "TOKEN", 18) 21 | ); 22 | 23 | ERC20 erc20 = ERC20(runtime.create(bytecode)); 24 | assertEq(erc20.name(), "Token"); 25 | assertEq(erc20.symbol(), "TOKEN"); 26 | assertEq(erc20.decimals(), 18); 27 | } 28 | 29 | function testCreate2() public { 30 | bytes memory bytecode = abi.encodePacked( 31 | type(ERC20).creationCode, 32 | abi.encode("Token", "TOKEN", 18) 33 | ); 34 | 35 | ERC20 erc20 = ERC20(runtime.create2(bytecode, 0)); 36 | assertEq(address(erc20), runtime.predict(keccak256(bytecode), 0)); 37 | assertEq(erc20.name(), "Token"); 38 | assertEq(erc20.symbol(), "TOKEN"); 39 | assertEq(erc20.decimals(), 18); 40 | } 41 | 42 | function testExecuteCreate2() public { 43 | bytes memory bytecode = abi.encodePacked( 44 | type(ERC20).creationCode, 45 | abi.encode("Token", "TOKEN", 18) 46 | ); 47 | 48 | address[] memory targets = new address[](1); 49 | uint256[] memory values = new uint256[](1); 50 | bytes[] memory datas = new bytes[](1); 51 | string memory message; 52 | targets[0] = address(runtime); 53 | values[0] = 0; 54 | datas[0] = abi.encodePacked( 55 | bytes4(keccak256(bytes("create2(bytes,bytes32)"))), // selector 56 | abi.encode(bytecode, bytes32(0)) // encoded arguments 57 | ); 58 | message = "deploy erc20"; 59 | 60 | bytes[] memory results = runtime.execute(targets, values, datas, message); 61 | assertEq( 62 | abi.decode(results[0], (address)), 63 | runtime.predict(keccak256(bytecode), 0) 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/mocks/Target.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | contract Target { 5 | uint256 public value; 6 | 7 | event Updated(uint256 value_); 8 | 9 | function update(uint256 value_) public { 10 | value = value_; 11 | 12 | emit Updated(value); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/mocks/Vm.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IVm { 5 | /** 6 | * Cheatcodes 7 | */ 8 | 9 | /// @dev Returns the cheatcode contract address. 10 | function vm() external view returns (IVm); 11 | 12 | /// @dev Sets `block.timestamp` to `x`. 13 | function warp(uint256 x) external; 14 | 15 | /// @dev Sets `block.number` to `x`. 16 | function roll(uint x) external; 17 | 18 | /// @dev Sets the slot `loc` of contract `c` to `val`. 19 | function store(address c, bytes32 loc, bytes32 val) external; 20 | 21 | /// @dev Reads the slot `loc` of contract `c`. 22 | function load(address c, bytes32 loc) external returns (bytes32); 23 | 24 | /// @dev Signs the `digest` using the private key `sk`. Note that signatures produced via `hevm.sign` 25 | /// will leak the private key. 26 | function sign(uint sk, bytes32 digest) external returns (uint8 v, bytes32 r, bytes32 s); 27 | 28 | /// @dev Derives an ethereum address from the private key `sk`. Note that `hevm.addr(0)` will fail with 29 | /// `BadCheatCode` as `0` is an invalid ECDSA private key. 30 | function addr(uint sk) external returns (address); 31 | 32 | /// @dev Executes the arguments as a command in the system shell and returns stdout. Note that this 33 | /// cheatcode means test authors can execute arbitrary code on user machines as part of a call to 34 | /// `dapp test`, for this reason all calls to `ffi` will fail unless the `--ffi` flag is passed. 35 | function ffi(string[] calldata data) external returns (bytes memory); 36 | 37 | /// @dev Sets an account's balance 38 | function deal(address who, uint256 amount) external; 39 | 40 | /// @dev Sets the contract code at some address contract code 41 | function etch(address where, bytes calldata what) external; 42 | 43 | /// @dev Sets the *next* call's msg.sender to be the input address 44 | function prank(address sender) external; 45 | 46 | /// @dev Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called 47 | function startPrank(address sender) external; 48 | 49 | /// @dev Resets subsequent calls' msg.sender to be `address(this)` 50 | function stopPrank() external; 51 | 52 | /// @dev Tells the evm to expect that the next call reverts with specified error bytes. 53 | function expectRevert(bytes calldata expectedError) external; 54 | 55 | /// @dev Expects the next emitted event. Params check topic 1, topic 2, topic 3 and data are the same. 56 | function expectEmit(bool x, bool y, bool z, bool w) external; 57 | 58 | /** 59 | * Extensions 60 | */ 61 | 62 | /// @dev Increment `block.number` by `x`. 63 | function mine(uint256 x) external; 64 | 65 | /// @dev Increment `block.timestamp` by `x`. 66 | function timetravel(uint256 x) external; 67 | 68 | /// @dev Reset the `block.number` and `block.timestamp`. 69 | function reset() external; 70 | } 71 | 72 | contract Vm is IVm { 73 | /// @inheritdoc IVm 74 | IVm public vm = IVm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); 75 | 76 | /// @inheritdoc IVm 77 | function warp(uint256 x) public { 78 | vm.warp(x); 79 | } 80 | 81 | /// @inheritdoc IVm 82 | function roll(uint256 x) public { 83 | vm.roll(x); 84 | } 85 | 86 | /// @inheritdoc IVm 87 | function store(address c, bytes32 loc, bytes32 val) public { 88 | vm.store(c, loc, val); 89 | } 90 | 91 | /// @inheritdoc IVm 92 | function load(address c, bytes32 loc) public returns (bytes32) { 93 | return vm.load(c, loc); 94 | } 95 | 96 | /// @inheritdoc IVm 97 | function sign(uint sk, bytes32 digest) public returns (uint8 v, bytes32 r, bytes32 s) { 98 | (v, r, s) = vm.sign(sk, digest); 99 | } 100 | 101 | /// @inheritdoc IVm 102 | function addr(uint sk) public returns (address) { 103 | return vm.addr(sk); 104 | } 105 | 106 | /// @inheritdoc IVm 107 | function ffi(string[] calldata data) public returns (bytes memory) { 108 | return vm.ffi(data); 109 | } 110 | 111 | /// @inheritdoc IVm 112 | function deal(address who, uint256 amount) public { 113 | vm.deal(who, amount); 114 | } 115 | 116 | /// @inheritdoc IVm 117 | function etch(address where, bytes calldata what) public { 118 | vm.etch(where, what); 119 | } 120 | 121 | /// @inheritdoc IVm 122 | function prank(address sender) public { 123 | vm.prank(sender); 124 | } 125 | 126 | /// @inheritdoc IVm 127 | function startPrank(address sender) public { 128 | vm.startPrank(sender); 129 | } 130 | 131 | /// @inheritdoc IVm 132 | function stopPrank() public { 133 | vm.stopPrank(); 134 | } 135 | 136 | /// @inheritdoc IVm 137 | function expectRevert(bytes memory expectedError) public { 138 | vm.expectRevert(expectedError); 139 | } 140 | 141 | /// @inheritdoc IVm 142 | function expectEmit(bool x, bool y, bool z, bool w) public { 143 | vm.expectEmit(x, y, z, w); 144 | } 145 | 146 | /// @inheritdoc IVm 147 | function mine(uint256 x) public { 148 | vm.roll(block.number + x); 149 | } 150 | 151 | /// @inheritdoc IVm 152 | function timetravel(uint256 x) public { 153 | vm.warp(block.timestamp + x); 154 | } 155 | 156 | /// @inheritdoc IVm 157 | function reset() public { 158 | vm.roll(0); 159 | vm.warp(0); 160 | } 161 | } 162 | --------------------------------------------------------------------------------