-
Notifications
You must be signed in to change notification settings - Fork 23
Lombard v2 pool backward compatibility #1440
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
97dca81
f304f2e
b630d8f
b53101d
6e4ea50
c78fa14
c700ee6
4d79408
6f0223c
04d79fb
894fb3e
cf9dcdc
70db73d
3847066
d1d304a
dce3b68
f121fcc
cc1f3d4
73fc783
c197222
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,8 @@ | |||||
| pragma solidity ^0.8.24; | ||||||
|
|
||||||
| import {ICrossChainVerifierResolver} from "../../interfaces/ICrossChainVerifierResolver.sol"; | ||||||
| import {IBridgeV1} from "./interfaces/IBridgeV1.sol"; | ||||||
| import {IMailbox} from "./interfaces/IMailbox.sol"; | ||||||
| import {ITypeAndVersion} from "@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol"; | ||||||
|
|
||||||
| import {Pool} from "../../libraries/Pool.sol"; | ||||||
|
|
@@ -16,34 +18,87 @@ import {SafeERC20} from "@openzeppelin/contracts@4.8.3/token/ERC20/utils/SafeERC | |||||
| /// the pool performs validation, rate limiting, accounting and event emission. | ||||||
| /// IPoolV2.lockOrBurn forwards tokens to the verifier. | ||||||
| /// IPoolV2.releaseOrMint does not move tokens, _releaseOrMint is a no-op. | ||||||
| /// TODO: Add explicit V1 support/backwards compatibility. | ||||||
| /// IPoolV1.lockOrBurn and IPoolV1.releaseOrMint make this pool backwards compatible with old lanes. | ||||||
| contract LombardTokenPool is TokenPool, ITypeAndVersion { | ||||||
| using SafeERC20 for IERC20; | ||||||
| using SafeERC20 for IERC20Metadata; | ||||||
|
|
||||||
| error ZeroVerifierNotAllowed(); | ||||||
| error OutboundImplementationNotFoundForVerifier(); | ||||||
| error ZeroBridge(); | ||||||
| error ZeroLombardChainId(); | ||||||
| error PathNotExist(uint64 remoteChainSelector); | ||||||
| error InvalidMessageVersion(uint8 expected, uint8 received); | ||||||
| error RemoteTokenMismatch(bytes32 bridge, bytes32 pool); | ||||||
| error InvalidReceiver(bytes receiver); | ||||||
| error ChainNotSupported(uint64 remoteChainSelector); | ||||||
| error InvalidAllowedCaller(bytes allowedCaller); | ||||||
| error ExecutionError(); | ||||||
| error HashMismatch(); | ||||||
|
|
||||||
| event LombardVerifierSet(address indexed verifier); | ||||||
| /// The following events are emitted for Lombard-specific configuration updates and are utilized by Lombard. | ||||||
| /// @param remoteChainSelector CCIP selector of destination chain. | ||||||
| /// @param lChainId The chain ID according to Lombard Multi Chain ID convention. | ||||||
| /// @param allowedCaller The address that's allowed to call the bridge on the destination chain. | ||||||
| event PathSet(uint64 indexed remoteChainSelector, bytes32 indexed lChainId, bytes32 allowedCaller); | ||||||
| /// @param remoteChainSelector CCIP selector of destination chain. | ||||||
| /// @param lChainId The chain id of destination chain by Lombard Multi Chain Id conversion. | ||||||
| /// @param allowedCaller The address that's allowed to call the bridge on the destination chain. | ||||||
| event PathRemoved(uint64 indexed remoteChainSelector, bytes32 indexed lChainId, bytes32 allowedCaller); | ||||||
| event LombardConfigurationSet(address indexed verifier, address indexed bridge, address indexed tokenAdapter); | ||||||
|
|
||||||
| struct Path { | ||||||
| /// @notice The address that's allowed to call the bridge on the destination chain. | ||||||
| bytes32 allowedCaller; | ||||||
| /// @notice Lombard chain id of destination chain. | ||||||
| bytes32 lChainId; | ||||||
| } | ||||||
|
|
||||||
| string public constant override typeAndVersion = "LombardTokenPool 1.7.0-dev"; | ||||||
|
|
||||||
| /// @notice Lombard verifier proxy / resolver address. lockOrBurn fetches the outbound implementation and forwards tokens to it. | ||||||
| address private immutable i_lombardVerifierResolver; | ||||||
| /// @notice Supported bridge message version. | ||||||
| uint8 internal constant SUPPORTED_BRIDGE_MSG_VERSION = 1; | ||||||
| /// @notice The address of bridge contract. | ||||||
| IBridgeV1 public immutable i_bridge; | ||||||
| /// @notice Lombard verifier resolver address. lockOrBurn fetches the outbound implementation and forwards tokens to it. | ||||||
| address internal immutable i_lombardVerifierResolver; | ||||||
| /// @notice Optional token adapter used for chains like Avalanche BTC.b. Since each pool manages a single token, | ||||||
| /// and the adapter is a source-chain-level replacement for that token, there can only be one adapter per pool. | ||||||
| address internal immutable i_tokenAdapter; | ||||||
|
|
||||||
| /// @notice Mapping of CCIP chain selector to chain specific config. | ||||||
| mapping(uint64 chainSelector => Path path) internal s_chainSelectorToPath; | ||||||
|
|
||||||
| /// @param verifier The address of Lombard verifier resolver. Used in V2 flows to fetch the outbound | ||||||
| /// implementation that handles token burns and cross-chain attestations. | ||||||
| /// @param bridge The Lombard BridgeV1 contract that handles cross-chain token transfers. | ||||||
| /// @param adapter Optional source-chain token address override. Used for non-upgradeable tokens like BTC.b | ||||||
| /// on Avalanche where an adapter contract performs mint/burn on behalf of the actual token. When set, this | ||||||
| /// address is passed to bridge.deposit() instead of the pool's token address. Set to address(0) if not needed. | ||||||
| constructor( | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's at least add comments for the lombard params, what an adapter is and that it's optional
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
| IERC20Metadata token, | ||||||
| address verifier, | ||||||
| IBridgeV1 bridge, | ||||||
| address adapter, | ||||||
| address advancedPoolHooks, | ||||||
RensR marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| address rmnProxy, | ||||||
| address router, | ||||||
| uint8 fallbackDecimals | ||||||
| ) TokenPool(token, _getTokenDecimals(token, fallbackDecimals), advancedPoolHooks, rmnProxy, router) { | ||||||
| if (address(bridge) == address(0)) { | ||||||
| revert ZeroBridge(); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WIll fix later after confirmation from Lombard, this is from the old code |
||||||
| } | ||||||
| uint8 bridgeMsgVersion = bridge.MSG_VERSION(); | ||||||
| if (bridgeMsgVersion != SUPPORTED_BRIDGE_MSG_VERSION) { | ||||||
| revert InvalidMessageVersion(SUPPORTED_BRIDGE_MSG_VERSION, bridgeMsgVersion); | ||||||
| } | ||||||
| if (verifier == address(0)) { | ||||||
| revert ZeroVerifierNotAllowed(); | ||||||
| } | ||||||
| i_bridge = bridge; | ||||||
| i_lombardVerifierResolver = verifier; | ||||||
| emit LombardVerifierSet(verifier); | ||||||
| i_tokenAdapter = adapter; | ||||||
| emit LombardConfigurationSet(verifier, address(bridge), adapter); | ||||||
| } | ||||||
|
|
||||||
| // ================================================================ | ||||||
|
|
@@ -67,22 +122,141 @@ contract LombardTokenPool is TokenPool, ITypeAndVersion { | |||||
| return super.lockOrBurn(lockOrBurnIn, blockConfirmationRequested, tokenArgs); | ||||||
| } | ||||||
|
|
||||||
| /// @notice Backwards compatible lockOrBurn for lanes using the V1 flow. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the invariant here is that there will always be a pre 1.7.0 OffRamp available to process any in-flight or failed V1 Lombard messages? This is why we don't need any proxy?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whether we use a proxy or not is unrelated to if there exists a pre-1.7 ramp. A proxy isn't needed because Lombard will only use one method long term, while USDC will always run multiple protocols and maintain liquidity for some. |
||||||
| /// @dev Token minting is performed by the Lombard bridge's mailbox during deliverAndHandle. | ||||||
| /// This pool only validates the proof and emits events; no _lockOrBurn call is needed. | ||||||
| function lockOrBurn( | ||||||
| Pool.LockOrBurnInV1 calldata | ||||||
| ) public pure override(TokenPool) returns (Pool.LockOrBurnOutV1 memory lockOrBurnOut) { | ||||||
| // TODO: Implement V1 path for backward compatability with old lanes. | ||||||
| return lockOrBurnOut; | ||||||
| Pool.LockOrBurnInV1 calldata lockOrBurnIn | ||||||
| ) public override(TokenPool) returns (Pool.LockOrBurnOutV1 memory lockOrBurnOut) { | ||||||
| _validateLockOrBurn(lockOrBurnIn, WAIT_FOR_FINALITY, ""); | ||||||
|
|
||||||
| Path memory path = s_chainSelectorToPath[lockOrBurnIn.remoteChainSelector]; | ||||||
| if (path.allowedCaller == bytes32(0)) { | ||||||
| revert PathNotExist(lockOrBurnIn.remoteChainSelector); | ||||||
RensR marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| } | ||||||
|
|
||||||
| // For some tokens we need to override the source token with an adapter | ||||||
| address sourceTokenOrAdapter = i_tokenAdapter != address(0) ? i_tokenAdapter : address(i_token); | ||||||
| // verify bridge destination token equal to pool | ||||||
| bytes32 bridgeDestToken = i_bridge.getAllowedDestinationToken(path.lChainId, sourceTokenOrAdapter); | ||||||
| bytes32 poolDestToken = abi.decode(getRemoteToken(lockOrBurnIn.remoteChainSelector), (bytes32)); | ||||||
| if (bridgeDestToken != poolDestToken) { | ||||||
| revert RemoteTokenMismatch(bridgeDestToken, poolDestToken); | ||||||
| } | ||||||
|
|
||||||
| if (lockOrBurnIn.receiver.length != 32) { | ||||||
| revert InvalidReceiver(lockOrBurnIn.receiver); | ||||||
| } | ||||||
|
|
||||||
| (, bytes32 payloadHash) = i_bridge.deposit({ | ||||||
| destinationChain: path.lChainId, | ||||||
| token: sourceTokenOrAdapter, | ||||||
| sender: lockOrBurnIn.originalSender, | ||||||
| recipient: abi.decode(lockOrBurnIn.receiver, (bytes32)), | ||||||
| amount: lockOrBurnIn.amount, | ||||||
| destinationCaller: path.allowedCaller | ||||||
| }); | ||||||
|
|
||||||
| emit LockedOrBurned({ | ||||||
| remoteChainSelector: lockOrBurnIn.remoteChainSelector, | ||||||
| token: address(i_token), | ||||||
| sender: lockOrBurnIn.originalSender, | ||||||
| amount: lockOrBurnIn.amount | ||||||
| }); | ||||||
|
|
||||||
| return Pool.LockOrBurnOutV1({ | ||||||
| destTokenAddress: getRemoteToken(lockOrBurnIn.remoteChainSelector), | ||||||
| destPoolData: abi.encode(payloadHash) | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| // ================================================================ | ||||||
| // │ Release or Mint │ | ||||||
| // ================================================================ | ||||||
|
|
||||||
| /// @notice Backwards compatible releaseOrMint for CCIP 1.5/1.6 lanes. Verifies the bridge payload proof. | ||||||
| function releaseOrMint( | ||||||
| Pool.ReleaseOrMintInV1 calldata | ||||||
| ) public pure override(TokenPool) returns (Pool.ReleaseOrMintOutV1 memory releaseOrMintOut) { | ||||||
| // TODO: Implement V1 path for backward compatability with old lanes. | ||||||
| return releaseOrMintOut; | ||||||
| Pool.ReleaseOrMintInV1 calldata releaseOrMintIn | ||||||
| ) public virtual override returns (Pool.ReleaseOrMintOutV1 memory) { | ||||||
| _validateReleaseOrMint(releaseOrMintIn, releaseOrMintIn.sourceDenominatedAmount, WAIT_FOR_FINALITY); | ||||||
|
|
||||||
| (bytes memory rawPayload, bytes memory proof) = abi.decode(releaseOrMintIn.offchainTokenData, (bytes, bytes)); | ||||||
|
|
||||||
| (bytes32 payloadHash, bool executed,) = IMailbox(i_bridge.mailbox()).deliverAndHandle(rawPayload, proof); | ||||||
| if (!executed) { | ||||||
| revert ExecutionError(); | ||||||
| } | ||||||
| // we know payload hash returned on source chain. | ||||||
| if (payloadHash != abi.decode(releaseOrMintIn.sourcePoolData, (bytes32))) { | ||||||
| revert HashMismatch(); | ||||||
| } | ||||||
|
|
||||||
| emit ReleasedOrMinted({ | ||||||
| remoteChainSelector: releaseOrMintIn.remoteChainSelector, | ||||||
| token: address(i_token), | ||||||
| sender: msg.sender, | ||||||
| recipient: releaseOrMintIn.receiver, | ||||||
| amount: releaseOrMintIn.sourceDenominatedAmount | ||||||
| }); | ||||||
|
|
||||||
| return Pool.ReleaseOrMintOutV1({destinationAmount: releaseOrMintIn.sourceDenominatedAmount}); | ||||||
| } | ||||||
|
|
||||||
| // ================================================================ | ||||||
| // │ Path config │ | ||||||
| // ================================================================ | ||||||
|
|
||||||
| /// @notice Gets the path for a given CCIP chain selector. | ||||||
| /// @param remoteChainSelector CCIP chain selector of remote chain. | ||||||
| /// @return Path struct containing lChainId and allowedCaller. | ||||||
| function getPath( | ||||||
| uint64 remoteChainSelector | ||||||
| ) external view returns (Path memory) { | ||||||
| return s_chainSelectorToPath[remoteChainSelector]; | ||||||
| } | ||||||
|
|
||||||
| /// @notice Sets the Lombard chain id and allowed caller for a CCIP chain selector. | ||||||
| /// @param remoteChainSelector CCIP chain selector of remote chain. | ||||||
| /// @param lChainId Lombard chain id of remote chain. | ||||||
| /// @param allowedCaller The address of TokenPool on destination chain. | ||||||
| function setPath(uint64 remoteChainSelector, bytes32 lChainId, bytes calldata allowedCaller) external onlyOwner { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better to just accept bytes32 for allowedCaller & abi.encode it for the isRemotePool check? Then we can remove the allowedCaller length check right?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. old Lombard function will discuss with them and change |
||||||
| if (!isSupportedChain(remoteChainSelector)) { | ||||||
| revert ChainNotSupported(remoteChainSelector); | ||||||
| } | ||||||
|
|
||||||
| if (lChainId == bytes32(0)) { | ||||||
| revert ZeroLombardChainId(); | ||||||
| } | ||||||
|
|
||||||
| // only remote pool is expected allowed caller. | ||||||
| if (!isRemotePool(remoteChainSelector, allowedCaller)) { | ||||||
| revert InvalidRemotePoolForChain(remoteChainSelector, allowedCaller); | ||||||
| } | ||||||
|
|
||||||
| if (allowedCaller.length != 32) { | ||||||
| revert InvalidAllowedCaller(allowedCaller); | ||||||
| } | ||||||
| bytes32 decodedAllowedCaller = abi.decode(allowedCaller, (bytes32)); | ||||||
|
|
||||||
| s_chainSelectorToPath[remoteChainSelector] = Path({lChainId: lChainId, allowedCaller: decodedAllowedCaller}); | ||||||
|
|
||||||
| emit PathSet(remoteChainSelector, lChainId, decodedAllowedCaller); | ||||||
| } | ||||||
|
|
||||||
| /// @notice Removes path mapping for a destination chain. | ||||||
| /// @param remoteChainSelector CCIP chain selector of destination chain. | ||||||
| function removePath( | ||||||
| uint64 remoteChainSelector | ||||||
| ) external onlyOwner { | ||||||
| Path memory path = s_chainSelectorToPath[remoteChainSelector]; | ||||||
|
|
||||||
| if (path.allowedCaller == bytes32(0)) { | ||||||
| revert PathNotExist(remoteChainSelector); | ||||||
| } | ||||||
|
|
||||||
| delete s_chainSelectorToPath[remoteChainSelector]; | ||||||
|
|
||||||
| emit PathRemoved(remoteChainSelector, path.lChainId, path.allowedCaller); | ||||||
| } | ||||||
|
|
||||||
| // ================================================================ | ||||||
|
|
@@ -97,8 +271,11 @@ contract LombardTokenPool is TokenPool, ITypeAndVersion { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// @notice Returns the verifier resolver address. | ||||||
| function getVerifierResolver() external view returns (address) { | ||||||
| return i_lombardVerifierResolver; | ||||||
| /// @notice Returns the Lombard-specific configuration for this pool. | ||||||
| /// @return verifierResolver The address of the Lombard verifier resolver. | ||||||
| /// @return bridge The address of the Lombard bridge contract. | ||||||
| /// @return tokenAdapter The optional token adapter address (address(0) if not used). | ||||||
| function getLombardConfig() external view returns (address verifierResolver, address bridge, address tokenAdapter) { | ||||||
| return (i_lombardVerifierResolver, address(i_bridge), i_tokenAdapter); | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.24; | ||
|
|
||
| /// @custom:security-contact legal@lombard.finance | ||
| interface IBridgeV1 { | ||
| event DestinationBridgeSet(bytes32 indexed destinationChain, bytes32 indexed destinationBridge); | ||
| event DestinationTokenAdded( | ||
| bytes32 indexed destinationChain, bytes32 indexed destinationToken, address indexed sourceToken | ||
| ); | ||
| event DestinationTokenRemoved( | ||
| bytes32 indexed destinationChain, bytes32 indexed destinationToken, address indexed sourceToken | ||
| ); | ||
| event RateLimitsSet(address indexed token, bytes32 indexed sourceChainId, uint256 limit, uint256 window); | ||
|
|
||
| event SenderConfigChanged(address indexed sender, uint32 feeDiscount, bool whitelisted); | ||
|
|
||
| /// @notice Emitted when the is a deposit in the bridge | ||
| event DepositToBridge(address indexed fromAddress, bytes32 indexed toAddress, bytes32 indexed payloadHash); | ||
|
|
||
| /// @notice Emitted when a withdraw is made from the bridge | ||
| event WithdrawFromBridge(address indexed recipient, bytes32 indexed chainId, address indexed token, uint256 amount); | ||
|
|
||
| function mailbox() external view returns (address); | ||
|
|
||
| function MSG_VERSION() external view returns (uint8); | ||
|
|
||
| function deposit( | ||
| bytes32 destinationChain, | ||
| address token, | ||
| address sender, | ||
| bytes32 recipient, | ||
| uint256 amount, | ||
| bytes32 destinationCaller | ||
| ) external payable returns (uint256, bytes32); | ||
|
|
||
| function getAllowedDestinationToken(bytes32 destinationChain, address sourceToken) external view returns (bytes32); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.24; | ||
|
|
||
| interface IMailbox { | ||
| error Mailbox_ZeroChainId(); | ||
| error Mailbox_ZeroConsortium(); | ||
| error Mailbox_ZeroMailbox(); | ||
| error Mailbox_ZeroRecipient(); | ||
| error Mailbox_ZeroAmount(); | ||
| error Mailbox_MessagePathEnabled(bytes32 id); | ||
| error Mailbox_MessagePathDisabled(bytes32 id); | ||
| error Mailbox_UnexpectedDestinationCaller(address expected, address actual); | ||
| error Mailbox_HandlerNotImplemented(); | ||
| error Mailbox_PayloadOversize(uint32 max, uint256 actual); | ||
| error Mailbox_NotEnoughFee(uint256 expected, uint256 actual); | ||
| error Mailbox_CallFailed(); | ||
|
|
||
| event MessagePathEnabled( | ||
| bytes32 indexed destinationChain, | ||
| bytes32 indexed inboundMessagePath, | ||
| bytes32 indexed outboundMessagePath, | ||
| bytes32 destinationMailbox | ||
| ); | ||
|
|
||
| event MessagePathDisabled( | ||
| bytes32 indexed destinationChain, | ||
| bytes32 indexed inboundMessagePath, | ||
| bytes32 indexed outboundMessagePath, | ||
| bytes32 destinationMailbox | ||
| ); | ||
|
|
||
| event MessageSent( | ||
| bytes32 indexed destinationLChainId, address indexed msgSender, bytes32 indexed recipient, bytes payload | ||
| ); | ||
|
|
||
| /// Message payment receipt | ||
| event MessagePaid(bytes32 indexed payloadHash, address indexed msgSender, uint256 payloadSize, uint256 value); | ||
|
|
||
| event MessageDelivered( | ||
| bytes32 indexed payloadHash, address indexed caller, uint256 indexed nonce, bytes32 msgSender, bytes payload | ||
| ); | ||
|
|
||
| event MessageHandled(bytes32 indexed payloadHash, address indexed destinationCaller, bytes executionResult); | ||
|
|
||
| event MessageHandleError( | ||
| bytes32 indexed payloadHash, address indexed destinationCaller, string reason, bytes customError | ||
| ); | ||
|
|
||
| event SenderConfigUpdated(address indexed sender, uint64 maxPayloadSize, bool feeDisabled); | ||
|
|
||
| event DefaultPayloadSizeSet(uint64 maxPayloadSize); | ||
|
|
||
| event FeePerByteSet(uint256 fee); | ||
|
|
||
| event FeeWithdrawn(address indexed treasury, uint256 amount); | ||
|
|
||
| function deliverAndHandle( | ||
| bytes calldata rawPayload, | ||
| bytes calldata proof | ||
| ) external returns (bytes32, bool, bytes memory); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For PathSet & PathRemoved, we should specify if Lombard depends on them & how they are used given that they are named this way for a reason.