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: add base governance executor #35

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
704 changes: 704 additions & 0 deletions abis/baseGovernanceExecutor.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions abis/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as baseGovernanceExecutorAbi } from './baseGovernanceExecutor.json'
export { default as capAutomatorAbi } from './capAutomator.json'
export { default as d3mHubAbi } from './d3mHub.json'
export { default as dsNoteAbi } from './dsNote.json'
Expand Down
41 changes: 41 additions & 0 deletions contracts/MockReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.10;

interface IExecutor {
function queue(
address[] memory targets,
uint256[] memory values,
string[] memory signatures,
bytes[] memory calldatas,
bool[] memory withDelegatecalls
) external;
}

contract MockReceiver {
address public executor;

constructor(address _executor) {
executor = _executor;
}

function __callQueueOnExecutor(address _payload) public {
address[] memory targets = new address[](1);
targets[0] = address(_payload);
uint256[] memory values = new uint256[](1);
values[0] = 0;
string[] memory signatures = new string[](1);
signatures[0] = "execute()";
bytes[] memory calldatas = new bytes[](1);
calldatas[0] = "";
bool[] memory withDelegatecalls = new bool[](1);
withDelegatecalls[0] = true;

IExecutor(executor).queue(
targets,
values,
signatures,
calldatas,
withDelegatecalls
);
}
}
6 changes: 6 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ if (TEST_NETWORK == 'mainnet') {
url: `https://rpc.ankr.com/gnosis`,
blockNumber: 33991690, // 2024-05-17 01:39:20
}
} else if (TEST_NETWORK == 'base') {
console.log('\nRunning Base Chain test suite')
forking = {
url: `https://base-mainnet.g.alchemy.com/v2/${ALCHEMY_ID}`,
blockNumber: 22209900, // 2024-11-10 03:25:47
}
}

// ================================= CONFIG =========================================
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
"lint:fix": "yarn lint --fix",
"lint:sol": "solhint contracts/**/*.sol",
"test:mainnet": "export TEST_NETWORK=\"mainnet\" && npx hardhat test test/web3-functions/mainnet/*",
"test:base": "export TEST_NETWORK=\"base\" && npx hardhat test test/web3-functions/base/*",
"test:gnosis": "export TEST_NETWORK=\"gnosis\" && npx hardhat test test/web3-functions/gnosis/*",
"test:utils": "npx hardhat test test/utils/*",
"test": "yarn test:utils && yarn test:gnosis && yarn test:mainnet",
"test": "yarn test:utils && yarn test:base && yarn test:gnosis && yarn test:mainnet",
"verify": "npx hardhat etherscan-verify",
"w3f:test": "npx w3f test web3-functions/simple/index.ts --logs --chain-id=88153591557"
},
Expand Down
14 changes: 14 additions & 0 deletions scripts/configs/governance-executor/governance-executor-base.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"domain": "base",
"args": {
"domain": "base",
"sendSlackMessages": true
},
"secrets": {
"SLACK_WEBHOOK_URL": "SPARK_INFO_SLACK_WEBHOOK_URL"
},
"trigger": {
"type": "time",
"interval": "3600000"
}
}
225 changes: 225 additions & 0 deletions test/web3-functions/base/governance-executor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import hre from 'hardhat'
import chai from 'chai'
import { Contract, ContractFactory } from '@ethersproject/contracts'
import { Web3FunctionHardhat } from '@gelatonetwork/web3-functions-sdk/hardhat-plugin'
import { Web3FunctionResultCallData } from '@gelatonetwork/web3-functions-sdk/*'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import {
SnapshotRestorer,
impersonateAccount,
mine,
setBalance,
takeSnapshot,
} from '@nomicfoundation/hardhat-network-helpers'
import { baseGovernanceExecutorAbi } from '../../../abis'
import { addresses } from '../../../utils'

const { w3f, ethers } = hre
const { expect } = chai

