Skip to content

Commit

Permalink
Fix/allow approval in execution (#296)
Browse files Browse the repository at this point in the history
* feat: add proposal checker with multiple allowed stages

* fix: mimic code style better

* test: add approval tests

* test: snapshots oops

* chore: add changeset

* chore: snapshots

* fix: changeset typo

* fix: use impersonateAccount and add a balance helper

* fix: hex -> number/bigint/bignumber

---------

Co-authored-by: bowd <[email protected]>
Co-authored-by: boqdan <[email protected]>
  • Loading branch information
3 people authored Jul 24, 2024
1 parent 6d6c3fb commit de1e5a0
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-squids-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@celo/celocli': patch
---

Allows approving proposals when in Execution or Referendum stages
105 changes: 101 additions & 4 deletions packages/cli/src/commands/governance/approve.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { StrongAddress } from '@celo/base'
import { newKitFromWeb3 } from '@celo/contractkit'
import { GovernanceWrapper } from '@celo/contractkit/lib/wrappers/Governance'
import { testWithAnvil } from '@celo/dev-utils/lib/anvil-test'
import { GovernanceWrapper, ProposalStage } from '@celo/contractkit/lib/wrappers/Governance'
import { impersonateAccount, testWithAnvil } from '@celo/dev-utils/lib/anvil-test'
import { timeTravel } from '@celo/dev-utils/lib/ganache-test'
import { ux } from '@oclif/core'
import Web3 from 'web3'
Expand Down Expand Up @@ -72,7 +72,7 @@ testWithAnvil('governance:approve cmd', (web3: Web3) => {
" ✔ 1 is an existing proposal ",
],
[
" ✔ 1 is in stage Referendum ",
" ✔ 1 is in stage Referendum or Execution ",
],
[
" ✔ 1 not already approved ",
Expand Down Expand Up @@ -110,7 +110,7 @@ testWithAnvil('governance:approve cmd', (web3: Web3) => {
" ✔ 1 is an existing proposal ",
],
[
" ✔ 1 is in stage Referendum ",
" ✔ 1 is in stage Referendum or Execution ",
],
[
" ✔ 1 not already approved ",
Expand All @@ -128,4 +128,101 @@ testWithAnvil('governance:approve cmd', (web3: Web3) => {
`)
expect(writeMock.mock.calls).toMatchInlineSnapshot(`[]`)
})

describe('approve succeeds if stage is "Referendum or Execution" or "Approval"', () => {
test('can be approved if version >= 3 (default)', async () => {
const logMock = jest.spyOn(console, 'log')

await governance
.propose([], 'https://example.com')
.sendAndWaitForReceipt({ value: minDeposit })

const approver = await governance.getApprover()
await impersonateAccount(web3, approver, 1000000000000000000n)

let proposalId = (await governance.getQueue())[0].proposalID
await expect(governance.getProposalStage(proposalId)).resolves.toBe(ProposalStage.Queued)
await expect(
testLocallyWithWeb3Node(
Approve,
['--from', approver, '--proposalID', proposalId.toString()],
web3
)
).rejects.not.toBeUndefined()
const schedule = await governance.proposalSchedule(proposalId)
expect(logMock.mock.calls.map((args) => args.map(stripAnsiCodes))).toMatchInlineSnapshot(`
[
[
"Running Checks:",
],
[
" ✔ 0x078B932B0d1e56554974A431B8B33973D94E002b is approver address ",
],
[
" ✔ 2 is an existing proposal ",
],
[
"Expiration: ${schedule.Expiration?.toString()} (~${schedule.Expiration?.toExponential(
3
)})
Queued: ${schedule.Queued?.toString()} (~${schedule.Queued?.toExponential(3)})",
],
[
" ✘ 2 is in stage Referendum or Execution ",
],
[
" ✔ 2 not already approved ",
],
]
`)
logMock.mockClear()

const dequeueFrequency = (await governance.dequeueFrequency()).toNumber()
await timeTravel(dequeueFrequency, web3)
await governance.dequeueProposalsIfReady().sendAndWaitForReceipt()

await governance.vote(proposalId, 'Yes')
await expect(governance.getProposalStage(proposalId)).resolves.toBe(ProposalStage.Referendum)
await testLocallyWithWeb3Node(
Approve,
['--from', approver, '--proposalID', proposalId.toString()],
web3
)
const txHash = stripAnsiCodes(logMock.mock.calls.at(-3)![0].split(':')[1].trim())
expect(logMock.mock.calls.map((args) => args.map(stripAnsiCodes))).toMatchInlineSnapshot(`
[
[
"Running Checks:",
],
[
" ✔ 0x078B932B0d1e56554974A431B8B33973D94E002b is approver address ",
],
[
" ✔ 2 is an existing proposal ",
],
[
" ✔ 2 is in stage Referendum or Execution ",
],
[
" ✔ 2 not already approved ",
],
[
"All checks passed",
],
[
"SendTransaction: approveTx",
],
[
"txHash: ${txHash}",
],
[
"ProposalApproved:",
],
[
"proposalId: 2",
],
]
`)
})
})
})
9 changes: 1 addition & 8 deletions packages/cli/src/commands/governance/approve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,9 @@ export default class Approve extends BaseCommand {
await governance.dequeueProposalsIfReady().sendAndWaitForReceipt()
}

const governanceVersion = await governance.version()

await checkBuilder
.proposalExists(id)
.proposalInStage(
id,
governanceVersion.storage === '1' && Number(governanceVersion.major) < 3
? 'Approval'
: 'Referendum'
)
.proposalInStages(id, ['Referendum', 'Execution'])
.addCheck(`${id} not already approved`, async () => !(await governance.isApproved(id)))
.runChecks()
governanceTx = await governance.approve(id)
Expand Down
11 changes: 9 additions & 2 deletions packages/cli/src/utils/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,17 @@ class CheckBuilder {
)

proposalInStage = (proposalID: string, stage: keyof typeof ProposalStage) =>
this.proposalInStages(proposalID, [stage])

proposalInStages = (proposalID: string, stages: (keyof typeof ProposalStage)[]) =>
this.addCheck(
`${proposalID} is in stage ${stage}`,
`${proposalID} is in stage ${stages.join(' or ')}`,
this.withGovernance(async (governance) => {
const match = (await governance.getProposalStage(proposalID)) === stage
const stage = await governance.getProposalStage(proposalID)
let match = false
for (const allowedStage of stages) {
match = stage === allowedStage || match
}
if (!match) {
const schedule = await governance.proposalSchedule(proposalID)
printValueMapRecursive(schedule)
Expand Down
14 changes: 12 additions & 2 deletions packages/dev-utils/src/anvil-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { StrongAddress } from '@celo/base'
import { PROXY_ADMIN_ADDRESS } from '@celo/connect'
import { Anvil, CreateAnvilOptions, createAnvil } from '@viem/anvil'
import BigNumber from 'bignumber.js'
import Web3 from 'web3'
import {
TEST_BALANCE,
Expand Down Expand Up @@ -52,8 +53,17 @@ export function testWithAnvil(name: string, fn: (web3: Web3) => void) {
})
}

export function impersonateAccount(web3: Web3, address: string) {
return jsonRpcCall(web3, 'anvil_impersonateAccount', [address])
export function impersonateAccount(
web3: Web3,
address: string,
withBalance?: number | bigint | BigNumber
) {
return Promise.all([
jsonRpcCall(web3, 'anvil_impersonateAccount', [address]),
withBalance
? jsonRpcCall(web3, 'anvil_setBalance', [address, `0x${withBalance.toString(16)}`])
: undefined,
])
}

export function stopImpersonatingAccount(web3: Web3, address: string) {
Expand Down
15 changes: 10 additions & 5 deletions packages/dev-utils/src/chain-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ export async function setCommissionUpdateDelay(
withImpersonatedAccount(web3, DEFAULT_OWNER_ADDRESS, async () => {
const validators = newValidators(web3, validatorsContractAddress)

await validators.methods.setCommissionUpdateDelay(delayInBlocks).send({
from: DEFAULT_OWNER_ADDRESS,
})
const { transactionHash } = await validators.methods
.setCommissionUpdateDelay(delayInBlocks)
.send({
from: DEFAULT_OWNER_ADDRESS,
})
await web3.eth.getTransactionReceipt(transactionHash)
})
}

Expand All @@ -26,9 +29,10 @@ export async function setDequeueFrequency(
withImpersonatedAccount(web3, DEFAULT_OWNER_ADDRESS, async () => {
const governance = newGovernance(web3, governanceContractAddress)

await governance.methods.setDequeueFrequency(frequency).send({
const { transactionHash } = await governance.methods.setDequeueFrequency(frequency).send({
from: DEFAULT_OWNER_ADDRESS,
})
await web3.eth.getTransactionReceipt(transactionHash)
})
}

Expand All @@ -40,8 +44,9 @@ export async function setReferendumStageDuration(
withImpersonatedAccount(web3, DEFAULT_OWNER_ADDRESS, async () => {
const governance = newGovernance(web3, governanceContractAddress)

await governance.methods.setReferendumStageDuration(duration).send({
const { transactionHash } = await governance.methods.setReferendumStageDuration(duration).send({
from: DEFAULT_OWNER_ADDRESS,
})
await web3.eth.getTransactionReceipt(transactionHash)
})
}

0 comments on commit de1e5a0

Please sign in to comment.