Skip to content
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

release: v1.2.3 #95

Merged
merged 129 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
129 commits
Select commit Hold shift + click to select a range
b97102c
Skeleton for bridged usdc gateway
gvladika May 17, 2024
0913a39
burn locked USDC draft
gvladika May 17, 2024
e54ad2c
Draft L2USDCGateway
gvladika May 17, 2024
6ca29d3
Use ownable
gvladika May 20, 2024
7ce93c7
Make depositing pausable
gvladika May 20, 2024
deab8b0
Make withdrawals pausable
gvladika May 20, 2024
7822064
Send L1-L2 msg to pause withdrawals
gvladika May 20, 2024
03e1ea3
Mark initializer, check if paused before burning
gvladika May 20, 2024
402c649
Add test skeleton
gvladika May 20, 2024
5f065e0
Add events
gvladika May 21, 2024
9a0d25c
Add tests
gvladika May 21, 2024
bc1c2a2
Add pauseDeposits tests
gvladika May 21, 2024
28aa86a
Test burnLockedUSDC
gvladika May 21, 2024
3d388ff
Add outboundTransfer tests
gvladika May 21, 2024
ad28ba8
Add finalize inbound transfe test
gvladika May 22, 2024
ae406d1
Check if params are address(0)
gvladika May 22, 2024
558c81a
Add l2 usdc gateway tests
gvladika May 22, 2024
55d0f2a
Add more tests
gvladika May 22, 2024
a81f5e5
Add pauseWithdrawals tests
gvladika May 23, 2024
4b60ef6
Check outbound transfer reverts when withdrawals are paused
gvladika May 23, 2024
799d31b
Implement ownable directly into base contract
gvladika May 23, 2024
40ec0cd
Merge branch 'main' into bridged-usdc-gw
gvladika May 23, 2024
84d43f1
First draft of e2e test
gvladika May 24, 2024
e133a66
Fix registration
gvladika May 24, 2024
e3b6dfb
Wait for L2 msg
gvladika May 24, 2024
761d791
Pause and check it was successful
gvladika May 24, 2024
1bf071e
Still in progress
gvladika May 24, 2024
d305518
Use mock USDC implementations
gvladika May 26, 2024
9feb6ff
Test burning the L1 USDC supply
gvladika May 26, 2024
1f867a0
Simulate ownership transfer
gvladika May 26, 2024
dac051e
Run test only for eth based chains
gvladika May 26, 2024
dfd5b48
Make existing tests work with eth based chains
gvladika May 27, 2024
65d604b
Run CI e2e tests for both erc20/eth chains
gvladika May 27, 2024
b79d4d8
Fix format
gvladika May 27, 2024
1ce46a3
Fix test
gvladika May 27, 2024
33d8be2
Add natspec
gvladika May 27, 2024
58a0ecc
Add natspec
gvladika May 27, 2024
8975eb4
Make L2 isdc gateway ownable. Don't use cross-chain msgs for pausing
gvladika May 29, 2024
3774d25
Provide referent bridged USDC implementation - BridgedUsdcCustomToken
gvladika May 29, 2024
44c26b7
Fund accounts
gvladika May 29, 2024
c1c088c
Add fee token version of custom usdc gateway
gvladika Jun 3, 2024
28d299c
Add Foundry tests for L1 fee token usdc gateway
gvladika Jun 4, 2024
68e76dd
Add coverage
gvladika Jun 4, 2024
d8f336c
Add e2e test for L1 fee token usdc gateway
gvladika Jun 4, 2024
de4a2d9
Fix test
gvladika Jun 4, 2024
76b93ab
First draft
gvladika Jun 5, 2024
f971a57
Add Solady
gvladika Jun 6, 2024
89bede4
Remove solady
gvladika Jun 6, 2024
d3b69b8
Add helper function to deploy usdc token from bytecode
gvladika Jun 6, 2024
4b86a99
Deploy and init FiatTokenArbitrumOrbitV2_2 in FOundry tests
gvladika Jun 6, 2024
bc9865b
Add back initialization
gvladika Jun 6, 2024
59457fc
Use FiatTokenArbitrumOrbitV2_2 for usdc token
gvladika Jun 6, 2024
cb941df
Update test
gvladika Jun 6, 2024
98d711e
Add initializtion of usdc token
gvladika Jun 7, 2024
5b1f6bf
Update refs
gvladika Jun 7, 2024
0c899ae
Remove unused
gvladika Jun 7, 2024
53dc0d2
Set minter in separate call
gvladika Jun 7, 2024
46550ee
Set minter in separate call in e2e tests
gvladika Jun 7, 2024
c22559c
Rename contracts
gvladika Jun 7, 2024
2ee063f
Merge branch 'bridged-usdc-gw' into arb-usdc
gvladika Jun 7, 2024
16f5e8c
FOrmatting
gvladika Jun 7, 2024
766d5e1
Use latest token implementation
gvladika Jun 7, 2024
bda2829
Formatting
gvladika Jun 7, 2024
4951bad
Merge branch 'bridged-usdc-gw' into arb-usdc
gvladika Jun 7, 2024
09436a5
Make USDC gateway burn and mint tokens directly
gvladika Jun 17, 2024
8b4460f
Adapt l1address check for USDC gateway
gvladika Jun 18, 2024
f5009a9
Add ability to unpause bridging
gvladika Jun 18, 2024
52288c2
Make burning the USDC only avaiable to separate burner account
gvladika Jun 18, 2024
1ad8584
Keep base gateway unchanged
gvladika Jun 18, 2024
d1d9f5a
Send L2 supply from l2gw to l1gw when withdrawals are paused
gvladika Jun 18, 2024
1b654d6
Remove ability to unpause
gvladika Jun 18, 2024
635c6ea
Simplify finalizeInboundTransfer
gvladika Jun 19, 2024
e60232b
Extract IFiatToken interface
gvladika Jun 19, 2024
97c062e
Merge branch 'bridged-usdc-gw' into arb-usdc
gvladika Jun 19, 2024
9dab9f4
Use FiatTokenV2_2
gvladika Jun 19, 2024
925b340
Bump SDK
gvladika Jun 20, 2024
d19bb45
Adapt network registration to the new SDK requirements
gvladika Jun 20, 2024
d345b73
Update e2e test to latest design
gvladika Jun 20, 2024
9a33cc0
Update e2e test for fee token chains
gvladika Jun 20, 2024
700be14
Merge pull request #88 from OffchainLabs/arb-usdc
gvladika Jun 20, 2024
ae73006
Re-set L2 supply after burning
gvladika Jun 20, 2024
5d5e589
Update slither db
gvladika Jun 20, 2024
538e4c5
Update natspec
gvladika Jun 20, 2024
226c57f
Re-order storage
gvladika Jun 20, 2024
053e1f0
Add test
gvladika Jun 20, 2024
a55ebb0
Remove unused contract
gvladika Jun 20, 2024
4f9f7ba
Remove implementation from child contract
gvladika Jun 21, 2024
8628836
Check address validity when finalizing deposit in internal function
gvladika Jun 21, 2024
11ebb25
Use _isValidTokenAddress in withdrawal entrypoint
gvladika Jun 21, 2024
43d1eb9
Natspec
gvladika Jun 21, 2024
e3de5b3
Slither
gvladika Jun 21, 2024
de23c24
Add package with circle's usdc code
gvladika Jun 21, 2024
d218bd9
Use bytecode from package
gvladika Jun 21, 2024
c8a4f72
deploy sig checker from imported bytecode
gvladika Jun 21, 2024
afb8544
Merge pull request #89 from OffchainLabs/refactor-internal-func
gzeoneth Jun 24, 2024
363f13f
chore: bump stablecoin version
gzeoneth Jun 24, 2024
4febc71
Remove unrelated script
gvladika Jun 24, 2024
5b8a2c5
Do not send L2 to L1 msg
gvladika Jul 1, 2024
84b1864
Owner ssets the burn amount
gvladika Jul 1, 2024
3b5eb00
Deposits can be unpaused
gvladika Jul 1, 2024
b7abeeb
Make withdrawals unpausable
gvladika Jul 1, 2024
d277437
Update e2e
gvladika Jul 1, 2024
e6ecd2d
Slither exception
gvladika Jul 1, 2024
49d69c6
Add transferUSDCRoles function
gvladika Jul 2, 2024
24b3da7
Adapt e2e tests
gvladika Jul 2, 2024
eca36f7
Slither update
gvladika Jul 2, 2024
5211e5e
Update natspec
gvladika Jul 2, 2024
684416f
Slither update
gvladika Jul 3, 2024
25672c7
Update natspec
gvladika Jul 10, 2024
43b48db
Merge branch 'develop' into bridged-usdc-gw
gvladika Jul 10, 2024
b877058
ADjust fee amounts
gvladika Jul 10, 2024
08a65a7
Use deafult nitro-testnode branch
gvladika Jul 11, 2024
965eb5f
Add missing storage layout files
gvladika Jul 11, 2024
d2c09d2
Add missing sig files
gvladika Jul 11, 2024
130a75d
Add script to check for unused errors
gvladika Jul 23, 2024
712f970
Add check to CI
gvladika Jul 23, 2024
3633405
Use gh action to check for errors
gvladika Jul 24, 2024
c6a94af
Test CI
gvladika Jul 24, 2024
a9d61ad
Remove test
gvladika Jul 24, 2024
d1421d8
Merge pull request #92 from OffchainLabs/use-action
gvladika Jul 24, 2024
8e4e4ae
Use main
gvladika Jul 26, 2024
bf39f87
Merge pull request #91 from OffchainLabs/unused-errors
gvladika Jul 26, 2024
8a9f1d6
feat: usdc bridge deployment (#93)
gvladika Aug 16, 2024
d548775
Add note about token compatibility in the IArbToken interface (#83)
TucksonDev Aug 19, 2024
0be7818
fix outboundTransferCustomRefund comment; sender, not _to, is callVal…
DZGoldman Aug 19, 2024
6f0a61f
Merge remote-tracking branch 'origin/develop' into bridged-usdc-gw
gzeoneth Aug 19, 2024
10bd936
chore: bump version to 1.2.3
gzeoneth Aug 19, 2024
0b4efcf
chore: slither
gzeoneth Aug 19, 2024
fd2d31b
Merge pull request #87 from OffchainLabs/bridged-usdc-gw
gzeoneth Aug 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:
run: yarn test

test-contracts:
name: Test storage layout and signatures
name: Test storage layout, signatures and look for unused errors
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand Down Expand Up @@ -98,6 +98,12 @@ jobs:
- name: Test function signatures
run: yarn run test:signatures

- name: Run unused Solidity errors checker
uses: OffchainLabs/actions/check-unused-errors@main
with:
directory: './contracts'
exceptions_file: './test/unused-errors/exceptions.txt'

test-e2e:
name: Test e2e
runs-on: ubuntu-latest
Expand All @@ -112,7 +118,6 @@ jobs:
no-token-bridge: true
no-l3-token-bridge: true
token-bridge-branch: '${{ github.head_ref }}'
nitro-testnode-ref: node-18

- name: Setup node/yarn
uses: actions/setup-node@v3
Expand All @@ -136,6 +141,9 @@ jobs:
- name: Verify creation code generation
run: yarn test:creation-code

- name: Test e2e orbit token bridge actions
run: yarn hardhat test test-e2e/orbitTokenBridge.ts

test-e2e-custom-fee-token:
name: Test e2e on custom fee token chain
runs-on: ubuntu-latest
Expand All @@ -151,7 +159,6 @@ jobs:
no-token-bridge: true
no-l3-token-bridge: true
token-bridge-branch: '${{ github.head_ref }}'
nitro-testnode-ref: node-18

- name: Setup node/yarn
uses: actions/setup-node@v3
Expand Down Expand Up @@ -194,7 +201,6 @@ jobs:
no-l3-token-bridge: true
token-bridge-branch: '${{ github.head_ref }}'
nitro-contracts-branch: 'develop'
nitro-testnode-ref: 'non18-decimal-token-node-18'

- name: Setup node/yarn
uses: actions/setup-node@v3
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ network.json

# Gambit (mutation test) files
gambit_out/
test-mutation/mutant_test_env/
test-mutation/mutant_test_env/

# bridged usdc deployment script
registerUsdcGatewayTx.json
2 changes: 2 additions & 0 deletions contracts/tokenbridge/arbitrum/IArbToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
/**
* @title Minimum expected interface for L2 token that interacts with the L2 token bridge (this is the interface necessary
* for a custom token that interacts with the bridge, see TestArbCustomToken.sol for an example implementation).
* @dev For the token to be compatible out of the box with the tooling available (e.g., the Arbitrum bridge), it is
* recommended to keep the implementation of this interface as close as possible to the `TestArbCustomToken` example.
*/

// solhint-disable-next-line compiler-version
Expand Down
69 changes: 40 additions & 29 deletions contracts/tokenbridge/arbitrum/gateway/L2ArbitrumGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ abstract contract L2ArbitrumGateway is L2ArbitrumMessenger, TokenGateway {
uint256, /* _maxGas */
uint256, /* _gasPriceBid */
bytes calldata _data
) public payable override returns (bytes memory res) {
) public payable virtual override returns (bytes memory res) {
// This function is set as public and virtual so that subclasses can override
// it and add custom validation for callers (ie only whitelisted users)

Expand All @@ -163,7 +163,7 @@ abstract contract L2ArbitrumGateway is L2ArbitrumMessenger, TokenGateway {
{
address l2Token = calculateL2TokenAddress(_l1Token);
require(l2Token.isContract(), "TOKEN_NOT_DEPLOYED");
require(IArbToken(l2Token).l1Address() == _l1Token, "NOT_EXPECTED_L1_TOKEN");
require(_isValidTokenAddress(_l1Token, l2Token), "NOT_EXPECTED_L1_TOKEN");

_amount = outboundEscrowTransfer(l2Token, _from, _amount);
id = triggerWithdrawal(_l1Token, _from, _to, _amount, _extraData);
Expand Down Expand Up @@ -252,33 +252,14 @@ abstract contract L2ArbitrumGateway is L2ArbitrumMessenger, TokenGateway {
);
if (shouldHalt) return;
}
// ignores gatewayData if token already deployed

{
// validate if L1 address supplied matches that of the expected L2 address
(bool success, bytes memory _l1AddressData) = expectedAddress.staticcall(
abi.encodeWithSelector(IArbToken.l1Address.selector)
);

bool shouldWithdraw;
if (!success || _l1AddressData.length < 32) {
shouldWithdraw = true;
} else {
// we do this in the else branch since we want to avoid reverts
// and `toAddress` reverts if _l1AddressData has a short length
// `_l1AddressData` should be 12 bytes of padding then 20 bytes for the address
address expectedL1Address = BytesLib.toAddress(_l1AddressData, 12);
if (expectedL1Address != _token) {
shouldWithdraw = true;
}
}

if (shouldWithdraw) {
// we don't need the return value from triggerWithdrawal since this is forcing
// a withdrawal back to the L1 instead of composing with a L2 dapp
triggerWithdrawal(_token, address(this), _from, _amount, "");
return;
}
// validate if L1 address supplied matches that of the expected L2 address
bool shouldWithdraw = !_isValidTokenAddress(_token, expectedAddress);
if (shouldWithdraw) {
// we don't need the return value from triggerWithdrawal since this is forcing
// a withdrawal back to the L1 instead of composing with a L2 dapp
triggerWithdrawal(_token, address(this), _from, _amount, "");
return;
}

inboundEscrowTransfer(expectedAddress, _to, _amount);
Expand All @@ -296,4 +277,34 @@ abstract contract L2ArbitrumGateway is L2ArbitrumMessenger, TokenGateway {
uint256 _amount,
bytes memory gatewayData
) internal virtual returns (bool shouldHalt);
}

/**
* @notice Check if expected token address matches the provided one
* @param _l1Address provided address of L1 token
* @param _expectedL2Address address of L2 gateway expects
* @return true if addresses match, false otherwise
*/
function _isValidTokenAddress(address _l1Address, address _expectedL2Address)
internal
view
virtual
returns (bool)
{
(bool success, bytes memory _l1AddressData) =
_expectedL2Address.staticcall(abi.encodeWithSelector(IArbToken.l1Address.selector));

if (!success || _l1AddressData.length < 32) {
return false;
} else {
// we do this in the else branch since we want to avoid reverts
// and `toAddress` reverts if _l1AddressData has a short length
// `_l1AddressData` should be 12 bytes of padding then 20 bytes for the address
address expectedL1Address = BytesLib.toAddress(_l1AddressData, 12);
if (expectedL1Address != _l1Address) {
return false;
}
}

return true;
}
}
216 changes: 216 additions & 0 deletions contracts/tokenbridge/arbitrum/gateway/L2USDCGateway.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.4;

import "./L2ArbitrumGateway.sol";
import {IFiatToken, IFiatTokenProxy} from "../../ethereum/gateway/L1USDCGateway.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/**
* @title Child chain custom gateway for USDC implementing Bridged USDC Standard.
* @notice Reference to the Circle's Bridged USDC Standard:
* https://github.com/circlefin/stablecoin-evm/blob/master/doc/bridged_USDC_standard.md
*
* @dev This contract can be used on new Orbit chains which want to provide USDC
* bridging solution and keep the possibility to upgrade to native USDC at
* some point later. This solution will NOT be used in existing Arbitrum chains.
*
* Parent chain custom gateway to be used along this child chain custom gateway is
* L1USDCGateway (when eth is used to pay fees) or L1OrbitUSDCGateway (when custom fee token is used).
* This custom gateway differs from standard gateway in the following ways:
* - it supports a single parent chain - child chain USDC token pair
* - it is ownable
* - withdrawals can be paused by the owner
* - owner can set an "transferrer" account which will be able to transfer USDC ownership
* - transferrer can transfer USDC owner and proxyAdmin
*
* NOTE: before withdrawing funds, make sure that recipient address is not blacklisted on the parent chain.
* Also, make sure that USDC token itself is not paused. Otherwise funds might get stuck.
*/
contract L2USDCGateway is L2ArbitrumGateway {
using SafeERC20 for IERC20;
using Address for address;

address public l1USDC;
address public l2USDC;
address public owner;
address public usdcOwnershipTransferrer;
bool public withdrawalsPaused;

event WithdrawalsPaused();
event WithdrawalsUnpaused();
event OwnerSet(address indexed owner);
event USDCOwnershipTransferrerSet(address indexed usdcOwnershipTransferrer);
event USDCOwnershipTransferred(address indexed newOwner, address indexed newProxyAdmin);

error L2USDCGateway_WithdrawalsAlreadyPaused();
error L2USDCGateway_WithdrawalsAlreadyUnpaused();
error L2USDCGateway_WithdrawalsPaused();
error L2USDCGateway_InvalidL1USDC();
error L2USDCGateway_InvalidL2USDC();
error L2USDCGateway_NotOwner();
error L2USDCGateway_InvalidOwner();
error L2USDCGateway_NotUSDCOwnershipTransferrer();

modifier onlyOwner() {
if (msg.sender != owner) {
revert L2USDCGateway_NotOwner();
}
_;
}

function initialize(
address _l1Counterpart,
address _router,
address _l1USDC,
address _l2USDC,
address _owner
) public {
if (_l1USDC == address(0)) {
revert L2USDCGateway_InvalidL1USDC();
}
if (_l2USDC == address(0)) {
revert L2USDCGateway_InvalidL2USDC();
}
if (_owner == address(0)) {
revert L2USDCGateway_InvalidOwner();
}
L2ArbitrumGateway._initialize(_l1Counterpart, _router);
l1USDC = _l1USDC;
l2USDC = _l2USDC;
owner = _owner;
}

/**
* @notice Pause all withdrawals. This can only be called by the owner.
*/
function pauseWithdrawals() external onlyOwner {
if (withdrawalsPaused) {
revert L2USDCGateway_WithdrawalsAlreadyPaused();
}
withdrawalsPaused = true;
emit WithdrawalsPaused();
}

/**
* @notice Unpause withdrawals. This can only be called by the owner.
*/
function unpauseWithdrawals() external onlyOwner {
if (!withdrawalsPaused) {
revert L2USDCGateway_WithdrawalsAlreadyUnpaused();
}
withdrawalsPaused = false;
emit WithdrawalsUnpaused();
}

/**
* @notice Sets a new owner.
*/
function setOwner(address newOwner) external onlyOwner {
if (newOwner == address(0)) {
revert L2USDCGateway_InvalidOwner();
}
owner = newOwner;
emit OwnerSet(newOwner);
}

/**
* @notice Sets the account which is able to transfer USDC role away from the gateway to some other account.
*/
function setUsdcOwnershipTransferrer(address _usdcOwnershipTransferrer) external onlyOwner {
usdcOwnershipTransferrer = _usdcOwnershipTransferrer;
emit USDCOwnershipTransferrerSet(_usdcOwnershipTransferrer);
}

/**
* @notice In accordance with bridged USDC standard, the ownership of the USDC token contract is transferred
* to the new owner, and the proxy admin is transferred to the caller (usdcOwnershipTransferrer).
* @dev For transfer to be successful, this gateway should be both the owner and the proxy admin of L2 USDC token.
*/
function transferUSDCRoles(address _owner) external {
if (msg.sender != usdcOwnershipTransferrer) {
revert L2USDCGateway_NotUSDCOwnershipTransferrer();
}

IFiatTokenProxy(l2USDC).changeAdmin(msg.sender);
IFiatToken(l2USDC).transferOwnership(_owner);

emit USDCOwnershipTransferred(_owner, msg.sender);
}

/**
* @notice Entrypoint for withdrawing USDC, can be used only if withdrawals are not paused.
*/
function outboundTransfer(
address _l1Token,
address _to,
uint256 _amount,
uint256, /* _maxGas */
uint256, /* _gasPriceBid */
bytes calldata _data
) public payable override returns (bytes memory res) {
if (withdrawalsPaused) {
revert L2USDCGateway_WithdrawalsPaused();
}
return super.outboundTransfer(_l1Token, _to, _amount, 0, 0, _data);
}

/**
* @notice Only parent chain - child chain USDC token pair is supported
*/
function calculateL2TokenAddress(address l1ERC20) public view override returns (address) {
if (l1ERC20 != l1USDC) {
// invalid L1 usdc address
return address(0);
}
return l2USDC;
}

function inboundEscrowTransfer(address _l2Address, address _dest, uint256 _amount)
internal
override
{
IFiatToken(_l2Address).mint(_dest, _amount);
}

function outboundEscrowTransfer(address _l2Token, address _from, uint256 _amount)
internal
override
returns (uint256)
{
// fetch the USDC tokens from the user and then burn them
IERC20(_l2Token).safeTransferFrom(_from, address(this), _amount);
IFiatToken(_l2Token).burn(_amount);

return _amount;
}

/**
* @notice Withdraw back the USDC if child chain side is not set up properly
*/
function handleNoContract(
address l1ERC20,
address, /* expectedL2Address */
address _from,
address, /* _to */
uint256 _amount,
bytes memory /* deployData */
) internal override returns (bool shouldHalt) {
// it is assumed that the custom token is deployed to child chain before deposits are made
triggerWithdrawal(l1ERC20, address(this), _from, _amount, "");
return true;
}

/**
* @notice We need to override this function because base implementation assumes that L2 token implements
* `l1Address()` function from IArbToken interface. In the case of USDC gateway IArbToken logic is
* part of this contract, so we just check that addresses match the expected L1 and L2 USDC address.
*/
function _isValidTokenAddress(address _l1Address, address _expectedL2Address)
internal
view
override
returns (bool)
{
return _l1Address == l1USDC && _expectedL2Address == l2USDC;
}
}
Loading
Loading