Add `InterchainQueryRouter` implementation (#1137)

* Add InterchainQueryRouter implementation

* Add e2e query and callback test

* Add comments to query contracts

* Add deploy script to infra

* Add salt config

* Deploy IQS to mainnet

Co-authored-by: nambrot <nambrot@googlemail.com>
pull/1158/head
Yorke Rhodes 2 years ago committed by GitHub
parent fe904f22cc
commit a2f4810027
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      solidity/app/contracts/Router.sol
  2. 2
      solidity/app/contracts/test/TestRouter.sol
  3. 2
      typescript/helloworld/contracts/HelloWorld.sol
  4. 10
      typescript/ica/contracts/InterchainAccountRouter.sol
  5. 110
      typescript/ica/contracts/InterchainQueryRouter.sol
  6. 26
      typescript/ica/contracts/OwnableMulticall.sol
  7. 39
      typescript/ica/contracts/test/TestQuery.sol
  8. 6
      typescript/ica/index.ts
  9. 10
      typescript/ica/src/contracts.ts
  10. 45
      typescript/ica/src/deploy.ts
  11. 84
      typescript/ica/test/queries.test.ts
  12. 26
      typescript/infra/config/environments/mainnet/interchain/queries/addresses.json
  13. 66
      typescript/infra/config/environments/mainnet/interchain/queries/verification.json
  14. 4
      typescript/infra/config/environments/testnet2/chains.ts
  15. 20
      typescript/infra/config/environments/testnet2/interchain/queries/addresses.json
  16. 50
      typescript/infra/config/environments/testnet2/interchain/queries/verification.json
  17. 45
      typescript/infra/scripts/interchain/deploy-queries.ts
  18. 2
      typescript/sdk/src/core/TestCoreApp.ts

@ -82,7 +82,7 @@ abstract contract Router is AbacusConnectionClient, IMessageRecipient {
function handle( function handle(
uint32 _origin, uint32 _origin,
bytes32 _sender, bytes32 _sender,
bytes memory _message bytes calldata _message
) external virtual override onlyInbox onlyRemoteRouter(_origin, _sender) { ) external virtual override onlyInbox onlyRemoteRouter(_origin, _sender) {
// TODO: callbacks on success/failure // TODO: callbacks on success/failure
_handle(_origin, _sender, _message); _handle(_origin, _sender, _message);
@ -92,7 +92,7 @@ abstract contract Router is AbacusConnectionClient, IMessageRecipient {
function _handle( function _handle(
uint32 _origin, uint32 _origin,
bytes32 _sender, bytes32 _sender,
bytes memory _message bytes calldata _message
) internal virtual; ) internal virtual;
// ============ Internal functions ============ // ============ Internal functions ============

@ -14,7 +14,7 @@ contract TestRouter is Router {
function _handle( function _handle(
uint32, uint32,
bytes32, bytes32,
bytes memory bytes calldata
) internal pure override {} ) internal pure override {}
function isRemoteRouter(uint32 _domain, bytes32 _potentialRemoteRouter) function isRemoteRouter(uint32 _domain, bytes32 _potentialRemoteRouter)

@ -75,7 +75,7 @@ contract HelloWorld is Router {
function _handle( function _handle(
uint32 _origin, uint32 _origin,
bytes32 _sender, bytes32 _sender,
bytes memory _message bytes calldata _message
) internal override { ) internal override {
received += 1; received += 1;
receivedFrom[_origin] += 1; receivedFrom[_origin] += 1;

@ -5,7 +5,6 @@ import {OwnableMulticall, Call} from "./OwnableMulticall.sol";
// ============ External Imports ============ // ============ External Imports ============
import {Router} from "@hyperlane-xyz/app/contracts/Router.sol"; import {Router} from "@hyperlane-xyz/app/contracts/Router.sol";
import {TypeCasts} from "@hyperlane-xyz/core/contracts/libs/TypeCasts.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
@ -18,6 +17,12 @@ contract InterchainAccountRouter is Router {
bytes constant bytecode = type(OwnableMulticall).creationCode; bytes constant bytecode = type(OwnableMulticall).creationCode;
bytes32 constant bytecodeHash = bytes32(keccak256(bytecode)); bytes32 constant bytecodeHash = bytes32(keccak256(bytecode));
event InterchainAccountCreated(
uint32 indexed origin,
address sender,
address account
);
function initialize( function initialize(
address _owner, address _owner,
address _abacusConnectionManager, address _abacusConnectionManager,
@ -53,6 +58,7 @@ contract InterchainAccountRouter is Router {
address interchainAccount = _getInterchainAccount(salt); address interchainAccount = _getInterchainAccount(salt);
if (!Address.isContract(interchainAccount)) { if (!Address.isContract(interchainAccount)) {
interchainAccount = Create2.deploy(0, salt, bytecode); interchainAccount = Create2.deploy(0, salt, bytecode);
emit InterchainAccountCreated(_origin, _sender, interchainAccount);
} }
return OwnableMulticall(interchainAccount); return OwnableMulticall(interchainAccount);
} }
@ -76,7 +82,7 @@ contract InterchainAccountRouter is Router {
function _handle( function _handle(
uint32 _origin, uint32 _origin,
bytes32, // router sender bytes32, // router sender
bytes memory _message bytes calldata _message
) internal override { ) internal override {
(address sender, Call[] memory calls) = abi.decode( (address sender, Call[] memory calls) = abi.decode(
_message, _message,

@ -0,0 +1,110 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
import {OwnableMulticall, Call} from "./OwnableMulticall.sol";
// ============ External Imports ============
import {Router} from "@hyperlane-xyz/app/contracts/Router.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract InterchainQueryRouter is Router, OwnableMulticall {
enum Action {
DISPATCH,
RESOLVE
}
event QueryDispatched(
uint32 indexed destinationDomain,
address indexed sender
);
event QueryReturned(uint32 indexed originDomain, address indexed sender);
event QueryResolved(
uint32 indexed destinationDomain,
address indexed sender
);
function initialize(
address _owner,
address _abacusConnectionManager,
address _interchainGasPaymaster
) public initializer {
// Transfer ownership of the contract to deployer
_transferOwnership(_owner);
// Set the addresses for the ACM and IGP
// Alternatively, this could be done later in an initialize method
_setAbacusConnectionManager(_abacusConnectionManager);
_setInterchainGasPaymaster(_interchainGasPaymaster);
}
/**
* @param _destinationDomain Domain of destination chain
* @param call Call (to and data packed struct) to be made on destination chain.
* @param callback Callback function selector on `msg.sender` and optionally abi-encoded prefix arguments.
*/
function query(
uint32 _destinationDomain,
Call calldata call,
bytes calldata callback
) external {
// TODO: fix this ugly arrayification
Call[] memory calls = new Call[](1);
calls[0] = call;
bytes[] memory callbacks = new bytes[](1);
callbacks[0] = callback;
query(_destinationDomain, calls, callbacks);
}
/**
* @param _destinationDomain Domain of destination chain
* @param calls Array of calls (to and data packed struct) to be made on destination chain in sequence.
* @param callbacks Array of callback function selectors on `msg.sender` and optionally abi-encoded prefix arguments.
*/
function query(
uint32 _destinationDomain,
Call[] memory calls,
bytes[] memory callbacks
) public {
require(
calls.length == callbacks.length,
"InterchainQueryRouter: calls and callbacks must be same length"
);
_dispatch(
_destinationDomain,
abi.encode(Action.DISPATCH, msg.sender, calls, callbacks)
);
emit QueryDispatched(_destinationDomain, msg.sender);
}
// TODO: add REJECT behavior ala NodeJS Promise API
function _handle(
uint32 _origin,
bytes32, // router sender
bytes calldata _message
) internal override {
// TODO: fix double ABI decoding with calldata slices
Action action = abi.decode(_message, (Action));
if (action == Action.DISPATCH) {
(
,
address sender,
Call[] memory calls,
bytes[] memory callbacks
) = abi.decode(_message, (Action, address, Call[], bytes[]));
bytes[] memory resolveCallbacks = _call(calls, callbacks);
_dispatch(
_origin,
abi.encode(Action.RESOLVE, sender, resolveCallbacks)
);
emit QueryReturned(_origin, sender);
} else if (action == Action.RESOLVE) {
(, address sender, bytes[] memory resolveCallbacks) = abi.decode(
_message,
(Action, address, bytes[])
);
proxyCallBatch(sender, resolveCallbacks);
emit QueryResolved(_origin, sender);
}
}
}

@ -31,4 +31,30 @@ contract OwnableMulticall is OwnableUpgradeable {
} }
} }
} }
function _call(Call[] memory calls, bytes[] memory callbacks)
internal
returns (bytes[] memory resolveCalls)
{
resolveCalls = new bytes[](callbacks.length);
for (uint256 i = 0; i < calls.length; i++) {
(bool success, bytes memory returnData) = calls[i].to.call(
calls[i].data
);
require(success, "Multicall: call failed");
resolveCalls[i] = bytes.concat(callbacks[i], returnData);
}
}
// TODO: deduplicate
function proxyCallBatch(address to, bytes[] memory calls) internal {
for (uint256 i = 0; i < calls.length; i += 1) {
(bool success, bytes memory returnData) = to.call(calls[i]);
if (!success) {
assembly {
revert(add(returnData, 32), returnData)
}
}
}
}
} }

@ -0,0 +1,39 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
import {InterchainQueryRouter} from "../InterchainQueryRouter.sol";
import {Call} from "../OwnableMulticall.sol";
import {TypeCasts} from "@hyperlane-xyz/core/contracts/libs/TypeCasts.sol";
contract TestQuery {
InterchainQueryRouter public router;
event Owner(uint256, address);
constructor(address _router) {
router = InterchainQueryRouter(_router);
}
/**
* @dev Fetches owner of InterchainQueryRouter on provided domain and passes along with provided secret to `this.receiveRouterOwner`
*/
function queryRouterOwner(uint32 domain, uint256 secret) external {
Call memory call = Call({
to: TypeCasts.bytes32ToAddress(router.routers(domain)),
data: abi.encodeWithSignature("owner()")
});
bytes memory callback = bytes.concat(
this.receiveRouterOwer.selector,
bytes32(secret)
);
router.query(domain, call, callback);
}
/**
* @dev `msg.sender` must be restricted to `this.router` to prevent any local account from spoofing query data.
*/
function receiveRouterOwer(uint256 secret, address owner) external {
require(msg.sender == address(router), "TestQuery: not from router");
emit Owner(secret, owner);
}
}

@ -7,4 +7,10 @@ export {
InterchainAccountFactories, InterchainAccountFactories,
interchainAccountFactories, interchainAccountFactories,
} from './src/contracts'; } from './src/contracts';
export { InterchainQueryConfig, InterchainQueryDeployer } from './src/deploy';
export {
InterchainQueryContracts,
InterchainQueryFactories,
interchainQueryFactories,
} from './src/contracts';
export * as types from './types'; export * as types from './types';

@ -3,6 +3,8 @@ import { RouterContracts, RouterFactories } from '@hyperlane-xyz/sdk';
import { import {
InterchainAccountRouter, InterchainAccountRouter,
InterchainAccountRouter__factory, InterchainAccountRouter__factory,
InterchainQueryRouter,
InterchainQueryRouter__factory,
} from '../types'; } from '../types';
export type InterchainAccountFactories = export type InterchainAccountFactories =
@ -14,3 +16,11 @@ export const interchainAccountFactories: InterchainAccountFactories = {
export type InterchainAccountContracts = export type InterchainAccountContracts =
RouterContracts<InterchainAccountRouter>; RouterContracts<InterchainAccountRouter>;
export type InterchainQueryFactories = RouterFactories<InterchainQueryRouter>;
export const interchainQueryFactories: InterchainQueryFactories = {
router: new InterchainQueryRouter__factory(),
};
export type InterchainQueryContracts = RouterContracts<InterchainQueryRouter>;

@ -7,12 +7,18 @@ import {
RouterConfig, RouterConfig,
} from '@hyperlane-xyz/sdk'; } from '@hyperlane-xyz/sdk';
import { InterchainAccountRouter__factory } from '../types'; import {
InterchainAccountRouter__factory,
InterchainQueryRouter__factory,
} from '../types';
import { import {
InterchainAccountContracts, InterchainAccountContracts,
InterchainAccountFactories, InterchainAccountFactories,
InterchainQueryContracts,
InterchainQueryFactories,
interchainAccountFactories, interchainAccountFactories,
interchainQueryFactories,
} from './contracts'; } from './contracts';
export type InterchainAccountConfig = RouterConfig; export type InterchainAccountConfig = RouterConfig;
@ -50,3 +56,40 @@ export class InterchainAccountDeployer<
}; };
} }
} }
export type InterchainQueryConfig = RouterConfig;
export class InterchainQueryDeployer<
Chain extends ChainName,
> extends HyperlaneRouterDeployer<
Chain,
InterchainQueryConfig,
InterchainQueryContracts,
InterchainQueryFactories
> {
constructor(
multiProvider: MultiProvider<Chain>,
configMap: ChainMap<Chain, InterchainQueryConfig>,
protected core: HyperlaneCore<Chain>,
protected create2salt = 'asdasdsd',
) {
super(multiProvider, configMap, interchainQueryFactories, {});
}
// Custom contract deployment logic can go here
// If no custom logic is needed, call deployContract for the router
async deployContracts(chain: Chain, config: InterchainQueryConfig) {
const initCalldata =
InterchainQueryRouter__factory.createInterface().encodeFunctionData(
'initialize',
[config.owner, config.connectionManager, config.interchainGasPaymaster],
);
const router = await this.deployContract(chain, 'router', [], {
create2Salt: this.create2salt,
initCalldata,
});
return {
router,
};
}
}

@ -0,0 +1,84 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import { expect } from 'chai';
import { ethers } from 'hardhat';
import {
ChainMap,
ChainNameToDomainId,
MultiProvider,
RouterConfig,
TestChainNames,
TestCoreApp,
TestCoreDeployer,
getChainToOwnerMap,
getTestMultiProvider,
testChainConnectionConfigs,
} from '@hyperlane-xyz/sdk';
import { InterchainQueryDeployer } from '../src/deploy';
import { InterchainQueryRouter } from '../types';
import { TestQuery, TestQuery__factory } from '../types';
describe('InterchainQueryRouter', async () => {
const localChain = 'test1';
const remoteChain = 'test2';
const localDomain = ChainNameToDomainId[localChain];
const remoteDomain = ChainNameToDomainId[remoteChain];
let signer: SignerWithAddress;
let local: InterchainQueryRouter;
let remote: InterchainQueryRouter;
let multiProvider: MultiProvider<TestChainNames>;
let coreApp: TestCoreApp;
let config: ChainMap<TestChainNames, RouterConfig>;
let testQuery: TestQuery;
before(async () => {
[signer] = await ethers.getSigners();
multiProvider = getTestMultiProvider(signer);
const coreDeployer = new TestCoreDeployer(multiProvider);
const coreContractsMaps = await coreDeployer.deploy();
coreApp = new TestCoreApp(coreContractsMaps, multiProvider);
config = coreApp.extendWithConnectionClientConfig(
getChainToOwnerMap(testChainConnectionConfigs, signer.address),
);
});
beforeEach(async () => {
const InterchainQuery = new InterchainQueryDeployer(
multiProvider,
config,
coreApp,
);
const contracts = await InterchainQuery.deploy();
local = contracts[localChain].router;
remote = contracts[remoteChain].router;
testQuery = await new TestQuery__factory(signer).deploy(local.address);
});
it('completes query round trip and invokes callback', async () => {
const secret = 123;
const expectedOwner = await remote.owner();
await expect(testQuery.queryRouterOwner(remoteDomain, secret))
.to.emit(local, 'QueryDispatched')
.withArgs(remoteDomain, testQuery.address);
const result = await coreApp.processOutboundMessages(localChain);
const response = result.get(remoteChain)![0];
await expect(response)
.to.emit(remote, 'QueryReturned')
.withArgs(localDomain, testQuery.address);
const result2 = await coreApp.processOutboundMessages(remoteChain);
const response2 = result2.get(localChain)![0];
await expect(response2)
.to.emit(local, 'QueryResolved')
.withArgs(remoteDomain, testQuery.address);
await expect(response2)
.to.emit(testQuery, 'Owner')
.withArgs(secret, expectedOwner);
});
});

@ -0,0 +1,26 @@
{
"bsc": {
"router": "0xbbaAD55AD524345C06596392ed569CB1618380C5"
},
"avalanche": {
"router": "0xbbaAD55AD524345C06596392ed569CB1618380C5"
},
"polygon": {
"router": "0xbbaAD55AD524345C06596392ed569CB1618380C5"
},
"celo": {
"router": "0xbbaAD55AD524345C06596392ed569CB1618380C5"
},
"arbitrum": {
"router": "0xbbaAD55AD524345C06596392ed569CB1618380C5"
},
"optimism": {
"router": "0xbbaAD55AD524345C06596392ed569CB1618380C5"
},
"ethereum": {
"router": "0xbbaAD55AD524345C06596392ed569CB1618380C5"
},
"moonbeam": {
"router": "0x441a01Fca2eD731C0Fc4633998332f9FEDB17575"
}
}

@ -0,0 +1,66 @@
{
"bsc": [
{
"name": "router",
"address": "0xbbaAD55AD524345C06596392ed569CB1618380C5",
"isProxy": false,
"constructorArguments": ""
}
],
"avalanche": [
{
"name": "router",
"address": "0xbbaAD55AD524345C06596392ed569CB1618380C5",
"isProxy": false,
"constructorArguments": ""
}
],
"polygon": [
{
"name": "router",
"address": "0xbbaAD55AD524345C06596392ed569CB1618380C5",
"isProxy": false,
"constructorArguments": ""
}
],
"celo": [
{
"name": "router",
"address": "0xbbaAD55AD524345C06596392ed569CB1618380C5",
"isProxy": false,
"constructorArguments": ""
}
],
"arbitrum": [
{
"name": "router",
"address": "0xbbaAD55AD524345C06596392ed569CB1618380C5",
"isProxy": false,
"constructorArguments": ""
}
],
"optimism": [
{
"name": "router",
"address": "0xbbaAD55AD524345C06596392ed569CB1618380C5",
"isProxy": false,
"constructorArguments": ""
}
],
"ethereum": [
{
"name": "router",
"address": "0xbbaAD55AD524345C06596392ed569CB1618380C5",
"isProxy": false,
"constructorArguments": ""
}
],
"moonbeam": [
{
"name": "router",
"address": "0x441a01Fca2eD731C0Fc4633998332f9FEDB17575",
"constructorArguments": "",
"isProxy": false
}
]
}

@ -7,8 +7,8 @@ export const testnetConfigs = {
...chainConnectionConfigs.mumbai, ...chainConnectionConfigs.mumbai,
confirmations: 3, confirmations: 3,
overrides: { overrides: {
maxFeePerGas: 2 * 10 ** 9, // 1000 gwei maxFeePerGas: 70 * 10 ** 9, // 1000 gwei
maxPriorityFeePerGas: 1 * 10 ** 9, // 40 gwei maxPriorityFeePerGas: 40 * 10 ** 9, // 40 gwei
}, },
}, },
bsctestnet: chainConnectionConfigs.bsctestnet, bsctestnet: chainConnectionConfigs.bsctestnet,

