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:testSetOwner(address) (runs: 256, μ: 990801, ~: 990801) |
||||
InterchainAccountRouterTest:testSetOwner(address) (runs: 256, μ: 990723, ~: 990801) |
||||
InterchainQueryRouterTest:testQueryAddress(address) (runs: 256, μ: 1345905, ~: 1345905) |
||||
InterchainQueryRouterTest:testQueryUint256(uint256) (runs: 256, μ: 1686587, ~: 1686587) |
||||
LiquidityLayerRouterTest:testDispatchWithTokenTransfersMovesTokens() (gas: 552484) |
||||
LiquidityLayerRouterTest:testDispatchWithTokensCallsAdapter() (gas: 558572) |
||||
LiquidityLayerRouterTest:testDispatchWithTokensRevertsWithFailedTransferIn() (gas: 28626) |
||||
LiquidityLayerRouterTest:testDispatchWithTokensRevertsWithUnkownBridgeAdapter() (gas: 20611) |
||||
LiquidityLayerRouterTest:testDispatchWithTokensTransfersOnDestination() (gas: 788665) |
||||
LiquidityLayerRouterTest:testProcessingRevertsIfBridgeAdapterReverts() (gas: 603576) |
||||
LiquidityLayerRouterTest:testDispatchWithTokensTransfersOnDestination() (gas: 788693) |
||||
LiquidityLayerRouterTest:testProcessingRevertsIfBridgeAdapterReverts() (gas: 603550) |
||||
LiquidityLayerRouterTest:testSetLiquidityLayerAdapter() (gas: 23429) |
||||
MessagingTest:testSendMessage(string) (runs: 256, μ: 277677, ~: 296009) |
||||
MessagingTest:testSendMessage(string) (runs: 256, μ: 276993, ~: 296009) |
||||
PausableReentrancyGuardTest:testNonreentrant() (gas: 14428) |
||||
PausableReentrancyGuardTest:testNonreentrantNotPaused() (gas: 14163) |
||||
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 |
||||
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"; |
||||
|
||||
contract MockCircleBridge is ICircleBridge { |
||||
contract MockCircleTokenMessenger is ITokenMessenger { |
||||
uint64 public nextNonce = 0; |
||||
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": { |
||||
"circleBridgeAdapter": "0xc262a656c99B3a2f1B196dc5BeDa8f4f80D4a878", |
||||
"router": "0x952228cA63f85130534981844050c82b89f373E7" |
||||
"circleBridgeAdapter": "0x17EB33454AAEF8E91510540a0ebF4a8213dd740D", |
||||
"portalAdapter": "0xe589a05be1304b43A6FEb9c5D6a6EEEA35656271", |
||||
"router": "0x3428e12EfDb2446c1E7feC3f1CED099A8a7cD541" |
||||
}, |
||||
"fuji": { |
||||
"circleBridgeAdapter": "0xc262a656c99B3a2f1B196dc5BeDa8f4f80D4a878", |
||||
"router": "0x952228cA63f85130534981844050c82b89f373E7" |
||||
"mumbai": { |
||||
"portalAdapter": "0xe589a05be1304b43A6FEb9c5D6a6EEEA35656271", |
||||
"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