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