Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: treasury executor role #68

Merged
merged 5 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions contracts/TreasuryRootstockCollective.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ contract TreasuryRootstockCollective is AccessControl, ReentrancyGuard, ITreasur

mapping(address => bool) public whitelist;
bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE");
bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");

/**
* @dev Sets the values for {initialOwner} and {guardian}
Expand All @@ -40,7 +41,8 @@ contract TreasuryRootstockCollective is AccessControl, ReentrancyGuard, ITreasur
}

/**
* @dev Withdraw an ERC20 token to a third-party address.
* @dev Withdraw an ERC20 token to a third-party address. This function is
* a target for the DAO proposals executor
* @param token The ERC20 token
* @param to The third-party address
* @param amount The value to send
Expand All @@ -49,11 +51,11 @@ contract TreasuryRootstockCollective is AccessControl, ReentrancyGuard, ITreasur
address token,
address to,
uint256 amount
) external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant {
) external onlyRole(EXECUTOR_ROLE) nonReentrant {
require(whitelist[token], "Token forbidden");
require(IERC20(token).balanceOf(address(this)) >= amount, "Insufficient ERC20 balance");
IERC20(token).safeTransfer(to, amount);
emit WithdrawnERC20(token, to, amount);
IERC20(token).safeTransfer(to, amount);
}

/**
Expand All @@ -65,8 +67,8 @@ contract TreasuryRootstockCollective is AccessControl, ReentrancyGuard, ITreasur
require(whitelist[token], "Token forbidden");
require(to != address(0), "Zero Address is not allowed");
uint256 amount = IERC20(token).balanceOf(address(this));
IERC20(token).safeTransfer(to, amount);
emit WithdrawnERC20(token, to, amount);
IERC20(token).safeTransfer(to, amount);
}

/**
Expand All @@ -78,16 +80,17 @@ contract TreasuryRootstockCollective is AccessControl, ReentrancyGuard, ITreasur
* such as `onlyRole`, which ensures that only authorized users
* can execute the function, and `nonReentrant`.
* which protects against reentrant attacks.
* This function is a target for the DAO proposals executor.
* @param to The third-party address
* @param amount The value to send
*/
// slither-disable-next-line arbitrary-send-eth
function withdraw(address payable to, uint256 amount) external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant {
function withdraw(address payable to, uint256 amount) external onlyRole(EXECUTOR_ROLE) nonReentrant {
require(address(this).balance >= amount, "Insufficient Balance");
require(to != address(0), "Zero Address is not allowed");
emit Withdrawn(to, amount);
bool success = to.send(amount);
require(success, "Failed to sent");
emit Withdrawn(to, amount);
}

/**
Expand All @@ -104,9 +107,9 @@ contract TreasuryRootstockCollective is AccessControl, ReentrancyGuard, ITreasur
function emergencyWithdraw(address payable to) external nonReentrant onlyRole(GUARDIAN_ROLE) {
require(to != address(0), "Zero Address is not allowed");
uint256 amount = address(this).balance;
emit Withdrawn(to, amount);
bool success = to.send(amount);
require(success, "Failed to sent");
emit Withdrawn(to, amount);
}

/**
Expand All @@ -120,10 +123,10 @@ contract TreasuryRootstockCollective is AccessControl, ReentrancyGuard, ITreasur

/**
* @dev Add to whitelist a new ERC20 token
* requires admin role
* requires guardian role
* @param token ERC20 token
*/
function addToWhitelist(address token) external onlyRole(DEFAULT_ADMIN_ROLE) {
function addToWhitelist(address token) external onlyRole(GUARDIAN_ROLE) {
shenshin marked this conversation as resolved.
Show resolved Hide resolved
_addToWhitelist(token);
}

Expand All @@ -138,10 +141,10 @@ contract TreasuryRootstockCollective is AccessControl, ReentrancyGuard, ITreasur

/**
* @dev Remove from whitelist an ERC20 token
* requires admin role
* requires guardian role
* @param token ERC20 token
*/
function removeFromWhitelist(address token) external onlyRole(DEFAULT_ADMIN_ROLE) {
function removeFromWhitelist(address token) external onlyRole(GUARDIAN_ROLE) {
shenshin marked this conversation as resolved.
Show resolved Hide resolved
_removeFromWhitelist(token);
}

Expand Down
17 changes: 12 additions & 5 deletions ignition/modules/GovernorModule.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { buildModule } from '@nomicfoundation/hardhat-ignition/modules'
import TimelockModule from './TimelockModule'
import StRifModule from './StRifModule'
import TreasuryModule from './TreasuryModule'

/**
* Deploys proxy contract before deploying the Governor
Expand Down Expand Up @@ -36,7 +37,7 @@ export const governorProxyModule = buildModule('GovernorProxy', m => {
/**
* Main DAO deployment module.
* Deploys Governor along with other related contracts
* (Timelock, StRIF). Usage:
* (Timelock, StRIF, Treasury). Usage:
* ```shell
* npx hardhat ignition deploy \
* ignition/modules/GovernorModule.ts \
Expand All @@ -46,7 +47,13 @@ export const governorProxyModule = buildModule('GovernorProxy', m => {
*/
const governorModule = buildModule('Governor', m => {
const { governorProxy, timelock, stRif } = m.useModule(governorProxyModule)
// Use proxy address to interact with the deployed contract

// set Timelock as the Executor on the Treasury
const { treasury } = m.useModule(TreasuryModule)
const treasuryExecutorRole = m.staticCall(treasury, 'EXECUTOR_ROLE')
m.call(treasury, 'grantRole', [treasuryExecutorRole, timelock])

// Use proxy address to interact with the deployed Governor contract
shenshin marked this conversation as resolved.
Show resolved Hide resolved
const governor = m.contractAt('GovernorRootstockCollective', governorProxy)

// grant Timelock Proposer role to the Governor
Expand All @@ -56,12 +63,12 @@ const governorModule = buildModule('Governor', m => {
})

// grant Timelock Executor role to the Governor
const executorRole = m.staticCall(timelock, 'EXECUTOR_ROLE')
m.call(timelock, 'grantRole', [executorRole, governor], {
const timelockExecutorRole = m.staticCall(timelock, 'EXECUTOR_ROLE')
m.call(timelock, 'grantRole', [timelockExecutorRole, governor], {
id: 'grant_executor_role',
})

return { governor, timelock, stRif }
return { governor, timelock, stRif, treasury }
})

export default governorModule
10 changes: 3 additions & 7 deletions ignition/modules/TreasuryModule.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { buildModule } from '@nomicfoundation/ignition-core'

/**
* Usage:
* ```shell
* npx hardhat ignition deploy \
* ignition/modules/TreasuryModule.ts \
* --parameters params/testnet.json \
* --network rootstockTestnet
* ```
* Deploys Treasury contract.
* The Treasury is deployed along with
* other DAO contracts using the Governor module.
*/
const treasuryModule = buildModule('Treasury', m => {
const owner = m.getParameter('owner')
Expand Down
33 changes: 22 additions & 11 deletions test/Treasury.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers'
import { deployContracts } from './deployContracts'
import { RIFToken, StRIFToken, TreasuryRootstockCollective } from '../typechain-types'
import {
RIFToken,
StRIFToken,
TreasuryRootstockCollective,
DaoTimelockUpgradableRootstockCollective,
} from '../typechain-types'
import { AddressLike, parseEther } from 'ethers'
import { expect } from 'chai'

Expand All @@ -17,18 +22,24 @@
token3: AddressLike
let rif: RIFToken
let treasury: TreasuryRootstockCollective
let timelock: DaoTimelockUpgradableRootstockCollective
let stRIF: StRIFToken
let GuardianRole: string, AdminRole: string
let GuardianRole: string, ExecutorRole: string

before(async () => {
;[deployer, owner, beneficiary, guardian, collector, token1, token2, token3] = await ethers.getSigners()
;({ stRIF, rif, treasury } = await loadFixture(deployContracts))
;;[deployer, owner, beneficiary, guardian, collector, token1, token2, token3] = await ethers.getSigners()

Check warning on line 30 in test/Treasury.test.ts

View workflow job for this annotation

GitHub Actions / test

Delete `;`
;({ stRIF, rif, treasury, timelock } = await loadFixture(deployContracts))
await treasury.addToWhitelist(rif)
GuardianRole = await treasury.GUARDIAN_ROLE()
AdminRole = await treasury.DEFAULT_ADMIN_ROLE()
ExecutorRole = await treasury.EXECUTOR_ROLE()
const grantRoleTx = await treasury.grantRole(ExecutorRole, deployer)
await grantRoleTx.wait()
})

describe('Withdraw token and RBTC to any account', () => {
it('Timelock should be granted the Executor role after deployment', async () => {
expect(await treasury.hasRole(ExecutorRole, await timelock.getAddress())).to.be.true
})
it('Receive RBTC', async () => {
const amount = parseEther('50')
let balance = await ethers.provider.getBalance(treasury)
Expand Down Expand Up @@ -56,7 +67,7 @@
const sentTx = treasury.connect(owner).withdraw(beneficiary, amount)
await expect(sentTx)
.to.be.revertedWithCustomError(treasury, 'AccessControlUnauthorizedAccount')
.withArgs(owner.address, AdminRole)
.withArgs(owner.address, ExecutorRole)
})

it('Can not withdraw RBTC to a beneficiary if balance is insufficient', async () => {
Expand Down Expand Up @@ -96,7 +107,7 @@
const sentTx = treasury.connect(owner).withdrawERC20(rif, beneficiary, amount)
await expect(sentTx)
.to.be.revertedWithCustomError(treasury, 'AccessControlUnauthorizedAccount')
.withArgs(owner.address, AdminRole)
.withArgs(owner.address, ExecutorRole)
})

it('Withdraw with a non ERC20 token address should revert', async () => {
Expand Down Expand Up @@ -164,21 +175,21 @@
expect(flag).to.equal(true)
})

it('Only account with Admin Role can add new token to whitelist', async () => {
it('Only account with Guardian Role can add new token to whitelist', async () => {
await treasury.connect(deployer).removeFromWhitelist(stRIF)
expect(await treasury.whitelist(stRIF)).to.equal(false)

const sentTx = treasury.connect(beneficiary).addToWhitelist(stRIF)
await expect(sentTx)
.to.be.revertedWithCustomError(treasury, 'AccessControlUnauthorizedAccount')
.withArgs(beneficiary.address, AdminRole)
.withArgs(beneficiary.address, GuardianRole)
})

it('Only account with Admin Role can remove a token to whitelist', async () => {
it('Only account with Guardian Role can remove a token to whitelist', async () => {
const sentTx = treasury.connect(beneficiary).removeFromWhitelist(rif)
await expect(sentTx)
.to.be.revertedWithCustomError(treasury, 'AccessControlUnauthorizedAccount')
.withArgs(beneficiary.address, AdminRole)
.withArgs(beneficiary.address, GuardianRole)
expect(await treasury.whitelist(rif)).to.equal(true)
})

Expand Down
16 changes: 5 additions & 11 deletions test/deployContracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,12 @@ import {
} from '../typechain-types'
import RifModule from '../ignition/modules/RifModule'
import GovernorModule from '../ignition/modules/GovernorModule'
import TreasuryModule from '../ignition/modules/TreasuryModule'
import EarlyAdoptersModule from '../ignition/modules/EarlyAdoptersModule'

export const deployContracts = async () => {
const [sender] = await ethers.getSigners()
// deploy RIF before the rest DAO contracts
const { rif } = await ignition.deploy(RifModule, { defaultSender: sender?.address })
// deploy Treasury
const { treasury } = await ignition.deploy(TreasuryModule, {
parameters: {
Treasury: {
owner: sender.address,
guardian: sender.address,
},
},
})
// insert RIF address as a parameter to stRIF deployment module
const dao = await ignition.deploy(GovernorModule, {
parameters: {
Expand All @@ -44,14 +34,18 @@ export const deployContracts = async () => {
minDelay: 900,
admin: sender.address,
},
Treasury: {
owner: sender.address,
guardian: sender.address,
},
},
})
return {
rif: rif as unknown as RIFToken,
stRIF: dao.stRif as unknown as StRIFToken,
timelock: dao.timelock as unknown as DaoTimelockUpgradableRootstockCollective,
governor: dao.governor as unknown as GovernorRootstockCollective,
treasury: treasury as unknown as TreasuryRootstockCollective,
treasury: dao.treasury as unknown as TreasuryRootstockCollective,
}
}

Expand Down
Loading