@ -0,0 +1,20 @@
{
"alfajores": {
"router": "0xd09072A2a076671cf615EE3dDaBb71EcE00d7b38"
},
"fuji": {
"router": "0xd09072A2a076671cf615EE3dDaBb71EcE00d7b38"
},
"mumbai": {
"router": "0xd09072A2a076671cf615EE3dDaBb71EcE00d7b38"
},
"bsctestnet": {
"router": "0xd09072A2a076671cf615EE3dDaBb71EcE00d7b38"
},
"goerli": {
"router": "0xd09072A2a076671cf615EE3dDaBb71EcE00d7b38"
},
"moonbasealpha": {
"router": "0xd09072A2a076671cf615EE3dDaBb71EcE00d7b38"
}
}

@ -0,0 +1,50 @@
{
"alfajores": [
{
"name": "InterchainQueryRouter",
"address": "0xd09072A2a076671cf615EE3dDaBb71EcE00d7b38",
"isProxy": false,
"constructorArguments": ""
}
],
"fuji": [
{
"name": "InterchainQueryRouter",
"address": "0xd09072A2a076671cf615EE3dDaBb71EcE00d7b38",
"isProxy": false,
"constructorArguments": ""
}
],
"mumbai": [
{
"name": "InterchainQueryRouter",
"address": "0xd09072A2a076671cf615EE3dDaBb71EcE00d7b38",
"isProxy": false,
"constructorArguments": ""
}
],
"bsctestnet": [
{
"name": "InterchainQueryRouter",
"address": "0xd09072A2a076671cf615EE3dDaBb71EcE00d7b38",
"isProxy": false,
"constructorArguments": ""
}
],
"goerli": [
{
"name": "InterchainQueryRouter",
"address": "0xd09072A2a076671cf615EE3dDaBb71EcE00d7b38",
"isProxy": false,
"constructorArguments": ""
}
],
"moonbasealpha": [
{
"name": "InterchainQueryRouter",
"address": "0xd09072A2a076671cf615EE3dDaBb71EcE00d7b38",
"isProxy": false,
"constructorArguments": ""
}
]
}

