feat: govern using ICAs (#3381)
### Description - Add inferICAEncodedSubmissionType for submitting transactions through ICA routers ### Drive-by changes - SafeUrl doesn't throw for chains we don't have safe service for. (bc while checking for native SubmissionType.SAFE we'll still check) ### Related issues - fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3199 ### Backward compatibility Yes ### Testing Unit tests --------- Signed-off-by: Paul Balaji <paul@hyperlane.xyz> Co-authored-by: Paul Balaji <paul@hyperlane.xyz> Co-authored-by: J M Rossy <jm.rossy@gmail.com> Co-authored-by: Trevor Porter <trkporter@ucdavis.edu> Co-authored-by: Daniel Savu <23065004+daniel-savu@users.noreply.github.com>pull/3495/head
parent
38358ececd
commit
d792309b8d
@ -0,0 +1,217 @@ |
||||
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; |
||||
import { expect } from 'chai'; |
||||
import { BigNumber } from 'ethers'; |
||||
import { ethers } from 'hardhat'; |
||||
|
||||
import { |
||||
InterchainAccountRouter, |
||||
TestRecipient, |
||||
TestRecipient__factory, |
||||
} from '@hyperlane-xyz/core'; |
||||
import { |
||||
AccountConfig, |
||||
ChainMap, |
||||
ChainName, |
||||
Chains, |
||||
HyperlaneApp, |
||||
HyperlaneAppChecker, |
||||
HyperlaneContractsMap, |
||||
HyperlaneIsmFactory, |
||||
HyperlaneProxyFactoryDeployer, |
||||
InterchainAccount, |
||||
InterchainAccountDeployer, |
||||
MultiProvider, |
||||
OwnableConfig, |
||||
RouterConfig, |
||||
TestCoreApp, |
||||
TestCoreDeployer, |
||||
randomAddress, |
||||
resolveOrDeployAccountOwner, |
||||
} from '@hyperlane-xyz/sdk'; |
||||
import { InterchainAccountFactories } from '@hyperlane-xyz/sdk/dist/middleware/account/contracts'; |
||||
import { Address, CallData, eqAddress } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { |
||||
AnnotatedCallData, |
||||
HyperlaneAppGovernor, |
||||
} from '../src/govern/HyperlaneAppGovernor'; |
||||
|
||||
export class TestApp extends HyperlaneApp<{}> {} |
||||
|
||||
export class TestChecker extends HyperlaneAppChecker<TestApp, OwnableConfig> { |
||||
async checkChain(_: string): Promise<void> { |
||||
this.addViolation({ |
||||
chain: Chains.test2, |
||||
type: 'test', |
||||
expected: 0, |
||||
actual: 1, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
export class HyperlaneTestGovernor extends HyperlaneAppGovernor< |
||||
TestApp, |
||||
OwnableConfig |
||||
> { |
||||
protected async mapViolationsToCalls() { |
||||
return; |
||||
} |
||||
|
||||
mockPushCall(chain: string, call: AnnotatedCallData): void { |
||||
this.pushCall(chain, call); |
||||
} |
||||
|
||||
async govern(_ = true, chain?: ChainName) { |
||||
// 2. For each call, infer how it should be submitted on-chain.
|
||||
await this.inferCallSubmissionTypes(); |
||||
|
||||
// 3. Prompt the user to confirm that the count, description,
|
||||
// and submission methods look correct before submitting.
|
||||
const chains = chain ? [chain] : Object.keys(this.calls); |
||||
for (const chain of chains) { |
||||
await this.mockSendCalls(chain, this.calls[chain]); |
||||
} |
||||
} |
||||
|
||||
protected async mockSendCalls( |
||||
chain: ChainName, |
||||
calls: CallData[], |
||||
): Promise<void> { |
||||
for (const call of calls) { |
||||
await this.checker.multiProvider.sendTransaction(chain, { |
||||
to: call.to, |
||||
data: call.data, |
||||
value: call.value, |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
|
||||
describe('ICA governance', async () => { |
||||
const localChain = Chains.test1; |
||||
const remoteChain = Chains.test2; |
||||
|
||||
let signer: SignerWithAddress; |
||||
let multiProvider: MultiProvider; |
||||
let accountConfig: AccountConfig; |
||||
let coreApp: TestCoreApp; |
||||
// let local: InterchainAccountRouter;
|
||||
let remote: InterchainAccountRouter; |
||||
let routerConfig: ChainMap<RouterConfig>; |
||||
let contracts: HyperlaneContractsMap<InterchainAccountFactories>; |
||||
let icaApp: InterchainAccount; |
||||
let recipient: TestRecipient; |
||||
let accountOwner: Address; |
||||
let governor: HyperlaneTestGovernor; |
||||
|
||||
before(async () => { |
||||
[signer] = await ethers.getSigners(); |
||||
multiProvider = MultiProvider.createTestMultiProvider({ signer }); |
||||
const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider); |
||||
const ismFactory = new HyperlaneIsmFactory( |
||||
await ismFactoryDeployer.deploy(multiProvider.mapKnownChains(() => ({}))), |
||||
multiProvider, |
||||
); |
||||
|
||||
coreApp = await new TestCoreDeployer(multiProvider, ismFactory).deployApp(); |
||||
routerConfig = coreApp.getRouterConfig(signer.address); |
||||
}); |
||||
|
||||
beforeEach(async () => { |
||||
contracts = await new InterchainAccountDeployer(multiProvider).deploy( |
||||
routerConfig, |
||||
); |
||||
// local = contracts[localChain].interchainAccountRouter;
|
||||
remote = contracts[remoteChain].interchainAccountRouter; |
||||
icaApp = new InterchainAccount(contracts, multiProvider); |
||||
|
||||
accountConfig = { |
||||
origin: Chains.test1, |
||||
owner: signer.address, |
||||
localRouter: remote.address, |
||||
}; |
||||
|
||||
const recipientF = new TestRecipient__factory(signer); |
||||
recipient = await recipientF.deploy(); |
||||
|
||||
const contractsMap = { |
||||
[remoteChain]: { |
||||
recipient, |
||||
}, |
||||
[localChain]: { |
||||
recipient, |
||||
}, |
||||
}; |
||||
// missing ica
|
||||
const configMap = { |
||||
[localChain]: { owner: signer.address }, |
||||
[remoteChain]: { |
||||
owner: { origin: Chains.test1, owner: signer.address }, |
||||
}, |
||||
}; |
||||
|
||||
const app = new TestApp(contractsMap, multiProvider); |
||||
const checker = new TestChecker(multiProvider, app, configMap); |
||||
governor = new HyperlaneTestGovernor(checker, icaApp); |
||||
|
||||
accountOwner = await resolveOrDeployAccountOwner( |
||||
multiProvider, |
||||
remoteChain, |
||||
accountConfig, |
||||
); |
||||
await recipient.transferOwnership(accountOwner); |
||||
}); |
||||
|
||||
it('changes ISM on the remote recipient', async () => { |
||||
// precheck
|
||||
const actualOwner = await recipient.owner(); |
||||
expect(actualOwner).to.equal(accountOwner); |
||||
|
||||
// arrange
|
||||
const newIsm = randomAddress(); |
||||
await governor.checker.checkChain(Chains.test2); |
||||
const call = { |
||||
to: recipient.address, |
||||
data: recipient.interface.encodeFunctionData( |
||||
'setInterchainSecurityModule', |
||||
[newIsm], |
||||
), |
||||
value: BigNumber.from(0), |
||||
description: 'Setting ISM on the test recipient', |
||||
}; |
||||
governor.mockPushCall(remoteChain, call); |
||||
|
||||
// act
|
||||
await governor.govern(); // this is where the ICA inference happens
|
||||
await coreApp.processMessages(); |
||||
|
||||
// assert
|
||||
const actualIsm = await recipient.interchainSecurityModule(); |
||||
expect(eqAddress(actualIsm, newIsm)).to.be.true; |
||||
}); |
||||
|
||||
it('transfer ownership back to the deployer', async () => { |
||||
// precheck
|
||||
let actualOwner = await recipient.owner(); |
||||
expect(actualOwner).to.equal(accountOwner); |
||||
|
||||
// arrange
|
||||
const call = { |
||||
to: recipient.address, |
||||
data: recipient.interface.encodeFunctionData('transferOwnership', [ |
||||
signer.address, |
||||
]), |
||||
value: BigNumber.from(0), |
||||
description: 'Transfer ownership', |
||||
}; |
||||
governor.mockPushCall(remoteChain, call); |
||||
|
||||
// act
|
||||
await governor.govern(); |
||||
await coreApp.processMessages(); |
||||
|
||||
// assert
|
||||
actualOwner = await recipient.owner(); |
||||
expect(actualOwner).to.equal(signer.address); |
||||
}); |
||||
}); |
Loading…
Reference in new issue