Skip to content
Merged
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
80 changes: 73 additions & 7 deletions yarn-project/ethereum/src/contracts/governance.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createExtendedL1Client } from '@aztec/ethereum';
import { createExtendedL1Client, getPublicClient } from '@aztec/ethereum';
import { Fr } from '@aztec/foundation/fields';
import { type Logger, createLogger } from '@aztec/foundation/log';

Expand All @@ -9,19 +9,21 @@ import { foundry } from 'viem/chains';
import { DefaultL1ContractsConfig } from '../config.js';
import { deployL1Contracts } from '../deploy_l1_contracts.js';
import { startAnvil } from '../test/start_anvil.js';
import type { ExtendedViemWalletClient } from '../types.js';
import { GovernanceContract } from './governance.js';
import type { ExtendedViemWalletClient, ViemClient } from '../types.js';
import { GovernanceContract, ReadOnlyGovernanceContract } from './governance.js';

describe('Governance', () => {
let anvil: Anvil;
let rpcUrl: string;
let privateKey: PrivateKeyAccount;
let logger: Logger;
let publicClient: ViemClient;
let walletClient: ExtendedViemWalletClient;

let vkTreeRoot: Fr;
let protocolContractTreeRoot: Fr;
let l1Client: ExtendedViemWalletClient;
let governance: GovernanceContract;
let governanceAddress: `0x${string}`;

beforeAll(async () => {
logger = createLogger('ethereum:test:governance');
// this is the 6th address that gets funded by the junk mnemonic
Expand All @@ -31,7 +33,8 @@ describe('Governance', () => {

({ anvil, rpcUrl } = await startAnvil());

l1Client = createExtendedL1Client([rpcUrl], privateKey, foundry);
walletClient = createExtendedL1Client([rpcUrl], privateKey, foundry);
publicClient = getPublicClient({ l1RpcUrls: [rpcUrl], l1ChainId: 31337 });

const deployed = await deployL1Contracts([rpcUrl], privateKey, foundry, logger, {
...DefaultL1ContractsConfig,
Expand All @@ -41,14 +44,77 @@ describe('Governance', () => {
genesisArchiveRoot: Fr.random(),
});

governance = new GovernanceContract(deployed.l1ContractAddresses.governanceAddress.toString(), l1Client);
governanceAddress = deployed.l1ContractAddresses.governanceAddress.toString() as `0x${string}`;
});

afterAll(async () => {
await anvil.stop().catch(err => createLogger('cleanup').error(err));
});

describe('ReadOnlyGovernanceContract', () => {
let readOnlyGovernance: ReadOnlyGovernanceContract;

beforeEach(() => {
readOnlyGovernance = new ReadOnlyGovernanceContract(governanceAddress, publicClient);
});

it('can be instantiated with public client but not wallet methods', () => {
expect(readOnlyGovernance).toBeDefined();
expect(readOnlyGovernance.client).toBe(publicClient);

// Verify wallet-specific methods are not available
expect(readOnlyGovernance).not.toHaveProperty('deposit');
expect(readOnlyGovernance).not.toHaveProperty('proposeWithLock');
expect(readOnlyGovernance).not.toHaveProperty('vote');
expect(readOnlyGovernance).not.toHaveProperty('executeProposal');
});

it('has all read-only methods', () => {
expect(readOnlyGovernance.getGovernanceProposerAddress).toBeDefined();
expect(readOnlyGovernance.getConfiguration).toBeDefined();
expect(readOnlyGovernance.getProposal).toBeDefined();
expect(readOnlyGovernance.getProposalState).toBeDefined();
expect(readOnlyGovernance.awaitProposalActive).toBeDefined();
expect(readOnlyGovernance.awaitProposalExecutable).toBeDefined();
});
});

describe('GovernanceContract', () => {
let governance: GovernanceContract;

beforeEach(() => {
governance = new GovernanceContract(governanceAddress, walletClient);
});

it('can be instantiated with wallet client and has write methods', () => {
expect(governance).toBeDefined();
expect(governance.client).toBe(walletClient);

// Verify wallet-specific methods are available
expect(governance.deposit).toBeDefined();
expect(governance.proposeWithLock).toBeDefined();
expect(governance.vote).toBeDefined();
expect(governance.executeProposal).toBeDefined();
});

it('inherits all read-only methods from ReadOnlyGovernanceContract', () => {
expect(governance.getGovernanceProposerAddress).toBeDefined();
expect(governance.getConfiguration).toBeDefined();
expect(governance.getProposal).toBeDefined();
expect(governance.getProposalState).toBeDefined();
expect(governance.awaitProposalActive).toBeDefined();
expect(governance.awaitProposalExecutable).toBeDefined();
});

it('cannot be instantiated with public client', () => {
expect(() => {
new GovernanceContract(governanceAddress, publicClient as any);
}).toThrow();
});
});

it('gets configuration', async () => {
const governance = new GovernanceContract(governanceAddress, walletClient);
expect(governance).toBeDefined();
const config = await governance.getConfiguration();
expect(config).toBeDefined();
Expand Down
94 changes: 43 additions & 51 deletions yarn-project/ethereum/src/contracts/governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {

import type { L1ContractAddresses } from '../l1_contract_addresses.js';
import { L1TxUtils } from '../l1_tx_utils.js';
import { type ExtendedViemWalletClient, type ViemClient, isExtendedClient } from '../types.js';
import type { ExtendedViemWalletClient, ViemClient } from '../types.js';

export type L1GovernanceContractAddresses = Pick<
L1ContractAddresses,
Expand Down Expand Up @@ -47,68 +47,37 @@ export function extractProposalIdFromLogs(logs: Log[]): bigint {
return parsedLogs[0].args.proposalId;
}

export class GovernanceContract {
private readonly publicGovernance: GetContractReturnType<typeof GovernanceAbi, ViemClient>;
private readonly walletGovernance: GetContractReturnType<typeof GovernanceAbi, ExtendedViemWalletClient> | undefined;
export class ReadOnlyGovernanceContract {
protected readonly governanceContract: GetContractReturnType<typeof GovernanceAbi, ViemClient>;

constructor(address: Hex, public readonly client: ViemClient) {
this.publicGovernance = getContract({ address, abi: GovernanceAbi, client: client });
this.walletGovernance = isExtendedClient(client) ? getContract({ address, abi: GovernanceAbi, client }) : undefined;
this.governanceContract = getContract({ address, abi: GovernanceAbi, client: client });
}

public get address() {
return EthAddress.fromString(this.publicGovernance.address);
return EthAddress.fromString(this.governanceContract.address);
}

public async getGovernanceProposerAddress() {
return EthAddress.fromString(await this.publicGovernance.read.governanceProposer());
return EthAddress.fromString(await this.governanceContract.read.governanceProposer());
}

public getConfiguration() {
return this.publicGovernance.read.getConfiguration();
return this.governanceContract.read.getConfiguration();
}

public getProposal(proposalId: bigint) {
return this.publicGovernance.read.getProposal([proposalId]);
return this.governanceContract.read.getProposal([proposalId]);
}

public async getProposalState(proposalId: bigint): Promise<ProposalState> {
const state = await this.publicGovernance.read.getProposalState([proposalId]);
const state = await this.governanceContract.read.getProposalState([proposalId]);
if (state < 0 || state > ProposalState.Expired) {
throw new Error(`Invalid proposal state: ${state}`);
}
return state as ProposalState;
}

private assertWalletGovernance(): NonNullable<typeof this.walletGovernance> {
if (!this.walletGovernance) {
throw new Error('Wallet client is required for this operation');
}
return this.walletGovernance;
}

public async deposit(onBehalfOf: Hex, amount: bigint) {
const walletGovernance = this.assertWalletGovernance();
const depositTx = await walletGovernance.write.deposit([onBehalfOf, amount]);
await this.client.waitForTransactionReceipt({ hash: depositTx });
}

public async proposeWithLock({
payloadAddress,
withdrawAddress,
}: {
payloadAddress: Hex;
withdrawAddress: Hex;
}): Promise<bigint> {
const walletGovernance = this.assertWalletGovernance();
const proposeTx = await walletGovernance.write.proposeWithLock([payloadAddress, withdrawAddress]);
const receipt = await this.client.waitForTransactionReceipt({ hash: proposeTx });
if (receipt.status !== 'success') {
throw new Error(`Proposal failed: ${receipt.status}`);
}
return extractProposalIdFromLogs(receipt.logs);
}

public async awaitProposalActive({ proposalId, logger }: { proposalId: bigint; logger: Logger }) {
const state = await this.getProposalState(proposalId);
if (state === ProposalState.Active) {
Expand Down Expand Up @@ -157,14 +126,39 @@ export class GovernanceContract {
await sleep(secondsToExecutable * 1000);
}
}
}

export class GovernanceContract extends ReadOnlyGovernanceContract {
protected override readonly governanceContract: GetContractReturnType<typeof GovernanceAbi, ExtendedViemWalletClient>;

constructor(address: Hex, public override readonly client: ExtendedViemWalletClient) {
super(address, client);
this.governanceContract = getContract({ address, abi: GovernanceAbi, client });
}

public async deposit(onBehalfOf: Hex, amount: bigint) {
const depositTx = await this.governanceContract.write.deposit([onBehalfOf, amount]);
await this.client.waitForTransactionReceipt({ hash: depositTx });
}

public async proposeWithLock({
payloadAddress,
withdrawAddress,
}: {
payloadAddress: Hex;
withdrawAddress: Hex;
}): Promise<bigint> {
const proposeTx = await this.governanceContract.write.proposeWithLock([payloadAddress, withdrawAddress]);
const receipt = await this.client.waitForTransactionReceipt({ hash: proposeTx });
if (receipt.status !== 'success') {
throw new Error(`Proposal failed: ${receipt.status}`);
}
return extractProposalIdFromLogs(receipt.logs);
}

public async getPower(): Promise<bigint> {
if (!isExtendedClient(this.client)) {
throw new Error('Wallet client is required for this operation');
}
const walletGovernance = this.assertWalletGovernance();
const now = await this.client.getBlock();
return walletGovernance.read.powerAt([this.client!.account.address, now.timestamp]);
return this.governanceContract.read.powerAt([this.client.account.address, now.timestamp]);
}

public async vote({
Expand All @@ -180,7 +174,6 @@ export class GovernanceContract {
retries: number;
logger: Logger;
}) {
const walletGovernance = this.assertWalletGovernance();
const l1TxUtils = new L1TxUtils(this.client, logger);
const retryDelaySeconds = 12;

Expand All @@ -197,7 +190,7 @@ export class GovernanceContract {
const encodedVoteData = encodeFunctionData(voteFunctionData);

const { receipt } = await l1TxUtils.sendAndMonitorTransaction({
to: walletGovernance.address,
to: this.governanceContract.address,
data: encodedVoteData,
});

Expand All @@ -207,7 +200,7 @@ export class GovernanceContract {
} else {
const args = {
...voteFunctionData,
address: walletGovernance.address,
address: this.governanceContract.address,
};
const errorMsg = await l1TxUtils.tryGetErrorFromRevertedTx(encodedVoteData, args, undefined, []);
logger.error(`Error voting on proposal ${proposalId}: ${errorMsg}`);
Expand Down Expand Up @@ -238,7 +231,6 @@ export class GovernanceContract {
retries: number;
logger: Logger;
}) {
const walletGovernance = this.assertWalletGovernance();
const l1TxUtils = new L1TxUtils(this.client, logger);
const retryDelaySeconds = 12;
let success = false;
Expand All @@ -252,7 +244,7 @@ export class GovernanceContract {
const encodedExecuteData = encodeFunctionData(executeFunctionData);

const { receipt } = await l1TxUtils.sendAndMonitorTransaction({
to: walletGovernance.address,
to: this.governanceContract.address,
data: encodedExecuteData,
});

Expand All @@ -262,7 +254,7 @@ export class GovernanceContract {
} else {
const args = {
...executeFunctionData,
address: walletGovernance.address,
address: this.governanceContract.address,
};
const errorMsg = await l1TxUtils.tryGetErrorFromRevertedTx(encodedExecuteData, args, undefined, []);
logger.error(`Error executing proposal ${proposalId}: ${errorMsg}`);
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/ethereum/src/contracts/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { type GetContractReturnType, type Hex, getContract } from 'viem';

import type { L1ContractAddresses } from '../l1_contract_addresses.js';
import type { ViemClient } from '../types.js';
import { GovernanceContract } from './governance.js';
import { ReadOnlyGovernanceContract } from './governance.js';
import { RollupContract } from './rollup.js';

export class RegistryContract {
Expand Down Expand Up @@ -65,7 +65,7 @@ export class RegistryContract {
Pick<L1ContractAddresses, 'governanceProposerAddress' | 'governanceAddress'>
> {
const governanceAddress = await this.registry.read.getGovernance();
const governance = new GovernanceContract(governanceAddress, this.client);
const governance = new ReadOnlyGovernanceContract(governanceAddress, this.client);
const governanceProposerAddress = await governance.getGovernanceProposerAddress();
return {
governanceAddress: governance.address,
Expand Down
Loading