From fd54116155f9ed66b14524c351cc6c16de191b58 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Wed, 2 Nov 2022 12:14:18 -0700 Subject: [PATCH] Add TokenBridge deployment (#1215) * Add TokenBridge deployment * PR review * Remove only * Fix hardhat * Fix ethers * PR review * PR review * lint * Undo merging into core addresses --- .../adapters/CircleBridgeAdapter.sol | 3 +- solidity/contracts/mock/MockCircleBridge.sol | 38 ++++ .../mock/MockCircleMessageTransmitter.sol | 43 +++++ .../contracts/mock/MockTokenBridgeAdapter.sol | 46 ----- solidity/test/TokenBridgeRouter.t.sol | 73 ++++++-- .../middleware/token-bridge/addresses.json | 10 + .../middleware/token-bridge/verification.json | 30 +++ .../testnet2/testrecipient/addresses.json | 18 +- .../testnet2/testrecipient/verification.json | 48 +++++ .../environments/testnet2/token-bridge.ts | 32 ++++ .../deploy-accounts.ts} | 0 .../deploy-queries.ts | 0 .../scripts/middleware/deploy-token-bridge.ts | 48 +++++ typescript/infra/src/testrecipient/index.ts | 16 +- .../src/deploy/middleware/TokenBridgeApp.ts | 7 + .../middleware/TokenBridgeRouterDeployer.ts | 174 ++++++++++++++++++ .../deploy/router/HyperlaneRouterDeployer.ts | 2 +- typescript/sdk/src/index.ts | 9 + typescript/sdk/src/middleware.ts | 17 ++ .../middleware/tokenbridge.hardhat-test.ts | 138 ++++++++++++++ 20 files changed, 683 insertions(+), 69 deletions(-) create mode 100644 solidity/contracts/mock/MockCircleBridge.sol create mode 100644 solidity/contracts/mock/MockCircleMessageTransmitter.sol delete mode 100644 solidity/contracts/mock/MockTokenBridgeAdapter.sol create mode 100644 typescript/infra/config/environments/testnet2/middleware/token-bridge/addresses.json create mode 100644 typescript/infra/config/environments/testnet2/middleware/token-bridge/verification.json create mode 100644 typescript/infra/config/environments/testnet2/token-bridge.ts rename typescript/infra/scripts/{interchain/deploy.ts => middleware/deploy-accounts.ts} (100%) rename typescript/infra/scripts/{interchain => middleware}/deploy-queries.ts (100%) create mode 100644 typescript/infra/scripts/middleware/deploy-token-bridge.ts create mode 100644 typescript/sdk/src/deploy/middleware/TokenBridgeApp.ts create mode 100644 typescript/sdk/src/deploy/middleware/TokenBridgeRouterDeployer.ts create mode 100644 typescript/sdk/src/middleware/tokenbridge.hardhat-test.ts diff --git a/solidity/contracts/middleware/token-bridge/adapters/CircleBridgeAdapter.sol b/solidity/contracts/middleware/token-bridge/adapters/CircleBridgeAdapter.sol index b7df31087..f857afa7c 100644 --- a/solidity/contracts/middleware/token-bridge/adapters/CircleBridgeAdapter.sol +++ b/solidity/contracts/middleware/token-bridge/adapters/CircleBridgeAdapter.sol @@ -121,8 +121,7 @@ contract CircleBridgeAdapter is ITokenBridgeAdapter, Router { ); emit BridgedToken(_nonce); - - return abi.encodePacked(_nonce, _tokenSymbol); + return abi.encode(_nonce, _tokenSymbol); } // Returns the token and amount sent diff --git a/solidity/contracts/mock/MockCircleBridge.sol b/solidity/contracts/mock/MockCircleBridge.sol new file mode 100644 index 000000000..dc1d9c066 --- /dev/null +++ b/solidity/contracts/mock/MockCircleBridge.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.13; + +import {ICircleBridge} from "../middleware/token-bridge/interfaces/circle/ICircleBridge.sol"; +import {MockToken} from "./MockToken.sol"; + +contract MockCircleBridge is ICircleBridge { + uint64 public nextNonce = 0; + MockToken token; + + constructor(MockToken _token) { + token = _token; + } + + function depositForBurn( + uint256 _amount, + uint32, + bytes32, + address _burnToken + ) external returns (uint64 _nonce) { + nextNonce = nextNonce + 1; + _nonce = nextNonce; + require(address(token) == _burnToken); + token.transferFrom(msg.sender, address(this), _amount); + token.burn(_amount); + } + + function depositForBurnWithCaller( + uint256, + uint32, + bytes32, + address, + bytes32 + ) external returns (uint64 _nonce) { + nextNonce = nextNonce + 1; + _nonce = nextNonce; + } +} diff --git a/solidity/contracts/mock/MockCircleMessageTransmitter.sol b/solidity/contracts/mock/MockCircleMessageTransmitter.sol new file mode 100644 index 000000000..3361b17d1 --- /dev/null +++ b/solidity/contracts/mock/MockCircleMessageTransmitter.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.13; + +import {ICircleMessageTransmitter} from "../middleware/token-bridge/interfaces/circle/ICircleMessageTransmitter.sol"; +import {MockToken} from "./MockToken.sol"; + +contract MockCircleMessageTransmitter is ICircleMessageTransmitter { + mapping(bytes32 => bool) processedNonces; + MockToken token; + + constructor(MockToken _token) { + token = _token; + } + + function receiveMessage(bytes memory, bytes calldata) + external + pure + returns (bool success) + { + success = true; + } + + function hashSourceAndNonce(uint32 _source, uint256 _nonce) + public + pure + returns (bytes32) + { + return keccak256(abi.encodePacked(_source, _nonce)); + } + + function process( + bytes32 _nonceId, + address _recipient, + uint256 _amount + ) public { + processedNonces[_nonceId] = true; + token.mint(_recipient, _amount); + } + + function usedNonces(bytes32 _nonceId) external view returns (bool) { + return processedNonces[_nonceId]; + } +} diff --git a/solidity/contracts/mock/MockTokenBridgeAdapter.sol b/solidity/contracts/mock/MockTokenBridgeAdapter.sol deleted file mode 100644 index 08b04e651..000000000 --- a/solidity/contracts/mock/MockTokenBridgeAdapter.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.13; - -import {ITokenBridgeAdapter} from "../middleware/token-bridge/interfaces/ITokenBridgeAdapter.sol"; -import {MockToken} from "./MockToken.sol"; - -contract MockTokenBridgeAdapter is ITokenBridgeAdapter { - uint256 public nonce = 0; - MockToken token; - - mapping(uint256 => bool) public isProcessed; - - constructor(MockToken _token) { - token = _token; - } - - function sendTokens( - uint32, - bytes32, - address _token, - uint256 _amount - ) external override returns (bytes memory _adapterData) { - require(_token == address(token), "cant bridge this token"); - token.burn(_amount); - nonce = nonce + 1; - return abi.encode(nonce); - } - - function process(uint256 _nonce) public { - isProcessed[_nonce] = true; - } - - function receiveTokens( - uint32 _originDomain, // Hyperlane domain - address _recipientAddress, - uint256 _amount, - bytes calldata _adapterData // The adapter data from the message - ) external override returns (address, uint256) { - _originDomain; - uint256 _nonce = abi.decode(_adapterData, (uint256)); - // Check if the transfer was processed first - require(isProcessed[_nonce], "Transfer has not been processed yet"); - token.mint(_recipientAddress, _amount); - return (address(0), 0); - } -} diff --git a/solidity/test/TokenBridgeRouter.t.sol b/solidity/test/TokenBridgeRouter.t.sol index cb5e30672..8ad70e43a 100644 --- a/solidity/test/TokenBridgeRouter.t.sol +++ b/solidity/test/TokenBridgeRouter.t.sol @@ -3,9 +3,11 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; import {TokenBridgeRouter} from "../contracts/middleware/token-bridge/TokenBridgeRouter.sol"; +import {CircleBridgeAdapter} from "../contracts/middleware/token-bridge/adapters/CircleBridgeAdapter.sol"; import {MockToken} from "../contracts/mock/MockToken.sol"; import {TestTokenRecipient} from "../contracts/test/TestTokenRecipient.sol"; -import {MockTokenBridgeAdapter} from "../contracts/mock/MockTokenBridgeAdapter.sol"; +import {MockCircleMessageTransmitter} from "../contracts/mock/MockCircleMessageTransmitter.sol"; +import {MockCircleBridge} from "../contracts/mock/MockCircleBridge.sol"; import {MockHyperlaneEnvironment} from "./MockHyperlaneEnvironment.sol"; import {TypeCasts} from "../contracts/libs/TypeCasts.sol"; @@ -16,8 +18,11 @@ contract TokenBridgeRouterTest is Test { TokenBridgeRouter originTokenBridgeRouter; TokenBridgeRouter destinationTokenBridgeRouter; - // Origin bridge adapter - MockTokenBridgeAdapter bridgeAdapter; + MockCircleMessageTransmitter messageTransmitter; + MockCircleBridge circleBridge; + CircleBridgeAdapter originBridgeAdapter; + CircleBridgeAdapter destinationBridgeAdapter; + string bridge = "FooBridge"; uint32 originDomain = 123; @@ -32,7 +37,12 @@ contract TokenBridgeRouterTest is Test { function setUp() public { token = new MockToken(); - bridgeAdapter = new MockTokenBridgeAdapter(token); + + circleBridge = new MockCircleBridge(token); + messageTransmitter = new MockCircleMessageTransmitter(token); + originBridgeAdapter = new CircleBridgeAdapter(); + destinationBridgeAdapter = new CircleBridgeAdapter(); + recipient = new TestTokenRecipient(); originTokenBridgeRouter = new TokenBridgeRouter(); @@ -64,14 +74,40 @@ contract TokenBridgeRouterTest is Test { TypeCasts.addressToBytes32(address(originTokenBridgeRouter)) ); + originBridgeAdapter.initialize( + address(this), + address(circleBridge), + address(messageTransmitter), + address(originTokenBridgeRouter) + ); + + destinationBridgeAdapter.initialize( + address(this), + address(circleBridge), + address(messageTransmitter), + address(destinationTokenBridgeRouter) + ); + + originBridgeAdapter.addToken(address(token), "USDC"); + destinationBridgeAdapter.addToken(address(token), "USDC"); + + originBridgeAdapter.enrollRemoteRouter( + destinationDomain, + TypeCasts.addressToBytes32(address(destinationBridgeAdapter)) + ); + destinationBridgeAdapter.enrollRemoteRouter( + destinationDomain, + TypeCasts.addressToBytes32(address(originBridgeAdapter)) + ); + originTokenBridgeRouter.setTokenBridgeAdapter( bridge, - address(bridgeAdapter) + address(originBridgeAdapter) ); destinationTokenBridgeRouter.setTokenBridgeAdapter( bridge, - address(bridgeAdapter) + address(destinationBridgeAdapter) ); token.mint(address(this), amount); @@ -81,18 +117,18 @@ contract TokenBridgeRouterTest is Test { // Expect the TokenBridgeAdapterSet event. // Expect topic0 & data to match vm.expectEmit(true, false, false, true); - emit TokenBridgeAdapterSet(bridge, address(bridgeAdapter)); + emit TokenBridgeAdapterSet(bridge, address(originBridgeAdapter)); // Set the token bridge adapter originTokenBridgeRouter.setTokenBridgeAdapter( bridge, - address(bridgeAdapter) + address(originBridgeAdapter) ); // Expect the bridge adapter to have been set assertEq( originTokenBridgeRouter.tokenBridgeAdapters(bridge), - address(bridgeAdapter) + address(originBridgeAdapter) ); } @@ -136,9 +172,9 @@ contract TokenBridgeRouterTest is Test { function testDispatchWithTokensCallsAdapter() public { vm.expectCall( - address(bridgeAdapter), + address(originBridgeAdapter), abi.encodeWithSelector( - bridgeAdapter.sendTokens.selector, + originBridgeAdapter.sendTokens.selector, destinationDomain, TypeCasts.addressToBytes32(address(recipient)), address(token), @@ -167,7 +203,7 @@ contract TokenBridgeRouterTest is Test { bridge ); - vm.expectRevert("Transfer has not been processed yet"); + vm.expectRevert("Circle message not processed yet"); testEnvironment.processNextPendingMessage(); } @@ -182,7 +218,18 @@ contract TokenBridgeRouterTest is Test { bridge ); - bridgeAdapter.process(bridgeAdapter.nonce()); + bytes32 nonceId = messageTransmitter.hashSourceAndNonce( + destinationBridgeAdapter.hyperlaneDomainToCircleDomain( + originDomain + ), + circleBridge.nextNonce() + ); + + messageTransmitter.process( + nonceId, + address(destinationBridgeAdapter), + amount + ); testEnvironment.processNextPendingMessage(); assertEq(recipient.lastData(), messageBody); assertEq(token.balanceOf(address(recipient)), amount); diff --git a/typescript/infra/config/environments/testnet2/middleware/token-bridge/addresses.json b/typescript/infra/config/environments/testnet2/middleware/token-bridge/addresses.json new file mode 100644 index 000000000..cc13cb5b5 --- /dev/null +++ b/typescript/infra/config/environments/testnet2/middleware/token-bridge/addresses.json @@ -0,0 +1,10 @@ +{ + "goerli": { + "circleBridgeAdapter": "0xc262a656c99B3a2f1B196dc5BeDa8f4f80D4a878", + "router": "0x952228cA63f85130534981844050c82b89f373E7" + }, + "fuji": { + "circleBridgeAdapter": "0xc262a656c99B3a2f1B196dc5BeDa8f4f80D4a878", + "router": "0x952228cA63f85130534981844050c82b89f373E7" + } +} diff --git a/typescript/infra/config/environments/testnet2/middleware/token-bridge/verification.json b/typescript/infra/config/environments/testnet2/middleware/token-bridge/verification.json new file mode 100644 index 000000000..9d80efc5e --- /dev/null +++ b/typescript/infra/config/environments/testnet2/middleware/token-bridge/verification.json @@ -0,0 +1,30 @@ +{ + "goerli": [ + { + "name": "TokenBridgeRouter", + "address": "0x952228cA63f85130534981844050c82b89f373E7", + "isProxy": false, + "constructorArguments": "" + }, + { + "name": "CircleBridgeAdapter", + "address": "0xc262a656c99B3a2f1B196dc5BeDa8f4f80D4a878", + "isProxy": false, + "constructorArguments": "" + } + ], + "fuji": [ + { + "name": "TokenBridgeRouter", + "address": "0x952228cA63f85130534981844050c82b89f373E7", + "isProxy": false, + "constructorArguments": "" + }, + { + "name": "CircleBridgeAdapter", + "address": "0xc262a656c99B3a2f1B196dc5BeDa8f4f80D4a878", + "isProxy": false, + "constructorArguments": "" + } + ] +} diff --git a/typescript/infra/config/environments/testnet2/testrecipient/addresses.json b/typescript/infra/config/environments/testnet2/testrecipient/addresses.json index 2cd0464ee..39afe7077 100644 --- a/typescript/infra/config/environments/testnet2/testrecipient/addresses.json +++ b/typescript/infra/config/environments/testnet2/testrecipient/addresses.json @@ -1,20 +1,26 @@ { "alfajores": { - "TestRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE" + "TestRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", + "TestTokenRecipient": "0x36597C9C49F3c5887A86466398480ddB66aD0759" }, "fuji": { - "TestRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE" + "TestRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", + "TestTokenRecipient": "0x36597C9C49F3c5887A86466398480ddB66aD0759" }, "mumbai": { - "TestRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE" + "TestRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", + "TestTokenRecipient": "0x36597C9C49F3c5887A86466398480ddB66aD0759" }, "bsctestnet": { - "TestRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE" + "TestRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", + "TestTokenRecipient": "0x36597C9C49F3c5887A86466398480ddB66aD0759" }, "goerli": { - "TestRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE" + "TestRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", + "TestTokenRecipient": "0x36597C9C49F3c5887A86466398480ddB66aD0759" }, "moonbasealpha": { - "TestRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE" + "TestRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", + "TestTokenRecipient": "0x36597C9C49F3c5887A86466398480ddB66aD0759" } } diff --git a/typescript/infra/config/environments/testnet2/testrecipient/verification.json b/typescript/infra/config/environments/testnet2/testrecipient/verification.json index 4a2f91045..d33a83cc9 100644 --- a/typescript/infra/config/environments/testnet2/testrecipient/verification.json +++ b/typescript/infra/config/environments/testnet2/testrecipient/verification.json @@ -5,6 +5,12 @@ "address": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "isProxy": false, "constructorArguments": "" + }, + { + "name": "TestTokenRecipient", + "address": "0x36597C9C49F3c5887A86466398480ddB66aD0759", + "isProxy": false, + "constructorArguments": "" } ], "kovan": [ @@ -13,6 +19,12 @@ "address": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "isProxy": false, "constructorArguments": "" + }, + { + "name": "TestTokenRecipient", + "address": "0x36597C9C49F3c5887A86466398480ddB66aD0759", + "isProxy": false, + "constructorArguments": "" } ], "fuji": [ @@ -21,6 +33,12 @@ "address": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "isProxy": false, "constructorArguments": "" + }, + { + "name": "TestTokenRecipient", + "address": "0x36597C9C49F3c5887A86466398480ddB66aD0759", + "isProxy": false, + "constructorArguments": "" } ], "mumbai": [ @@ -29,6 +47,12 @@ "address": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "isProxy": false, "constructorArguments": "" + }, + { + "name": "TestTokenRecipient", + "address": "0x36597C9C49F3c5887A86466398480ddB66aD0759", + "isProxy": false, + "constructorArguments": "" } ], "bsctestnet": [ @@ -37,6 +61,12 @@ "address": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "isProxy": false, "constructorArguments": "" + }, + { + "name": "TestTokenRecipient", + "address": "0x36597C9C49F3c5887A86466398480ddB66aD0759", + "isProxy": false, + "constructorArguments": "" } ], "arbitrumrinkeby": [ @@ -45,6 +75,12 @@ "address": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "isProxy": false, "constructorArguments": "" + }, + { + "name": "TestTokenRecipient", + "address": "0x36597C9C49F3c5887A86466398480ddB66aD0759", + "isProxy": false, + "constructorArguments": "" } ], "optimismkovan": [ @@ -53,6 +89,12 @@ "address": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "isProxy": false, "constructorArguments": "" + }, + { + "name": "TestTokenRecipient", + "address": "0x36597C9C49F3c5887A86466398480ddB66aD0759", + "isProxy": false, + "constructorArguments": "" } ], "goerli": [ @@ -61,6 +103,12 @@ "address": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "isProxy": false, "constructorArguments": "" + }, + { + "name": "TestTokenRecipient", + "address": "0x36597C9C49F3c5887A86466398480ddB66aD0759", + "isProxy": false, + "constructorArguments": "" } ], "moonbasealpha": [ diff --git a/typescript/infra/config/environments/testnet2/token-bridge.ts b/typescript/infra/config/environments/testnet2/token-bridge.ts new file mode 100644 index 000000000..fe9429ecd --- /dev/null +++ b/typescript/infra/config/environments/testnet2/token-bridge.ts @@ -0,0 +1,32 @@ +import { + BridgeAdapterType, + ChainMap, + Chains, + CircleBridgeAdapterConfig, + chainMetadata, +} from '@hyperlane-xyz/sdk'; + +const circleDomainMapping = [ + { hyperlaneDomain: chainMetadata[Chains.goerli].id, circleDomain: 0 }, + { hyperlaneDomain: chainMetadata[Chains.fuji].id, circleDomain: 1 }, +]; + +export const circleBridgeAdapterConfig: ChainMap< + any, + CircleBridgeAdapterConfig +> = { + [Chains.goerli]: { + type: BridgeAdapterType.Circle, + circleBridgeAddress: '0xdabec94b97f7b5fca28f050cc8eeac2dc9920476', + messageTransmitterAddress: '0x40a61d3d2afcf5a5d31fcdf269e575fb99dd87f7', + usdcAddress: '0x07865c6e87b9f70255377e024ace6630c1eaa37f', + circleDomainMapping, + }, + [Chains.fuji]: { + type: BridgeAdapterType.Circle, + circleBridgeAddress: '0x0fc1103927af27af808d03135214718bcedbe9ad', + messageTransmitterAddress: '0x52fffb3ee8fa7838e9858a2d5e454007b9027c3c', + usdcAddress: '0x5425890298aed601595a70ab815c96711a31bc65', + circleDomainMapping, + }, +}; diff --git a/typescript/infra/scripts/interchain/deploy.ts b/typescript/infra/scripts/middleware/deploy-accounts.ts similarity index 100% rename from typescript/infra/scripts/interchain/deploy.ts rename to typescript/infra/scripts/middleware/deploy-accounts.ts diff --git a/typescript/infra/scripts/interchain/deploy-queries.ts b/typescript/infra/scripts/middleware/deploy-queries.ts similarity index 100% rename from typescript/infra/scripts/interchain/deploy-queries.ts rename to typescript/infra/scripts/middleware/deploy-queries.ts diff --git a/typescript/infra/scripts/middleware/deploy-token-bridge.ts b/typescript/infra/scripts/middleware/deploy-token-bridge.ts new file mode 100644 index 000000000..31fc96cf7 --- /dev/null +++ b/typescript/infra/scripts/middleware/deploy-token-bridge.ts @@ -0,0 +1,48 @@ +import path from 'path'; + +import { + HyperlaneCore, + TokenBridgeDeployer, + objMap, + tokenBridgeFactories, +} from '@hyperlane-xyz/sdk'; + +import { circleBridgeAdapterConfig } from '../../config/environments/testnet2/token-bridge'; +import { deployWithArtifacts } from '../../src/deploy'; +import { getConfiguration } from '../helloworld/utils'; +import { + getCoreEnvironmentConfig, + getEnvironment, + getEnvironmentDirectory, +} from '../utils'; + +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), + 'middleware/token-bridge', + ); + + // config gcp deployer key as owner + const ownerConfigMap = await getConfiguration(environment, multiProvider); + + const deployer = new TokenBridgeDeployer( + multiProvider, + objMap(circleBridgeAdapterConfig, (chain, conf) => ({ + bridgeAdapterConfigs: [conf], + ...ownerConfigMap[chain], + })), + core, + 'TokenBridgeDeploy2', + ); + + await deployWithArtifacts(dir, tokenBridgeFactories, deployer); +} + +main() + .then(() => console.info('Deployment complete')) + .catch(console.error); diff --git a/typescript/infra/src/testrecipient/index.ts b/typescript/infra/src/testrecipient/index.ts index 58a88f88b..23809d4fc 100644 --- a/typescript/infra/src/testrecipient/index.ts +++ b/typescript/infra/src/testrecipient/index.ts @@ -1,4 +1,9 @@ -import { TestRecipient, TestRecipient__factory } from '@hyperlane-xyz/core'; +import { + TestRecipient, + TestRecipient__factory, + TestTokenRecipient, + TestTokenRecipient__factory, +} from '@hyperlane-xyz/core'; import { ChainName, HyperlaneDeployer, @@ -7,10 +12,12 @@ import { export const factories = { TestRecipient: new TestRecipient__factory(), + TestTokenRecipient: new TestTokenRecipient__factory(), }; type Contracts = { TestRecipient: TestRecipient; + TestTokenRecipient: TestTokenRecipient; }; export class TestRecipientDeployer< @@ -30,8 +37,15 @@ export class TestRecipientDeployer< [], { create2Salt: 'testtest32' }, ); + const TestTokenRecipient = await this.deployContract( + chain, + 'TestTokenRecipient', + [], + { create2Salt: 'TestTokenRecipient' }, + ); return { TestRecipient, + TestTokenRecipient, }; } } diff --git a/typescript/sdk/src/deploy/middleware/TokenBridgeApp.ts b/typescript/sdk/src/deploy/middleware/TokenBridgeApp.ts new file mode 100644 index 000000000..ec9be5a39 --- /dev/null +++ b/typescript/sdk/src/deploy/middleware/TokenBridgeApp.ts @@ -0,0 +1,7 @@ +import { HyperlaneApp } from '../../HyperlaneApp'; +import { TokenBridgeContracts } from '../../middleware'; +import { ChainName } from '../../types'; + +export class TokenBridgeApp< + Chain extends ChainName = ChainName, +> extends HyperlaneApp {} diff --git a/typescript/sdk/src/deploy/middleware/TokenBridgeRouterDeployer.ts b/typescript/sdk/src/deploy/middleware/TokenBridgeRouterDeployer.ts new file mode 100644 index 000000000..2e0e42af5 --- /dev/null +++ b/typescript/sdk/src/deploy/middleware/TokenBridgeRouterDeployer.ts @@ -0,0 +1,174 @@ +import { ethers } from 'ethers'; + +import { + CircleBridgeAdapter, + CircleBridgeAdapter__factory, + TokenBridgeRouter, + TokenBridgeRouter__factory, +} from '@hyperlane-xyz/core'; +import { objMap } from '@hyperlane-xyz/sdk/src/utils/objects'; + +import { HyperlaneCore } from '../../core/HyperlaneCore'; +import { + TokenBridgeContracts, + TokenBridgeFactories, + tokenBridgeFactories, +} from '../../middleware'; +import { MultiProvider } from '../../providers/MultiProvider'; +import { ChainMap, ChainName } from '../../types'; +import { HyperlaneRouterDeployer } from '../router/HyperlaneRouterDeployer'; +import { RouterConfig } from '../router/types'; + +export enum BridgeAdapterType { + Circle = 'Circle', +} + +export interface CircleBridgeAdapterConfig { + type: BridgeAdapterType.Circle; + circleBridgeAddress: string; + messageTransmitterAddress: string; + usdcAddress: string; + circleDomainMapping: { + hyperlaneDomain: number; + circleDomain: number; + }[]; +} + +export type BridgeAdapterConfig = CircleBridgeAdapterConfig; + +export type TokenBridgeConfig = RouterConfig & { + bridgeAdapterConfigs: BridgeAdapterConfig[]; +}; + +export class TokenBridgeDeployer< + Chain extends ChainName, +> extends HyperlaneRouterDeployer< + Chain, + TokenBridgeConfig, + TokenBridgeContracts, + TokenBridgeFactories +> { + constructor( + multiProvider: MultiProvider, + configMap: ChainMap, + protected core: HyperlaneCore, + protected create2salt = 'TokenBridgeDeployerSalt', + ) { + super(multiProvider, configMap, tokenBridgeFactories, {}); + } + + async enrollRemoteRouters( + contractsMap: ChainMap, + ): Promise { + // Enroll the TokenBridgeRouter with each other + await super.enrollRemoteRouters(contractsMap); + + // Enroll the circle adapters with each other + await super.enrollRemoteRouters( + objMap(contractsMap, (_chain, contracts) => ({ + router: contracts.circleBridgeAdapter!, + })), + ); + } + + // Custom contract deployment logic can go here + // If no custom logic is needed, call deployContract for the router + async deployContracts( + chain: Chain, + config: TokenBridgeConfig, + ): Promise { + const initCalldata = + TokenBridgeRouter__factory.createInterface().encodeFunctionData( + 'initialize', + [config.owner, config.connectionManager, config.interchainGasPaymaster], + ); + const router = await this.deployContract(chain, 'router', [], { + create2Salt: this.create2salt, + initCalldata, + }); + + const bridgeAdapters: Partial = {}; + + for (const adapterConfig of config.bridgeAdapterConfigs) { + if (adapterConfig.type === BridgeAdapterType.Circle) { + bridgeAdapters.circleBridgeAdapter = + await this.deployCircleBridgeAdapter( + chain, + adapterConfig, + config.owner, + router, + ); + } + } + + return { + ...bridgeAdapters, + router, + }; + } + + async deployCircleBridgeAdapter( + chain: Chain, + adapterConfig: CircleBridgeAdapterConfig, + owner: string, + router: TokenBridgeRouter, + ): Promise { + const cc = this.multiProvider.getChainConnection(chain); + const initCalldata = + CircleBridgeAdapter__factory.createInterface().encodeFunctionData( + 'initialize', + [ + owner, + adapterConfig.circleBridgeAddress, + adapterConfig.messageTransmitterAddress, + router.address, + ], + ); + const circleBridgeAdapter = await this.deployContract( + chain, + 'circleBridgeAdapter', + [], + { + create2Salt: this.create2salt, + initCalldata, + }, + ); + + if ( + (await circleBridgeAdapter.tokenSymbolToAddress('USDC')) === + ethers.constants.AddressZero + ) { + this.logger(`Set USDC token contract`); + await cc.handleTx( + circleBridgeAdapter.addToken(adapterConfig.usdcAddress, 'USDC'), + ); + } + // Set domain mappings + for (const { + circleDomain, + hyperlaneDomain, + } of adapterConfig.circleDomainMapping) { + const expectedCircleDomain = + await circleBridgeAdapter.hyperlaneDomainToCircleDomain( + hyperlaneDomain, + ); + if (expectedCircleDomain === circleDomain) continue; + + this.logger( + `Set circle domain ${circleDomain} for hyperlane domain ${hyperlaneDomain}`, + ); + await cc.handleTx( + circleBridgeAdapter.addDomain(hyperlaneDomain, circleDomain), + ); + } + + this.logger('Set CircleTokenBridgeAdapter on Router'); + await cc.handleTx( + router.setTokenBridgeAdapter( + adapterConfig.type, + circleBridgeAdapter.address, + ), + ); + return circleBridgeAdapter; + } +} diff --git a/typescript/sdk/src/deploy/router/HyperlaneRouterDeployer.ts b/typescript/sdk/src/deploy/router/HyperlaneRouterDeployer.ts index 814704046..a584d85b3 100644 --- a/typescript/sdk/src/deploy/router/HyperlaneRouterDeployer.ts +++ b/typescript/sdk/src/deploy/router/HyperlaneRouterDeployer.ts @@ -68,7 +68,7 @@ export abstract class HyperlaneRouterDeployer< } async enrollRemoteRouters( - contractsMap: ChainMap, + contractsMap: ChainMap, ): Promise { this.logger( `Enrolling deployed routers with each other (if not already)...`, diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 27e51941d..29523d855 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -115,9 +115,18 @@ export { InterchainAccountDeployer, InterchainQueryDeployer, } from './deploy/middleware/deploy'; +export { + TokenBridgeDeployer, + BridgeAdapterType, + BridgeAdapterConfig, + CircleBridgeAdapterConfig, +} from './deploy/middleware/TokenBridgeRouterDeployer'; +export { TokenBridgeApp } from './deploy/middleware/TokenBridgeApp'; + export { interchainAccountFactories, interchainQueryFactories, + tokenBridgeFactories, } from './middleware'; export { RouterConfig } from './deploy/router/types'; export { getTestMultiProvider, getChainToOwnerMap } from './deploy/utils'; diff --git a/typescript/sdk/src/middleware.ts b/typescript/sdk/src/middleware.ts index caba89f25..9f8d42eaa 100644 --- a/typescript/sdk/src/middleware.ts +++ b/typescript/sdk/src/middleware.ts @@ -1,8 +1,12 @@ import { + CircleBridgeAdapter, + CircleBridgeAdapter__factory, InterchainAccountRouter, InterchainAccountRouter__factory, InterchainQueryRouter, InterchainQueryRouter__factory, + TokenBridgeRouter, + TokenBridgeRouter__factory, } from '@hyperlane-xyz/core'; import { RouterContracts, RouterFactories } from './router'; @@ -24,3 +28,16 @@ export const interchainQueryFactories: InterchainQueryFactories = { }; export type InterchainQueryContracts = RouterContracts; + +export type TokenBridgeFactories = RouterFactories & { + circleBridgeAdapter: CircleBridgeAdapter__factory; +}; + +export const tokenBridgeFactories: TokenBridgeFactories = { + router: new TokenBridgeRouter__factory(), + circleBridgeAdapter: new CircleBridgeAdapter__factory(), +}; + +export type TokenBridgeContracts = RouterContracts & { + circleBridgeAdapter?: CircleBridgeAdapter; +}; diff --git a/typescript/sdk/src/middleware/tokenbridge.hardhat-test.ts b/typescript/sdk/src/middleware/tokenbridge.hardhat-test.ts new file mode 100644 index 000000000..2b9c509fa --- /dev/null +++ b/typescript/sdk/src/middleware/tokenbridge.hardhat-test.ts @@ -0,0 +1,138 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; + +import { + MockCircleBridge, + MockCircleBridge__factory, + MockCircleMessageTransmitter, + MockCircleMessageTransmitter__factory, + MockToken, + MockToken__factory, + TestTokenBridgeMessageRecipient__factory, + TokenBridgeRouter, +} from '@hyperlane-xyz/core'; +import { utils } from '@hyperlane-xyz/utils'; + +import { testChainConnectionConfigs } from '../consts/chainConnectionConfigs'; +import { TestCoreApp } from '../core/TestCoreApp'; +import { TestCoreDeployer } from '../core/TestCoreDeployer'; +import { TokenBridgeApp } from '../deploy/middleware/TokenBridgeApp'; +import { + BridgeAdapterType, + CircleBridgeAdapterConfig, + TokenBridgeConfig, + TokenBridgeDeployer, +} from '../deploy/middleware/TokenBridgeRouterDeployer'; +import { getChainToOwnerMap, getTestMultiProvider } from '../deploy/utils'; +import { ChainNameToDomainId } from '../domains'; +import { MultiProvider } from '../providers/MultiProvider'; +import { ChainMap, TestChainNames } from '../types'; +import { objMap } from '../utils/objects'; + +describe('TokenBridgeRouter', async () => { + const localChain = 'test1'; + const remoteChain = 'test2'; + const localDomain = ChainNameToDomainId[localChain]; + const remoteDomain = ChainNameToDomainId[remoteChain]; + + let signer: SignerWithAddress; + let local: TokenBridgeRouter; + let multiProvider: MultiProvider; + let coreApp: TestCoreApp; + + let tokenBridgeApp: TokenBridgeApp; + let config: ChainMap; + let mockToken: MockToken; + let circleBridge: MockCircleBridge; + let messageTransmitter: MockCircleMessageTransmitter; + + before(async () => { + [signer] = await ethers.getSigners(); + + multiProvider = getTestMultiProvider(signer); + + const coreDeployer = new TestCoreDeployer(multiProvider); + const coreContractsMaps = await coreDeployer.deploy(); + coreApp = new TestCoreApp(coreContractsMaps, multiProvider); + + const mockTokenF = new MockToken__factory(signer); + mockToken = await mockTokenF.deploy(); + const circleBridgeF = new MockCircleBridge__factory(signer); + circleBridge = await circleBridgeF.deploy(mockToken.address); + const messageTransmitterF = new MockCircleMessageTransmitter__factory( + signer, + ); + messageTransmitter = await messageTransmitterF.deploy(mockToken.address); + + config = coreApp.extendWithConnectionClientConfig( + objMap( + getChainToOwnerMap(testChainConnectionConfigs, signer.address), + (_chain, conf) => ({ + ...conf, + bridgeAdapterConfigs: [ + { + type: BridgeAdapterType.Circle, + circleBridgeAddress: circleBridge.address, + messageTransmitterAddress: messageTransmitter.address, + usdcAddress: mockToken.address, + circleDomainMapping: [ + { + hyperlaneDomain: localDomain, + circleDomain: localDomain, + }, + { + hyperlaneDomain: remoteDomain, + circleDomain: remoteDomain, + }, + ], + } as CircleBridgeAdapterConfig, + ], + }), + ), + ); + }); + + beforeEach(async () => { + const TokenBridge = new TokenBridgeDeployer(multiProvider, config, coreApp); + const contracts = await TokenBridge.deploy(); + + tokenBridgeApp = new TokenBridgeApp(contracts, multiProvider); + + local = tokenBridgeApp.getContracts(localChain).router; + }); + + it('can transfer tokens', async () => { + const recipientF = new TestTokenBridgeMessageRecipient__factory(signer); + const recipient = await recipientF.deploy(); + + const amount = 1000; + await mockToken.mint(signer.address, amount); + await mockToken.approve(local.address, amount); + await local.dispatchWithTokens( + remoteDomain, + utils.addressToBytes32(recipient.address), + '0x00', + mockToken.address, + amount, + BridgeAdapterType.Circle, + ); + + const transferNonce = await circleBridge.nextNonce(); + const nonceId = await messageTransmitter.hashSourceAndNonce( + localDomain, + transferNonce, + ); + + await messageTransmitter.process( + nonceId, + tokenBridgeApp.getContracts(remoteChain).circleBridgeAdapter!.address, + amount, + ); + await coreApp.processMessages(); + + expect((await mockToken.balanceOf(recipient.address)).toNumber()).to.eql( + amount, + ); + }); +});