feat: implement HyperlaneHaasGovernor (#4435)

feat: implement HyperlaneHaasGovernor
- allows one-time governance of actions resulting from both ICA+Core
checkers
- updates check-deploy to support the `-m haas` module
- also fixes a bug to do with submitting calls to the current SAFE owner
when a new ICA owner has just been configured

example:

![image](https://github.com/user-attachments/assets/8d92053b-d3eb-4ac0-86b0-7330305719df)

![image](https://github.com/user-attachments/assets/f3db2d48-0995-404f-8861-fb34c6fd08f1)

---------

Signed-off-by: pbio <10051819+paulbalaji@users.noreply.github.com>
pull/4442/head
Paul Balaji 3 months ago committed by GitHub
parent 15c1e3ba9e
commit 24ac8de29c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      typescript/infra/config/environments/mainnet3/owners.ts
  2. 1
      typescript/infra/scripts/agent-utils.ts
  3. 8
      typescript/infra/scripts/check/check-deploy.ts
  4. 20
      typescript/infra/scripts/check/check-utils.ts
  5. 4
      typescript/infra/scripts/check/check-warp-deploy.ts
  6. 76
      typescript/infra/src/govern/HyperlaneAppGovernor.ts
  7. 2
      typescript/infra/src/govern/HyperlaneCoreGovernor.ts
  8. 105
      typescript/infra/src/govern/HyperlaneHaasGovernor.ts
  9. 2
      typescript/infra/src/govern/ProxiedRouterGovernor.ts
  10. 2
      typescript/infra/test/govern.hardhat-test.ts
  11. 14
      typescript/sdk/src/providers/SmartProvider/SmartProvider.ts

@ -83,6 +83,12 @@ export const ethereumChainOwners: ChainMap<OwnableConfig> = Object.fromEntries(
validatorAnnounce: DEPLOYER, // unused
testRecipient: DEPLOYER,
fallbackRoutingHook: DEPLOYER,
// Because of the logic above of setting the owner to the Safe or ICA address,
// the checker/governor tooling does not know what type of owner it is.
// So we need to keep the Safe and ICA addresses somewhere in the config
// to be able to track down which addresses are SAFEs, ICAs, or standard SIGNERS.
...(safes[local] && { _safeAddress: safes[local] }),
...(icas[local] && { _icaAddress: icas[local] }),
},
},
];

@ -71,6 +71,7 @@ export enum Modules {
TEST_RECIPIENT = 'testrecipient',
HELLO_WORLD = 'helloworld',
WARP = 'warp',
HAAS = 'haas',
}
export const REGISTRY_MODULES = [

@ -28,24 +28,24 @@ async function main() {
);
if (fork) {
await governor.checker.checkChain(fork);
await governor.checkChain(fork);
if (govern) {
await governor.govern(false, fork);
}
} else if (chain) {
await governor.checker.checkChain(chain);
await governor.checkChain(chain);
if (govern) {
await governor.govern(true, chain);
}
} else {
await governor.checker.check();
await governor.check();
if (govern) {
await governor.govern();
}
}
if (!govern) {
const violations = governor.checker.violations;
const violations = governor.getCheckerViolations();
if (violations.length > 0) {
logViolations(violations);

@ -26,6 +26,7 @@ import { getWarpConfig } from '../../config/warp.js';
import { DeployEnvironment } from '../../src/config/environment.js';
import { HyperlaneAppGovernor } from '../../src/govern/HyperlaneAppGovernor.js';
import { HyperlaneCoreGovernor } from '../../src/govern/HyperlaneCoreGovernor.js';
import { HyperlaneHaasGovernor } from '../../src/govern/HyperlaneHaasGovernor.js';
import { HyperlaneIgpGovernor } from '../../src/govern/HyperlaneIgpGovernor.js';
import { ProxiedRouterGovernor } from '../../src/govern/ProxiedRouterGovernor.js';
import { Role } from '../../src/roles.js';
@ -106,7 +107,7 @@ export async function getGovernor(
const icaChainAddresses = objFilter(
chainAddresses,
(chain, addresses): addresses is Record<string, string> =>
(chain, _): _ is Record<string, string> =>
!!chainAddresses[chain]?.interchainAccountRouter,
);
const ica = InterchainAccount.fromAddressesMap(
@ -137,6 +138,23 @@ export async function getGovernor(
),
);
governor = new ProxiedRouterGovernor(checker);
} else if (module === Modules.HAAS) {
const icaChecker = new InterchainAccountChecker(
multiProvider,
ica,
objFilter(
routerConfig,
(chain, _): _ is InterchainAccountConfig => !!icaChainAddresses[chain],
),
);
const coreChecker = new HyperlaneCoreChecker(
multiProvider,
core,
envConfig.core,
ismFactory,
chainAddresses,
);
governor = new HyperlaneHaasGovernor(ica, icaChecker, coreChecker);
} else if (module === Modules.INTERCHAIN_QUERY_SYSTEM) {
const iqs = InterchainQuery.fromAddressesMap(chainAddresses, multiProvider);
const checker = new InterchainQueryChecker(

@ -40,9 +40,9 @@ async function main() {
fork,
);
await governor.checker.check();
await governor.check();
const violations: any = governor.checker.violations;
const violations: any = governor.getCheckerViolations();
if (violations.length > 0) {
logViolations(violations);

@ -53,7 +53,7 @@ export abstract class HyperlaneAppGovernor<
App extends HyperlaneApp<any>,
Config extends OwnableConfig,
> {
readonly checker: HyperlaneAppChecker<App, Config>;
protected readonly checker: HyperlaneAppChecker<App, Config>;
protected calls: ChainMap<AnnotatedCallData[]>;
private canPropose: ChainMap<Map<string, boolean>>;
readonly interchainAccount?: InterchainAccount;
@ -70,6 +70,18 @@ export abstract class HyperlaneAppGovernor<
}
}
async check() {
await this.checker.check();
}
async checkChain(chain: ChainName) {
await this.checker.checkChain(chain);
}
getCheckerViolations() {
return this.checker.violations;
}
async govern(confirm = true, chain?: ChainName) {
if (this.checker.violations.length === 0) return;
// 1. Produce calls from checker violations.
@ -100,7 +112,9 @@ export abstract class HyperlaneAppGovernor<
`> ${calls.length} calls will be submitted via ${SubmissionType[submissionType]}`,
);
calls.map((c) =>
console.log(`> > ${c.description} (to: ${c.to} data: ${c.data})`),
console.log(
`> > ${c.description} (to: ${c.to} data: ${c.data}) value: ${c.value}`,
),
);
if (!requestConfirmation) return true;
@ -151,22 +165,35 @@ export abstract class HyperlaneAppGovernor<
new SignerMultiSend(this.checker.multiProvider, chain),
);
const safeOwner = this.checker.configMap[chain].owner;
await retryAsync(
() =>
sendCallsForType(
SubmissionType.SAFE,
new SafeMultiSend(this.checker.multiProvider, chain, safeOwner),
),
10,
);
const safeOwner =
this.checker.configMap[chain].ownerOverrides?._safeAddress;
if (!safeOwner) {
console.warn(`No Safe owner found for chain ${chain}`);
} else {
await retryAsync(
() =>
sendCallsForType(
SubmissionType.SAFE,
new SafeMultiSend(this.checker.multiProvider, chain, safeOwner),
),
10,
);
}
await sendCallsForType(SubmissionType.MANUAL, new ManualMultiSend(chain));
}
protected pushCall(chain: ChainName, call: AnnotatedCallData) {
this.calls[chain] = this.calls[chain] || [];
this.calls[chain].push(call);
const isDuplicate = this.calls[chain].some(
(existingCall) =>
existingCall.to === call.to &&
existingCall.data === call.data &&
existingCall.value?.eq(call.value || 0),
);
if (!isDuplicate) {
this.calls[chain].push(call);
}
}
protected async mapViolationsToCalls(): Promise<void> {
@ -201,19 +228,7 @@ export abstract class HyperlaneAppGovernor<
for (const chain of Object.keys(this.calls)) {
try {
for (const call of this.calls[chain]) {
let inferredCall: InferredCall;
inferredCall = await this.inferCallSubmissionType(chain, call);
// If it's a manual call, it means that we're not able to make the call
// from a signer or Safe. In this case, we try to infer if it must be sent
// from an ICA controlled by a remote owner. This new inferred call will be
// unchanged if the call is not an ICA call after all.
if (inferredCall.type === SubmissionType.MANUAL) {
inferredCall = await this.inferICAEncodedSubmissionType(
chain,
call,
);
}
const inferredCall = await this.inferCallSubmissionType(chain, call);
pushNewCall(inferredCall);
}
} catch (error) {
@ -285,6 +300,7 @@ export abstract class HyperlaneAppGovernor<
eqAddress(bytes32ToAddress(accountConfig.owner), submitterAddress)
);
},
true, // Flag this as an ICA call
);
if (subType !== SubmissionType.MANUAL) {
return {
@ -311,6 +327,7 @@ export abstract class HyperlaneAppGovernor<
chain: ChainName,
submitterAddress: Address,
) => boolean,
isICACall: boolean = false,
): Promise<InferredCall> {
const multiProvider = this.checker.multiProvider;
const signer = multiProvider.getSigner(chain);
@ -361,7 +378,8 @@ export abstract class HyperlaneAppGovernor<
}
// 2. Check if the call will succeed via Gnosis Safe.
const safeAddress = this.checker.configMap[chain].owner;
const safeAddress =
this.checker.configMap[chain].ownerOverrides?._safeAddress;
if (typeof safeAddress === 'string') {
// 2a. Confirm that the signer is a Safe owner or delegate.
@ -413,6 +431,12 @@ export abstract class HyperlaneAppGovernor<
}
}
// Only try ICA encoding if this isn't already an ICA call
if (!isICACall) {
return this.inferICAEncodedSubmissionType(chain, call);
}
// If it is an ICA call and we've reached this point, default to manual submission
return {
type: SubmissionType.MANUAL,
chain,

@ -67,7 +67,7 @@ export class HyperlaneCoreGovernor extends HyperlaneAppGovernor<
}
}
protected async mapViolationToCall(violation: CheckerViolation) {
public async mapViolationToCall(violation: CheckerViolation) {
switch (violation.type) {
case ViolationType.Owner: {
return this.handleOwnerViolation(violation as OwnerViolation);

@ -0,0 +1,105 @@
import {
ChainName,
CheckerViolation,
CoreConfig,
HyperlaneCore,
HyperlaneCoreChecker,
InterchainAccount,
InterchainAccountChecker,
} from '@hyperlane-xyz/sdk';
import {
AnnotatedCallData,
HyperlaneAppGovernor,
} from './HyperlaneAppGovernor.js';
import { HyperlaneCoreGovernor } from './HyperlaneCoreGovernor.js';
import { ProxiedRouterGovernor } from './ProxiedRouterGovernor.js';
export class HyperlaneHaasGovernor extends HyperlaneAppGovernor<
HyperlaneCore,
CoreConfig
> {
protected readonly icaGovernor: ProxiedRouterGovernor<any, any>;
protected readonly coreGovernor: HyperlaneCoreGovernor;
constructor(
ica: InterchainAccount,
private readonly icaChecker: InterchainAccountChecker,
private readonly coreChecker: HyperlaneCoreChecker,
) {
super(coreChecker, ica);
this.icaGovernor = new ProxiedRouterGovernor(this.icaChecker);
this.coreGovernor = new HyperlaneCoreGovernor(this.coreChecker, this.ica);
}
protected mapViolationToCall(
_: CheckerViolation,
): Promise<{ chain: string; call: AnnotatedCallData } | undefined> {
throw new Error(`HyperlaneHaasGovernor has no native map of violations.`);
}
// Handle ICA violations before Core violations
protected async mapViolationsToCalls(): Promise<void> {
// Handle ICA violations first
const icaCallObjs = await Promise.all(
this.icaChecker.violations.map((violation) =>
this.icaGovernor.mapViolationToCall(violation),
),
);
// Process ICA call objects
for (const callObj of icaCallObjs) {
if (callObj) {
this.pushCall(callObj.chain, callObj.call);
}
}
// Then handle Core violations
const coreCallObjs = await Promise.all(
this.coreChecker.violations.map((violation) =>
this.coreGovernor.mapViolationToCall(violation),
),
);
// Process Core call objects
for (const callObj of coreCallObjs) {
if (callObj) {
this.pushCall(callObj.chain, callObj.call);
}
}
}
async check() {
await this.icaChecker.check();
await this.coreChecker.check();
}
async checkChain(chain: ChainName) {
await this.icaChecker.checkChain(chain);
await this.coreChecker.checkChain(chain);
}
getCheckerViolations() {
return [...this.icaChecker.violations, ...this.coreChecker.violations];
}
async govern(confirm = true, chain?: ChainName) {
const totalViolations =
this.icaChecker.violations.length + this.coreChecker.violations.length;
if (totalViolations === 0) return;
// 1. Map violations to calls
await this.mapViolationsToCalls();
// 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.sendCalls(chain, confirm);
}
}
}

@ -19,7 +19,7 @@ export class ProxiedRouterGovernor<
App extends RouterApp<any>,
Config extends RouterConfig,
> extends HyperlaneAppGovernor<App, Config> {
protected async mapViolationToCall(violation: CheckerViolation) {
public async mapViolationToCall(violation: CheckerViolation) {
switch (violation.type) {
case ConnectionClientViolationType.InterchainSecurityModule:
return this.handleIsmViolation(violation as ConnectionClientViolation);

@ -166,7 +166,7 @@ describe('ICA governance', async () => {
// arrange
const newIsm = randomAddress();
await governor.checker.checkChain(TestChainName.test2);
await governor.checkChain(TestChainName.test2);
const call = {
to: recipient.address,
data: recipient.interface.encodeFunctionData(

@ -338,12 +338,14 @@ export class HyperlaneSmartProvider
} else if (result.status === ProviderStatus.Timeout) {
this.throwCombinedProviderErrors(
[result, ...providerResultErrors],
`All providers timed out for method ${method}`,
`All providers timed out on chain ${this._network.name} for method ${method}`,
);
} else if (result.status === ProviderStatus.Error) {
this.throwCombinedProviderErrors(
[result.error, ...providerResultErrors],
`All providers failed for method ${method} and params ${JSON.stringify(
`All providers failed on chain ${
this._network.name
} for method ${method} and params ${JSON.stringify(
params,
null,
2,
@ -357,11 +359,9 @@ export class HyperlaneSmartProvider
} else {
this.throwCombinedProviderErrors(
providerResultErrors,
`All providers failed for method ${method} and params ${JSON.stringify(
params,
null,
2,
)}`,
`All providers failed on chain ${
this._network.name
} for method ${method} and params ${JSON.stringify(params, null, 2)}`,
);
}
}

Loading…
Cancel
Save