From 4421057c8d7ac37250305c2bbf60099a269f547e Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Wed, 8 Oct 2025 15:24:27 -0400 Subject: [PATCH 1/6] feat: OP Adapter update Signed-off-by: Faisal Usmani --- contracts/chain-adapters/OP_Adapter.sol | 49 ++++++++++++++----------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/contracts/chain-adapters/OP_Adapter.sol b/contracts/chain-adapters/OP_Adapter.sol index d33817649..59081e048 100644 --- a/contracts/chain-adapters/OP_Adapter.sol +++ b/contracts/chain-adapters/OP_Adapter.sol @@ -35,31 +35,43 @@ contract OP_Adapter is CrossDomainEnabled, AdapterInterface, CircleCCTPAdapter { IL1StandardBridge public immutable L1_STANDARD_BRIDGE; IOpUSDCBridgeAdapter public immutable L1_OP_USDC_BRIDGE; + error InvalidBridgeConfig(); + /** * @notice Constructs new Adapter. * @param _l1Weth WETH address on L1. + * @param _l1Usdc USDC address on L1. * @param _crossDomainMessenger XDomainMessenger Destination chain system contract. * @param _l1StandardBridge Standard bridge contract. - * @param _l1Usdc USDC address on L1. + * @param _l1USDCBridge OP USDC bridge contract. + * @param _cctpTokenMessenger CCTP token messenger contract. + * @param _recipientCircleDomainId Circle domain ID of the destination chain. */ constructor( WETH9Interface _l1Weth, IERC20 _l1Usdc, address _crossDomainMessenger, IL1StandardBridge _l1StandardBridge, - IOpUSDCBridgeAdapter _l1USDCBridge + IOpUSDCBridgeAdapter _l1USDCBridge, + ITokenMessenger _cctpTokenMessenger, + uint32 _recipientCircleDomainId ) CrossDomainEnabled(_crossDomainMessenger) - CircleCCTPAdapter( - _l1Usdc, - // Hardcode cctp messenger to 0x0 to disable CCTP bridging. - ITokenMessenger(address(0)), - CircleDomainIds.UNINITIALIZED - ) + CircleCCTPAdapter(_l1Usdc, _cctpTokenMessenger, _recipientCircleDomainId) { L1_WETH = _l1Weth; L1_STANDARD_BRIDGE = _l1StandardBridge; L1_OP_USDC_BRIDGE = _l1USDCBridge; + + address zero = address(0); + if (address(_l1Usdc) != zero) { + bool opUSDCBridgeDisabled = address(_l1USDCBridge) == zero; + bool cctpUSDCBridgeDisabled = address(_cctpTokenMessenger) == zero; + // Bridged and Native USDC are mutually exclusive. + if (opUSDCBridgeDisabled == cctpUSDCBridgeDisabled) { + revert InvalidBridgeConfig(); + } + } } /** @@ -79,24 +91,17 @@ contract OP_Adapter is CrossDomainEnabled, AdapterInterface, CircleCCTPAdapter { * @param amount Amount of L1 tokens to deposit and L2 tokens to receive. * @param to Bridge recipient. */ - function relayTokens( - address l1Token, - address l2Token, - uint256 amount, - address to - ) external payable override { + function relayTokens(address l1Token, address l2Token, uint256 amount, address to) external payable override { // If the l1Token is weth then unwrap it to ETH then send the ETH to the standard bridge. if (l1Token == address(L1_WETH)) { L1_WETH.withdraw(amount); L1_STANDARD_BRIDGE.depositETHTo{ value: amount }(to, L2_GAS_LIMIT, ""); - } else if (l1Token == address(usdcToken)) { - if (_isCCTPEnabled()) { - _transferUsdc(to, amount); - } else { - // Use the relevant OP USDC bridge to received bridged USDC on L2. - IERC20(l1Token).safeIncreaseAllowance(address(L1_OP_USDC_BRIDGE), amount); - L1_OP_USDC_BRIDGE.sendMessage(to, amount, L2_GAS_LIMIT); - } + } else if (l1Token == address(usdcToken) && _isCCTPEnabled()) { + _transferUsdc(to, amount); + } else if (l1Token == address(usdcToken) && address(L1_OP_USDC_BRIDGE) != address(0)) { + // Use the relevant OP USDC bridge to received bridged USDC on L2. + IERC20(l1Token).safeIncreaseAllowance(address(L1_OP_USDC_BRIDGE), amount); + L1_OP_USDC_BRIDGE.sendMessage(to, amount, L2_GAS_LIMIT); } else { IERC20(l1Token).safeIncreaseAllowance(address(L1_STANDARD_BRIDGE), amount); L1_STANDARD_BRIDGE.depositERC20To(l1Token, l2Token, to, amount, L2_GAS_LIMIT, ""); From b19d379a808bc6985ee4dce790b0df7be122cb89 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Wed, 8 Oct 2025 15:36:02 -0400 Subject: [PATCH 2/6] Undo the branch logic Signed-off-by: Faisal Usmani --- contracts/chain-adapters/OP_Adapter.sol | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/contracts/chain-adapters/OP_Adapter.sol b/contracts/chain-adapters/OP_Adapter.sol index 59081e048..9261e07f1 100644 --- a/contracts/chain-adapters/OP_Adapter.sol +++ b/contracts/chain-adapters/OP_Adapter.sol @@ -96,12 +96,14 @@ contract OP_Adapter is CrossDomainEnabled, AdapterInterface, CircleCCTPAdapter { if (l1Token == address(L1_WETH)) { L1_WETH.withdraw(amount); L1_STANDARD_BRIDGE.depositETHTo{ value: amount }(to, L2_GAS_LIMIT, ""); - } else if (l1Token == address(usdcToken) && _isCCTPEnabled()) { - _transferUsdc(to, amount); - } else if (l1Token == address(usdcToken) && address(L1_OP_USDC_BRIDGE) != address(0)) { - // Use the relevant OP USDC bridge to received bridged USDC on L2. - IERC20(l1Token).safeIncreaseAllowance(address(L1_OP_USDC_BRIDGE), amount); - L1_OP_USDC_BRIDGE.sendMessage(to, amount, L2_GAS_LIMIT); + } else if (l1Token == address(usdcToken)) { + if (_isCCTPEnabled()) { + _transferUsdc(to, amount); + } else { + // Use the relevant OP USDC bridge to received bridged USDC on L2. + IERC20(l1Token).safeIncreaseAllowance(address(L1_OP_USDC_BRIDGE), amount); + L1_OP_USDC_BRIDGE.sendMessage(to, amount, L2_GAS_LIMIT); + } } else { IERC20(l1Token).safeIncreaseAllowance(address(L1_STANDARD_BRIDGE), amount); L1_STANDARD_BRIDGE.depositERC20To(l1Token, l2Token, to, amount, L2_GAS_LIMIT, ""); From c2742fc9688937a67b796b7c3d727834c75d1dea Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Wed, 8 Oct 2025 18:14:27 -0400 Subject: [PATCH 3/6] revert formatting Signed-off-by: Faisal Usmani From 660fb05841006e1be4798b4bc76fe76b31c8e5aa Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Wed, 8 Oct 2025 18:16:02 -0400 Subject: [PATCH 4/6] Added tests Signed-off-by: Faisal Usmani --- test/evm/foundry/local/OP_Adapter.t.sol | 95 +++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 test/evm/foundry/local/OP_Adapter.t.sol diff --git a/test/evm/foundry/local/OP_Adapter.t.sol b/test/evm/foundry/local/OP_Adapter.t.sol new file mode 100644 index 000000000..e2edfada9 --- /dev/null +++ b/test/evm/foundry/local/OP_Adapter.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { Test } from "forge-std/Test.sol"; + +import { ERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IL1StandardBridge } from "@eth-optimism/contracts/L1/messaging/IL1StandardBridge.sol"; +import { IOpUSDCBridgeAdapter } from "../../../../contracts/external/interfaces/IOpUSDCBridgeAdapter.sol"; +import { ITokenMessenger } from "../../../../contracts/external/interfaces/CCTPInterfaces.sol"; + +import { OP_Adapter } from "../../../../contracts/chain-adapters/OP_Adapter.sol"; +import { WETH9Interface } from "../../../../contracts/external/interfaces/WETH9Interface.sol"; +import { WETH9 } from "../../../../contracts/external/WETH9.sol"; + +contract OP_AdapterTest is Test { + ERC20 l1Usdc; + WETH9 l1Weth; + + IL1StandardBridge standardBridge; + IOpUSDCBridgeAdapter opUSDCBridge; + ITokenMessenger cctpMessenger; + + uint32 constant RECIPIENT_CIRCLE_DOMAIN_ID = 1; + + function setUp() public { + l1Usdc = new ERC20("l1Usdc", "l1Usdc"); + l1Weth = new WETH9(); + + standardBridge = IL1StandardBridge(makeAddr("standardBridge")); + opUSDCBridge = IOpUSDCBridgeAdapter(makeAddr("opUSDCBridge")); + cctpMessenger = ITokenMessenger(makeAddr("cctpMessenger")); + } + + function testUSDCNotSet() public { + new OP_Adapter( + WETH9Interface(address(l1Weth)), + IERC20(address(0)), + address(0), + standardBridge, + IOpUSDCBridgeAdapter(address(0)), + ITokenMessenger(address(0)), + RECIPIENT_CIRCLE_DOMAIN_ID + ); + } + + function testL1UsdcBridgeSet() public { + new OP_Adapter( + WETH9Interface(address(l1Weth)), + IERC20(address(l1Usdc)), + address(0), + standardBridge, + opUSDCBridge, + ITokenMessenger(address(0)), + RECIPIENT_CIRCLE_DOMAIN_ID + ); + } + + function testCctpMessengerSet() public { + new OP_Adapter( + WETH9Interface(address(l1Weth)), + IERC20(address(0)), + address(0), + standardBridge, + IOpUSDCBridgeAdapter(address(0)), + cctpMessenger, + RECIPIENT_CIRCLE_DOMAIN_ID + ); + } + + function testNeitherSet() public { + vm.expectRevert(OP_Adapter.InvalidBridgeConfig.selector); + new OP_Adapter( + WETH9Interface(address(l1Weth)), + IERC20(address(l1Usdc)), + address(0), + standardBridge, + IOpUSDCBridgeAdapter(address(0)), + ITokenMessenger(address(0)), + RECIPIENT_CIRCLE_DOMAIN_ID + ); + } + + function testBothSet() public { + vm.expectRevert(OP_Adapter.InvalidBridgeConfig.selector); + new OP_Adapter( + WETH9Interface(address(l1Weth)), + IERC20(address(l1Usdc)), + address(0), + standardBridge, + opUSDCBridge, + cctpMessenger, + RECIPIENT_CIRCLE_DOMAIN_ID + ); + } +} From 9bc48161c126a658bd085892dfa303eb53df1dc5 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Wed, 8 Oct 2025 18:28:10 -0400 Subject: [PATCH 5/6] Fixed test Signed-off-by: Faisal Usmani --- test/evm/hardhat/chain-adapters/OP_Adapter.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/evm/hardhat/chain-adapters/OP_Adapter.ts b/test/evm/hardhat/chain-adapters/OP_Adapter.ts index c5e2778bd..e3b612ed5 100644 --- a/test/evm/hardhat/chain-adapters/OP_Adapter.ts +++ b/test/evm/hardhat/chain-adapters/OP_Adapter.ts @@ -12,9 +12,11 @@ import { toWei, getContractFactory, seedWallet, + toBN, } from "../../../../utils/utils"; import { hubPoolFixture, enableTokensForLP } from "../fixtures/HubPool.Fixture"; import { constructSingleChainTree } from "../MerkleLib.utils"; +import { ZERO_ADDRESS } from "@uma/common"; let hubPool: Contract, adapter: Contract, weth: Contract, usdc: Contract, mockSpoke: Contract, timer: Contract; let l2Weth: string, l2Usdc: string; @@ -63,7 +65,9 @@ describe("OP Adapter", function () { usdc.address, l1CrossDomainMessenger.address, l1StandardBridge.address, - opUSDCBridge.address + opUSDCBridge.address, + ZERO_ADDRESS, + toBN("322") ); // Seed the HubPool some funds so it can send L1->L2 messages. await hubPool.connect(liquidityProvider).loadEthForL2Calls({ value: toWei("1") }); From 66eb6d51ed4c8c928fee4bc681aeb6ebe7cfbcb7 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Tue, 25 Nov 2025 15:56:50 -0500 Subject: [PATCH 6/6] Fixed test Signed-off-by: Faisal Usmani --- script/111DeployUniversalSpokePool.s.sol | 1 - test/evm/foundry/local/OP_Adapter.t.sol | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/script/111DeployUniversalSpokePool.s.sol b/script/111DeployUniversalSpokePool.s.sol index 1fbaeb339..712116e57 100644 --- a/script/111DeployUniversalSpokePool.s.sol +++ b/script/111DeployUniversalSpokePool.s.sol @@ -82,7 +82,6 @@ contract DeployUniversalSpokePool is Script, Test, DeploymentUtils { console.log("HubPool address:", info.hubPool); console.log("Helios address:", helios); console.log("L1 HubPoolStore address:", l1HubPoolStore); - console.log("Wrapped Native Token address:", weth); console.log("USDC address:", usdcAddress); console.log("CCTP Token Messenger:", cctpTokenMessenger); console.log("OFT DST EID:", oftDstEid); diff --git a/test/evm/foundry/local/OP_Adapter.t.sol b/test/evm/foundry/local/OP_Adapter.t.sol index e2edfada9..e9a5aa8fd 100644 --- a/test/evm/foundry/local/OP_Adapter.t.sol +++ b/test/evm/foundry/local/OP_Adapter.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import { Test } from "forge-std/Test.sol"; -import { ERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20, IERC20 } from "@openzeppelin/contracts-v4/token/ERC20/ERC20.sol"; import { IL1StandardBridge } from "@eth-optimism/contracts/L1/messaging/IL1StandardBridge.sol"; import { IOpUSDCBridgeAdapter } from "../../../../contracts/external/interfaces/IOpUSDCBridgeAdapter.sol"; import { ITokenMessenger } from "../../../../contracts/external/interfaces/CCTPInterfaces.sol";