Skip to content

Commit

Permalink
Merge pull request #6 from gnosis/feat/zodiac-compliant
Browse files Browse the repository at this point in the history
Feat: Delay module is zodiac compliant
  • Loading branch information
auryn-macmillan authored Aug 10, 2021
2 parents 2d89331 + e29cf88 commit f3274df
Show file tree
Hide file tree
Showing 10 changed files with 1,752 additions and 1,554 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 15
node-version: 14
- uses: actions/cache@v2
with:
path: '**/node_modules'
Expand All @@ -19,4 +19,4 @@ jobs:
- name: Coveralls
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v15.6.0
v14.17.4
2 changes: 1 addition & 1 deletion .solcover.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
skipFiles: ['test/Imports.sol', 'test/TestExecutor.sol'],
skipFiles: ['test/Imports.sol', 'test/TestExecutor.sol', 'test/Mock.sol'],
mocha: {
grep: "@skip-on-coverage", // Find everything with this tag
invert: true // Run the grep's inverse set.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# DAO Module
# Delay Module
[![Build Status](/~https://github.com/gnosis/SafeDelay/workflows/SafeDelay/badge.svg?branch=main)](/~https://github.com/gnosis/SafeDelay/actions)
[![Coverage Status](https://coveralls.io/repos/github/gnosis/SafeDelay/badge.svg?branch=main)](https://coveralls.io/github/gnosis/SafeDelay)

Expand Down
165 changes: 30 additions & 135 deletions contracts/DelayModule.sol
Original file line number Diff line number Diff line change
@@ -1,28 +1,9 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.0;

contract Enum {
enum Operation {
Call,
DelegateCall
}
}
import "@gnosis/zodiac/contracts/core/Modifier.sol";

interface Executor {
/// @dev Allows a Module to execute a transaction.
/// @param to Destination address of module transaction.
/// @param value Ether value of module transaction.
/// @param data Data payload of module transaction.
/// @param operation Operation type of module transaction.
function execTransactionFromModule(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation
) external returns (bool success);
}

contract DelayModule {
contract DelayModule is Modifier {
event DelayModuleSetup(address indexed initiator, address indexed safe);
event TransactionAdded(
uint256 indexed queueNonce,
Expand All @@ -33,12 +14,6 @@ contract DelayModule {
Enum.Operation operation
);

event EnabledModule(address module);
event DisabledModule(address module);

address internal constant SENTINEL_MODULES = address(0x1);

Executor public executor;
uint256 public txCooldown;
uint256 public txExpiration;
uint256 public txNonce;
Expand All @@ -47,51 +22,44 @@ contract DelayModule {
mapping(uint256 => bytes32) public txHash;
// Mapping of queue nonce to creation timestamp.
mapping(uint256 => uint256) public txCreatedAt;
// Mapping of modules
mapping(address => address) internal modules;

constructor(
Executor _executor,
uint256 cooldown,
uint256 expiration
address _owner,
address _executor,
uint256 _cooldown,
uint256 _expiration
) {
setUp(_executor, cooldown, expiration);
setUp(_owner, _executor, _cooldown, _expiration);
}

/// @param _owner Address of the owner
/// @param _executor Address of the executor (e.g. a Safe)
/// @param cooldown Cooldown in seconds that should be required after a transaction is proposed
/// @param expiration Duration that a proposed transaction is valid for after the cooldown, in seconds (or 0 if valid forever)
/// @param _cooldown Cooldown in seconds that should be required after a transaction is proposed
/// @param _expiration Duration that a proposed transaction is valid for after the cooldown, in seconds (or 0 if valid forever)
/// @notice There need to be at least 60 seconds between end of cooldown and expiration
function setUp(
Executor _executor,
uint256 cooldown,
uint256 expiration
address _owner,
address _executor,
uint256 _cooldown,
uint256 _expiration
) public {
require(executor == address(0), "Module is already initialized");
require(
address(executor) == address(0),
"Module is already initialized"
);
require(
expiration == 0 || expiration >= 60,
_expiration == 0 || _expiration >= 60,
"Expiratition must be 0 or at least 60 seconds"
);
if (address(_executor) != address(0)) setupModules();

executor = _executor;
txExpiration = expiration;
txCooldown = cooldown;
txExpiration = _expiration;
txCooldown = _cooldown;

emit DelayModuleSetup(msg.sender, address(_executor));
}

modifier executorOnly() {
require(msg.sender == address(executor), "Not authorized");
_;
}
if (_executor != address(0)) {
__Ownable_init();
transferOwnership(_owner);
setupModules();
}

modifier moduleOnly() {
require(modules[msg.sender] != address(0), "Module not authorized");
_;
emit DelayModuleSetup(msg.sender, _executor);
}

function setupModules() internal {
Expand All @@ -102,50 +70,18 @@ contract DelayModule {
modules[SENTINEL_MODULES] = SENTINEL_MODULES;
}

/// @dev Disables a module that can add transactions to the queue
/// @param prevModule Module that pointed to the module to be removed in the linked list
/// @param module Module to be removed
/// @notice This can only be called by the executor
function disableModule(address prevModule, address module)
public
executorOnly()
{
require(
module != address(0) && module != SENTINEL_MODULES,
"Invalid module"
);
require(modules[prevModule] == module, "Module already disabled");
modules[prevModule] = modules[module];
modules[module] = address(0);
emit DisabledModule(module);
}

/// @dev Enables a module that can add transactions to the queue
/// @param module Address of the module to be enabled
/// @notice This can only be called by the executor
function enableModule(address module) public executorOnly() {
require(
module != address(0) && module != SENTINEL_MODULES,
"Invalid module"
);
require(modules[module] == address(0), "Module already enabled");
modules[module] = modules[SENTINEL_MODULES];
modules[SENTINEL_MODULES] = module;
emit EnabledModule(module);
}

/// @dev Sets the cooldown before a transaction can be executed.
/// @param cooldown Cooldown in seconds that should be required before the transaction can be executed
/// @notice This can only be called by the executor
function setTxCooldown(uint256 cooldown) public executorOnly() {
function setTxCooldown(uint256 cooldown) public onlyOwner {
txCooldown = cooldown;
}

/// @dev Sets the duration for which a transaction is valid.
/// @param expiration Duration that a transaction is valid in seconds (or 0 if valid forever) after the cooldown
/// @notice There need to be at least 60 seconds between end of cooldown and expiration
/// @notice This can only be called by the executor
function setTxExpiration(uint256 expiration) public executorOnly() {
function setTxExpiration(uint256 expiration) public onlyOwner {
require(
expiration == 0 || expiration >= 60,
"Expiratition must be 0 or at least 60 seconds"
Expand All @@ -156,7 +92,7 @@ contract DelayModule {
/// @dev Sets transaction nonce. Used to invalidate or skip transactions in queue.
/// @param _nonce New transaction nonce
/// @notice This can only be called by the executor
function setTxNonce(uint256 _nonce) public executorOnly() {
function setTxNonce(uint256 _nonce) public onlyOwner {
require(
_nonce > txNonce,
"New nonce must be higher than current txNonce"
Expand All @@ -176,7 +112,7 @@ contract DelayModule {
uint256 value,
bytes calldata data,
Enum.Operation operation
) public moduleOnly() {
) public override moduleOnly returns (bool success) {
txHash[queueNonce] = getTransactionHash(to, value, data, operation);
txCreatedAt[queueNonce] = block.timestamp;
emit TransactionAdded(
Expand All @@ -188,6 +124,7 @@ contract DelayModule {
operation
);
queueNonce++;
success = true;
}

/// @dev Executes the next transaction only if the cooldown has passed and the transaction has not expired
Expand Down Expand Up @@ -215,10 +152,7 @@ contract DelayModule {
txHash[txNonce] == getTransactionHash(to, value, data, operation),
"Transaction hashes do not match"
);
require(
executor.execTransactionFromModule(to, value, data, operation),
"Module transaction failed"
);
require(exec(to, value, data, operation), "Module transaction failed");
txNonce++;
}

Expand Down Expand Up @@ -249,43 +183,4 @@ contract DelayModule {
function getTxCreatedAt(uint256 _nonce) public view returns (uint256) {
return (txCreatedAt[_nonce]);
}

/// @dev Returns if an module is enabled
/// @return True if the module is enabled
function isModuleEnabled(address _module) public view returns (bool) {
return SENTINEL_MODULES != _module && modules[_module] != address(0);
}

/// @dev Returns array of modules.
/// @param start Start of the page.
/// @param pageSize Maximum number of modules that should be returned.
/// @return array Array of modules.
/// @return next Start of the next page.
function getModulesPaginated(address start, uint256 pageSize)
external
view
returns (address[] memory array, address next)
{
// Init array with max page size
array = new address[](pageSize);

// Populate return array
uint256 moduleCount = 0;
address currentModule = modules[start];
while (
currentModule != address(0x0) &&
currentModule != SENTINEL_MODULES &&
moduleCount < pageSize
) {
array[moduleCount] = currentModule;
currentModule = modules[currentModule];
moduleCount++;
}
next = currentModule;
// Set correct size of returned array
// solhint-disable-next-line no-inline-assembly
assembly {
mstore(array, moduleCount)
}
}
}
16 changes: 11 additions & 5 deletions docs/setup_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,30 @@ The first step is to deploy the module. Every Safe will have their own module. T

### Deploying the module

Hardhat tasks can be used to deploy a delay-module instance. There are two different tasks to deploy the module, the first one is through a normal deployment and passing arguments to the constructor (with the task `setup`), or, deploy the Module through a [Minimal Proxy Factory](https://eips.ethereum.org/EIPS/eip-1167) and save on gas costs (with the task `factorySetup`) - In rinkeby the address of the Proxy Factory is: `0x569F2e024D0aD6bBfBd8135097DFa7D0641Ae79b` and the Master Copy of the Delay Module: `0xAe78DF7a4184917dA49f7fCA6139f924A79D0488`.

Hardhat tasks can be used to deploy a delay-module instance. There are two different tasks to deploy the module, the first one is through a normal deployment and passing arguments to the constructor (with the task `setup`), or, deploy the Module through a [Minimal Proxy Factory](https://eips.ethereum.org/EIPS/eip-1167) and save on gas costs (with the task `factory-setup`) - In rinkeby the address of the Proxy Factory is: `0xd067410a85ffC8C55f7245DE4BfE16C95329D232` and the Master Copy of the Delay Module: `0x3cc7aBD1908906e2102D302249c82d083975e1EF`.
These setup tasks requires the following parameters:

These setup tasks requires the following parameters: `dao` (the address of the Safe). There are also optional parameters (cooldown and expiration, by default they are set to 24 hours and 7 days, respectively), for more information run `yarn hardhat setup --help` or `yarn hardhat factory-setup --help`.
- `executor` - the address of the executor.
- `owner` - the address of the owner
- `cooldown` - optional, by default is set to 24 hours
- `expiration` - optional, by default is set to 7 days

For more information run `yarn hardhat setup --help` or `yarn hardhat factorySetup --help`.

An example for this on Rinkeby would be:
`yarn hardhat --network rinkeby setup --dao <safe_address>`
`yarn hardhat --network rinkeby setup --owner <owner_address> --executor <executor_address>`

or

`yarn hardhat --network rinkeby factory-setup --factory <factory_address> --mastercopy <mastercopy_address> --dao <safe_address>`
`yarn hardhat --network rinkeby factorySetup --factory <factory_address> --mastercopy <mastercopy_address> --owner <owner_address> --executor <executor_address>`

This should return the address of the deployed delay-module. For this guide we assume this to be `0x4242424242424242424242424242424242424242`

Once the module is deployed you should verify the source code (Note: If you used the factory deployment the contract should be already verified). If you use a network that is Etherscan compatible and you configure the `ETHERSCAN_API_KEY` in your environment you can use the provided hardhat task to do this.

An example for this on Rinkeby would be:
`yarn hardhat --network rinkeby verifyEtherscan --module 0x4242424242424242424242424242424242424242 --dao <safe_address>`
`yarn hardhat --network rinkeby verifyEtherscan --module 0x4242424242424242424242424242424242424242 --owner <owner_address> --executor <executor_address>`

### Enabling the module

Expand Down
57 changes: 29 additions & 28 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,40 @@
"author": "auryn.macmillan@gnosis.io",
"license": "MIT",
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.0.0",
"@nomiclabs/hardhat-etherscan": "^2.1.0",
"@nomiclabs/hardhat-waffle": "^2.0.0",
"@types/chai": "^4.2.14",
"@types/mocha": "^8.2.0",
"@types/node": "^14.14.21",
"@types/yargs": "^16.0.0",
"@typescript-eslint/eslint-plugin": "^4.7.0",
"@typescript-eslint/parser": "^4.7.0",
"chai": "^4.2.0",
"debug": "^4.2.0",
"eslint": "^7.13.0",
"eslint-config-prettier": "^8.0.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-no-only-tests": "^2.4.0",
"eslint-plugin-prettier": "^3.1.4",
"ethereum-waffle": "^3.2.0",
"hardhat": "^2.0.8",
"hardhat-deploy": "^0.7.0-beta.38",
"husky": "^5.1.3",
"prettier": "^2.1.2",
"prettier-plugin-solidity": "^1.0.0-alpha.60",
"solhint": "^3.3.2",
"solhint-plugin-prettier": "^0.0.5",
"solidity-coverage": "^0.7.13",
"ts-node": "^9.1.1",
"typescript": "^4.1.3"
"@gnosis/zodiac": "github:gnosis/zodiac#master",
"@nomiclabs/hardhat-ethers": "2.0.0",
"@nomiclabs/hardhat-etherscan": "2.1.0",
"@nomiclabs/hardhat-waffle": "2.0.0",
"@types/chai": "4.2.14",
"@types/mocha": "8.2.0",
"@types/node": "14.14.21",
"@types/yargs": "16.0.0",
"@typescript-eslint/eslint-plugin": "4.7.0",
"@typescript-eslint/parser": "4.7.0",
"chai": "4.2.0",
"debug": "4.2.0",
"eslint": "7.13.0",
"eslint-config-prettier": "8.0.0",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-no-only-tests": "2.4.0",
"eslint-plugin-prettier": "3.1.4",
"ethereum-waffle": "3.2.0",
"hardhat": "2.0.8",
"hardhat-deploy": "0.7.0-beta.38",
"husky": "5.1.3",
"prettier": "2.1.2",
"prettier-plugin-solidity": "1.0.0-alpha.60",
"solhint": "3.3.2",
"solhint-plugin-prettier": "0.0.5",
"solidity-coverage": "0.7.13",
"ts-node": "9.1.1",
"typescript": "4.1.3"
},
"dependencies": {
"@gnosis.pm/mock-contract": "^4.0.0",
"argv": "^0.0.2",
"dotenv": "^8.0.0",
"ethers": "^5.0.19",
"ethers": "5.4.4",
"solc": "0.8.1",
"yargs": "^16.1.1"
},
Expand Down
Loading

0 comments on commit f3274df

Please sign in to comment.