Port LiquidiyLayer PRs to v2 (#1497)

* Add PortalAdapter to LiquidityLayer (#1290)

* Add PortalAdapter to LiquidityLayer

* newlines

* Fix up circle relayer

* Dont require context when getting args

* Fix up context argument parsing

* Liquidity Layer Relayer Infras (#1292)

* PR review

* PR review

* Use redeployed CCTP contracts (#1482)

* Use redeployed CCTP contracts

* Fix build

* Rename more things to TokenMessenger and fix nonceHash calculation

* New artifacts

* Fix build

* Deploy relaers

* Gas snapshot

* PR review

* Add new line
pull/1554/head
Nam Chu Hoai 2 years ago committed by GitHub
parent 82acc39870
commit 891ca4ccdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      solidity/.gas-snapshot
  2. 1
      solidity/contracts/middleware/liquidity-layer/LiquidityLayerRouter.sol
  3. 20
      solidity/contracts/middleware/liquidity-layer/adapters/CircleBridgeAdapter.sol
  4. 216
      solidity/contracts/middleware/liquidity-layer/adapters/PortalAdapter.sol
  5. 6
      solidity/contracts/middleware/liquidity-layer/interfaces/circle/ITokenMessenger.sol
  6. 87
      solidity/contracts/middleware/liquidity-layer/interfaces/portal/IPortalTokenBridge.sol
  7. 2
      solidity/contracts/mock/MockCircleMessageTransmitter.sol
  8. 4
      solidity/contracts/mock/MockCircleTokenMessenger.sol
  9. 89
      solidity/contracts/mock/MockPortalBridge.sol
  10. 12
      solidity/test/LiquidityLayerRouter.t.sol
  11. 133
      solidity/test/middleware/liquidity-layer/PortalAdapter.t.sol
  12. 4
      typescript/infra/config/environments/mainnet2/token-bridge.ts
  13. 58
      typescript/infra/config/environments/test/liquidityLayer.ts
  14. 24
      typescript/infra/config/environments/test/middleware/liquidity-layer/addresses.json
  15. 94
      typescript/infra/config/environments/test/middleware/liquidity-layer/verification.json
  16. 2
      typescript/infra/config/environments/testnet3/index.ts
  17. 15
      typescript/infra/config/environments/testnet3/middleware.ts
  18. 58
      typescript/infra/config/environments/testnet3/token-bridge.ts
  19. 24
      typescript/infra/helm/liquidity-layer-relayers/Chart.yaml
  20. 42
      typescript/infra/helm/liquidity-layer-relayers/templates/_helpers.tpl
  21. 30
      typescript/infra/helm/liquidity-layer-relayers/templates/circle-relayer-deployment.yaml
  22. 55
      typescript/infra/helm/liquidity-layer-relayers/templates/env-var-external-secret.yaml
  23. 30
      typescript/infra/helm/liquidity-layer-relayers/templates/portal-relayer-deployment.yaml
  24. 9
      typescript/infra/helm/liquidity-layer-relayers/values.yaml
  25. 4
      typescript/infra/scripts/helloworld/kathy.ts
  26. 20
      typescript/infra/scripts/middleware/circle-relayer.ts
  27. 13
      typescript/infra/scripts/middleware/deploy-liquidity-layer.ts
  28. 34
      typescript/infra/scripts/middleware/deploy-relayers.ts
  29. 58
      typescript/infra/scripts/middleware/portal-relayer.ts
  30. 17
      typescript/infra/scripts/utils.ts
  31. 2
      typescript/infra/src/config/environment.ts
  32. 8
      typescript/infra/src/config/middleware.ts
  33. 73
      typescript/infra/src/middleware/liquidity-layer-relayer.ts
  34. 161
      typescript/sdk/src/deploy/middleware/LiquidityLayerApp.ts
  35. 141
      typescript/sdk/src/deploy/middleware/LiquidityLayerRouterDeployer.ts
  36. 1
      typescript/sdk/src/index.ts
  37. 5
      typescript/sdk/src/middleware.ts
  38. 80
      typescript/sdk/src/middleware/liquidity-layer.hardhat-test.ts
  39. 4
      typescript/sdk/src/providers/MultiProvider.ts
  40. 9
      typescript/sdk/src/utils/objects.ts
  41. 4
      typescript/utils/src/utils.ts

@ -1,15 +1,18 @@
InterchainAccountRouterTest:testOwner() (gas: 210381) InterchainAccountRouterTest:testOwner() (gas: 210381)
InterchainAccountRouterTest:testSetOwner(address) (runs: 256, μ: 990801, ~: 990801) InterchainAccountRouterTest:testSetOwner(address) (runs: 256, μ: 990723, ~: 990801)
InterchainQueryRouterTest:testQueryAddress(address) (runs: 256, μ: 1345905, ~: 1345905) InterchainQueryRouterTest:testQueryAddress(address) (runs: 256, μ: 1345905, ~: 1345905)
InterchainQueryRouterTest:testQueryUint256(uint256) (runs: 256, μ: 1686587, ~: 1686587) InterchainQueryRouterTest:testQueryUint256(uint256) (runs: 256, μ: 1686587, ~: 1686587)
LiquidityLayerRouterTest:testDispatchWithTokenTransfersMovesTokens() (gas: 552484) LiquidityLayerRouterTest:testDispatchWithTokenTransfersMovesTokens() (gas: 552484)
LiquidityLayerRouterTest:testDispatchWithTokensCallsAdapter() (gas: 558572) LiquidityLayerRouterTest:testDispatchWithTokensCallsAdapter() (gas: 558572)
LiquidityLayerRouterTest:testDispatchWithTokensRevertsWithFailedTransferIn() (gas: 28626) LiquidityLayerRouterTest:testDispatchWithTokensRevertsWithFailedTransferIn() (gas: 28626)
LiquidityLayerRouterTest:testDispatchWithTokensRevertsWithUnkownBridgeAdapter() (gas: 20611) LiquidityLayerRouterTest:testDispatchWithTokensRevertsWithUnkownBridgeAdapter() (gas: 20611)
LiquidityLayerRouterTest:testDispatchWithTokensTransfersOnDestination() (gas: 788665) LiquidityLayerRouterTest:testDispatchWithTokensTransfersOnDestination() (gas: 788693)
LiquidityLayerRouterTest:testProcessingRevertsIfBridgeAdapterReverts() (gas: 603576) LiquidityLayerRouterTest:testProcessingRevertsIfBridgeAdapterReverts() (gas: 603550)
LiquidityLayerRouterTest:testSetLiquidityLayerAdapter() (gas: 23429) LiquidityLayerRouterTest:testSetLiquidityLayerAdapter() (gas: 23429)
MessagingTest:testSendMessage(string) (runs: 256, μ: 277677, ~: 296009) MessagingTest:testSendMessage(string) (runs: 256, μ: 276993, ~: 296009)
PausableReentrancyGuardTest:testNonreentrant() (gas: 14428) PausableReentrancyGuardTest:testNonreentrant() (gas: 14428)
PausableReentrancyGuardTest:testNonreentrantNotPaused() (gas: 14163) PausableReentrancyGuardTest:testNonreentrantNotPaused() (gas: 14163)
PausableReentrancyGuardTest:testPause() (gas: 13635) PausableReentrancyGuardTest:testPause() (gas: 13635)
PortalAdapterTest:testAdapter(uint256) (runs: 256, μ: 135467, ~: 135583)
PortalAdapterTest:testReceivingRevertsWithoutTransferCompletion(uint256) (runs: 256, μ: 140406, ~: 140522)
PortalAdapterTest:testReceivingWorks(uint256) (runs: 256, μ: 229403, ~: 229520)

@ -4,7 +4,6 @@ pragma solidity ^0.8.13;
import {Router} from "../../Router.sol"; import {Router} from "../../Router.sol";
import {ILiquidityLayerRouter} from "../../../interfaces/ILiquidityLayerRouter.sol"; import {ILiquidityLayerRouter} from "../../../interfaces/ILiquidityLayerRouter.sol";
import {ICircleBridge} from "./interfaces/circle/ICircleBridge.sol";
import {ICircleMessageTransmitter} from "./interfaces/circle/ICircleMessageTransmitter.sol"; import {ICircleMessageTransmitter} from "./interfaces/circle/ICircleMessageTransmitter.sol";
import {ILiquidityLayerAdapter} from "./interfaces/ILiquidityLayerAdapter.sol"; import {ILiquidityLayerAdapter} from "./interfaces/ILiquidityLayerAdapter.sol";
import {ILiquidityLayerMessageRecipient} from "../../../interfaces/ILiquidityLayerMessageRecipient.sol"; import {ILiquidityLayerMessageRecipient} from "../../../interfaces/ILiquidityLayerMessageRecipient.sol";

@ -3,15 +3,15 @@ pragma solidity ^0.8.13;
import {Router} from "../../../Router.sol"; import {Router} from "../../../Router.sol";
import {ICircleBridge} from "../interfaces/circle/ICircleBridge.sol"; import {ITokenMessenger} from "../interfaces/circle/ITokenMessenger.sol";
import {ICircleMessageTransmitter} from "../interfaces/circle/ICircleMessageTransmitter.sol"; import {ICircleMessageTransmitter} from "../interfaces/circle/ICircleMessageTransmitter.sol";
import {ILiquidityLayerAdapter} from "../interfaces/ILiquidityLayerAdapter.sol"; import {ILiquidityLayerAdapter} from "../interfaces/ILiquidityLayerAdapter.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract CircleBridgeAdapter is ILiquidityLayerAdapter, Router { contract CircleBridgeAdapter is ILiquidityLayerAdapter, Router {
/// @notice The CircleBridge contract. /// @notice The TokenMessenger contract.
ICircleBridge public circleBridge; ITokenMessenger public tokenMessenger;
/// @notice The Circle MessageTransmitter contract. /// @notice The Circle MessageTransmitter contract.
ICircleMessageTransmitter public circleMessageTransmitter; ICircleMessageTransmitter public circleMessageTransmitter;
@ -61,20 +61,20 @@ contract CircleBridgeAdapter is ILiquidityLayerAdapter, Router {
/** /**
* @param _owner The new owner. * @param _owner The new owner.
* @param _circleBridge The CircleBridge contract. * @param _tokenMessenger The TokenMessenger contract.
* @param _circleMessageTransmitter The Circle MessageTransmitter contract. * @param _circleMessageTransmitter The Circle MessageTransmitter contract.
* @param _liquidityLayerRouter The LiquidityLayerRouter contract. * @param _liquidityLayerRouter The LiquidityLayerRouter contract.
*/ */
function initialize( function initialize(
address _owner, address _owner,
address _circleBridge, address _tokenMessenger,
address _circleMessageTransmitter, address _circleMessageTransmitter,
address _liquidityLayerRouter address _liquidityLayerRouter
) public initializer { ) public initializer {
// Transfer ownership of the contract to deployer // Transfer ownership of the contract to deployer
_transferOwnership(_owner); _transferOwnership(_owner);
circleBridge = ICircleBridge(_circleBridge); tokenMessenger = ITokenMessenger(_tokenMessenger);
circleMessageTransmitter = ICircleMessageTransmitter( circleMessageTransmitter = ICircleMessageTransmitter(
_circleMessageTransmitter _circleMessageTransmitter
); );
@ -105,11 +105,11 @@ contract CircleBridgeAdapter is ILiquidityLayerAdapter, Router {
// Approve the token to Circle. We assume that the LiquidityLayerRouter // Approve the token to Circle. We assume that the LiquidityLayerRouter
// has already transferred the token to this contract. // has already transferred the token to this contract.
require( require(
IERC20(_token).approve(address(circleBridge), _amount), IERC20(_token).approve(address(tokenMessenger), _amount),
"!approval" "!approval"
); );
uint64 _nonce = circleBridge.depositForBurn( uint64 _nonce = tokenMessenger.depositForBurn(
_amount, _amount,
_circleDomain, _circleDomain,
_remoteRouter, // Mint to the remote router _remoteRouter, // Mint to the remote router
@ -236,8 +236,6 @@ contract CircleBridgeAdapter is ILiquidityLayerAdapter, Router {
pure pure
returns (bytes32) returns (bytes32)
{ {
// The hash is of a uint256 nonce, not a uint64 one. return keccak256(abi.encodePacked(_originCircleDomain, _nonce));
return
keccak256(abi.encodePacked(_originCircleDomain, uint256(_nonce)));
} }
} }

@ -0,0 +1,216 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
import {Router} from "../../../Router.sol";
import {IPortalTokenBridge} from "../interfaces/portal/IPortalTokenBridge.sol";
import {ILiquidityLayerAdapter} from "../interfaces/ILiquidityLayerAdapter.sol";
import {TypeCasts} from "../../../libs/TypeCasts.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract PortalAdapter is ILiquidityLayerAdapter, Router {
/// @notice The Portal TokenBridge contract.
IPortalTokenBridge public portalTokenBridge;
/// @notice The LiquidityLayerRouter contract.
address public liquidityLayerRouter;
/// @notice Hyperlane domain => Wormhole domain.
mapping(uint32 => uint16) public hyperlaneDomainToWormholeDomain;
/// @notice transferId => token address
mapping(bytes32 => address) public portalTransfersProcessed;
uint32 localDomain;
// We could technically use Portal's sequence number here but it doesn't
// get passed through, so we would have to parse the VAA twice
// 224 bits should be large enough and allows us to pack into a single slot
// with a Hyperlane domain
uint224 public nonce = 0;
/**
* @notice Emits the nonce of the Portal message when a token is bridged.
* @param nonce The nonce of the Portal message.
* @param portalSequence The sequence of the Portal message.
* @param destination The hyperlane domain of the destination
*/
event BridgedToken(
uint256 nonce,
uint64 portalSequence,
uint32 destination
);
/**
* @notice Emitted when the Hyperlane domain to Wormhole domain mapping is updated.
* @param hyperlaneDomain The Hyperlane domain.
* @param wormholeDomain The Wormhole domain.
*/
event DomainAdded(uint32 indexed hyperlaneDomain, uint32 wormholeDomain);
modifier onlyLiquidityLayerRouter() {
require(msg.sender == liquidityLayerRouter, "!liquidityLayerRouter");
_;
}
/**
* @param _localDomain The local hyperlane domain
* @param _owner The new owner.
* @param _portalTokenBridge The Portal TokenBridge contract.
* @param _liquidityLayerRouter The LiquidityLayerRouter contract.
*/
function initialize(
uint32 _localDomain,
address _owner,
address _portalTokenBridge,
address _liquidityLayerRouter
) public initializer {
// Transfer ownership of the contract to deployer
_transferOwnership(_owner);
localDomain = _localDomain;
portalTokenBridge = IPortalTokenBridge(_portalTokenBridge);
liquidityLayerRouter = _liquidityLayerRouter;
}
/**
* Sends tokens as requested by the router
* @param _destinationDomain The hyperlane domain of the destination
* @param _token The token address
* @param _amount The amount of tokens to send
*/
function sendTokens(
uint32 _destinationDomain,
bytes32, // _recipientAddress, unused
address _token,
uint256 _amount
) external onlyLiquidityLayerRouter returns (bytes memory) {
nonce = nonce + 1;
uint16 _wormholeDomain = hyperlaneDomainToWormholeDomain[
_destinationDomain
];
bytes32 _remoteRouter = _mustHaveRemoteRouter(_destinationDomain);
// Approve the token to Portal. We assume that the LiquidityLayerRouter
// has already transferred the token to this contract.
require(
IERC20(_token).approve(address(portalTokenBridge), _amount),
"!approval"
);
uint64 _portalSequence = portalTokenBridge.transferTokensWithPayload(
_token,
_amount,
_wormholeDomain,
_remoteRouter,
// Nonce for grouping Portal messages in the same tx, not relevant for us
// https://book.wormhole.com/technical/evm/coreLayer.html#emitting-a-vaa
0,
// Portal Payload used in completeTransfer
abi.encode(localDomain, nonce)
);
emit BridgedToken(nonce, _portalSequence, _destinationDomain);
return abi.encode(nonce);
}
/**
* Sends the tokens to the recipient as requested by the router
* @param _originDomain The hyperlane domain of the origin
* @param _recipient The address of the recipient
* @param _amount The amount of tokens to send
* @param _adapterData The adapter data from the origin chain, containing the nonce
*/
function receiveTokens(
uint32 _originDomain, // Hyperlane domain
address _recipient,
uint256 _amount,
bytes calldata _adapterData // The adapter data from the message
) external onlyLiquidityLayerRouter returns (address, uint256) {
// Get the nonce information from the adapterData
uint224 _nonce = abi.decode(_adapterData, (uint224));
address _tokenAddress = portalTransfersProcessed[
transferId(_originDomain, _nonce)
];
require(
_tokenAddress != address(0x0),
"Portal Transfer has not yet been completed"
);
IERC20 _token = IERC20(_tokenAddress);
// Transfer the token out to the recipient
// TODO: use safeTransfer
// Portal doesn't charge any fee, so we can safely transfer out the
// exact amount that was bridged over.
require(_token.transfer(_recipient, _amount), "!transfer out");
return (_tokenAddress, _amount);
}
/**
* Completes the Portal transfer which sends the funds to this adapter.
* The router can call receiveTokens to move those funds to the ultimate recipient.
* @param encodedVm The VAA from the Wormhole Guardians
*/
function completeTransfer(bytes memory encodedVm) public {
bytes memory _tokenBridgeTransferWithPayload = portalTokenBridge
.completeTransferWithPayload(encodedVm);
IPortalTokenBridge.TransferWithPayload
memory _transfer = portalTokenBridge.parseTransferWithPayload(
_tokenBridgeTransferWithPayload
);
(uint32 _originDomain, uint224 _nonce) = abi.decode(
_transfer.payload,
(uint32, uint224)
);
// Logic taken from here https://github.com/wormhole-foundation/wormhole/blob/dev.v2/ethereum/contracts/bridge/Bridge.sol#L503
address tokenAddress = _transfer.tokenChain ==
hyperlaneDomainToWormholeDomain[localDomain]
? TypeCasts.bytes32ToAddress(_transfer.tokenAddress)
: portalTokenBridge.wrappedAsset(
_transfer.tokenChain,
_transfer.tokenAddress
);
portalTransfersProcessed[
transferId(_originDomain, _nonce)
] = tokenAddress;
}
// This contract is only a Router to be aware of remote router addresses,
// and doesn't actually send/handle Hyperlane messages directly
function _handle(
uint32, // origin
bytes32, // sender
bytes calldata // message
) internal pure override {
revert("No messages expected");
}
function addDomain(uint32 _hyperlaneDomain, uint16 _wormholeDomain)
external
onlyOwner
{
hyperlaneDomainToWormholeDomain[_hyperlaneDomain] = _wormholeDomain;
emit DomainAdded(_hyperlaneDomain, _wormholeDomain);
}
/**
* The key that is used to track fulfilled Portal transfers
* @param _hyperlaneDomain The hyperlane of the origin
* @param _nonce The nonce of the adapter on the origin
*/
function transferId(uint32 _hyperlaneDomain, uint224 _nonce)
public
pure
returns (bytes32)
{
return bytes32(abi.encodePacked(_hyperlaneDomain, _nonce));
}
}

@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13; pragma solidity ^0.8.13;
interface ICircleBridge { interface ITokenMessenger {
event MessageSent(bytes message); event MessageSent(bytes message);
/** /**
@ -9,7 +9,7 @@ interface ICircleBridge {
* Emits a `DepositForBurn` event. * Emits a `DepositForBurn` event.
* @dev reverts if: * @dev reverts if:
* - given burnToken is not supported * - given burnToken is not supported
* - given destinationDomain has no CircleBridge registered * - given destinationDomain has no TokenMessenger registered
* - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance * - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance
* to this contract is less than `amount`. * to this contract is less than `amount`.
* - burn() reverts. For example, if `amount` is 0. * - burn() reverts. For example, if `amount` is 0.
@ -37,7 +37,7 @@ interface ICircleBridge {
* @dev reverts if: * @dev reverts if:
* - given destinationCaller is zero address * - given destinationCaller is zero address
* - given burnToken is not supported * - given burnToken is not supported
* - given destinationDomain has no CircleBridge registered * - given destinationDomain has no TokenMessenger registered
* - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance * - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance
* to this contract is less than `amount`. * to this contract is less than `amount`.
* - burn() reverts. For example, if `amount` is 0. * - burn() reverts. For example, if `amount` is 0.

@ -0,0 +1,87 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
// Portal's interface from their docs
interface IPortalTokenBridge {
struct Transfer {
uint8 payloadID;
uint256 amount;
bytes32 tokenAddress;
uint16 tokenChain;
bytes32 to;
uint16 toChain;
uint256 fee;
}
struct TransferWithPayload {
uint8 payloadID;
uint256 amount;
bytes32 tokenAddress;
uint16 tokenChain;
bytes32 to;
uint16 toChain;
bytes32 fromAddress;
bytes payload;
}
struct AssetMeta {
uint8 payloadID;
bytes32 tokenAddress;
uint16 tokenChain;
uint8 decimals;
bytes32 symbol;
bytes32 name;
}
struct RegisterChain {
bytes32 module;
uint8 action;
uint16 chainId;
uint16 emitterChainID;
bytes32 emitterAddress;
}
struct UpgradeContract {
bytes32 module;
uint8 action;
uint16 chainId;
bytes32 newContract;
}
struct RecoverChainId {
bytes32 module;
uint8 action;
uint256 evmChainId;
uint16 newChainId;
}
event ContractUpgraded(
address indexed oldContract,
address indexed newContract
);
function transferTokensWithPayload(
address token,
uint256 amount,
uint16 recipientChain,
bytes32 recipient,
uint32 nonce,
bytes memory payload
) external payable returns (uint64 sequence);
function completeTransferWithPayload(bytes memory encodedVm)
external
returns (bytes memory);
function parseTransferWithPayload(bytes memory encoded)
external
pure
returns (TransferWithPayload memory transfer);
function wrappedAsset(uint16 tokenChainId, bytes32 tokenAddress)
external
view
returns (address);
function isWrappedAsset(address token) external view returns (bool);
}

@ -20,7 +20,7 @@ contract MockCircleMessageTransmitter is ICircleMessageTransmitter {
success = true; success = true;
} }
function hashSourceAndNonce(uint32 _source, uint256 _nonce) function hashSourceAndNonce(uint32 _source, uint64 _nonce)
public public
pure pure
returns (bytes32) returns (bytes32)

@ -1,10 +1,10 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13; pragma solidity ^0.8.13;
import {ICircleBridge} from "../middleware/liquidity-layer/interfaces/circle/ICircleBridge.sol"; import {ITokenMessenger} from "../middleware/liquidity-layer/interfaces/circle/ITokenMessenger.sol";
import {MockToken} from "./MockToken.sol"; import {MockToken} from "./MockToken.sol";
contract MockCircleBridge is ICircleBridge { contract MockCircleTokenMessenger is ITokenMessenger {
uint64 public nextNonce = 0; uint64 public nextNonce = 0;
MockToken token; MockToken token;

@ -0,0 +1,89 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
import {IPortalTokenBridge} from "../middleware/liquidity-layer/interfaces/portal/IPortalTokenBridge.sol";
import {MockToken} from "./MockToken.sol";
import {TypeCasts} from "../libs/TypeCasts.sol";
contract MockPortalBridge is IPortalTokenBridge {
uint256 nextNonce = 0;
MockToken token;
constructor(MockToken _token) {
token = _token;
}
function transferTokensWithPayload(
address,
uint256 amount,
uint16,
bytes32,
uint32,
bytes memory
) external payable returns (uint64 sequence) {
nextNonce = nextNonce + 1;
token.transferFrom(msg.sender, address(this), amount);
token.burn(amount);
return uint64(nextNonce);
}
function wrappedAsset(uint16, bytes32) external view returns (address) {
return address(token);
}
function isWrappedAsset(address) external pure returns (bool) {
return true;
}
function completeTransferWithPayload(bytes memory encodedVm)
external
returns (bytes memory)
{
(uint32 _originDomain, uint224 _nonce, uint256 _amount) = abi.decode(
encodedVm,
(uint32, uint224, uint256)
);
token.mint(msg.sender, _amount);
// Format it so that parseTransferWithPayload returns the desired payload
return
abi.encode(
TypeCasts.addressToBytes32(address(token)),
adapterData(_originDomain, _nonce, address(token))
);
}
function parseTransferWithPayload(bytes memory encoded)
external
pure
returns (TransferWithPayload memory transfer)
{
(bytes32 tokenAddress, bytes memory payload) = abi.decode(
encoded,
(bytes32, bytes)
);
transfer.payload = payload;
transfer.tokenAddress = tokenAddress;
}
function adapterData(
uint32 _originDomain,
uint224 _nonce,
address _token
) public pure returns (bytes memory) {
return
abi.encode(
_originDomain,
_nonce,
TypeCasts.addressToBytes32(_token)
);
}
function mockPortalVaa(
uint32 _originDomain,
uint224 _nonce,
uint256 _amount
) public pure returns (bytes memory) {
return abi.encode(_originDomain, _nonce, _amount);
}
}

@ -7,7 +7,7 @@ import {CircleBridgeAdapter} from "../contracts/middleware/liquidity-layer/adapt
import {MockToken} from "../contracts/mock/MockToken.sol"; import {MockToken} from "../contracts/mock/MockToken.sol";
import {TestTokenRecipient} from "../contracts/test/TestTokenRecipient.sol"; import {TestTokenRecipient} from "../contracts/test/TestTokenRecipient.sol";
import {MockCircleMessageTransmitter} from "../contracts/mock/MockCircleMessageTransmitter.sol"; import {MockCircleMessageTransmitter} from "../contracts/mock/MockCircleMessageTransmitter.sol";
import {MockCircleBridge} from "../contracts/mock/MockCircleBridge.sol"; import {MockCircleTokenMessenger} from "../contracts/mock/MockCircleTokenMessenger.sol";
import {MockHyperlaneEnvironment} from "../contracts/mock/MockHyperlaneEnvironment.sol"; import {MockHyperlaneEnvironment} from "../contracts/mock/MockHyperlaneEnvironment.sol";
import {TypeCasts} from "../contracts/libs/TypeCasts.sol"; import {TypeCasts} from "../contracts/libs/TypeCasts.sol";
@ -19,7 +19,7 @@ contract LiquidityLayerRouterTest is Test {
LiquidityLayerRouter destinationLiquidityLayerRouter; LiquidityLayerRouter destinationLiquidityLayerRouter;
MockCircleMessageTransmitter messageTransmitter; MockCircleMessageTransmitter messageTransmitter;
MockCircleBridge circleBridge; MockCircleTokenMessenger tokenMessenger;
CircleBridgeAdapter originBridgeAdapter; CircleBridgeAdapter originBridgeAdapter;
CircleBridgeAdapter destinationBridgeAdapter; CircleBridgeAdapter destinationBridgeAdapter;
@ -38,7 +38,7 @@ contract LiquidityLayerRouterTest is Test {
function setUp() public { function setUp() public {
token = new MockToken(); token = new MockToken();
circleBridge = new MockCircleBridge(token); tokenMessenger = new MockCircleTokenMessenger(token);
messageTransmitter = new MockCircleMessageTransmitter(token); messageTransmitter = new MockCircleMessageTransmitter(token);
originBridgeAdapter = new CircleBridgeAdapter(); originBridgeAdapter = new CircleBridgeAdapter();
destinationBridgeAdapter = new CircleBridgeAdapter(); destinationBridgeAdapter = new CircleBridgeAdapter();
@ -78,14 +78,14 @@ contract LiquidityLayerRouterTest is Test {
originBridgeAdapter.initialize( originBridgeAdapter.initialize(
owner, owner,
address(circleBridge), address(tokenMessenger),
address(messageTransmitter), address(messageTransmitter),
address(originLiquidityLayerRouter) address(originLiquidityLayerRouter)
); );
destinationBridgeAdapter.initialize( destinationBridgeAdapter.initialize(
owner, owner,
address(circleBridge), address(tokenMessenger),
address(messageTransmitter), address(messageTransmitter),
address(destinationLiquidityLayerRouter) address(destinationLiquidityLayerRouter)
); );
@ -224,7 +224,7 @@ contract LiquidityLayerRouterTest is Test {
destinationBridgeAdapter.hyperlaneDomainToCircleDomain( destinationBridgeAdapter.hyperlaneDomainToCircleDomain(
originDomain originDomain
), ),
circleBridge.nextNonce() tokenMessenger.nextNonce()
); );
messageTransmitter.process( messageTransmitter.process(

@ -0,0 +1,133 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import {TypeCasts} from "../../../contracts/libs/TypeCasts.sol";
import {IPortalTokenBridge} from "../../../contracts/middleware/liquidity-layer/interfaces/portal/IPortalTokenBridge.sol";
import {PortalAdapter} from "../../../contracts/middleware/liquidity-layer/adapters/PortalAdapter.sol";
import {TestTokenRecipient} from "../../../contracts/test/TestTokenRecipient.sol";
import {MockToken} from "../../../contracts/mock/MockToken.sol";
import {MockPortalBridge} from "../../../contracts/mock/MockPortalBridge.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract PortalAdapterTest is Test {
PortalAdapter originAdapter;
PortalAdapter destinationAdapter;
MockPortalBridge portalBridge;
uint32 originDomain = 123;
uint32 destinationDomain = 321;
TestTokenRecipient recipient;
MockToken token;
function setUp() public {
token = new MockToken();
recipient = new TestTokenRecipient();
originAdapter = new PortalAdapter();
destinationAdapter = new PortalAdapter();
portalBridge = new MockPortalBridge(token);
originAdapter.initialize(
originDomain,
address(this),
address(portalBridge),
address(this)
);
destinationAdapter.initialize(
destinationDomain,
address(this),
address(portalBridge),
address(this)
);
originAdapter.enrollRemoteRouter(
destinationDomain,
TypeCasts.addressToBytes32(address(destinationAdapter))
);
destinationAdapter.enrollRemoteRouter(
destinationDomain,
TypeCasts.addressToBytes32(address(originAdapter))
);
}
function testAdapter(uint256 amount) public {
// Transfers of 0 are invalid
vm.assume(amount > 0);
// Calls MockPortalBridge with the right paramters
vm.expectCall(
address(portalBridge),
abi.encodeCall(
portalBridge.transferTokensWithPayload,
(
address(token),
amount,
0,
TypeCasts.addressToBytes32(address(destinationAdapter)),
0,
abi.encode(originDomain, originAdapter.nonce() + 1)
)
)
);
token.mint(address(originAdapter), amount);
originAdapter.sendTokens(
destinationDomain,
TypeCasts.addressToBytes32(address(recipient)),
address(token),
amount
);
}
function testReceivingRevertsWithoutTransferCompletion(uint256 amount)
public
{
// Transfers of 0 are invalid
vm.assume(amount > 0);
token.mint(address(originAdapter), amount);
bytes memory adapterData = originAdapter.sendTokens(
destinationDomain,
TypeCasts.addressToBytes32(address(recipient)),
address(token),
amount
);
vm.expectRevert("Portal Transfer has not yet been completed");
destinationAdapter.receiveTokens(
originDomain,
address(recipient),
amount,
adapterData
);
}
function testReceivingWorks(uint256 amount) public {
// Transfers of 0 are invalid
vm.assume(amount > 0);
token.mint(address(originAdapter), amount);
bytes memory adapterData = originAdapter.sendTokens(
destinationDomain,
TypeCasts.addressToBytes32(address(recipient)),
address(token),
amount
);
destinationAdapter.completeTransfer(
portalBridge.mockPortalVaa(
originDomain,
originAdapter.nonce(),
amount
)
);
destinationAdapter.receiveTokens(
originDomain,
address(recipient),
amount,
adapterData
);
}
}

@ -18,14 +18,14 @@ export const circleBridgeAdapterConfig: ChainMap<
> = { > = {
[Chains.goerli]: { [Chains.goerli]: {
type: BridgeAdapterType.Circle, type: BridgeAdapterType.Circle,
circleBridgeAddress: '0xdabec94b97f7b5fca28f050cc8eeac2dc9920476', tokenMessengerAddress: '0xdabec94b97f7b5fca28f050cc8eeac2dc9920476',
messageTransmitterAddress: '0x40a61d3d2afcf5a5d31fcdf269e575fb99dd87f7', messageTransmitterAddress: '0x40a61d3d2afcf5a5d31fcdf269e575fb99dd87f7',
usdcAddress: '0x07865c6e87b9f70255377e024ace6630c1eaa37f', usdcAddress: '0x07865c6e87b9f70255377e024ace6630c1eaa37f',
circleDomainMapping, circleDomainMapping,
}, },
[Chains.fuji]: { [Chains.fuji]: {
type: BridgeAdapterType.Circle, type: BridgeAdapterType.Circle,
circleBridgeAddress: '0x0fc1103927af27af808d03135214718bcedbe9ad', tokenMessengerAddress: '0x0fc1103927af27af808d03135214718bcedbe9ad',
messageTransmitterAddress: '0x52fffb3ee8fa7838e9858a2d5e454007b9027c3c', messageTransmitterAddress: '0x52fffb3ee8fa7838e9858a2d5e454007b9027c3c',
usdcAddress: '0x5425890298aed601595a70ab815c96711a31bc65', usdcAddress: '0x5425890298aed601595a70ab815c96711a31bc65',
circleDomainMapping, circleDomainMapping,

@ -1,8 +1,8 @@
import { import {
BridgeAdapterConfig,
BridgeAdapterType, BridgeAdapterType,
ChainMap, ChainMap,
Chains, Chains,
CircleBridgeAdapterConfig,
chainMetadata, chainMetadata,
} from '@hyperlane-xyz/sdk'; } from '@hyperlane-xyz/sdk';
@ -11,22 +11,62 @@ const circleDomainMapping = [
{ hyperlaneDomain: chainMetadata[Chains.fuji].id, circleDomain: 1 }, { hyperlaneDomain: chainMetadata[Chains.fuji].id, circleDomain: 1 },
]; ];
export const circleBridgeAdapterConfig: ChainMap< const wormholeDomainMapping = [
any, { hyperlaneDomain: chainMetadata[Chains.goerli].id, wormholeDomain: 2 },
CircleBridgeAdapterConfig { hyperlaneDomain: chainMetadata[Chains.fuji].id, wormholeDomain: 6 },
> = { { hyperlaneDomain: chainMetadata[Chains.mumbai].id, wormholeDomain: 5 },
{ hyperlaneDomain: chainMetadata[Chains.bsctestnet].id, wormholeDomain: 4 },
{ hyperlaneDomain: chainMetadata[Chains.alfajores].id, wormholeDomain: 14 },
];
export const bridgeAdapterConfigs: ChainMap<any, BridgeAdapterConfig> = {
[Chains.goerli]: { [Chains.goerli]: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0xF890982f9310df57d00f659cf4fd87e65adEd8d7',
wormholeDomainMapping,
},
circle: {
type: BridgeAdapterType.Circle, type: BridgeAdapterType.Circle,
circleBridgeAddress: '0xdabec94b97f7b5fca28f050cc8eeac2dc9920476', tokenMessengerAddress: '0xd0c3da58f55358142b8d3e06c1c30c5c6114efe8',
messageTransmitterAddress: '0x40a61d3d2afcf5a5d31fcdf269e575fb99dd87f7', messageTransmitterAddress: '0x26413e8157cd32011e726065a5462e97dd4d03d9',
usdcAddress: '0x07865c6e87b9f70255377e024ace6630c1eaa37f', usdcAddress: '0x07865c6e87b9f70255377e024ace6630c1eaa37f',
circleDomainMapping, circleDomainMapping,
}, },
},
[Chains.fuji]: { [Chains.fuji]: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0x61E44E506Ca5659E6c0bba9b678586fA2d729756',
wormholeDomainMapping,
},
circle: {
type: BridgeAdapterType.Circle, type: BridgeAdapterType.Circle,
circleBridgeAddress: '0x0fc1103927af27af808d03135214718bcedbe9ad', tokenMessengerAddress: '0xeb08f243e5d3fcff26a9e38ae5520a669f4019d0',
messageTransmitterAddress: '0x52fffb3ee8fa7838e9858a2d5e454007b9027c3c', messageTransmitterAddress: '0xa9fb1b3009dcb79e2fe346c16a604b8fa8ae0a79',
usdcAddress: '0x5425890298aed601595a70ab815c96711a31bc65', usdcAddress: '0x5425890298aed601595a70ab815c96711a31bc65',
circleDomainMapping, circleDomainMapping,
}, },
},
[Chains.mumbai]: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0x377D55a7928c046E18eEbb61977e714d2a76472a',
wormholeDomainMapping,
},
},
[Chains.bsctestnet]: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0x9dcF9D205C9De35334D646BeE44b2D2859712A09',
wormholeDomainMapping,
},
},
[Chains.alfajores]: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0x05ca6037eC51F8b712eD2E6Fa72219FEaE74E153',
wormholeDomainMapping,
},
},
}; };

@ -1,10 +1,24 @@
{ {
"fuji": {
"circleBridgeAdapter": "0x17EB33454AAEF8E91510540a0ebF4a8213dd740D",
"portalAdapter": "0xe589a05be1304b43A6FEb9c5D6a6EEEA35656271",
"router": "0x3428e12EfDb2446c1E7feC3f1CED099A8a7cD541"
},
"goerli": { "goerli": {
"circleBridgeAdapter": "0xc262a656c99B3a2f1B196dc5BeDa8f4f80D4a878", "circleBridgeAdapter": "0x17EB33454AAEF8E91510540a0ebF4a8213dd740D",
"router": "0x952228cA63f85130534981844050c82b89f373E7" "portalAdapter": "0xe589a05be1304b43A6FEb9c5D6a6EEEA35656271",
"router": "0x3428e12EfDb2446c1E7feC3f1CED099A8a7cD541"
}, },
"fuji": { "mumbai": {
"circleBridgeAdapter": "0xc262a656c99B3a2f1B196dc5BeDa8f4f80D4a878", "portalAdapter": "0xe589a05be1304b43A6FEb9c5D6a6EEEA35656271",
"router": "0x952228cA63f85130534981844050c82b89f373E7" "router": "0x3428e12EfDb2446c1E7feC3f1CED099A8a7cD541"
},
"bsctestnet": {
"portalAdapter": "0xe589a05be1304b43A6FEb9c5D6a6EEEA35656271",
"router": "0x3428e12EfDb2446c1E7feC3f1CED099A8a7cD541"
},
"alfajores": {
"portalAdapter": "0xe589a05be1304b43A6FEb9c5D6a6EEEA35656271",
"router": "0x3428e12EfDb2446c1E7feC3f1CED099A8a7cD541"
} }
} }

@ -1,28 +1,106 @@
{ {
"goerli": [ "fuji": [
{
"name": "LiquidityLayerRouter",
"address": "0x3428e12EfDb2446c1E7feC3f1CED099A8a7cD541",
"isProxy": false,
"constructorArguments": ""
},
{
"name": "CircleBridgeAdapter",
"address": "0xb54AD7AE42B7c505100594365CdBC4b28Ef51FE6",
"isProxy": false,
"constructorArguments": ""
},
{
"name": "PortalAdapter",
"address": "0xe589a05be1304b43A6FEb9c5D6a6EEEA35656271",
"isProxy": false,
"constructorArguments": ""
},
{ {
"name": "TokenBridgeRouter", "name": "CircleBridgeAdapter",
"address": "0x952228cA63f85130534981844050c82b89f373E7", "address": "0x54FCA26E5FF828847D8caF471e44cD5727C73B0d",
"isProxy": false, "isProxy": false,
"constructorArguments": "" "constructorArguments": ""
}, },
{ {
"name": "CircleBridgeAdapter", "name": "CircleBridgeAdapter",
"address": "0xc262a656c99B3a2f1B196dc5BeDa8f4f80D4a878", "address": "0x17EB33454AAEF8E91510540a0ebF4a8213dd740D",
"isProxy": false, "isProxy": false,
"constructorArguments": "" "constructorArguments": ""
} }
], ],
"fuji": [ "goerli": [
{ {
"name": "TokenBridgeRouter", "name": "LiquidityLayerRouter",
"address": "0x952228cA63f85130534981844050c82b89f373E7", "address": "0x3428e12EfDb2446c1E7feC3f1CED099A8a7cD541",
"isProxy": false, "isProxy": false,
"constructorArguments": "" "constructorArguments": ""
}, },
{ {
"name": "CircleBridgeAdapter", "name": "CircleBridgeAdapter",
"address": "0xc262a656c99B3a2f1B196dc5BeDa8f4f80D4a878", "address": "0xb54AD7AE42B7c505100594365CdBC4b28Ef51FE6",
"isProxy": false,
"constructorArguments": ""
},
{
"name": "PortalAdapter",
"address": "0xe589a05be1304b43A6FEb9c5D6a6EEEA35656271",
"isProxy": false,
"constructorArguments": ""
},
{
"name": "CircleBridgeAdapter",
"address": "0x54FCA26E5FF828847D8caF471e44cD5727C73B0d",
"isProxy": false,
"constructorArguments": ""
},
{
"name": "CircleBridgeAdapter",
"address": "0x17EB33454AAEF8E91510540a0ebF4a8213dd740D",
"isProxy": false,
"constructorArguments": ""
}
],
"mumbai": [
{
"name": "LiquidityLayerRouter",
"address": "0x3428e12EfDb2446c1E7feC3f1CED099A8a7cD541",
"isProxy": false,
"constructorArguments": ""
},
{
"name": "PortalAdapter",
"address": "0xe589a05be1304b43A6FEb9c5D6a6EEEA35656271",
"isProxy": false,
"constructorArguments": ""
}
],
"bsctestnet": [
{
"name": "LiquidityLayerRouter",
"address": "0x3428e12EfDb2446c1E7feC3f1CED099A8a7cD541",
"isProxy": false,
"constructorArguments": ""
},
{
"name": "PortalAdapter",
"address": "0xe589a05be1304b43A6FEb9c5D6a6EEEA35656271",
"isProxy": false,
"constructorArguments": ""
}
],
"alfajores": [
{
"name": "LiquidityLayerRouter",
"address": "0x3428e12EfDb2446c1E7feC3f1CED099A8a7cD541",
"isProxy": false,
"constructorArguments": ""
},
{
"name": "PortalAdapter",
"address": "0xe589a05be1304b43A6FEb9c5D6a6EEEA35656271",
"isProxy": false, "isProxy": false,
"constructorArguments": "" "constructorArguments": ""
} }

@ -14,6 +14,7 @@ import { core } from './core';
import { keyFunderConfig } from './funding'; import { keyFunderConfig } from './funding';
import { helloWorld } from './helloworld'; import { helloWorld } from './helloworld';
import { infrastructure } from './infrastructure'; import { infrastructure } from './infrastructure';
import { liquidityLayerRelayerConfig } from './middleware';
export const environment: CoreEnvironmentConfig<TestnetChains> = { export const environment: CoreEnvironmentConfig<TestnetChains> = {
environment: environmentName, environment: environmentName,
@ -36,4 +37,5 @@ export const environment: CoreEnvironmentConfig<TestnetChains> = {
infra: infrastructure, infra: infrastructure,
helloWorld, helloWorld,
keyFunderConfig, keyFunderConfig,
liquidityLayerRelayerConfig,
}; };

@ -0,0 +1,15 @@
import { ConnectionType } from '../../../src/config/agent';
import { LiquidityLayerRelayerConfig } from '../../../src/config/middleware';
import { environment } from './chains';
export const liquidityLayerRelayerConfig: LiquidityLayerRelayerConfig = {
docker: {
repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo',
tag: 'sha-760a16b',
},
namespace: environment,
prometheusPushGateway:
'http://prometheus-pushgateway.monitoring.svc.cluster.local:9091',
connectionType: ConnectionType.Http,
};

@ -1,8 +1,8 @@
import { import {
BridgeAdapterConfig,
BridgeAdapterType, BridgeAdapterType,
ChainMap, ChainMap,
Chains, Chains,
CircleBridgeAdapterConfig,
chainMetadata, chainMetadata,
} from '@hyperlane-xyz/sdk'; } from '@hyperlane-xyz/sdk';
@ -11,22 +11,62 @@ const circleDomainMapping = [
{ hyperlaneDomain: chainMetadata[Chains.fuji].id, circleDomain: 1 }, { hyperlaneDomain: chainMetadata[Chains.fuji].id, circleDomain: 1 },
]; ];
export const circleBridgeAdapterConfig: ChainMap< const wormholeDomainMapping = [
any, { hyperlaneDomain: chainMetadata[Chains.goerli].id, wormholeDomain: 2 },
CircleBridgeAdapterConfig { hyperlaneDomain: chainMetadata[Chains.fuji].id, wormholeDomain: 6 },
> = { { hyperlaneDomain: chainMetadata[Chains.mumbai].id, wormholeDomain: 5 },
{ hyperlaneDomain: chainMetadata[Chains.bsctestnet].id, wormholeDomain: 4 },
{ hyperlaneDomain: chainMetadata[Chains.alfajores].id, wormholeDomain: 14 },
];
export const bridgeAdapterConfigs: ChainMap<any, BridgeAdapterConfig> = {
[Chains.goerli]: { [Chains.goerli]: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0xF890982f9310df57d00f659cf4fd87e65adEd8d7',
wormholeDomainMapping,
},
circle: {
type: BridgeAdapterType.Circle, type: BridgeAdapterType.Circle,
circleBridgeAddress: '0xdabec94b97f7b5fca28f050cc8eeac2dc9920476', tokenMessengerAddress: '0xd0c3da58f55358142b8d3e06c1c30c5c6114efe8',
messageTransmitterAddress: '0x40a61d3d2afcf5a5d31fcdf269e575fb99dd87f7', messageTransmitterAddress: '0x26413e8157cd32011e726065a5462e97dd4d03d9',
usdcAddress: '0x07865c6e87b9f70255377e024ace6630c1eaa37f', usdcAddress: '0x07865c6e87b9f70255377e024ace6630c1eaa37f',
circleDomainMapping, circleDomainMapping,
}, },
},
[Chains.fuji]: { [Chains.fuji]: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0x61E44E506Ca5659E6c0bba9b678586fA2d729756',
wormholeDomainMapping,
},
circle: {
type: BridgeAdapterType.Circle, type: BridgeAdapterType.Circle,
circleBridgeAddress: '0x0fc1103927af27af808d03135214718bcedbe9ad', tokenMessengerAddress: '0xeb08f243e5d3fcff26a9e38ae5520a669f4019d0',
messageTransmitterAddress: '0x52fffb3ee8fa7838e9858a2d5e454007b9027c3c', messageTransmitterAddress: '0xa9fb1b3009dcb79e2fe346c16a604b8fa8ae0a79',
usdcAddress: '0x5425890298aed601595a70ab815c96711a31bc65', usdcAddress: '0x5425890298aed601595a70ab815c96711a31bc65',
circleDomainMapping, circleDomainMapping,
}, },
},
[Chains.mumbai]: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0x377D55a7928c046E18eEbb61977e714d2a76472a',
wormholeDomainMapping,
},
},
[Chains.bsctestnet]: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0x9dcF9D205C9De35334D646BeE44b2D2859712A09',
wormholeDomainMapping,
},
},
[Chains.alfajores]: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0x05ca6037eC51F8b712eD2E6Fa72219FEaE74E153',
wormholeDomainMapping,
},
},
}; };

@ -0,0 +1,24 @@
apiVersion: v2
name: liquidity-layer-relayers
description: Liquidity Layer Relayers
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: '1.16.0'

@ -0,0 +1,42 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "hyperlane.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "hyperlane.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "hyperlane.labels" -}}
helm.sh/chart: {{ include "hyperlane.chart" . }}
hyperlane/deployment: {{ .Values.hyperlane.runEnv | quote }}
hyperlane/context: "hyperlane"
{{ include "hyperlane.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "hyperlane.selectorLabels" -}}
app.kubernetes.io/name: {{ include "hyperlane.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
The name of the ClusterSecretStore
*/}}
{{- define "hyperlane.cluster-secret-store.name" -}}
{{- default "external-secrets-gcp-cluster-secret-store" .Values.externalSecrets.clusterSecretStore }}
{{- end }}

@ -0,0 +1,30 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: circle-relayer
spec:
replicas: 1
selector:
matchLabels:
name: circle-relayer
template:
metadata:
labels:
name: circle-relayer
spec:
containers:
- name: circle-relayer
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
imagePullPolicy: IfNotPresent
command:
- ./node_modules/.bin/ts-node
- ./typescript/infra/scripts/middleware/circle-relayer.ts
- -e
- {{ .Values.hyperlane.runEnv }}
{{- if .Values.hyperlane.connectionType }}
- --connection-type
- {{ .Values.hyperlane.connectionType }}
{{- end }}
envFrom:
- secretRef:
name: liquidity-layer-env-var-secret

@ -0,0 +1,55 @@
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: liquidity-layer-env-var-external-secret
labels:
{{- include "hyperlane.labels" . | nindent 4 }}
spec:
secretStoreRef:
name: {{ include "hyperlane.cluster-secret-store.name" . }}
kind: ClusterSecretStore
refreshInterval: "1h"
# The secret that will be created
target:
name: liquidity-layer-env-var-secret
template:
type: Opaque
metadata:
labels:
{{- include "hyperlane.labels" . | nindent 10 }}
annotations:
update-on-redeploy: "{{ now }}"
data:
GCP_SECRET_OVERRIDES_ENABLED: "true"
GCP_SECRET_OVERRIDE_ABACUS_{{ .Values.hyperlane.runEnv | upper }}_KEY_DEPLOYER: {{ print "'{{ .deployer_key | toString }}'" }}
{{/*
* For each network, create an environment variable with the RPC endpoint.
* The templating of external-secrets will use the data section below to know how
* to replace the correct value in the created secret.
*/}}
{{- range .Values.hyperlane.chains }}
{{- if eq $.Values.hyperlane.connectionType "httpQuorum" }}
GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINTS_{{ . | upper }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }}
{{- else }}
GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINT_{{ . | upper }}: {{ printf "'{{ .%s_rpc | toString }}'" . }}
{{- end }}
{{- end }}
data:
- secretKey: deployer_key
remoteRef:
key: {{ printf "hyperlane-%s-key-deployer" .Values.hyperlane.runEnv }}
{{/*
* For each network, load the secret in GCP secret manager with the form: environment-rpc-endpoint-network,
* and associate it with the secret key networkname_rpc.
*/}}
{{- range .Values.hyperlane.chains }}
{{- if eq $.Values.hyperlane.connectionType "httpQuorum" }}
- secretKey: {{ printf "%s_rpcs" . }}
remoteRef:
key: {{ printf "%s-rpc-endpoints-%s" $.Values.hyperlane.runEnv . }}
{{- else }}
- secretKey: {{ printf "%s_rpc" . }}
remoteRef:
key: {{ printf "%s-rpc-endpoint-%s" $.Values.hyperlane.runEnv . }}
{{- end }}
{{- end }}

@ -0,0 +1,30 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: portal-relayer
spec:
replicas: 1
selector:
matchLabels:
name: portal-relayer
template:
metadata:
labels:
name: portal-relayer
spec:
containers:
- name: portal-relayer
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
imagePullPolicy: IfNotPresent
command:
- ./node_modules/.bin/ts-node
- ./typescript/infra/scripts/middleware/portal-relayer.ts
- -e
- {{ .Values.hyperlane.runEnv }}
{{- if .Values.hyperlane.connectionType }}
- --connection-type
- {{ .Values.hyperlane.connectionType }}
{{- end }}
envFrom:
- secretRef:
name: liquidity-layer-env-var-secret

@ -0,0 +1,9 @@
image:
repository: gcr.io/abacus-labs-dev/hyperlane-monorepo
tag:
abacus:
runEnv: testnet2
# Used for fetching secrets
chains: []
externalSecrets:
clusterSecretStore:

@ -16,7 +16,7 @@ import { ConnectionType } from '../../src/config/agent';
import { deployEnvToSdkEnv } from '../../src/config/environment'; import { deployEnvToSdkEnv } from '../../src/config/environment';
import { startMetricsServer } from '../../src/utils/metrics'; import { startMetricsServer } from '../../src/utils/metrics';
import { assertChain, diagonalize, sleep } from '../../src/utils/utils'; import { assertChain, diagonalize, sleep } from '../../src/utils/utils';
import { getArgs, getCoreEnvironmentConfig } from '../utils'; import { getArgsWithContext, getCoreEnvironmentConfig } from '../utils';
import { getApp } from './utils'; import { getApp } from './utils';
@ -64,7 +64,7 @@ const walletBalance = new Gauge({
const MAX_MESSAGES_ALLOWED_TO_SEND = 5; const MAX_MESSAGES_ALLOWED_TO_SEND = 5;
function getKathyArgs() { function getKathyArgs() {
const args = getArgs() const args = getArgsWithContext()
.boolean('cycle-once') .boolean('cycle-once')
.describe( .describe(
'cycle-once', 'cycle-once',

@ -6,17 +6,15 @@ import {
LiquidityLayerApp, LiquidityLayerApp,
buildContracts, buildContracts,
liquidityLayerFactories, liquidityLayerFactories,
objMap,
} from '@hyperlane-xyz/sdk'; } from '@hyperlane-xyz/sdk';
import { circleBridgeAdapterConfig } from '../config/environments/test/liquidityLayer'; import { bridgeAdapterConfigs } from '../../config/environments/testnet3/token-bridge';
import { readJSON, sleep } from '../src/utils/utils'; import { readJSON, sleep } from '../../src/utils/utils';
import { import {
getCoreEnvironmentConfig, getCoreEnvironmentConfig,
getEnvironment, getEnvironment,
getEnvironmentDirectory, getEnvironmentDirectory,
} from './utils'; } from '../utils';
async function check() { async function check() {
const environment = await getEnvironment(); const environment = await getEnvironment();
@ -24,7 +22,7 @@ async function check() {
const multiProvider = await config.getMultiProvider(); const multiProvider = await config.getMultiProvider();
const dir = path.join( const dir = path.join(
__dirname, __dirname,
'../', '../../',
getEnvironmentDirectory(environment), getEnvironmentDirectory(environment),
'middleware/liquidity-layer', 'middleware/liquidity-layer',
); );
@ -37,7 +35,7 @@ async function check() {
const app = new LiquidityLayerApp( const app = new LiquidityLayerApp(
contracts, contracts,
multiProvider, multiProvider,
objMap(circleBridgeAdapterConfig, (_chain, conf) => [conf]), bridgeAdapterConfigs,
); );
while (true) { while (true) {
@ -51,11 +49,9 @@ async function check() {
).flat(); ).flat();
// Poll for attestation data and submit // Poll for attestation data and submit
await Promise.all( for (const message of circleDispatches) {
circleDispatches.map((message) => await app.attemptCircleAttestationSubmission(message);
app.attemptCircleAttestationSubmission(message), }
),
);
await sleep(6000); await sleep(6000);
} }

@ -7,7 +7,7 @@ import {
objMap, objMap,
} from '@hyperlane-xyz/sdk'; } from '@hyperlane-xyz/sdk';
import { circleBridgeAdapterConfig } from '../../config/environments/test/liquidityLayer'; import { bridgeAdapterConfigs } from '../../config/environments/testnet3/token-bridge';
import { deployEnvToSdkEnv } from '../../src/config/environment'; import { deployEnvToSdkEnv } from '../../src/config/environment';
import { deployWithArtifacts } from '../../src/deploy'; import { deployWithArtifacts } from '../../src/deploy';
import { getConfiguration } from '../helloworld/utils'; import { getConfiguration } from '../helloworld/utils';
@ -33,17 +33,16 @@ async function main() {
// config gcp deployer key as owner // config gcp deployer key as owner
const ownerConfigMap = await getConfiguration(environment, multiProvider); const ownerConfigMap = await getConfiguration(environment, multiProvider);
const config = objMap(bridgeAdapterConfigs, (chain, conf) => ({
...conf,
...ownerConfigMap[chain],
}));
const deployer = new LiquidityLayerDeployer( const deployer = new LiquidityLayerDeployer(
multiProvider, multiProvider,
objMap(circleBridgeAdapterConfig, (chain, conf) => ({ config,
bridgeAdapterConfigs: [conf],
...ownerConfigMap[chain],
})),
core, core,
'LiquidityLayerDeploy2', 'LiquidityLayerDeploy2',
); );
await deployWithArtifacts(dir, liquidityLayerFactories, deployer); await deployWithArtifacts(dir, liquidityLayerFactories, deployer);
} }

@ -0,0 +1,34 @@
import { Contexts } from '../../config/contexts';
import {
getLiquidityLayerRelayerConfig,
runLiquidityLayerRelayerHelmCommand,
} from '../../src/middleware/liquidity-layer-relayer';
import { HelmCommand } from '../../src/utils/helm';
import {
assertCorrectKubeContext,
getContextAgentConfig,
getEnvironmentConfig,
} from '../utils';
async function main() {
const coreConfig = await getEnvironmentConfig();
await assertCorrectKubeContext(coreConfig);
const liquidityLayerRelayerConfig =
getLiquidityLayerRelayerConfig(coreConfig);
const agentConfig = await getContextAgentConfig(
coreConfig,
Contexts.Hyperlane,
);
await runLiquidityLayerRelayerHelmCommand(
HelmCommand.InstallOrUpgrade,
agentConfig,
liquidityLayerRelayerConfig,
);
}
main()
.then(() => console.log('Deploy successful!'))
.catch(console.error);

@ -0,0 +1,58 @@
import path from 'path';
import {
ChainMap,
LiquidityLayerApp,
buildContracts,
liquidityLayerFactories,
} from '@hyperlane-xyz/sdk';
import { bridgeAdapterConfigs } from '../../config/environments/testnet3/token-bridge';
import { readJSON, sleep } from '../../src/utils/utils';
import {
getCoreEnvironmentConfig,
getEnvironment,
getEnvironmentDirectory,
} from '../utils';
async function relayPortalTransfers() {
const environment = await getEnvironment();
const config = getCoreEnvironmentConfig(environment);
const multiProvider = await config.getMultiProvider();
const dir = path.join(
__dirname,
'../../',
getEnvironmentDirectory(environment),
'middleware/liquidity-layer',
);
const addresses = readJSON(dir, 'addresses.json');
// @ts-ignore
const contracts: ChainMap<any, LiquidityLayerContracts> = buildContracts(
addresses,
liquidityLayerFactories,
);
const app = new LiquidityLayerApp(
contracts,
multiProvider,
bridgeAdapterConfigs,
);
while (true) {
for (const chain of Object.keys(bridgeAdapterConfigs)) {
const txHashes = await app.fetchPortalBridgeTransactions(chain);
const portalMessages = (
await Promise.all(
txHashes.map((txHash) => app.parsePortalMessages(chain, txHash)),
)
).flat();
// Poll for attestation data and submit
for (const message of portalMessages) {
await app.attemptPortalTransferCompletion(message);
}
await sleep(10000);
}
}
}
relayPortalTransfers().then(console.log).catch(console.error);

@ -23,16 +23,20 @@ import { fetchProvider } from '../src/config/chain';
import { EnvironmentNames } from '../src/config/environment'; import { EnvironmentNames } from '../src/config/environment';
import { assertContext } from '../src/utils/utils'; import { assertContext } from '../src/utils/utils';
export function getArgsWithContext() {
return getArgs()
.describe('context', 'deploy context')
.coerce('context', assertContext)
.demandOption('context')
.alias('c', 'context');
}
export function getArgs() { export function getArgs() {
return yargs(process.argv.slice(2)) return yargs(process.argv.slice(2))
.describe('environment', 'deploy environment') .describe('environment', 'deploy environment')
.coerce('environment', assertEnvironment) .coerce('environment', assertEnvironment)
.demandOption('environment') .demandOption('environment')
.alias('e', 'environment') .alias('e', 'environment');
.describe('context', 'deploy context')
.coerce('context', assertContext)
.demandOption('context')
.alias('c', 'context');
} }
export async function getEnvironmentFromArgs(): Promise<string> { export async function getEnvironmentFromArgs(): Promise<string> {
@ -64,7 +68,8 @@ export async function getEnvironmentConfig() {
} }
export async function getContext(defaultContext?: string): Promise<Contexts> { export async function getContext(defaultContext?: string): Promise<Contexts> {
const argv = await getArgs().argv; const argv = await getArgsWithContext().argv;
// @ts-ignore
return assertContext(argv.context! || defaultContext!); return assertContext(argv.context! || defaultContext!);
} }

@ -15,6 +15,7 @@ import { AgentConfig, ConnectionType } from './agent';
import { KeyFunderConfig } from './funding'; import { KeyFunderConfig } from './funding';
import { HelloWorldConfig } from './helloworld'; import { HelloWorldConfig } from './helloworld';
import { InfrastructureConfig } from './infrastructure'; import { InfrastructureConfig } from './infrastructure';
import { LiquidityLayerRelayerConfig } from './middleware';
export const EnvironmentNames = Object.keys(environments); export const EnvironmentNames = Object.keys(environments);
export type DeployEnvironment = keyof typeof environments; export type DeployEnvironment = keyof typeof environments;
@ -37,6 +38,7 @@ export type CoreEnvironmentConfig<Chain extends ChainName> = {
) => Promise<MultiProvider<Chain>>; ) => Promise<MultiProvider<Chain>>;
helloWorld?: Partial<Record<Contexts, HelloWorldConfig<Chain>>>; helloWorld?: Partial<Record<Contexts, HelloWorldConfig<Chain>>>;
keyFunderConfig?: KeyFunderConfig; keyFunderConfig?: KeyFunderConfig;
liquidityLayerRelayerConfig?: LiquidityLayerRelayerConfig;
}; };
export type SdkEnvironment = keyof typeof coreEnvironments; export type SdkEnvironment = keyof typeof coreEnvironments;

@ -0,0 +1,8 @@
import { ConnectionType, DockerConfig } from './agent';
export interface LiquidityLayerRelayerConfig {
docker: DockerConfig;
namespace: string;
connectionType: ConnectionType.Http | ConnectionType.HttpQuorum;
prometheusPushGateway: string;
}

@ -0,0 +1,73 @@
import { ChainName } from '@hyperlane-xyz/sdk';
import { AgentConfig, CoreEnvironmentConfig } from '../config';
import { LiquidityLayerRelayerConfig } from '../config/middleware';
import { HelmCommand, helmifyValues } from '../utils/helm';
import { execCmd } from '../utils/utils';
export async function runLiquidityLayerRelayerHelmCommand<
Chain extends ChainName,
>(
helmCommand: HelmCommand,
agentConfig: AgentConfig<Chain>,
relayerConfig: LiquidityLayerRelayerConfig,
) {
const values = getLiquidityLayerRelayerHelmValues(agentConfig, relayerConfig);
if (helmCommand === HelmCommand.InstallOrUpgrade) {
// Delete secrets to avoid them being stale
try {
await execCmd(
`kubectl delete secrets --namespace ${agentConfig.namespace} --selector app.kubernetes.io/instance=liquidity-layer-relayers`,
{},
false,
false,
);
} catch (e) {
console.error(e);
}
}
return execCmd(
`helm ${helmCommand} liquidity-layer-relayers ./helm/liquidity-layer-relayers --namespace ${
relayerConfig.namespace
} ${values.join(' ')}`,
{},
false,
true,
);
}
function getLiquidityLayerRelayerHelmValues<Chain extends ChainName>(
agentConfig: AgentConfig<Chain>,
relayerConfig: LiquidityLayerRelayerConfig,
) {
const values = {
hyperlane: {
runEnv: agentConfig.environment,
// Only used for fetching RPC urls as env vars
chains: agentConfig.contextChainNames,
connectionType: relayerConfig.connectionType,
},
image: {
repository: relayerConfig.docker.repo,
tag: relayerConfig.docker.tag,
},
infra: {
prometheusPushGateway: relayerConfig.prometheusPushGateway,
},
};
return helmifyValues(values);
}
export function getLiquidityLayerRelayerConfig(
coreConfig: CoreEnvironmentConfig<any>,
): LiquidityLayerRelayerConfig {
const relayerConfig = coreConfig.liquidityLayerRelayerConfig;
if (!relayerConfig) {
throw new Error(
`Environment ${coreConfig.environment} does not have a LiquidityLayerRelayerConfig config`,
);
}
return relayerConfig;
}

@ -3,31 +3,41 @@ import { ethers } from 'ethers';
import { import {
CircleBridgeAdapter__factory, CircleBridgeAdapter__factory,
ICircleBridge__factory,
ICircleMessageTransmitter__factory, ICircleMessageTransmitter__factory,
ITokenMessenger__factory,
PortalAdapter__factory,
} from '@hyperlane-xyz/core'; } from '@hyperlane-xyz/core';
import { utils } from '@hyperlane-xyz/utils';
import { HyperlaneApp } from '../../HyperlaneApp'; import { HyperlaneApp } from '../../HyperlaneApp';
import { Chains } from '../../consts/chains'; import { Chains } from '../../consts/chains';
import { ChainNameToDomainId, DomainIdToChainName } from '../../domains';
import { LiquidityLayerContracts } from '../../middleware'; import { LiquidityLayerContracts } from '../../middleware';
import { MultiProvider } from '../../providers/MultiProvider'; import { MultiProvider } from '../../providers/MultiProvider';
import { ChainMap, ChainName } from '../../types'; import { ChainMap, ChainName } from '../../types';
import { objMap } from '../../utils/objects';
import { import { BridgeAdapterConfig } from './LiquidityLayerRouterDeployer';
BridgeAdapterConfig,
BridgeAdapterType, const PORTAL_VAA_SERVICE_TESTNET_BASE_URL =
CircleBridgeAdapterConfig, 'https://wormhole-v2-testnet-api.certus.one/v1/signed_vaa/';
} from './LiquidityLayerRouterDeployer'; const CIRCLE_ATTESTATIONS_BASE_URL =
'https://iris-api-sandbox.circle.com/attestations/';
const CircleBridgeInterface = ICircleBridge__factory.createInterface(); const PORTAL_VAA_SERVICE_SUCCESS_CODE = 5;
const TokenMessengerInterface = ITokenMessenger__factory.createInterface();
const CircleBridgeAdapterInterface = const CircleBridgeAdapterInterface =
CircleBridgeAdapter__factory.createInterface(); CircleBridgeAdapter__factory.createInterface();
const PortalAdapterInterface = PortalAdapter__factory.createInterface();
const BridgedTokenTopic = CircleBridgeAdapterInterface.getEventTopic( const BridgedTokenTopic = CircleBridgeAdapterInterface.getEventTopic(
CircleBridgeAdapterInterface.getEvent('BridgedToken'), CircleBridgeAdapterInterface.getEvent('BridgedToken'),
); );
const PortalBridgedTokenTopic = PortalAdapterInterface.getEventTopic(
PortalAdapterInterface.getEvent('BridgedToken'),
);
interface CircleBridgeMessage<Chain> { interface CircleBridgeMessage<Chain> {
chain: Chain; chain: Chain;
remoteChain: Chain; remoteChain: Chain;
@ -37,30 +47,25 @@ interface CircleBridgeMessage<Chain> {
domain: number; domain: number;
nonceHash: string; nonceHash: string;
} }
interface PortalBridgeMessage<Chain> {
origin: Chain;
nonce: number;
portalSequence: number;
destination: Chain;
}
export class LiquidityLayerApp< export class LiquidityLayerApp<
Chain extends ChainName = ChainName, Chain extends ChainName = ChainName,
> extends HyperlaneApp<LiquidityLayerContracts, Chain> { > extends HyperlaneApp<LiquidityLayerContracts, Chain> {
constructor( constructor(
public readonly contractsMap: ChainMap<Chain, LiquidityLayerContracts>, public readonly contractsMap: ChainMap<Chain, LiquidityLayerContracts>,
public readonly multiProvider: MultiProvider<Chain>, public readonly multiProvider: MultiProvider<Chain>,
public readonly bridgeAdapterConfigs: ChainMap< public readonly config: ChainMap<Chain, BridgeAdapterConfig>,
Chain,
BridgeAdapterConfig[]
>,
) { ) {
super(contractsMap, multiProvider); super(contractsMap, multiProvider);
} }
circleBridgeAdapterConfig(): ChainMap<Chain, CircleBridgeAdapterConfig> {
return objMap(
this.bridgeAdapterConfigs,
(_chain, config) =>
config.find(
(_) => _.type === BridgeAdapterType.Circle,
) as CircleBridgeAdapterConfig,
);
}
async fetchCircleMessageTransactions(chain: Chain): Promise<string[]> { async fetchCircleMessageTransactions(chain: Chain): Promise<string[]> {
const cc = this.multiProvider.getChainConnection(chain); const cc = this.multiProvider.getChainConnection(chain);
const params = new URLSearchParams({ const params = new URLSearchParams({
@ -75,6 +80,46 @@ export class LiquidityLayerApp<
return response.result.map((_: any) => _.transactionHash).flat(); return response.result.map((_: any) => _.transactionHash).flat();
} }
async fetchPortalBridgeTransactions(chain: Chain): Promise<string[]> {
const cc = this.multiProvider.getChainConnection(chain);
const params = new URLSearchParams({
module: 'logs',
action: 'getLogs',
address: this.getContracts(chain).portalAdapter!.address,
topic0: PortalBridgedTokenTopic,
});
const req = await fetch(`${cc.getApiUrl()}?${params}`);
const response = await req.json();
return response.result.map((_: any) => _.transactionHash).flat();
}
async parsePortalMessages(
chain: Chain,
txHash: string,
): Promise<PortalBridgeMessage<Chain>[]> {
const connection = this.multiProvider.getChainConnection(chain);
const receipt = await connection.provider.getTransactionReceipt(txHash);
const matchingLogs = receipt.logs
.map((_) => {
try {
return [PortalAdapterInterface.parseLog(_)];
} catch {
return [];
}
})
.flat();
if (matchingLogs.length == 0) return [];
const event = matchingLogs.find((_) => _!.name === 'BridgedToken')!;
const portalSequence = event.args.portalSequence.toNumber();
const nonce = event.args.nonce.toNumber();
const destination = DomainIdToChainName[event.args.destination];
return [
{ origin: chain, nonce, portalSequence, destination },
] as PortalBridgeMessage<Chain>[];
}
async parseCircleMessages( async parseCircleMessages(
chain: Chain, chain: Chain,
txHash: string, txHash: string,
@ -84,7 +129,7 @@ export class LiquidityLayerApp<
const matchingLogs = receipt.logs const matchingLogs = receipt.logs
.map((_) => { .map((_) => {
try { try {
return [CircleBridgeInterface.parseLog(_)]; return [TokenMessengerInterface.parseLog(_)];
} catch { } catch {
try { try {
return [CircleBridgeAdapterInterface.parseLog(_)]; return [CircleBridgeAdapterInterface.parseLog(_)];
@ -100,8 +145,10 @@ export class LiquidityLayerApp<
.message; .message;
const nonce = matchingLogs.find((_) => _!.name === 'BridgedToken')!.args const nonce = matchingLogs.find((_) => _!.name === 'BridgedToken')!.args
.nonce; .nonce;
const remoteChain = const remoteChain = chain === Chains.fuji ? Chains.goerli : Chains.fuji;
message.chain === Chains.fuji ? Chains.goerli : Chains.fuji; const domain = this.config[chain].circle!.circleDomainMapping.find(
(_) => _.hyperlaneDomain === ChainNameToDomainId[chain],
)!.circleDomain;
return [ return [
{ {
chain, chain,
@ -110,15 +157,68 @@ export class LiquidityLayerApp<
txHash, txHash,
message, message,
nonce, nonce,
domain: 0, domain,
nonceHash: ethers.utils.solidityKeccak256( nonceHash: ethers.utils.solidityKeccak256(
['uint32', 'uint256'], ['uint32', 'uint64'],
[0, nonce], [domain, nonce],
), ),
}, },
]; ];
} }
async attemptPortalTransferCompletion(
message: PortalBridgeMessage<Chain>,
): Promise<void> {
const destinationPortalAdapter = this.getContracts(message.destination)
.portalAdapter!;
const transferId = await destinationPortalAdapter.transferId(
ChainNameToDomainId[message.origin],
message.nonce,
);
const transferTokenAddress =
await destinationPortalAdapter.portalTransfersProcessed(transferId);
if (!utils.eqAddress(transferTokenAddress, ethers.constants.AddressZero)) {
console.log(
`Transfer with nonce ${message.nonce} from ${message.origin} to ${message.destination} already processed`,
);
return;
}
const wormholeOriginDomain = this.config[
message.destination
].portal!.wormholeDomainMapping.find(
(_) => _.hyperlaneDomain === ChainNameToDomainId[message.origin],
)?.wormholeDomain;
const emitter = utils.strip0x(
utils.addressToBytes32(
this.config[message.origin].portal!.portalBridgeAddress,
),
);
const vaa = await fetch(
`${PORTAL_VAA_SERVICE_TESTNET_BASE_URL}${wormholeOriginDomain}/${emitter}/${message.portalSequence}`,
).then((_) => _.json());
if (vaa.code && vaa.code === PORTAL_VAA_SERVICE_SUCCESS_CODE) {
console.log(`VAA not yet found for nonce ${message.nonce}`);
return;
}
const connection = this.multiProvider.getChainConnection(
message.destination,
);
console.debug(
`Complete portal transfer for nonce ${message.nonce} on ${message.destination}`,
);
await connection.handleTx(
destinationPortalAdapter.completeTransfer(
utils.ensure0x(Buffer.from(vaa.vaaBytes, 'base64').toString('hex')),
),
);
}
async attemptCircleAttestationSubmission( async attemptCircleAttestationSubmission(
message: CircleBridgeMessage<Chain>, message: CircleBridgeMessage<Chain>,
): Promise<void> { ): Promise<void> {
@ -126,8 +226,7 @@ export class LiquidityLayerApp<
message.remoteChain, message.remoteChain,
); );
const transmitter = ICircleMessageTransmitter__factory.connect( const transmitter = ICircleMessageTransmitter__factory.connect(
this.circleBridgeAdapterConfig()[message.remoteChain] this.config[message.remoteChain].circle!.messageTransmitterAddress,
.messageTransmitterAddress,
connection.signer!, connection.signer!,
); );
@ -140,7 +239,7 @@ export class LiquidityLayerApp<
const messageHash = ethers.utils.keccak256(message.message); const messageHash = ethers.utils.keccak256(message.message);
const attestationsB = await fetch( const attestationsB = await fetch(
`https://iris-api-sandbox.circle.com/attestations/${messageHash}`, `${CIRCLE_ATTESTATIONS_BASE_URL}${messageHash}`,
); );
const attestations = await attestationsB.json(); const attestations = await attestationsB.json();

@ -1,13 +1,15 @@
import { ethers } from 'ethers';
import { import {
CircleBridgeAdapter, CircleBridgeAdapter,
CircleBridgeAdapter__factory, CircleBridgeAdapter__factory,
LiquidityLayerRouter, LiquidityLayerRouter,
LiquidityLayerRouter__factory, LiquidityLayerRouter__factory,
PortalAdapter,
PortalAdapter__factory,
} from '@hyperlane-xyz/core'; } from '@hyperlane-xyz/core';
import { utils } from '@hyperlane-xyz/utils';
import { HyperlaneCore } from '../../core/HyperlaneCore'; import { HyperlaneCore } from '../../core/HyperlaneCore';
import { ChainNameToDomainId } from '../../domains';
import { import {
LiquidityLayerContracts, LiquidityLayerContracts,
LiquidityLayerFactories, LiquidityLayerFactories,
@ -15,18 +17,19 @@ import {
} from '../../middleware'; } from '../../middleware';
import { MultiProvider } from '../../providers/MultiProvider'; import { MultiProvider } from '../../providers/MultiProvider';
import { ChainMap, ChainName } from '../../types'; import { ChainMap, ChainName } from '../../types';
import { objMap } from '../../utils/objects'; import { objFilter, objMap } from '../../utils/objects';
import { RouterConfig } from '../router/types'; import { RouterConfig } from '../router/types';
import { MiddlewareRouterDeployer } from './deploy'; import { MiddlewareRouterDeployer } from './deploy';
export enum BridgeAdapterType { export enum BridgeAdapterType {
Circle = 'Circle', Circle = 'Circle',
Portal = 'Portal',
} }
export interface CircleBridgeAdapterConfig { export interface CircleBridgeAdapterConfig {
type: BridgeAdapterType.Circle; type: BridgeAdapterType.Circle;
circleBridgeAddress: string; tokenMessengerAddress: string;
messageTransmitterAddress: string; messageTransmitterAddress: string;
usdcAddress: string; usdcAddress: string;
circleDomainMapping: { circleDomainMapping: {
@ -35,12 +38,22 @@ export interface CircleBridgeAdapterConfig {
}[]; }[];
} }
export type BridgeAdapterConfig = CircleBridgeAdapterConfig; export interface PortalAdapterConfig {
type: BridgeAdapterType.Portal;
portalBridgeAddress: string;
wormholeDomainMapping: {
hyperlaneDomain: number;
wormholeDomain: number;
}[];
}
export type LiquidityLayerConfig = RouterConfig & { export type BridgeAdapterConfig = {
bridgeAdapterConfigs: BridgeAdapterConfig[]; circle?: CircleBridgeAdapterConfig;
portal?: PortalAdapterConfig;
}; };
export type LiquidityLayerConfig = RouterConfig & BridgeAdapterConfig;
export class LiquidityLayerDeployer< export class LiquidityLayerDeployer<
Chain extends ChainName, Chain extends ChainName,
> extends MiddlewareRouterDeployer< > extends MiddlewareRouterDeployer<
@ -61,14 +74,27 @@ export class LiquidityLayerDeployer<
async enrollRemoteRouters( async enrollRemoteRouters(
contractsMap: ChainMap<Chain, LiquidityLayerContracts>, contractsMap: ChainMap<Chain, LiquidityLayerContracts>,
): Promise<void> { ): Promise<void> {
// Enroll the LiquidityLayerRouter with each other this.logger(`Enroll LiquidityLayerRouters with each other`);
await super.enrollRemoteRouters(contractsMap); await super.enrollRemoteRouters(contractsMap);
// Enroll the circle adapters with each other this.logger(`Enroll CircleBridgeAdapters with each other`);
await super.enrollRemoteRouters(
objFilter(
objMap(contractsMap, (_chain, contracts) => ({
router: contracts.circleBridgeAdapter,
})),
(_): _ is { router: CircleBridgeAdapter } => !!_.router,
),
);
this.logger(`Enroll PortalAdapters with each other`);
await super.enrollRemoteRouters( await super.enrollRemoteRouters(
objFilter(
objMap(contractsMap, (_chain, contracts) => ({ objMap(contractsMap, (_chain, contracts) => ({
router: contracts.circleBridgeAdapter!, router: contracts.portalAdapter,
})), })),
(_): _ is { router: PortalAdapter } => !!_.router,
),
); );
} }
@ -89,16 +115,21 @@ export class LiquidityLayerDeployer<
const bridgeAdapters: Partial<LiquidityLayerContracts> = {}; const bridgeAdapters: Partial<LiquidityLayerContracts> = {};
for (const adapterConfig of config.bridgeAdapterConfigs) { if (config.circle) {
if (adapterConfig.type === BridgeAdapterType.Circle) { bridgeAdapters.circleBridgeAdapter = await this.deployCircleBridgeAdapter(
bridgeAdapters.circleBridgeAdapter =
await this.deployCircleBridgeAdapter(
chain, chain,
adapterConfig, config.circle,
config.owner, config.owner,
router, router,
); );
} }
if (config.portal) {
bridgeAdapters.portalAdapter = await this.deployPortalAdapter(
chain,
config.portal,
config.owner,
router,
);
} }
return { return {
@ -107,6 +138,68 @@ export class LiquidityLayerDeployer<
}; };
} }
async deployPortalAdapter(
chain: Chain,
adapterConfig: PortalAdapterConfig,
owner: string,
router: LiquidityLayerRouter,
): Promise<PortalAdapter> {
const cc = this.multiProvider.getChainConnection(chain);
const initCalldata =
PortalAdapter__factory.createInterface().encodeFunctionData(
'initialize',
[
ChainNameToDomainId[chain],
owner,
adapterConfig.portalBridgeAddress,
router.address,
],
);
const portalAdapter = await this.deployContract(
chain,
'portalAdapter',
[],
{
create2Salt: this.create2salt,
initCalldata,
},
);
for (const {
wormholeDomain,
hyperlaneDomain,
} of adapterConfig.wormholeDomainMapping) {
const expectedCircleDomain =
await portalAdapter.hyperlaneDomainToWormholeDomain(hyperlaneDomain);
if (expectedCircleDomain === wormholeDomain) continue;
this.logger(
`Set wormhole domain ${wormholeDomain} for hyperlane domain ${hyperlaneDomain}`,
);
await cc.handleTx(
portalAdapter.addDomain(hyperlaneDomain, wormholeDomain),
);
}
if (
!utils.eqAddress(
await router.liquidityLayerAdapters('Portal'),
portalAdapter.address,
)
) {
this.logger('Set Portal as LiquidityLayerAdapter on Router');
await cc.handleTx(
router.setLiquidityLayerAdapter(
adapterConfig.type,
portalAdapter.address,
),
);
}
return portalAdapter;
}
async deployCircleBridgeAdapter( async deployCircleBridgeAdapter(
chain: Chain, chain: Chain,
adapterConfig: CircleBridgeAdapterConfig, adapterConfig: CircleBridgeAdapterConfig,
@ -119,7 +212,7 @@ export class LiquidityLayerDeployer<
'initialize', 'initialize',
[ [
owner, owner,
adapterConfig.circleBridgeAddress, adapterConfig.tokenMessengerAddress,
adapterConfig.messageTransmitterAddress, adapterConfig.messageTransmitterAddress,
router.address, router.address,
], ],
@ -135,8 +228,10 @@ export class LiquidityLayerDeployer<
); );
if ( if (
(await circleBridgeAdapter.tokenSymbolToAddress('USDC')) === !utils.eqAddress(
ethers.constants.AddressZero await circleBridgeAdapter.tokenSymbolToAddress('USDC'),
adapterConfig.usdcAddress,
)
) { ) {
this.logger(`Set USDC token contract`); this.logger(`Set USDC token contract`);
await cc.handleTx( await cc.handleTx(
@ -162,13 +257,21 @@ export class LiquidityLayerDeployer<
); );
} }
this.logger('Set CircleLiquidityLayerAdapter on Router'); if (
!utils.eqAddress(
await router.liquidityLayerAdapters('Circle'),
circleBridgeAdapter.address,
)
) {
this.logger('Set Circle as LiquidityLayerAdapter on Router');
await cc.handleTx( await cc.handleTx(
router.setLiquidityLayerAdapter( router.setLiquidityLayerAdapter(
adapterConfig.type, adapterConfig.type,
circleBridgeAdapter.address, circleBridgeAdapter.address,
), ),
); );
}
return circleBridgeAdapter; return circleBridgeAdapter;
} }
} }

@ -127,6 +127,7 @@ export {
BridgeAdapterType, BridgeAdapterType,
BridgeAdapterConfig, BridgeAdapterConfig,
CircleBridgeAdapterConfig, CircleBridgeAdapterConfig,
PortalAdapterConfig,
} from './deploy/middleware/LiquidityLayerRouterDeployer'; } from './deploy/middleware/LiquidityLayerRouterDeployer';
export { LiquidityLayerApp } from './deploy/middleware/LiquidityLayerApp'; export { LiquidityLayerApp } from './deploy/middleware/LiquidityLayerApp';

@ -7,6 +7,8 @@ import {
InterchainQueryRouter__factory, InterchainQueryRouter__factory,
LiquidityLayerRouter, LiquidityLayerRouter,
LiquidityLayerRouter__factory, LiquidityLayerRouter__factory,
PortalAdapter,
PortalAdapter__factory,
} from '@hyperlane-xyz/core'; } from '@hyperlane-xyz/core';
import { RouterContracts, RouterFactories } from './router'; import { RouterContracts, RouterFactories } from './router';
@ -31,13 +33,16 @@ export type InterchainQueryContracts = RouterContracts<InterchainQueryRouter>;
export type LiquidityLayerFactories = RouterFactories<LiquidityLayerRouter> & { export type LiquidityLayerFactories = RouterFactories<LiquidityLayerRouter> & {
circleBridgeAdapter: CircleBridgeAdapter__factory; circleBridgeAdapter: CircleBridgeAdapter__factory;
portalAdapter: PortalAdapter__factory;
}; };
export const liquidityLayerFactories: LiquidityLayerFactories = { export const liquidityLayerFactories: LiquidityLayerFactories = {
router: new LiquidityLayerRouter__factory(), router: new LiquidityLayerRouter__factory(),
circleBridgeAdapter: new CircleBridgeAdapter__factory(), circleBridgeAdapter: new CircleBridgeAdapter__factory(),
portalAdapter: new PortalAdapter__factory(),
}; };
export type LiquidityLayerContracts = RouterContracts<LiquidityLayerRouter> & { export type LiquidityLayerContracts = RouterContracts<LiquidityLayerRouter> & {
circleBridgeAdapter?: CircleBridgeAdapter; circleBridgeAdapter?: CircleBridgeAdapter;
portalAdapter?: PortalAdapter;
}; };

@ -4,10 +4,12 @@ import { ethers } from 'hardhat';
import { import {
LiquidityLayerRouter, LiquidityLayerRouter,
MockCircleBridge,
MockCircleBridge__factory,
MockCircleMessageTransmitter, MockCircleMessageTransmitter,
MockCircleMessageTransmitter__factory, MockCircleMessageTransmitter__factory,
MockCircleTokenMessenger,
MockCircleTokenMessenger__factory,
MockPortalBridge,
MockPortalBridge__factory,
MockToken, MockToken,
MockToken__factory, MockToken__factory,
TestLiquidityLayerMessageRecipient__factory, TestLiquidityLayerMessageRecipient__factory,
@ -23,6 +25,7 @@ import {
CircleBridgeAdapterConfig, CircleBridgeAdapterConfig,
LiquidityLayerConfig, LiquidityLayerConfig,
LiquidityLayerDeployer, LiquidityLayerDeployer,
PortalAdapterConfig,
} from '../deploy/middleware/LiquidityLayerRouterDeployer'; } from '../deploy/middleware/LiquidityLayerRouterDeployer';
import { getChainToOwnerMap, getTestMultiProvider } from '../deploy/utils'; import { getChainToOwnerMap, getTestMultiProvider } from '../deploy/utils';
import { ChainNameToDomainId } from '../domains'; import { ChainNameToDomainId } from '../domains';
@ -44,7 +47,8 @@ describe('LiquidityLayerRouter', async () => {
let liquidityLayerApp: LiquidityLayerApp<TestChainNames>; let liquidityLayerApp: LiquidityLayerApp<TestChainNames>;
let config: ChainMap<TestChainNames, LiquidityLayerConfig>; let config: ChainMap<TestChainNames, LiquidityLayerConfig>;
let mockToken: MockToken; let mockToken: MockToken;
let circleBridge: MockCircleBridge; let circleTokenMessenger: MockCircleTokenMessenger;
let portalBridge: MockPortalBridge;
let messageTransmitter: MockCircleMessageTransmitter; let messageTransmitter: MockCircleMessageTransmitter;
before(async () => { before(async () => {
@ -58,8 +62,12 @@ describe('LiquidityLayerRouter', async () => {
const mockTokenF = new MockToken__factory(signer); const mockTokenF = new MockToken__factory(signer);
mockToken = await mockTokenF.deploy(); mockToken = await mockTokenF.deploy();
const circleBridgeF = new MockCircleBridge__factory(signer); const portalBridgeF = new MockPortalBridge__factory(signer);
circleBridge = await circleBridgeF.deploy(mockToken.address); const circleTokenMessengerF = new MockCircleTokenMessenger__factory(signer);
circleTokenMessenger = await circleTokenMessengerF.deploy(
mockToken.address,
);
portalBridge = await portalBridgeF.deploy(mockToken.address);
const messageTransmitterF = new MockCircleMessageTransmitter__factory( const messageTransmitterF = new MockCircleMessageTransmitter__factory(
signer, signer,
); );
@ -70,10 +78,9 @@ describe('LiquidityLayerRouter', async () => {
getChainToOwnerMap(testChainConnectionConfigs, signer.address), getChainToOwnerMap(testChainConnectionConfigs, signer.address),
(_chain, conf) => ({ (_chain, conf) => ({
...conf, ...conf,
bridgeAdapterConfigs: [ circle: {
{
type: BridgeAdapterType.Circle, type: BridgeAdapterType.Circle,
circleBridgeAddress: circleBridge.address, tokenMessengerAddress: circleTokenMessenger.address,
messageTransmitterAddress: messageTransmitter.address, messageTransmitterAddress: messageTransmitter.address,
usdcAddress: mockToken.address, usdcAddress: mockToken.address,
circleDomainMapping: [ circleDomainMapping: [
@ -87,7 +94,20 @@ describe('LiquidityLayerRouter', async () => {
}, },
], ],
} as CircleBridgeAdapterConfig, } as CircleBridgeAdapterConfig,
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: portalBridge.address,
wormholeDomainMapping: [
{
hyperlaneDomain: localDomain,
wormholeDomain: localDomain,
},
{
hyperlaneDomain: remoteDomain,
wormholeDomain: remoteDomain,
},
], ],
} as PortalAdapterConfig,
}), }),
), ),
); );
@ -101,16 +121,12 @@ describe('LiquidityLayerRouter', async () => {
); );
const contracts = await LiquidityLayer.deploy(); const contracts = await LiquidityLayer.deploy();
liquidityLayerApp = new LiquidityLayerApp( liquidityLayerApp = new LiquidityLayerApp(contracts, multiProvider, config);
contracts,
multiProvider,
objMap(config, (_chain, conf) => conf.bridgeAdapterConfigs),
);
local = liquidityLayerApp.getContracts(localChain).router; local = liquidityLayerApp.getContracts(localChain).router;
}); });
it('can transfer tokens', async () => { it('can transfer tokens via Circle', async () => {
const recipientF = new TestLiquidityLayerMessageRecipient__factory(signer); const recipientF = new TestLiquidityLayerMessageRecipient__factory(signer);
const recipient = await recipientF.deploy(); const recipient = await recipientF.deploy();
@ -126,7 +142,7 @@ describe('LiquidityLayerRouter', async () => {
BridgeAdapterType.Circle, BridgeAdapterType.Circle,
); );
const transferNonce = await circleBridge.nextNonce(); const transferNonce = await circleTokenMessenger.nextNonce();
const nonceId = await messageTransmitter.hashSourceAndNonce( const nonceId = await messageTransmitter.hashSourceAndNonce(
localDomain, localDomain,
transferNonce, transferNonce,
@ -143,4 +159,38 @@ describe('LiquidityLayerRouter', async () => {
amount, amount,
); );
}); });
it('can transfer tokens via Portal', async () => {
const recipientF = new TestLiquidityLayerMessageRecipient__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.Portal,
);
const originAdapter =
liquidityLayerApp.getContracts(localChain).portalAdapter!;
const destinationAdapter =
liquidityLayerApp.getContracts(remoteChain).portalAdapter!;
await destinationAdapter.completeTransfer(
await portalBridge.mockPortalVaa(
localDomain,
await originAdapter.nonce(),
amount,
),
);
await coreApp.processMessages();
expect((await mockToken.balanceOf(recipient.address)).toNumber()).to.eql(
amount,
);
});
}); });

@ -129,7 +129,9 @@ export class MultiProvider<
} }
if (!intersection.length) { if (!intersection.length) {
throw new Error(`No chains shared between MultiProvider and list`); throw new Error(
`No chains shared between MultiProvider and list (${ownChains} and ${chains})`,
);
} }
const intersectionChainMap = pick(this.chainMap, intersection); const intersectionChainMap = pick(this.chainMap, intersection);

@ -18,6 +18,15 @@ export function objMap<K extends string, I = any, O = any>(
>; >;
} }
export function objFilter<K extends string, I, O extends I>(
obj: Record<K, I>,
func: (v: I) => v is O,
): Record<K, O> {
return Object.fromEntries(
Object.entries<I>(obj).filter(([_, v]) => func(v)),
) as Record<K, O>;
}
// promiseObjectAll :: {k: Promise a} -> Promise {k: a} // promiseObjectAll :: {k: Promise a} -> Promise {k: a}
export function promiseObjAll<K extends string, V>(obj: { export function promiseObjAll<K extends string, V>(obj: {
[key in K]: Promise<V>; [key in K]: Promise<V>;

@ -19,6 +19,10 @@ export function deepEquals(v1: any, v2: any) {
return JSON.stringify(v1) === JSON.stringify(v2); return JSON.stringify(v1) === JSON.stringify(v2);
} }
export function eqAddress(a: string, b: string) {
return ethers.utils.getAddress(a) === ethers.utils.getAddress(b);
}
export const ensure0x = (hexstr: string) => export const ensure0x = (hexstr: string) =>
hexstr.startsWith('0x') ? hexstr : `0x${hexstr}`; hexstr.startsWith('0x') ? hexstr : `0x${hexstr}`;

Loading…
Cancel
Save