Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: hyperlane-xyz/hyperlane-monorepo
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 821f67cb8eaff3e03fbb848eada170875776d495
Choose a base ref
..
head repository: hyperlane-xyz/hyperlane-monorepo
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 25c5ce97e57707f6834ffb37da7d6ba5ae8bb62d
Choose a head ref
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"sealeveltest2": {
"hex": "0x2317f9615d4ebc2419ad4b88580e2a80a03b2c7a60bc960de7d6934dbc37a87e",
"base58": "3MzUPjP5LEkiHH82nEAe28Xtz9ztuMqWc8UmuKxrpVQH"
},
"sealeveltest1": {
"hex": "0xa77b4e2ed231894cc8cb8eee21adcc705d8489bccc6b2fcf40a358de23e60b7b",
"base58": "CGn8yNtSD3aTTqJfYhUb6s1aVTN75NzwtsFKo1e83aga"
},
"sealeveltest2": {
"hex": "0x2317f9615d4ebc2419ad4b88580e2a80a03b2c7a60bc960de7d6934dbc37a87e",
"base58": "3MzUPjP5LEkiHH82nEAe28Xtz9ztuMqWc8UmuKxrpVQH"
}
}
6 changes: 3 additions & 3 deletions rust/utils/run-locally/src/invariants.rs
Original file line number Diff line number Diff line change
@@ -3,8 +3,8 @@
use crate::config::Config;
use maplit::hashmap;

use crate::fetch_metric;
use crate::logging::log;
use crate::{fetch_metric, ZERO_MERKLE_INSERTION_KATHY_MESSAGES};
// use crate::solana::solana_termination_invariants_met;