describe('GovernanceExecutor', function () {
this.timeout(0)

let cleanStateRestorer: SnapshotRestorer
let snapshotRestorer: SnapshotRestorer

let governanceExecutorW3F: Web3FunctionHardhat
let keeper: SignerWithAddress

let payloadFactory: ContractFactory

let executor: Contract
let mockReceiver: Contract

let executorAddress: string
let executionDelay: number

const userArgs = { domain: 'base', sendSlackMessages: false }

before(async () => {
cleanStateRestorer = await takeSnapshot()
;[keeper] = await ethers.getSigners()

executorAddress = addresses.base.executor
const mockReceiverFactory = await ethers.getContractFactory('MockReceiver')
mockReceiver = await mockReceiverFactory.deploy(executorAddress)

await setBalance(executorAddress, ethers.utils.parseEther('1'))
await impersonateAccount(executorAddress)
const executorSigner = await ethers.getSigner(executorAddress)
executor = new Contract(executorAddress, baseGovernanceExecutorAbi, executorSigner)
const submissionRole = await executor.SUBMISSION_ROLE()
await executor.grantRole(submissionRole, mockReceiver.address)
executionDelay = Number(await executor.delay())

payloadFactory = await ethers.getContractFactory('EmptyPayload')

governanceExecutorW3F = w3f.get('governance-executor')
})

beforeEach(async () => {
snapshotRestorer = await takeSnapshot()
})

afterEach(async () => {
await snapshotRestorer.restore()
})

after(async () => {
await cleanStateRestorer.restore()
})

it('no actions to execute', async () => {
const { result } = await governanceExecutorW3F.run('onRun', { userArgs })

expect(result.canExec).to.equal(false)
!result.canExec && expect(result.message).to.equal('No actions to execute')
})

it('fails when domain is invalid', async () => {
const { result } = await governanceExecutorW3F.run('onRun', {
userArgs: { domain: 'invalid-domain', sendSlackMessages: false },
})

expect(result.canExec).to.equal(false)
!result.canExec && expect(result.message).to.equal('Invalid domain: invalid-domain')
})

it('does not execute actions when no actions are ready', async () => {
const payload = await payloadFactory.deploy()
await mockReceiver.__callQueueOnExecutor(payload.address)

const { result } = await governanceExecutorW3F.run('onRun', { userArgs })

expect(result.canExec).to.equal(false)
!result.canExec && expect(result.message).to.equal('No actions to execute')
})

it('executes single proposal', async () => {
const payload = await payloadFactory.deploy()
await mockReceiver.__callQueueOnExecutor(payload.address)

await mine(2, { interval: executionDelay - 2 })

const { result: negativeResult } = await governanceExecutorW3F.run('onRun', { userArgs })
expect(negativeResult.canExec).to.equal(false)

await mine(2, { interval: 2 })

const { result: positiveResult } = await governanceExecutorW3F.run('onRun', { userArgs })

expect(positiveResult.canExec).to.equal(true)

if (!positiveResult.canExec) {
throw ''
}
const callData = positiveResult.callData as Web3FunctionResultCallData[]

expect(callData).to.deep.equal([
{
to: executorAddress,
data: executor.interface.encodeFunctionData('execute', [0]),
},
])

expect(await executor.getCurrentState(0)).to.equal(0)

await keeper.sendTransaction({
to: callData[0].to,
data: callData[0].data,
})

expect(await executor.getCurrentState(0)).to.equal(1)
})

it('executes single proposal (second in timelock)', async () => {
const firstPayload = await payloadFactory.deploy()
const secondPayload = await payloadFactory.deploy()

await mockReceiver.__callQueueOnExecutor(firstPayload.address)

await mine(2, { interval: executionDelay - 2 })

await mockReceiver.__callQueueOnExecutor(secondPayload.address)

const { result: negativeResult } = await governanceExecutorW3F.run('onRun', { userArgs })
expect(negativeResult.canExec).to.equal(false)

await mine(2, { interval: 2 })

const { result: positiveResult } = await governanceExecutorW3F.run('onRun', { userArgs })

expect(positiveResult.canExec).to.equal(true)

if (!positiveResult.canExec) {
throw ''
}
const callData = positiveResult.callData as Web3FunctionResultCallData[]

expect(callData).to.deep.equal([
{
to: executorAddress,
data: executor.interface.encodeFunctionData('execute', [0]),
},
])

expect(await executor.getCurrentState(0)).to.equal(0)

await keeper.sendTransaction({
to: callData[0].to,
data: callData[0].data,
})

expect(await executor.getCurrentState(0)).to.equal(1)
})

it('executes multiple proposals', async () => {
const firstPayload = await payloadFactory.deploy()
const secondPayload = await payloadFactory.deploy()

await mockReceiver.__callQueueOnExecutor(firstPayload.address)
await mockReceiver.__callQueueOnExecutor(secondPayload.address)

await mine(2, { interval: executionDelay - 2 })

const { result: negativeResult } = await governanceExecutorW3F.run('onRun', { userArgs })
expect(negativeResult.canExec).to.equal(false)

await mine(2, { interval: 2 })

const { result: positiveResult } = await governanceExecutorW3F.run('onRun', { userArgs })

expect(positiveResult.canExec).to.equal(true)

if (!positiveResult.canExec) {
throw ''
}
const callData = positiveResult.callData as Web3FunctionResultCallData[]

expect(callData).to.deep.equal([
{
to: executorAddress,
data: executor.interface.encodeFunctionData('execute', [0]),
},
{
to: executorAddress,
data: executor.interface.encodeFunctionData('execute', [1]),
},
])

expect(await executor.getCurrentState(0)).to.equal(0)
expect(await executor.getCurrentState(1)).to.equal(0)

await keeper.sendTransaction({
to: callData[0].to,
data: callData[0].data,
})
await keeper.sendTransaction({
to: callData[1].to,
data: callData[1].data,
})

expect(await executor.getCurrentState(0)).to.equal(1)
expect(await executor.getCurrentState(1)).to.equal(1)
})
})
14 changes: 7 additions & 7 deletions test/web3-functions/gnosis/governance-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
takeSnapshot,
} from '@nomicfoundation/hardhat-network-helpers'
import { gnosisGovernanceExecutorAbi } from '../../../abis'
import { addresses, sendMessageToSlack } from '../../../utils'
import { addresses } from '../../../utils'

