UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

649 lines 32.9 kB
import chai, { expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { ethers } from 'ethers'; import hre from 'hardhat'; import { TimelockController__factory } from '@hyperlane-xyz/core'; import { assert, deepCopy, normalizeAddressEvm } from '@hyperlane-xyz/utils'; import { KNOWN_BASE_TIMELOCK_CONTRACT, TestChainName, baseTestChain, test1, } from '../../consts/testChains.js'; import { ZBytes32String } from '../../metadata/customZodTypes.js'; import { MultiProvider } from '../../providers/MultiProvider.js'; import { randomAddress } from '../../test/testUtils.js'; import { EvmTimelockDeployer } from './EvmTimelockDeployer.js'; import { EvmTimelockReader } from './EvmTimelockReader.js'; import { EMPTY_BYTES_32 } from './constants.js'; chai.use(chaiAsPromised); describe(EvmTimelockReader.name, () => { let contractOwner; let proposer; let executor; let providerChainTest1; let multiProvider; let timelockDeployer; let timelockReader; let timelockAddress; beforeEach(async () => { [contractOwner, proposer, executor] = await hre.ethers.getSigners(); assert(contractOwner.provider, 'Provider should be available'); providerChainTest1 = contractOwner.provider; // Initialize MultiProvider with test chain const testChain1Clone = deepCopy(test1); testChain1Clone.blockExplorers = []; multiProvider = new MultiProvider({ [TestChainName.test1]: testChain1Clone, }); multiProvider.setProvider(TestChainName.test1, providerChainTest1); multiProvider.setSharedSigner(contractOwner); // Deploy timelock contract timelockDeployer = new EvmTimelockDeployer(multiProvider); }); async function deployTestTimelock() { const config = { minimumDelay: 0, proposers: [proposer.address], executors: [executor.address], admin: contractOwner.address, }; const { TimelockController } = await timelockDeployer.deployContracts(TestChainName.test1, config); timelockAddress = TimelockController.address; assert(TimelockController.deployTransaction.blockNumber, 'Expected the Timelock deployment block number to be defined'); return TimelockController; } describe(EvmTimelockReader.fromConfig.name, () => { beforeEach(async () => { await deployTestTimelock(); }); it('should initialize EvmTimelockReader using fromConfig', async () => { const reader = EvmTimelockReader.fromConfig({ chain: TestChainName.test1, timelockAddress, multiProvider, }); expect(reader).to.be.instanceOf(EvmTimelockReader); expect(reader['timelockInstance'].address).to.equal(timelockAddress); expect(reader['chain']).to.equal(TestChainName.test1); }); it('should create reader with valid timelock address', async () => { expect(() => { EvmTimelockReader.fromConfig({ chain: TestChainName.test1, timelockAddress, multiProvider, }); }).to.not.throw(); }); }); describe(`${EvmTimelockReader.name} (RPC)`, () => { beforeEach(async () => { await deployTestTimelock(); timelockReader = EvmTimelockReader.fromConfig({ chain: TestChainName.test1, timelockAddress, multiProvider, }); }); describe(`${EvmTimelockReader.prototype.getScheduledOperations.name}`, () => { it('should return empty object when no transactions are scheduled', async () => { const scheduledTxs = await timelockReader.getScheduledOperations(); expect(scheduledTxs).to.deep.equal({}); }); const scheduleTestCases = [ { title: 'should retrieve single scheduled transaction correctly', timelockTx: { data: [ { to: randomAddress(), value: ethers.utils.parseEther('1'), data: '0x1234', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('test-salt'), }, }, { title: 'should handle multiple scheduled transactions in a batch', timelockTx: { data: [ { to: randomAddress(), value: ethers.utils.parseEther('1'), data: '0x1234', }, { to: randomAddress(), value: ethers.utils.parseEther('2'), data: '0x5678', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('batch-salt'), }, }, { title: 'should handle transactions with no salt (using EMPTY_BYTES_32)', timelockTx: { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: EMPTY_BYTES_32, }, }, ]; for (const { title, timelockTx } of scheduleTestCases) { it(title, async () => { const proposerTimelock = TimelockController__factory.connect(timelockAddress, proposer); const targets = timelockTx.data.map((tx) => tx.to); const values = timelockTx.data.map((tx) => tx.value ?? '0'); const dataArray = timelockTx.data.map((tx) => tx.data); const scheduleTx = await proposerTimelock.scheduleBatch(targets, values, dataArray, timelockTx.predecessor, timelockTx.salt, timelockTx.delay); await scheduleTx.wait(); const scheduledTxs = await timelockReader.getScheduledOperations(); const txIds = Object.keys(scheduledTxs); expect(txIds).to.have.length(1); const scheduledTx = scheduledTxs[txIds[0]]; expect(scheduledTx.data).to.have.length(timelockTx.data.length); for (let i = 0; i < timelockTx.data.length; i++) { expect(normalizeAddressEvm(scheduledTx.data[i].to)).to.equal(normalizeAddressEvm(timelockTx.data[i].to)); assert(scheduledTx.data[i].value, 'Expected value to be defined when reading from Timelock'); expect(scheduledTx.data[i].value?.toString()).to.equal(timelockTx.data[i].value?.toString() ?? '0'); expect(scheduledTx.data[i].data).to.equal(timelockTx.data[i].data); } expect(scheduledTx.delay).to.equal(timelockTx.delay); expect(scheduledTx.predecessor).to.equal(timelockTx.predecessor); expect(scheduledTx.salt).to.equal(timelockTx.salt); expect(scheduledTx.id).to.equal(txIds[0]); }); } }); describe(`${EvmTimelockReader.prototype.getCancelledOperationIds.name}`, () => { it('should return empty set when no transactions are cancelled', async () => { const cancelledIds = await timelockReader.getCancelledOperationIds(); expect(cancelledIds.size).to.equal(0); }); const cancelTestCases = [ { title: 'should retrieve single cancelled operation ID correctly', timelockTxs: [ { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x1234', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('cancel-test'), }, ], }, { title: 'should handle multiple cancelled operations', timelockTxs: [ { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x1234', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('cancel-1'), }, { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x5678', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('cancel-2'), }, ], }, ]; for (const { title, timelockTxs } of cancelTestCases) { it(title, async () => { const proposerTimelock = TimelockController__factory.connect(timelockAddress, proposer); const operationIds = []; // Schedule and cancel operations for (const timelockTx of timelockTxs) { const targets = timelockTx.data.map((tx) => tx.to); const values = timelockTx.data.map((tx) => tx.value ?? '0'); const dataArray = timelockTx.data.map((tx) => tx.data); // Schedule const scheduleTx = await proposerTimelock.scheduleBatch(targets, values, dataArray, timelockTx.predecessor, timelockTx.salt, timelockTx.delay); await scheduleTx.wait(); // Get operation ID const operationId = await proposerTimelock.hashOperationBatch(targets, values, dataArray, timelockTx.predecessor, timelockTx.salt); operationIds.push(operationId); // Cancel const cancelTx = await proposerTimelock.cancel(operationId); await cancelTx.wait(); } const cancelledIds = await timelockReader.getCancelledOperationIds(); expect(cancelledIds.size).to.equal(timelockTxs.length); for (const operationId of operationIds) { expect(cancelledIds.has(operationId)).to.be.true; } }); } }); describe(`${EvmTimelockReader.prototype.getExecutedOperationIds.name}`, () => { it('should return empty set when no transactions are executed', async () => { const executedIds = await timelockReader.getExecutedOperationIds(); expect(executedIds.size).to.equal(0); }); const executeTestCases = [ { title: 'should retrieve single executed operation ID correctly', timelockTxs: [ { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('execute-test'), }, ], }, { title: 'should handle multiple executed operations', timelockTxs: [ { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('execute-1'), }, { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('execute-2'), }, ], }, ]; for (const { title, timelockTxs } of executeTestCases) { it(title, async () => { const proposerTimelock = TimelockController__factory.connect(timelockAddress, proposer); const executorTimelock = TimelockController__factory.connect(timelockAddress, executor); const operationIds = []; // Schedule and execute operations for (const timelockTx of timelockTxs) { const targets = timelockTx.data.map((tx) => tx.to); const values = timelockTx.data.map((tx) => tx.value ?? '0'); const dataArray = timelockTx.data.map((tx) => tx.data); // Schedule const scheduleTx = await proposerTimelock.scheduleBatch(targets, values, dataArray, timelockTx.predecessor, timelockTx.salt, timelockTx.delay); await scheduleTx.wait(); // Get operation ID const operationId = await proposerTimelock.hashOperationBatch(targets, values, dataArray, timelockTx.predecessor, timelockTx.salt); operationIds.push(operationId); // Execute const executeTx = await executorTimelock.executeBatch(targets, values, dataArray, timelockTx.predecessor, timelockTx.salt); await executeTx.wait(); } const executedIds = await timelockReader.getExecutedOperationIds(); expect(executedIds.size).to.equal(timelockTxs.length); for (const operationId of operationIds) { expect(executedIds.has(operationId)).to.be.true; } }); } }); describe(`${EvmTimelockReader.prototype.getReadyOperationIds.name}`, () => { it('should return empty set for empty input', async () => { const readyIds = await timelockReader.getReadyOperationIds([]); expect(readyIds.size).to.equal(0); }); const readyTestCases = [ { title: 'should return ready operations correctly (no delay)', timelockTxs: [ { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('ready-test'), }, ], expectedReadyCount: 1, }, { title: 'should filter out non-ready operations (with delay)', timelockTxs: [ { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x', }, ], delay: 3600, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('not-ready'), }, ], expectedReadyCount: 0, }, { title: 'should handle mixed ready and non-ready operations', timelockTxs: [ { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('ready-1'), }, { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x', }, ], delay: 3600, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('not-ready-1'), }, ], expectedReadyCount: 1, }, ]; for (const { title, timelockTxs, expectedReadyCount } of readyTestCases) { it(title, async () => { const proposerTimelock = TimelockController__factory.connect(timelockAddress, proposer); const operationIds = []; // Schedule operations for (const timelockTx of timelockTxs) { const targets = timelockTx.data.map((tx) => tx.to); const values = timelockTx.data.map((tx) => tx.value ?? '0'); const dataArray = timelockTx.data.map((tx) => tx.data); // Schedule const scheduleTx = await proposerTimelock.scheduleBatch(targets, values, dataArray, timelockTx.predecessor, timelockTx.salt, timelockTx.delay); await scheduleTx.wait(); // Get operation ID const operationId = await proposerTimelock.hashOperationBatch(targets, values, dataArray, timelockTx.predecessor, timelockTx.salt); operationIds.push(operationId); } const readyIds = await timelockReader.getReadyOperationIds(operationIds); expect(readyIds.size).to.equal(expectedReadyCount); }); } }); describe(`${EvmTimelockReader.prototype.getScheduledExecutableTransactions.name}`, () => { it('should return empty object when no executable transactions exist', async () => { const executableTxs = await timelockReader.getScheduledExecutableTransactions(); expect(executableTxs).to.deep.equal({}); }); const executableTestCases = [ { title: 'should return scheduled executable transactions', scheduledTxs: [ { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x1234', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('executable'), }, ], expectedExecutableCount: 1, }, { title: 'should exclude cancelled transactions from executable list', scheduledTxs: [ { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x1234', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('cancelled'), }, ], cancelledTxIndexes: [0], expectedExecutableCount: 0, }, { title: 'should exclude executed transactions from executable list', scheduledTxs: [ { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('executed'), }, ], executedTxIndexes: [0], expectedExecutableCount: 0, }, { title: 'should handle mixed scheduled, cancelled, and executed transactions', scheduledTxs: [ { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x1234', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('executable-1'), }, { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x5678', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('cancelled-1'), }, { data: [ { to: randomAddress(), value: ethers.BigNumber.from(0), data: '0x', }, ], delay: 0, predecessor: EMPTY_BYTES_32, salt: ethers.utils.formatBytes32String('executed-1'), }, ], cancelledTxIndexes: [1], executedTxIndexes: [2], expectedExecutableCount: 1, }, ]; for (const { title, scheduledTxs, cancelledTxIndexes, executedTxIndexes, expectedExecutableCount, } of executableTestCases) { it(title, async () => { const proposerTimelock = TimelockController__factory.connect(timelockAddress, proposer); const executorTimelock = TimelockController__factory.connect(timelockAddress, executor); const operationIds = []; // Schedule all transactions for (const timelockTx of scheduledTxs) { const targets = timelockTx.data.map((tx) => tx.to); const values = timelockTx.data.map((tx) => tx.value ?? '0'); const dataArray = timelockTx.data.map((tx) => tx.data); // Schedule const scheduleTx = await proposerTimelock.scheduleBatch(targets, values, dataArray, timelockTx.predecessor, timelockTx.salt, timelockTx.delay); await scheduleTx.wait(); // Get operation ID const operationId = await proposerTimelock.hashOperationBatch(targets, values, dataArray, timelockTx.predecessor, timelockTx.salt); operationIds.push(operationId); } // Cancel specific transactions if (cancelledTxIndexes) { for (const index of cancelledTxIndexes) { const cancelTx = await proposerTimelock.cancel(operationIds[index]); await cancelTx.wait(); } } // Execute specific transactions if (executedTxIndexes) { for (const index of executedTxIndexes) { const timelockTx = scheduledTxs[index]; const targets = timelockTx.data.map((tx) => tx.to); const values = timelockTx.data.map((tx) => tx.value ?? '0'); const dataArray = timelockTx.data.map((tx) => tx.data); const executeTx = await executorTimelock.executeBatch(targets, values, dataArray, timelockTx.predecessor, timelockTx.salt); await executeTx.wait(); } } const executableTxs = await timelockReader.getScheduledExecutableTransactions(); const txIds = Object.keys(executableTxs); expect(txIds).to.have.length(expectedExecutableCount); // Verify structure of executable transactions for (const [txId, executableTx] of Object.entries(executableTxs)) { expect(executableTx.id).to.equal(txId); expect(executableTx.data).to.be.an('array'); expect(executableTx.data.length).to.be.greaterThan(0); expect(executableTx.encodedExecuteTransaction).to.be.a('string'); expect(executableTx.encodedExecuteTransaction.length).to.be.greaterThan(0); expect(executableTx.delay).to.be.a('number'); expect(executableTx.predecessor).to.be.a('string'); expect(executableTx.salt).to.be.a('string'); } }); } }); }); describe(`${EvmTimelockReader.name} (Block Explorer)`, () => { let reader; let multiProvider; beforeEach(async () => { multiProvider = new MultiProvider({ base: baseTestChain, }); reader = EvmTimelockReader.fromConfig({ chain: baseTestChain.name, timelockAddress: KNOWN_BASE_TIMELOCK_CONTRACT, multiProvider, }); }); describe(`${EvmTimelockReader.prototype.getScheduledOperations.name}`, () => { it('should retrieve scheduled transactions from block explorer API', async () => { const scheduledTxs = await reader.getScheduledOperations(); // Should find some scheduled transactions on this timelock expect(Object.keys(scheduledTxs).length).to.be.greaterThan(0); // Validate structure of returned transactions for (const [txId, tx] of Object.entries(scheduledTxs)) { expect(ZBytes32String.safeParse(txId).success).to.be.true; expect(tx.id).to.equal(txId); expect(tx.data.length).to.be.greaterThan(0); expect(tx.delay).not.to.be.undefined; expect(ZBytes32String.safeParse(tx.predecessor).success).to.be.true; expect(ZBytes32String.safeParse(tx.salt).success).to.be.true; } }); }); describe(`${EvmTimelockReader.prototype.getCancelledOperationIds.name}`, () => { it('should retrieve cancelled operation IDs from block explorer API', async () => { const cancelledIds = await reader.getCancelledOperationIds(); expect(cancelledIds).to.be.instanceOf(Set); for (const id of cancelledIds) { expect(ZBytes32String.safeParse(id).success).to.be.true; } }); }); describe(`${EvmTimelockReader.prototype.getExecutedOperationIds.name}`, () => { it('should retrieve executed operation IDs from block explorer API', async () => { const executedIds = await reader.getExecutedOperationIds(); // Should find some executed transactions on this timelock expect(executedIds.size).to.be.greaterThan(0); for (const id of executedIds) { expect(ZBytes32String.safeParse(id).success).to.be.true; } }); }); describe(`${EvmTimelockReader.prototype.getScheduledExecutableTransactions.name}`, () => { it('should retrieve scheduled executable transactions from block explorer API', async () => { const executableTxs = await reader.getScheduledExecutableTransactions(); for (const [txId, executableTx] of Object.entries(executableTxs)) { expect(executableTx.id).to.equal(txId); expect(ZBytes32String.safeParse(txId).success).to.be.true; expect(executableTx.data.length).to.be.greaterThan(0); expect(executableTx.encodedExecuteTransaction).to.be.a('string'); expect(executableTx.encodedExecuteTransaction.length).to.be.greaterThan(0); expect(executableTx.delay).not.to.be.undefined; expect(ZBytes32String.safeParse(executableTx.predecessor).success).to .be.true; expect(ZBytes32String.safeParse(executableTx.salt).success).to.be .true; } }); }); }); }); //# sourceMappingURL=EvmTimelockReader.hardhat-test.js.map