// This number should be even, so the messages can be split into two equal halves
@@ -23,7 +23,7 @@ pub fn termination_invariants_met(

let lengths = fetch_metric("9092", "hyperlane_submitter_queue_length", &hashmap! {})?;
assert!(!lengths.is_empty(), "Could not find queue length metric");
if lengths.iter().any(|n| *n != 0) {
if lengths.iter().sum::<u32>() != ZERO_MERKLE_INSERTION_KATHY_MESSAGES {
log!("Relayer queues not empty. Lengths: {:?}", lengths);
return Ok(false);
};
@@ -84,7 +84,7 @@ pub fn termination_invariants_met(
)?
.iter()
.sum::<u32>();
if dispatched_messages_scraped != eth_messages_expected {
if dispatched_messages_scraped != eth_messages_expected + ZERO_MERKLE_INSERTION_KATHY_MESSAGES {
log!(
"Scraper has scraped {} dispatched messages, expected {}",
dispatched_messages_scraped,
99 changes: 64 additions & 35 deletions rust/utils/run-locally/src/main.rs
Original file line number Diff line number Diff line change
@@ -52,9 +52,9 @@ const RELAYER_KEYS: &[&str] = &[
// test3
"0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356",
// sealeveltest1
"0x892bf6949af4233e62f854cb3618bc1a3ee3341dc71ada08c4d5deca239acf4f",
// sealeveltest2
"0x892bf6949af4233e62f854cb3618bc1a3ee3341dc71ada08c4d5deca239acf4f",
// "0x892bf6949af4233e62f854cb3618bc1a3ee3341dc71ada08c4d5deca239acf4f",
// // sealeveltest2
// "0x892bf6949af4233e62f854cb3618bc1a3ee3341dc71ada08c4d5deca239acf4f",
];
/// These private keys are from hardhat/anvil's testing accounts.
/// These must be consistent with the ISM config for the test.
@@ -64,16 +64,19 @@ const VALIDATOR_KEYS: &[&str] = &[
"0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba",
"0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e",
// sealevel
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
// "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
];

const VALIDATOR_ORIGIN_CHAINS: &[&str] = &["test1", "test2", "test3", "sealeveltest1"];
const VALIDATOR_ORIGIN_CHAINS: &[&str] = &["test1", "test2", "test3"];
// const VALIDATOR_ORIGIN_CHAINS: &[&str] = &["test1", "test2", "test3", "sealeveltest1"];

const AGENT_BIN_PATH: &str = "target/debug";
const INFRA_PATH: &str = "../typescript/infra";
// const TS_SDK_PATH: &str = "../typescript/sdk";
const MONOREPO_ROOT_PATH: &str = "../";

const ZERO_MERKLE_INSERTION_KATHY_MESSAGES: u32 = 10;

type DynPath = Box<dyn AsRef<Path>>;

static RUN_LOG_WATCHERS: AtomicBool = AtomicBool::new(true);
@@ -132,11 +135,11 @@ fn main() -> ExitCode {

let config = Config::load();

let solana_checkpoint_path = Path::new(SOLANA_CHECKPOINT_LOCATION);
fs::remove_dir_all(solana_checkpoint_path).unwrap_or_default();
let checkpoints_dirs: Vec<DynPath> = (0..VALIDATOR_COUNT - 1)
// let solana_checkpoint_path = Path::new(SOLANA_CHECKPOINT_LOCATION);
// fs::remove_dir_all(solana_checkpoint_path).unwrap_or_default();
let checkpoints_dirs: Vec<DynPath> = (0..VALIDATOR_COUNT)
.map(|_| Box::new(tempdir().unwrap()) as DynPath)
.chain([Box::new(solana_checkpoint_path) as DynPath])
// .chain([Box::new(solana_checkpoint_path) as DynPath])
.collect();
let rocks_db_dir = tempdir().unwrap();
let relayer_db = concat_path(&rocks_db_dir, "relayer");
@@ -168,8 +171,8 @@ fn main() -> ExitCode {
.hyp_env("DB", relayer_db.to_str().unwrap())
.hyp_env("CHAINS_TEST1_SIGNER_KEY", RELAYER_KEYS[0])
.hyp_env("CHAINS_TEST2_SIGNER_KEY", RELAYER_KEYS[1])
.hyp_env("CHAINS_SEALEVELTEST1_SIGNER_KEY", RELAYER_KEYS[3])
.hyp_env("CHAINS_SEALEVELTEST2_SIGNER_KEY", RELAYER_KEYS[4])
// .hyp_env("CHAINS_SEALEVELTEST1_SIGNER_KEY", RELAYER_KEYS[3])
// .hyp_env("CHAINS_SEALEVELTEST2_SIGNER_KEY", RELAYER_KEYS[4])
.hyp_env("RELAYCHAINS", "invalidchain,otherinvalid")
.hyp_env("ALLOWLOCALCHECKPOINTSYNCERS", "true")
.hyp_env(
@@ -196,7 +199,8 @@ fn main() -> ExitCode {
.arg("defaultSigner.key", RELAYER_KEYS[2])
.arg(
"relayChains",
"test1,test2,test3,sealeveltest1,sealeveltest2",
"test1,test2,test3",
// "test1,test2,test3,sealeveltest1,sealeveltest2",
);

let base_validator_env = common_agent_env
@@ -260,17 +264,17 @@ fn main() -> ExitCode {
.join(", ")
);
log!("Relayer DB in {}", relayer_db.display());
(0..3).for_each(|i| {
(0..VALIDATOR_COUNT).for_each(|i| {
log!("Validator {} DB in {}", i + 1, validator_dbs[i].display());
});

//
// Ready to run...
//

let (solana_path, solana_path_tempdir) = install_solana_cli_tools().join();
state.data.push(Box::new(solana_path_tempdir));
let solana_program_builder = build_solana_programs(solana_path.clone());
// let (solana_path, solana_path_tempdir) = install_solana_cli_tools().join();
// state.data.push(Box::new(solana_path_tempdir));
// let solana_program_builder = build_solana_programs(solana_path.clone());

// this task takes a long time in the CI so run it in parallel
log!("Building rust...");
@@ -281,13 +285,13 @@ fn main() -> ExitCode {
.arg("bin", "validator")
.arg("bin", "scraper")
.arg("bin", "init-db")
.arg("bin", "hyperlane-sealevel-client")
// .arg("bin", "hyperlane-sealevel-client")
.filter_logs(|l| !l.contains("workspace-inheritance"))
.run();

let start_anvil = start_anvil(config.clone());

let solana_program_path = solana_program_builder.join();
// let solana_program_path = solana_program_builder.join();

log!("Running postgres db...");
let postgres = Program::new("docker")
@@ -302,15 +306,15 @@ fn main() -> ExitCode {

build_rust.join();

let solana_ledger_dir = tempdir().unwrap();
let start_solana_validator = start_solana_test_validator(
solana_path.clone(),
solana_program_path,
solana_ledger_dir.as_ref().to_path_buf(),
);
// let solana_ledger_dir = tempdir().unwrap();
// let start_solana_validator = start_solana_test_validator(
// solana_path.clone(),
// solana_program_path,
// solana_ledger_dir.as_ref().to_path_buf(),
// );

let (_solana_config_path, solana_validator) = start_solana_validator.join();
state.push_agent(solana_validator);
// let (_solana_config_path, solana_validator) = start_solana_validator.join();
// state.push_agent(solana_validator);
state.push_agent(start_anvil.join());

// spawn 1st validator before any messages have been sent to test empty mailbox
@@ -325,24 +329,47 @@ fn main() -> ExitCode {
state.push_agent(scraper_env.spawn("SCR"));

// Send half the kathy messages before starting the rest of the agents
let kathy_env = Program::new("yarn")
let kathy_env_single_insertion = Program::new("yarn")
.working_dir(INFRA_PATH)
.cmd("kathy")
.arg("messages", (config.kathy_messages / 2).to_string())
.arg("messages", (config.kathy_messages / 4).to_string())
.arg("timeout", "1000");
kathy_env.clone().run().join();
kathy_env_single_insertion.clone().run().join();

let kathy_env_zero_insertion = Program::new("yarn")
.working_dir(INFRA_PATH)
.cmd("kathy")
.arg(
"messages",
(ZERO_MERKLE_INSERTION_KATHY_MESSAGES / 2).to_string(),
)
.arg("timeout", "1000")
// replacing the `aggregationHook` with the `interchainGasPaymaster` means there
// is no more `merkleTreeHook`, causing zero merkle insertions to occur.
.arg("default-hook", "interchainGasPaymaster");
kathy_env_zero_insertion.clone().run().join();

let kathy_env_double_insertion = Program::new("yarn")
.working_dir(INFRA_PATH)
.cmd("kathy")
.arg("messages", (config.kathy_messages / 4).to_string())
.arg("timeout", "1000")
// replacing the `protocolFees` required hook with the `merkleTreeHook`
// will cause double insertions to occur, which should be handled correctly
.arg("required-hook", "merkleTreeHook");
kathy_env_double_insertion.clone().run().join();

// Send some sealevel messages before spinning up the agents, to test the backward indexing cursor
// for _i in 0..(SOL_MESSAGES_EXPECTED / 2) {
// initiate_solana_hyperlane_transfer(solana_path.clone(), solana_config_path.clone()).join();
// }

// spawn the rest of the validators
for (i, validator_env) in validator_envs.into_iter().enumerate().skip(1) {
let validator = validator_env.spawn(make_static(format!("VL{}", 1 + i)));
state.push_agent(validator);
}

// Send some sealevel messages before spinning up the relayer, to test the backward indexing cursor
// for _i in 0..(SOL_MESSAGES_EXPECTED / 2) {
// initiate_solana_hyperlane_transfer(solana_path.clone(), solana_config_path.clone()).join();
// }

state.push_agent(relayer_env.spawn("RLY"));

// Send some sealevel messages after spinning up the relayer, to test the forward indexing cursor
@@ -354,7 +381,9 @@ fn main() -> ExitCode {
log!("Ctrl+C to end execution...");

// Send half the kathy messages after the relayer comes up
state.push_agent(kathy_env.flag("mineforever").spawn("KTY"));
kathy_env_double_insertion.clone().run().join();
kathy_env_zero_insertion.clone().run().join();
state.push_agent(kathy_env_single_insertion.flag("mineforever").spawn("KTY"));

let loop_start = Instant::now();
// give things a chance to fully start.
1 change: 1 addition & 0 deletions rust/utils/run-locally/src/solana.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![allow(dead_code)]
use std::fs;
use std::path::{Path, PathBuf};
use std::thread::sleep;
23 changes: 23 additions & 0 deletions solidity/contracts/test/TestSendReceiver.sol
Original file line number Diff line number Diff line change
@@ -6,8 +6,11 @@ import {TypeCasts} from "../libs/TypeCasts.sol";
import {IInterchainGasPaymaster} from "../interfaces/IInterchainGasPaymaster.sol";
import {IMessageRecipient} from "../interfaces/IMessageRecipient.sol";
import {IMailbox} from "../interfaces/IMailbox.sol";
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
import {IInterchainSecurityModule, ISpecifiesInterchainSecurityModule} from "../interfaces/IInterchainSecurityModule.sol";

import {StandardHookMetadata} from "../hooks/libs/StandardHookMetadata.sol";
import {MailboxClient} from "../client/MailboxClient.sol";

contract TestSendReceiver is IMessageRecipient {
using TypeCasts for address;
@@ -34,6 +37,26 @@ contract TestSendReceiver is IMessageRecipient {
);
}

function dispatchToSelf(
IMailbox _mailbox,
uint32 _destinationDomain,
bytes calldata _messageBody,
IPostDispatchHook hook
) external payable {
bytes memory hookMetadata = StandardHookMetadata.formatMetadata(
HANDLE_GAS_AMOUNT,
msg.sender
);
// TODO: handle topping up?
_mailbox.dispatch{value: msg.value}(
_destinationDomain,
address(this).addressToBytes32(),
_messageBody,
hookMetadata,
hook
);
}

function handle(
uint32,
bytes32,
3,970 changes: 3,176 additions & 794 deletions typescript/infra/config/environments/testnet4/core/verification.json

Large diffs are not rendered by default.

93 changes: 82 additions & 11 deletions typescript/infra/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -3,13 +3,50 @@ import '@nomiclabs/hardhat-waffle';
import { task } from 'hardhat/config';
import { HardhatRuntimeEnvironment } from 'hardhat/types';

import { TestSendReceiver__factory } from '@hyperlane-xyz/core';
import { ChainName, HyperlaneCore, MultiProvider } from '@hyperlane-xyz/sdk';
import { Mailbox, TestSendReceiver__factory } from '@hyperlane-xyz/core';
import {
ChainName,
HookType,
HyperlaneCore,
ModuleType,
MultiProvider,
} from '@hyperlane-xyz/sdk';
import { addressToBytes32 } from '@hyperlane-xyz/utils';

import { Modules, getAddresses } from './scripts/utils';
import { sleep } from './src/utils/utils';

enum MailboxHookType {
REQUIRED = 'requiredHook',
DEFAULT = 'defaultHook',
}

/**
* If a hookArg is provided, set the mailbox hook to the defaultHookArg.
* The hook is set either as the default hook or the required hook,
* depending on the mailboxHookType argument.
*/
async function setMailboxHook(
mailbox: Mailbox,
coreAddresses: any,
local: ChainName,
mailboxHookType: MailboxHookType,
hookArg: HookType,
) {
const hook = coreAddresses[local][hookArg];
switch (mailboxHookType) {
case MailboxHookType.REQUIRED: {
await mailbox.setRequiredHook(hook);
break;
}
case MailboxHookType.DEFAULT: {
await mailbox.setDefaultHook(hook);
break;
}
}
console.log(`set the ${mailboxHookType} hook on ${local} to ${hook}`);
}

const chainSummary = async (core: HyperlaneCore, chain: ChainName) => {
const coreContracts = core.getContracts(chain);
const mailbox = coreContracts.mailbox;
@@ -36,19 +73,33 @@ task('kathy', 'Dispatches random hyperlane messages')
)
.addParam('timeout', 'Time to wait between messages in ms.', '5000')
.addFlag('mineforever', 'Mine forever after sending messages')
.addParam(
MailboxHookType.DEFAULT,
'Default hook to call in postDispatch',
HookType.AGGREGATION,
)
.addParam(
MailboxHookType.REQUIRED,
'Required hook to call in postDispatch',
HookType.PROTOCOL_FEE,
)
.setAction(
async (
taskArgs: { messages: string; timeout: string; mineforever: boolean },
taskArgs: {
messages: string;
timeout: string;
mineforever: boolean;
defaultHook: HookType;
requiredHook: HookType;
},
hre: HardhatRuntimeEnvironment,
) => {
const timeout = Number.parseInt(taskArgs.timeout);
const environment = 'test';
const [signer] = await hre.ethers.getSigners();
const multiProvider = MultiProvider.createTestMultiProvider({ signer });
const core = HyperlaneCore.fromAddressesMap(
getAddresses(environment, Modules.CORE),
multiProvider,
);
const addresses = getAddresses(environment, Modules.CORE);
const core = HyperlaneCore.fromAddressesMap(addresses, multiProvider);

const randomElement = <T>(list: T[]) =>
list[Math.floor(Math.random() * list.length)];
@@ -71,15 +122,35 @@ task('kathy', 'Dispatches random hyperlane messages')
// Random remote chain
const remote: ChainName = randomElement(core.remoteChains(local));
const remoteId = multiProvider.getDomainId(remote);
const mailbox = core.getContracts(local).mailbox;
const contracts = core.getContracts(local);
const mailbox = contracts.mailbox;
await setMailboxHook(
mailbox,
addresses,
local,
MailboxHookType.DEFAULT,
taskArgs.defaultHook,
);
await setMailboxHook(
mailbox,
addresses,
local,
MailboxHookType.REQUIRED,
taskArgs.requiredHook,
);
const quote = await mailbox['quoteDispatch(uint32,bytes32,bytes)'](
remoteId,
addressToBytes32(recipient.address),
'0x1234',
);
await recipient.dispatchToSelf(mailbox.address, remoteId, '0x1234', {
value: quote,
});
await recipient['dispatchToSelf(address,uint32,bytes)'](
mailbox.address,
remoteId,
'0x1234',
{
value: quote,
},
);
console.log(
`send to ${recipient.address} on ${remote} via mailbox ${
mailbox.address
17 changes: 0 additions & 17 deletions typescript/sdk/src/consts/environments/testnet.json
Original file line number Diff line number Diff line change
@@ -190,22 +190,5 @@
"protocolFee": "0x244d1F7e30Be144A87602905baBF86630e8f39DC",
"mailbox": "0x2d1889fe5B092CD988972261434F7E5f26041115",
"validatorAnnounce": "0x99303EFF09332cDd93E8BC8b2F07b2416e4501e5"
},
"solanadevnet": {
"storageGasOracle": "0x0000000000000000000000000000000000000000",
"validatorAnnounce": "0x0000000000000000000000000000000000000000",
"proxyAdmin": "0x0000000000000000000000000000000000000000",
"mailbox": "4v25Dz9RccqUrTzmfHzJMsjd1iVoNrWzeJ4o6GYuJrVn",
"interchainGasPaymaster": "7hMPEGdgBQFsjEz3aaNwZp8WMFHs615zAM3erXBDJuJR",
"defaultIsmInterchainGasPaymaster": "0x0000000000000000000000000000000000000000",
"multisigIsm": "0x0000000000000000000000000000000000000000",
"testRecipient": "0x0000000000000000000000000000000000000000",
"interchainAccountIsm": "0x0000000000000000000000000000000000000000",
"aggregationIsmFactory": "0x0000000000000000000000000000000000000000",
"routingIsmFactory": "0x0000000000000000000000000000000000000000",
"interchainQueryRouter": "0x0000000000000000000000000000000000000000",
"interchainAccountRouter": "0x0000000000000000000000000000000000000000",
"merkleRootMultisigIsmFactory": "0x0000000000000000000000000000000000000000",
"messageIdMultisigIsmFactory": "0x0000000000000000000000000000000000000000"
}
}
19 changes: 13 additions & 6 deletions typescript/sdk/src/contracts/contracts.ts
Original file line number Diff line number Diff line change
@@ -34,8 +34,10 @@ export function serializeContractsMap<F extends HyperlaneFactories>(

export function serializeContracts<F extends HyperlaneFactories>(
contracts: HyperlaneContracts<F>,
): HyperlaneAddresses<F> {
return objMap(contracts, (_, contract) => contract.address);
): any {
return objMap(contracts, (_, contract) =>
contract.address ? contract.address : serializeContracts(contract),
);
}

function getFactory<F extends HyperlaneFactories>(
@@ -148,10 +150,15 @@ export function connectContracts<F extends HyperlaneFactories>(
contracts: HyperlaneContracts<F>,
connection: Connection,
): HyperlaneContracts<F> {
return objMap(
contracts,
(_, contract) => contract.connect(connection) as typeof contract,
);
const connectedContracts = objMap(contracts, (_, contract) => {
if (!contract.connect) {
return undefined;
}
return contract.connect(connection);
});
return Object.fromEntries(
Object.entries(connectedContracts).filter(([_, contract]) => !!contract),
) as HyperlaneContracts<F>;
}

export function connectContractsMap<F extends HyperlaneFactories>(
26 changes: 22 additions & 4 deletions typescript/sdk/src/core/CoreDeployer.hardhat-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import '@nomiclabs/hardhat-waffle';
import { expect } from 'chai';
import { assert, expect } from 'chai';
import { ethers } from 'hardhat';
import sinon from 'sinon';

@@ -73,7 +73,17 @@ describe('core', async () => {
sinon.restore(); // restore normal deployer behavior and test3 will be deployed
const result = await deployer.deploy(coreConfig);
expect(result).to.have.keys(['test1', 'test2', 'test3']);
expect(result.test3).to.have.keys(Object.keys(result.test2));
// Each test network key has entries about the other test networks, whre ISM details are stored.
// With this exception, the keys should be the same, so we check the intersections for equality.
const testnetKeysIntersection = Object.keys(result.test1).filter(
(key) =>
Object.keys(result.test2).includes(key) &&
Object.keys(result.test3).includes(key),
);
assert(
testnetKeysIntersection.length > 0,
'there are no common core contracts deployed between the local testnets',
);
});

it('can be resumed from partial contracts', async () => {
@@ -85,8 +95,16 @@ describe('core', async () => {
delete deployer.deployedContracts.test2!.mailbox;

const result = await deployer.deploy(coreConfig);
expect(result.test2).to.have.keys(Object.keys(result.test1));
expect(result.test3).to.have.keys(Object.keys(result.test1));

const testnetKeysIntersection = Object.keys(result.test1).filter(
(key) =>
Object.keys(result.test2).includes(key) &&
Object.keys(result.test3).includes(key),
);
assert(
testnetKeysIntersection.length > 0,
'there are no common core contracts deployed between the local testnets',
);
});

it('times out ', async () => {
43 changes: 24 additions & 19 deletions typescript/sdk/src/core/HyperlaneCoreDeployer.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import debug from 'debug';

import {
IInterchainSecurityModule__factory,
Mailbox,
ValidatorAnnounce,
} from '@hyperlane-xyz/core';
import { Mailbox, ValidatorAnnounce } from '@hyperlane-xyz/core';
import { Address } from '@hyperlane-xyz/utils';

import { HyperlaneContracts } from '../contracts/types';
import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer';
import { HyperlaneHookDeployer } from '../hook/HyperlaneHookDeployer';
import { HookConfig } from '../hook/types';
import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory';
import {
HyperlaneIsmFactory,
moduleMatchesConfig,
} from '../ism/HyperlaneIsmFactory';
import { IsmConfig } from '../ism/types';
import { MultiProvider } from '../providers/MultiProvider';
import { ChainMap, ChainName } from '../types';
@@ -59,9 +58,21 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer<
[domain],
);

const defaultIsm = await this.deployIsm(chain, config.defaultIsm);
let defaultIsm = await mailbox.defaultIsm();
const matches = await moduleMatchesConfig(
chain,
defaultIsm,
config.defaultIsm,
this.multiProvider,
this.ismFactory.getContracts(chain),
);
if (!matches) {
this.logger('Deploying default ISM');
defaultIsm = await this.deployIsm(chain, config.defaultIsm);
}

const hookAddresses = { mailbox: mailbox.address, proxyAdmin };

const defaultHook = await this.deployHook(
chain,
config.defaultHook,
@@ -112,23 +123,17 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer<
config,
coreAddresses,
);
this.addDeployedContracts(chain, hooks);
this.addDeployedContracts(
chain,
hooks,
this.hookDeployer.verificationInputs[chain],
);
return hooks[config.type].address;
}

async deployIsm(chain: ChainName, config: IsmConfig): Promise<Address> {
const key = 'defaultIsm';
if (this.cachedAddresses[chain][key]) {
this.addDeployedContracts(chain, {
[key]: IInterchainSecurityModule__factory.connect(
this.cachedAddresses[chain][key],
this.multiProvider.getSignerOrProvider(chain),
),
});
return this.cachedAddresses[chain][key];
}
const ism = await this.ismFactory.deploy(chain, config);
this.addDeployedContracts(chain, { [key]: ism });
this.addDeployedContracts(chain, this.ismFactory.deployedIsms[chain]);
return ism.address;
}

129 changes: 93 additions & 36 deletions typescript/sdk/src/deploy/HyperlaneDeployer.ts
Original file line number Diff line number Diff line change
@@ -34,9 +34,14 @@ import { MultiProvider } from '../providers/MultiProvider';
import { MailboxClientConfig } from '../router/types';
import { ChainMap, ChainName } from '../types';

import { UpgradeConfig, proxyAdmin } from './proxy';
import {
UpgradeConfig,
isProxy,
proxyAdmin,
proxyImplementation,
} from './proxy';
import { ContractVerificationInput } from './verify/types';
import { getContractVerificationInput } from './verify/utils';
import { buildVerificationInput, getContractVerificationInput } from './verify/utils';

export interface DeployerOptions {
logger?: Debugger;
@@ -115,11 +120,26 @@ export abstract class HyperlaneDeployer<
protected addDeployedContracts(
chain: ChainName,
contracts: HyperlaneContracts<any>,
) {
verificationInputs?: ContractVerificationInput[],
): void {
this.deployedContracts[chain] = {
...this.deployedContracts[chain],
...contracts,
};
if (verificationInputs)
this.addVerificationArtifacts(chain, verificationInputs);
}

protected addVerificationArtifacts(
chain: ChainName,
artifacts: ContractVerificationInput[],
): void {
this.verificationInputs[chain] = this.verificationInputs[chain] || [];
artifacts.forEach((artifact) => {
this.verificationInputs[chain].push(artifact);
});

// TODO: deduplicate
}

protected async runIf<T>(
@@ -251,21 +271,58 @@ export abstract class HyperlaneDeployer<
): Promise<ReturnType<F['deploy']>> {
const cachedContract = this.readCache(chain, factory, contractName);
if (cachedContract) {
const provider = this.multiProvider.getProvider(chain);

let recoveredInputs: ContractVerificationInput[] = [];
let implementation: string;
if (await isProxy(provider, cachedContract.address)) {
const admin = await proxyAdmin(provider, cachedContract.address);
implementation = await proxyImplementation(
provider,
cachedContract.address,
);
const proxyArgs = this.proxyConstructorArgs(
cachedContract.attach(implementation),
admin,
initializeArgs,
);
const encodedProxyConstructorArgs =
TransparentUpgradeableProxy__factory.createInterface().encodeDeploy(
proxyArgs,
);
recoveredInputs.push(
buildVerificationInput(
'TransparentUpgradeableProxy',
cachedContract.address,
encodedProxyConstructorArgs,
),
);
} else {
implementation = cachedContract.address;
}
const encodedConstructorArgs =
factory.interface.encodeDeploy(constructorArgs);
recoveredInputs.push(
buildVerificationInput(
contractName,
implementation,
encodedConstructorArgs,
),
);
this.addVerificationArtifacts(chain, recoveredInputs);
return cachedContract;
}

const signer = this.multiProvider.getSigner(chain);
const overrides = this.multiProvider.getTransactionOverrides(chain);

this.logger(`Deploy ${contractName} on ${chain}`);
const contract = await (factory
.connect(signer)
.deploy(...constructorArgs, overrides) as ReturnType<F['deploy']>);

await this.multiProvider.handleTx(chain, contract.deployTransaction);
const contract = await this.multiProvider.handleDeploy(
chain,
factory,
constructorArgs,
);

if (initializeArgs) {
this.logger(`Initialize ${contractName} on ${chain}`);
const overrides = this.multiProvider.getTransactionOverrides(chain);
const initTx = await contract.initialize(...initializeArgs, overrides);
await this.multiProvider.handleTx(chain, initTx);
}
@@ -275,19 +332,11 @@ export abstract class HyperlaneDeployer<
contract,
factory.bytecode,
);
this.addVerificationArtifact(chain, verificationInput);
this.addVerificationArtifacts(chain, [verificationInput]);

return contract;
}

protected addVerificationArtifact(
chain: ChainName,
artifact: ContractVerificationInput,
) {
this.verificationInputs[chain] = this.verificationInputs[chain] || [];
this.verificationInputs[chain].push(artifact);
}

async deployContract<K extends keyof Factories>(
chain: ChainName,
contractName: K,
@@ -378,25 +427,32 @@ export abstract class HyperlaneDeployer<
);
}

protected async deployProxy<C extends ethers.Contract>(
chain: ChainName,
protected proxyConstructorArgs<C extends ethers.Contract>(
implementation: C,
proxyAdmin: string,
initializeArgs?: Parameters<C['initialize']>,
): Promise<C> {
): [string, string, string] {
const initData = initializeArgs
? implementation.interface.encodeFunctionData(
'initialize',
initializeArgs,
)
: '0x';
return [implementation.address, proxyAdmin, initData];
}

protected async deployProxy<C extends ethers.Contract>(
chain: ChainName,
implementation: C,
proxyAdmin: string,
initializeArgs?: Parameters<C['initialize']>,
): Promise<C> {
this.logger(`Deploying transparent upgradable proxy`);
const constructorArgs: [string, string, string] = [
implementation.address,
const constructorArgs = this.proxyConstructorArgs(
implementation,
proxyAdmin,
initData,
];
initializeArgs,
);
const proxy = await this.deployContractFromFactory(
chain,
new TransparentUpgradeableProxy__factory(),
@@ -469,15 +525,6 @@ export abstract class HyperlaneDeployer<
constructorArgs: Parameters<Factories[K]['deploy']>,
initializeArgs?: Parameters<HyperlaneContracts<Factories>[K]['initialize']>,
): Promise<HyperlaneContracts<Factories>[K]> {
const cachedContract = this.readCache(
chain,
this.factories[contractName],
contractName.toString(),
);
if (cachedContract) {
return cachedContract;
}

// Try to initialize the implementation even though it may not be necessary
const implementation = await this.deployContract(
chain,
@@ -486,6 +533,16 @@ export abstract class HyperlaneDeployer<
initializeArgs,
);

// if implementation was recoverd as a proxy, return it
if (
await isProxy(
this.multiProvider.getProvider(chain),
implementation.address,
)
) {
return implementation;
}

// Initialize the proxy the same way
const contract = await this.deployProxy(
chain,
12 changes: 8 additions & 4 deletions typescript/sdk/src/deploy/HyperlaneProxyFactoryDeployer.ts
Original file line number Diff line number Diff line change
@@ -29,10 +29,14 @@ export class HyperlaneProxyFactoryDeployer extends HyperlaneDeployer<
this.factories,
) as (keyof ProxyFactoryFactories)[]) {
const factory = await this.deployContract(chain, factoryName, []);
this.addVerificationArtifact(chain, {
name: proxyFactoryImplementations[factoryName],
address: await factory.implementation(),
});
this.addVerificationArtifacts(chain, [
{
name: proxyFactoryImplementations[factoryName],
address: await factory.implementation(),
constructorArguments: '',
isProxy: true,
},
]);
contracts[factoryName] = factory;
}
return contracts as HyperlaneContracts<ProxyFactoryFactories>;
16 changes: 14 additions & 2 deletions typescript/sdk/src/deploy/verify/ContractVerifier.ts
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ enum ExplorerApiErrors {
ALREADY_VERIFIED_ALT = 'Already Verified',
VERIFICATION_PENDING = 'Pending in queue',
PROXY_FAILED = 'A corresponding implementation contract was unfortunately not detected for the proxy address.',
BYTECODE_MISMATCH = 'Fail - Unable to verify. Compiled contract deployment bytecode does NOT match the transaction deployment bytecode.',
}

export class ContractVerifier extends MultiGeneric<VerificationInput> {
@@ -106,7 +107,12 @@ export class ContractVerifier extends MultiGeneric<VerificationInput> {
case ExplorerApiErrors.ALREADY_VERIFIED_ALT:
return;
case ExplorerApiErrors.PROXY_FAILED:
this.logger(`Proxy verification failed, try manually?`);
this.logger(`Proxy verification failed for, try manually?`);
return;
case ExplorerApiErrors.BYTECODE_MISMATCH:
this.logger(
`Compiled bytecode does not match deployed bytecode, check constructor arguments?`,
);
return;
default:
this.logger(
@@ -233,7 +239,13 @@ export class ContractVerifier extends MultiGeneric<VerificationInput> {
}

if (await this.isAlreadyVerified(chain, input)) {
this.logger(`Contract ${input.name} already verified on ${chain}`);
const addressUrl = await this.multiProvider.tryGetExplorerAddressUrl(
chain,
input.address,
);
this.logger(
`Contract ${input.name} already verified on ${chain} at ${addressUrl}#code`,
);
// There is a rate limit of 5 requests per second
await sleep(200);
return;
3 changes: 2 additions & 1 deletion typescript/sdk/src/deploy/verify/utils.ts
Original file line number Diff line number Diff line change
@@ -40,7 +40,8 @@ export function getContractVerificationInput(
contract: ethers.Contract,
bytecode: string,
isProxy?: boolean,
constructorArgs?: string,
): ContractVerificationInput {
const args = getConstructorArguments(contract, bytecode);
const args = constructorArgs ?? getConstructorArguments(contract, bytecode);
return buildVerificationInput(name, contract.address, args, isProxy);
}
7 changes: 6 additions & 1 deletion typescript/sdk/src/hook/HyperlaneHookDeployer.ts
Original file line number Diff line number Diff line change
@@ -93,7 +93,12 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer<
);
}
const igpContracts = await this.igpDeployer.deployContracts(chain, config);
this.addDeployedContracts(chain, igpContracts);
// bubbling up addresses and verification input artifacts
this.addDeployedContracts(
chain,
igpContracts,
this.igpDeployer.verificationInputs[chain],
);
return igpContracts;
}

33 changes: 29 additions & 4 deletions typescript/sdk/src/ism/HyperlaneIsmFactory.ts
Original file line number Diff line number Diff line change
@@ -34,6 +34,11 @@ import {
} from './types';

export class HyperlaneIsmFactory extends HyperlaneApp<FactoryFactories> {
// The shape of this object is `ChainMap<Address | ChainMap<Address>`,
// although `any` is use here because that type breaks a lot of signatures.
// TODO: fix this in the next refactoring
public deployedIsms: ChainMap<any> = {};

static fromEnvironment<Env extends HyperlaneEnvironment>(
env: Env,
multiProvider: MultiProvider,
@@ -66,6 +71,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<FactoryFactories> {
config: IsmConfig,
origin?: ChainName,
): Promise<DeployedIsm> {
let contract: DeployedIsm;
if (typeof config === 'string') {
// TODO: return the appropriate ISM type
return IInterchainSecurityModule__factory.connect(
@@ -90,20 +96,38 @@ export class HyperlaneIsmFactory extends HyperlaneApp<FactoryFactories> {
);
break;
}
return this.deployMultisigIsm(chain, config);
contract = await this.deployMultisigIsm(chain, config);
} else if (config.type === ModuleType.ROUTING) {
this.logger(
`Deploying Routing ISM to ${chain} for verifying ${Object.keys(
config.domains,
)}`,
);
return this.deployRoutingIsm(chain, config);
contract = await this.deployRoutingIsm(chain, config);
} else if (config.type === ModuleType.AGGREGATION) {
this.logger(`Deploying Aggregation ISM to ${chain}`);
return this.deployAggregationIsm(chain, config);
contract = await this.deployAggregationIsm(chain, config, origin);
} else {
throw new Error(`Unsupported ISM type`);
}

const moduleType = ModuleType[config.type];
if (!this.deployedIsms[chain]) {
this.deployedIsms[chain] = {};
}
if (origin) {
// if we're deploying network-specific contracts (e.g. ISMs), store them as sub-entry
// under that network's key (`origin`)
if (!this.deployedIsms[chain][origin]) {
this.deployedIsms[chain][origin] = {};
}
this.deployedIsms[chain][origin][moduleType] = contract;
} else {
// otherwise store the entry directly
this.deployedIsms[chain][moduleType] = contract;
}

return contract;
}

private async deployMultisigIsm(chain: ChainName, config: MultisigIsmConfig) {
@@ -172,13 +196,14 @@ export class HyperlaneIsmFactory extends HyperlaneApp<FactoryFactories> {
private async deployAggregationIsm(
chain: ChainName,
config: AggregationIsmConfig,
origin?: ChainName,
) {
const signer = this.multiProvider.getSigner(chain);
const aggregationIsmFactory =
this.getContracts(chain).aggregationIsmFactory;
const addresses: Address[] = [];
for (const module of config.modules) {
addresses.push((await this.deploy(chain, module)).address);
addresses.push((await this.deploy(chain, module, origin)).address);
}
const address = await this.deployStaticAddressSet(
chain,