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
parent
fe904f22cc
commit
a2f4810027
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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 |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -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); |
Loading…
Reference in new issue