@ -0,0 +1,45 @@
import path from 'path';
import {
InterchainQueryDeployer,
interchainQueryFactories,
} from '@hyperlane-xyz/interchain-accounts';
import { HyperlaneCore } from '@hyperlane-xyz/sdk';
import { deployWithArtifacts } from '../../src/deploy';
import { getConfiguration } from '../helloworld/utils';
import {
getCoreEnvironmentConfig,
getEnvironment,
getEnvironmentDirectory,
} from '../utils';
// similar to hello world deploy script but uses freshly funded account for consistent addresses across chains
// should eventually be deduped
async function main() {
const environment = await getEnvironment();
const coreConfig = getCoreEnvironmentConfig(environment);
const multiProvider = await coreConfig.getMultiProvider();
const core = HyperlaneCore.fromEnvironment(environment, multiProvider as any);
const dir = path.join(
getEnvironmentDirectory(environment),
'interchain/queries',
);
// config gcp deployer key as owner
const configMap = await getConfiguration(environment, multiProvider);
const deployer = new InterchainQueryDeployer(
multiProvider,
configMap,
core,
'IQS-SALT-2',
);
await deployWithArtifacts(dir, interchainQueryFactories, deployer);
}
main()
.then(() => console.info('Deployment complete'))
.catch(console.error);

@ -58,7 +58,7 @@ export class TestCoreApp<
async processOutboundMessages<Local extends TestChain>( async processOutboundMessages<Local extends TestChain>(
origin: Local, origin: Local,
): Promise<Map<ChainName, any>> { ): Promise<Map<ChainName, ethers.providers.TransactionResponse[]>> {
const responses = new Map<ChainName, any>(); const responses = new Map<ChainName, any>();
const contracts = this.getContracts(origin); const contracts = this.getContracts(origin);
const outbox: TestOutbox = contracts.outbox.contract; const outbox: TestOutbox = contracts.outbox.contract;

Loading…
Cancel
Save