const { w3f, ethers } = hre
const { expect } = chai
Expand Down Expand Up @@ -97,12 +97,12 @@ describe('GovernanceExecutor', function () {
const payload = await payloadFactory.deploy()
await mockAMB.__callQueueOnExecutor(payload.address)

await mine(2, { interval: executionDelay - 1 })
await mine(2, { interval: executionDelay - 2 })

const { result: negativeResult } = await governanceExecutorW3F.run('onRun')
expect(negativeResult.canExec).to.equal(false)

await mine(2, { interval: 1 })
await mine(2, { interval: 2 })

const { result: positiveResult } = await governanceExecutorW3F.run('onRun')

Expand Down Expand Up @@ -136,14 +136,14 @@ describe('GovernanceExecutor', function () {

await mockAMB.__callQueueOnExecutor(firstPayload.address)

await mine(2, { interval: executionDelay - 1 })
await mine(2, { interval: executionDelay - 2 })

await mockAMB.__callQueueOnExecutor(secondPayload.address)

const { result: negativeResult } = await governanceExecutorW3F.run('onRun')
expect(negativeResult.canExec).to.equal(false)

await mine(2, { interval: 1 })
await mine(2, { interval: 2 })

const { result: positiveResult } = await governanceExecutorW3F.run('onRun')

Expand Down Expand Up @@ -178,12 +178,12 @@ describe('GovernanceExecutor', function () {
await mockAMB.__callQueueOnExecutor(firstPayload.address)
await mockAMB.__callQueueOnExecutor(secondPayload.address)

await mine(2, { interval: executionDelay - 1 })
await mine(2, { interval: executionDelay - 2 })

const { result: negativeResult } = await governanceExecutorW3F.run('onRun')
expect(negativeResult.canExec).to.equal(false)

await mine(2, { interval: 1 })
await mine(2, { interval: 2 })

const { result: positiveResult } = await governanceExecutorW3F.run('onRun')

Expand Down
4 changes: 4 additions & 0 deletions utils/addresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ export const addresses = {
executor: '0xc4218C1127cB24a0D6c1e7D25dc34e10f2625f5A',
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
},
base: {
executor: '0xF93B7122450A50AF3e5A76E1d546e95Ac1d0F579',
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
},
} as const
Loading
Loading