-
Notifications
You must be signed in to change notification settings - Fork 22
Lombard Token Pool v2 flow #1437
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
Changes from all commits
7994887
1b52959
fa76d28
47da7b3
02b6520
3a2bca7
86c261e
6eee4d4
e07b37a
b3e7ff6
75c4a27
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 |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.24; | ||
|
|
||
| import {ICrossChainVerifierResolver} from "../../interfaces/ICrossChainVerifierResolver.sol"; | ||
| import {ITypeAndVersion} from "@chainlink/contracts/src/v0.8/shared/interfaces/ITypeAndVersion.sol"; | ||
|
|
||
| import {Pool} from "../../libraries/Pool.sol"; | ||
| import {TokenPool} from "../TokenPool.sol"; | ||
|
|
||
| import {IERC20} from "@openzeppelin/[email protected]/token/ERC20/IERC20.sol"; | ||
| import {IERC20Metadata} from "@openzeppelin/[email protected]/token/ERC20/extensions/IERC20Metadata.sol"; | ||
| import {SafeERC20} from "@openzeppelin/[email protected]/token/ERC20/utils/SafeERC20.sol"; | ||
|
|
||
| /// @notice Lombard CCIP token pool. | ||
| /// For v2 flows, token movement (burn/mint) is handled by the Lombard verifier, | ||
| /// 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. | ||
| contract LombardTokenPool is TokenPool, ITypeAndVersion { | ||
| using SafeERC20 for IERC20; | ||
| using SafeERC20 for IERC20Metadata; | ||
|
|
||
| error ZeroVerifierNotAllowed(); | ||
| error OutboundImplementationNotFoundForVerifier(); | ||
|
|
||
| event LombardVerifierSet(address indexed verifier); | ||
|
|
||
| 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; | ||
|
|
||
| constructor( | ||
| IERC20Metadata token, | ||
| address verifier, | ||
| address advancedPoolHooks, | ||
| address rmnProxy, | ||
| address router, | ||
| uint8 fallbackDecimals | ||
| ) TokenPool(token, _getTokenDecimals(token, fallbackDecimals), advancedPoolHooks, rmnProxy, router) { | ||
| if (verifier == address(0)) { | ||
| revert ZeroVerifierNotAllowed(); | ||
| } | ||
| i_lombardVerifierResolver = verifier; | ||
| emit LombardVerifierSet(verifier); | ||
| } | ||
|
|
||
| // ================================================================ | ||
| // │ Lock or Burn │ | ||
| // ================================================================ | ||
|
|
||
| /// @notice For IPoolV2.lockOrBurn call, this contract only forwards tokens to the verifier. | ||
| /// @dev Forward the net amount to the verifier; actual burn/bridge is done there. | ||
| function lockOrBurn( | ||
| Pool.LockOrBurnInV1 calldata lockOrBurnIn, | ||
| uint16 blockConfirmationRequested, | ||
| bytes calldata tokenArgs | ||
| ) public override returns (Pool.LockOrBurnOutV1 memory lockOrBurnOut, uint256 destTokenAmount) { | ||
| address verifierImpl = ICrossChainVerifierResolver(i_lombardVerifierResolver).getOutboundImplementation( | ||
| lockOrBurnIn.remoteChainSelector, "" | ||
| ); | ||
| if (verifierImpl == address(0)) { | ||
| revert OutboundImplementationNotFoundForVerifier(); | ||
| } | ||
| i_token.safeTransfer(verifierImpl, lockOrBurnIn.amount); | ||
| return super.lockOrBurn(lockOrBurnIn, blockConfirmationRequested, tokenArgs); | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| // ================================================================ | ||
| // │ Release or Mint │ | ||
| // ================================================================ | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| // ================================================================ | ||
| // │ Internal utils │ | ||
| // ================================================================ | ||
|
|
||
| function _getTokenDecimals(IERC20Metadata token, uint8 fallbackDecimals) internal view returns (uint8) { | ||
| try token.decimals() returns (uint8 dec) { | ||
| return dec; | ||
| } catch { | ||
| return fallbackDecimals; | ||
| } | ||
| } | ||
|
|
||
| /// @notice Returns the verifier resolver address. | ||
| function getVerifierResolver() external view returns (address) { | ||
| return i_lombardVerifierResolver; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
| pragma solidity ^0.8.24; | ||
|
|
||
| import {LombardTokenPool} from "../../pools/Lombard/LombardTokenPool.sol"; | ||
| import {IERC20Metadata} from "@openzeppelin/[email protected]/token/ERC20/extensions/IERC20Metadata.sol"; | ||
|
|
||
| contract LombardTokenPoolHelper is LombardTokenPool { | ||
| constructor( | ||
| IERC20Metadata token, | ||
| address verifier, | ||
| address rmnProxy, | ||
| address router, | ||
| uint8 fallbackDecimals | ||
| ) LombardTokenPool(token, verifier, address(0), rmnProxy, router, fallbackDecimals) {} | ||
|
|
||
| function getTokenDecimals(IERC20Metadata token, uint8 fallbackDecimals) external view returns (uint8) { | ||
| return _getTokenDecimals(token, fallbackDecimals); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
| pragma solidity ^0.8.24; | ||
|
|
||
| import {BaseTest} from "../../BaseTest.t.sol"; | ||
| import {LombardTokenPoolHelper} from "../../helpers/LombardTokenPoolHelper.sol"; | ||
| import {MockVerifier} from "../../mocks/MockVerifier.sol"; | ||
| import {BurnMintERC20} from "@chainlink/contracts/src/v0.8/shared/token/ERC20/BurnMintERC20.sol"; | ||
|
|
||
| import {IERC20Metadata} from "@openzeppelin/[email protected]/token/ERC20/extensions/IERC20Metadata.sol"; | ||
|
|
||
| contract LombardTokenPool_getTokenDecimals is BaseTest { | ||
| BurnMintERC20 internal s_token; | ||
| LombardTokenPoolHelper internal s_helper; | ||
| MockVerifier internal s_resolver; | ||
| address internal constant VERIFIER = address(0xBEEF); | ||
|
|
||
| function setUp() public override { | ||
| super.setUp(); | ||
| s_token = new BurnMintERC20("Lombard", "LBD", 18, 0, 0); | ||
| s_resolver = new MockVerifier(""); | ||
| s_helper = | ||
| new LombardTokenPoolHelper(s_token, address(s_resolver), address(s_mockRMNRemote), address(s_sourceRouter), 18); | ||
| } | ||
|
|
||
| function test_getTokenDecimals_UsesTokenDecimals() public view { | ||
| uint8 dec = s_helper.getTokenDecimals(s_token, 6); | ||
| assertEq(dec, 18); | ||
| } | ||
|
|
||
| function test_getTokenDecimals_FallsBackOnRevert() public { | ||
| vm.mockCallRevert(address(s_token), abi.encodeWithSelector(IERC20Metadata.decimals.selector), "revert"); | ||
| uint8 dec = s_helper.getTokenDecimals(IERC20Metadata(address(s_token)), 6); | ||
| assertEq(dec, 6); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
| pragma solidity ^0.8.24; | ||
|
|
||
| import {Pool} from "../../../libraries/Pool.sol"; | ||
|
|
||
| import {LombardTokenPool} from "../../../pools/Lombard/LombardTokenPool.sol"; | ||
| import {LombardTokenPoolSetup} from "./LombardTokenPoolSetup.t.sol"; | ||
|
|
||
| contract LombardTokenPool_lockOrBurn is LombardTokenPoolSetup { | ||
| function setUp() public virtual override { | ||
| super.setUp(); | ||
| vm.startPrank(s_allowedOnRamp); | ||
| } | ||
|
|
||
| function test_lockOrBurn_ForwardsToVerifier() public { | ||
| uint256 amount = 1e18; | ||
| deal(address(s_token), address(s_pool), amount); | ||
|
|
||
| (Pool.LockOrBurnOutV1 memory out, uint256 destAmount) = s_pool.lockOrBurn( | ||
| Pool.LockOrBurnInV1({ | ||
| receiver: abi.encodePacked(address(0xDEAD)), | ||
| remoteChainSelector: DEST_CHAIN_SELECTOR, | ||
| originalSender: OWNER, | ||
| amount: amount, | ||
| localToken: address(s_token) | ||
| }), | ||
| 0, | ||
| "" | ||
| ); | ||
|
|
||
| assertEq(destAmount, amount); | ||
| assertEq(out.destTokenAddress, abi.encode(s_remoteToken)); | ||
| assertEq(out.destPoolData, abi.encode(uint8(DEFAULT_TOKEN_DECIMALS))); | ||
| assertEq(s_token.balanceOf(s_verifierResolver.getOutboundImplementation(DEST_CHAIN_SELECTOR, "")), amount); | ||
| assertEq(s_token.balanceOf(address(s_pool)), 0); | ||
| } | ||
|
|
||
| function test_lockOrBurn_RevertWhen_OutboundImplementationNotFoundForVerifier() public { | ||
| uint256 amount = 1e18; | ||
| deal(address(s_token), address(s_pool), amount); | ||
| vm.mockCall( | ||
| address(s_verifierResolver), | ||
| abi.encodeCall(s_verifierResolver.getOutboundImplementation, (DEST_CHAIN_SELECTOR, "")), | ||
| abi.encode(address(0)) | ||
| ); | ||
|
|
||
| vm.expectRevert(LombardTokenPool.OutboundImplementationNotFoundForVerifier.selector); | ||
| s_pool.lockOrBurn( | ||
| Pool.LockOrBurnInV1({ | ||
| receiver: abi.encodePacked(address(0xDEAD)), | ||
| remoteChainSelector: DEST_CHAIN_SELECTOR, | ||
| originalSender: OWNER, | ||
| amount: amount, | ||
| localToken: address(s_token) | ||
| }), | ||
| 0, | ||
| "" | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
| pragma solidity ^0.8.24; | ||
|
|
||
| import {LombardTokenPool} from "../../../pools/Lombard/LombardTokenPool.sol"; | ||
| import {MockVerifier} from "../../mocks/MockVerifier.sol"; | ||
| import {BurnMintERC20} from "@chainlink/contracts/src/v0.8/shared/token/ERC20/BurnMintERC20.sol"; | ||
| import {Test} from "forge-std/Test.sol"; | ||
|
|
||
| contract LombardTokenPool_constructor is Test { | ||
| BurnMintERC20 internal s_token; | ||
| address internal s_resolver; | ||
| address internal constant RMN = address(0xAA01); | ||
| address internal constant ROUTER = address(0xBB02); | ||
|
|
||
| function setUp() public { | ||
|
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. No need to setup for a single test, can be in the test
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. this file has one more test now
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. |
||
| s_token = new BurnMintERC20("Lombard", "LBD", 18, 0, 0); | ||
| s_resolver = address(new MockVerifier("")); | ||
| } | ||
|
|
||
| function test_constructor() public { | ||
| vm.expectEmit(); | ||
| emit LombardTokenPool.LombardVerifierSet(s_resolver); | ||
| LombardTokenPool pool = new LombardTokenPool(s_token, s_resolver, address(0), RMN, ROUTER, 18); | ||
| assertEq(pool.getVerifierResolver(), address(s_resolver)); | ||
| assertEq(pool.typeAndVersion(), "LombardTokenPool 1.7.0-dev"); | ||
| } | ||
|
|
||
| function test_constructor_ZeroVerifierNotAllowed() public { | ||
| vm.expectRevert(LombardTokenPool.ZeroVerifierNotAllowed.selector); | ||
| new LombardTokenPool(s_token, address(0), address(0), RMN, ROUTER, 18); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
| pragma solidity ^0.8.24; | ||
|
|
||
| import {Router} from "../../../Router.sol"; | ||
| import {TokenPool} from "../../../pools/TokenPool.sol"; | ||
| import {LombardTokenPoolHelper} from "../../helpers/LombardTokenPoolHelper.sol"; | ||
| import {MockVerifier} from "../../mocks/MockVerifier.sol"; | ||
| import {TokenPoolSetup} from "../TokenPool/TokenPoolSetup.t.sol"; | ||
|
|
||
| contract LombardTokenPoolSetup is TokenPoolSetup { | ||
| LombardTokenPoolHelper internal s_pool; | ||
| MockVerifier internal s_verifierResolver; | ||
| address internal constant VERIFIER_IMPL = address(0x2345); | ||
| address internal s_remotePool = makeAddr("remotePool"); | ||
| address internal s_remoteToken = makeAddr("remoteToken"); | ||
|
|
||
| function setUp() public virtual override { | ||
| super.setUp(); | ||
|
|
||
| s_verifierResolver = new MockVerifier(""); | ||
|
|
||
| s_pool = new LombardTokenPoolHelper( | ||
| s_token, address(s_verifierResolver), address(s_mockRMNRemote), address(s_sourceRouter), DEFAULT_TOKEN_DECIMALS | ||
| ); | ||
|
|
||
| // Configure remote chain. | ||
| bytes[] memory remotePools = new bytes[](1); | ||
| remotePools[0] = abi.encode(s_remotePool); | ||
|
|
||
| TokenPool.ChainUpdate[] memory chainUpdate = new TokenPool.ChainUpdate[](1); | ||
| chainUpdate[0] = TokenPool.ChainUpdate({ | ||
| remoteChainSelector: DEST_CHAIN_SELECTOR, | ||
| remotePoolAddresses: remotePools, | ||
| remoteTokenAddress: abi.encode(s_remoteToken), | ||
| outboundRateLimiterConfig: _getOutboundRateLimiterConfig(), | ||
| inboundRateLimiterConfig: _getInboundRateLimiterConfig() | ||
| }); | ||
|
|
||
| vm.startPrank(OWNER); | ||
| s_pool.applyChainUpdates(new uint64[](0), chainUpdate); | ||
|
|
||
| Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); | ||
| onRampUpdates[0] = Router.OnRamp({destChainSelector: DEST_CHAIN_SELECTOR, onRamp: s_allowedOnRamp}); | ||
| Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); | ||
| offRampUpdates[0] = Router.OffRamp({sourceChainSelector: DEST_CHAIN_SELECTOR, offRamp: s_allowedOffRamp}); | ||
| s_sourceRouter.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); | ||
| } | ||
| } |
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.
If we change to proxy we have to update this anyways, can just make the comment more explicit.
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.
will fix in next PR