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 linepull/1554/head
parent
82acc39870
commit
891ca4ccdb
@ -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) |
||||||
|
@ -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)); |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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 |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -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" |
||||||
} |
} |
||||||
} |
} |
||||||
|
@ -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, |
||||||
|
}; |
@ -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: |
@ -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); |
@ -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; |
||||||
|
} |
Loading…
Reference in new issue