Skip to content

Commit 0720878

Browse files
authored
feat(Linea): Add CCTP V2 support to Linea (#910)
1 parent cbd7773 commit 0720878

File tree

12 files changed

+327
-162
lines changed

12 files changed

+327
-162
lines changed

contracts/Linea_SpokePool.sol

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
pragma solidity ^0.8.19;
66

77
import "./SpokePool.sol";
8+
import "./libraries/CircleCCTPAdapter.sol";
89
import { IMessageService, ITokenBridge, IUSDCBridge } from "./external/interfaces/LineaInterfaces.sol";
910
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
1011
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
@@ -13,7 +14,7 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
1314
* @notice Linea specific SpokePool.
1415
* @custom:security-contact bugs@across.to
1516
*/
16-
contract Linea_SpokePool is SpokePool {
17+
contract Linea_SpokePool is SpokePool, CircleCCTPAdapter {
1718
using SafeERC20 for IERC20;
1819

1920
/**
@@ -29,14 +30,13 @@ contract Linea_SpokePool is SpokePool {
2930
/**
3031
* @notice Address of Linea's USDC Bridge contract on L2.
3132
*/
32-
IUSDCBridge public l2UsdcBridge;
33+
IUSDCBridge private DEPRECATED_l2UsdcBridge;
3334

3435
/**************************************
3536
* EVENTS *
3637
**************************************/
3738
event SetL2TokenBridge(address indexed newTokenBridge, address oldTokenBridge);
3839
event SetL2MessageService(address indexed newMessageService, address oldMessageService);
39-
event SetL2UsdcBridge(address indexed newUsdcBridge, address oldUsdcBridge);
4040

4141
/**
4242
* @notice Construct Linea-specific SpokePool.
@@ -50,16 +50,20 @@ contract Linea_SpokePool is SpokePool {
5050
constructor(
5151
address _wrappedNativeTokenAddress,
5252
uint32 _depositQuoteTimeBuffer,
53-
uint32 _fillDeadlineBuffer
54-
) SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer) {} // solhint-disable-line no-empty-blocks
53+
uint32 _fillDeadlineBuffer,
54+
IERC20 _l2Usdc,
55+
ITokenMessenger _cctpTokenMessenger
56+
)
57+
SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer)
58+
CircleCCTPAdapter(_l2Usdc, _cctpTokenMessenger, CircleDomainIds.Ethereum)
59+
{} // solhint-disable-line no-empty-blocks
5560

5661
/**
5762
* @notice Initialize Linea-specific SpokePool.
5863
* @param _initialDepositId Starting deposit ID. Set to 0 unless this is a re-deployment in order to mitigate
5964
* relay hash collisions.
6065
* @param _l2MessageService Address of Canonical Message Service. Can be reset by admin.
6166
* @param _l2TokenBridge Address of Canonical Token Bridge. Can be reset by admin.
62-
* @param _l2UsdcBridge Address of USDC Bridge. Can be reset by admin.
6367
* @param _crossDomainAdmin Cross domain admin to set. Can be changed by admin.
6468
* @param _withdrawalRecipient Address which receives token withdrawals. Can be changed by admin. For Spoke Pools on L2, this will
6569
* likely be the hub pool.
@@ -68,14 +72,12 @@ contract Linea_SpokePool is SpokePool {
6872
uint32 _initialDepositId,
6973
IMessageService _l2MessageService,
7074
ITokenBridge _l2TokenBridge,
71-
IUSDCBridge _l2UsdcBridge,
7275
address _crossDomainAdmin,
7376
address _withdrawalRecipient
7477
) public initializer {
7578
__SpokePool_init(_initialDepositId, _crossDomainAdmin, _withdrawalRecipient);
7679
_setL2TokenBridge(_l2TokenBridge);
7780
_setL2MessageService(_l2MessageService);
78-
_setL2UsdcBridge(_l2UsdcBridge);
7981
}
8082

8183
/**
@@ -106,14 +108,6 @@ contract Linea_SpokePool is SpokePool {
106108
_setL2MessageService(_l2MessageService);
107109
}
108110

109-
/**
110-
* @notice Change L2 USDC bridge address. Callable only by admin.
111-
* @param _l2UsdcBridge New address of L2 USDC bridge.
112-
*/
113-
function setL2UsdcBridge(IUSDCBridge _l2UsdcBridge) public onlyAdmin nonReentrant {
114-
_setL2UsdcBridge(_l2UsdcBridge);
115-
}
116-
117111
/**************************************
118112
* INTERNAL FUNCTIONS *
119113
**************************************/
@@ -139,27 +133,32 @@ contract Linea_SpokePool is SpokePool {
139133
function _bridgeTokensToHubPool(uint256 amountToReturn, address l2TokenAddress) internal override {
140134
// Linea's L2 Canonical Message Service, requires a minimum fee to be set.
141135
uint256 minFee = minimumFeeInWei();
142-
// We require that the caller pass in the fees as msg.value instead of pulling ETH out of this contract's balance.
143-
// Using the contract's balance would require a separate accounting system to keep LP funds separated from system funds
144-
// used to pay for L2->L1 messages.
145-
require(msg.value == minFee, "MESSAGE_FEE_MISMATCH");
146136

147137
// SpokePool is expected to receive ETH from the L1 HubPool, then we need to first unwrap it to ETH and then
148138
// send ETH directly via the Canonical Message Service.
149139
if (l2TokenAddress == address(wrappedNativeToken)) {
140+
// We require that the caller pass in the fees as msg.value instead of pulling ETH out of this contract's balance.
141+
// Using the contract's balance would require a separate accounting system to keep LP funds separated from system funds
142+
// used to pay for L2->L1 messages.
143+
require(msg.value == minFee, "MESSAGE_FEE_MISMATCH");
144+
150145
// msg.value is added here because the entire native balance (including msg.value) is auto-wrapped
151146
// before the execution of any wrapped token refund leaf. So it must be unwrapped before being sent as a
152147
// fee to the l2MessageService.
153148
WETH9Interface(l2TokenAddress).withdraw(amountToReturn + msg.value); // Unwrap into ETH.
154149
l2MessageService.sendMessage{ value: amountToReturn + msg.value }(withdrawalRecipient, msg.value, "");
155150
}
156151
// If the l1Token is USDC, then we need sent it via the USDC Bridge.
157-
else if (l2TokenAddress == l2UsdcBridge.usdc()) {
158-
IERC20(l2TokenAddress).safeIncreaseAllowance(address(l2UsdcBridge), amountToReturn);
159-
l2UsdcBridge.depositTo{ value: msg.value }(amountToReturn, withdrawalRecipient);
152+
else if (l2TokenAddress == address(usdcToken) && _isCCTPEnabled()) {
153+
_transferUsdc(withdrawalRecipient, amountToReturn);
160154
}
161155
// For other tokens, we can use the Canonical Token Bridge.
162156
else {
157+
// We require that the caller pass in the fees as msg.value instead of pulling ETH out of this contract's balance.
158+
// Using the contract's balance would require a separate accounting system to keep LP funds separated from system funds
159+
// used to pay for L2->L1 messages.
160+
require(msg.value == minFee, "MESSAGE_FEE_MISMATCH");
161+
163162
IERC20(l2TokenAddress).safeIncreaseAllowance(address(l2TokenBridge), amountToReturn);
164163
l2TokenBridge.bridgeToken{ value: msg.value }(l2TokenAddress, amountToReturn, withdrawalRecipient);
165164
}
@@ -178,12 +177,6 @@ contract Linea_SpokePool is SpokePool {
178177
emit SetL2TokenBridge(address(_l2TokenBridge), oldTokenBridge);
179178
}
180179

181-
function _setL2UsdcBridge(IUSDCBridge _l2UsdcBridge) internal {
182-
address oldUsdcBridge = address(l2UsdcBridge);
183-
l2UsdcBridge = _l2UsdcBridge;
184-
emit SetL2UsdcBridge(address(_l2UsdcBridge), oldUsdcBridge);
185-
}
186-
187180
function _setL2MessageService(IMessageService _l2MessageService) internal {
188181
address oldMessageService = address(l2MessageService);
189182
l2MessageService = _l2MessageService;

contracts/chain-adapters/Linea_Adapter.sol

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity ^0.8.0;
33

44
import "./interfaces/AdapterInterface.sol";
55
import "../external/interfaces/WETH9Interface.sol";
6+
import "../libraries/CircleCCTPAdapter.sol";
67

78
import { IMessageService, ITokenBridge, IUSDCBridge } from "../external/interfaces/LineaInterfaces.sol";
89

@@ -14,31 +15,29 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
1415
* @custom:security-contact bugs@across.to
1516
*/
1617
// solhint-disable-next-line contract-name-camelcase
17-
contract Linea_Adapter is AdapterInterface {
18+
contract Linea_Adapter is AdapterInterface, CircleCCTPAdapter {
1819
using SafeERC20 for IERC20;
1920

2021
WETH9Interface public immutable L1_WETH;
2122
IMessageService public immutable L1_MESSAGE_SERVICE;
2223
ITokenBridge public immutable L1_TOKEN_BRIDGE;
23-
IUSDCBridge public immutable L1_USDC_BRIDGE;
2424

2525
/**
2626
* @notice Constructs new Adapter.
2727
* @param _l1Weth WETH address on L1.
2828
* @param _l1MessageService Canonical message service contract on L1.
2929
* @param _l1TokenBridge Canonical token bridge contract on L1.
30-
* @param _l1UsdcBridge L1 USDC Bridge to ConsenSys's L2 Linea.
3130
*/
3231
constructor(
3332
WETH9Interface _l1Weth,
3433
IMessageService _l1MessageService,
3534
ITokenBridge _l1TokenBridge,
36-
IUSDCBridge _l1UsdcBridge
37-
) {
35+
IERC20 _l1Usdc,
36+
ITokenMessenger _cctpTokenMessenger
37+
) CircleCCTPAdapter(_l1Usdc, _cctpTokenMessenger, CircleDomainIds.Linea) {
3838
L1_WETH = _l1Weth;
3939
L1_MESSAGE_SERVICE = _l1MessageService;
4040
L1_TOKEN_BRIDGE = _l1TokenBridge;
41-
L1_USDC_BRIDGE = _l1UsdcBridge;
4241
}
4342

4443
/**
@@ -67,17 +66,15 @@ contract Linea_Adapter is AdapterInterface {
6766
uint256 amount,
6867
address to
6968
) external payable override {
69+
if (l1Token == address(usdcToken) && _isCCTPEnabled()) {
70+
_transferUsdc(to, amount);
71+
}
7072
// If the l1Token is WETH then unwrap it to ETH then send the ETH directly
7173
// via the Canoncial Message Service.
72-
if (l1Token == address(L1_WETH)) {
74+
else if (l1Token == address(L1_WETH)) {
7375
L1_WETH.withdraw(amount);
7476
L1_MESSAGE_SERVICE.sendMessage{ value: amount }(to, 0, "");
7577
}
76-
// If the l1Token is USDC, then we need sent it via the USDC Bridge.
77-
else if (l1Token == L1_USDC_BRIDGE.usdc()) {
78-
IERC20(l1Token).safeIncreaseAllowance(address(L1_USDC_BRIDGE), amount);
79-
L1_USDC_BRIDGE.depositTo(amount, to);
80-
}
8178
// For other tokens, we can use the Canonical Token Bridge.
8279
else {
8380
IERC20(l1Token).safeIncreaseAllowance(address(L1_TOKEN_BRIDGE), amount);

contracts/external/interfaces/CCTPInterfaces.sol

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,39 @@ interface ITokenMessenger {
5757
function localMinter() external view returns (ITokenMinter minter);
5858
}
5959

60+
// Source: https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/v2/TokenMessengerV2.sol#L138C1-L166C15
61+
interface ITokenMessengerV2 {
62+
/**
63+
* @notice Deposits and burns tokens from sender to be minted on destination domain.
64+
* Emits a `DepositForBurn` event.
65+
* @dev reverts if:
66+
* - given burnToken is not supported
67+
* - given destinationDomain has no TokenMessenger registered
68+
* - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance
69+
* to this contract is less than `amount`.
70+
* - burn() reverts. For example, if `amount` is 0.
71+
* - maxFee is greater than or equal to `amount`.
72+
* - MessageTransmitterV2#sendMessage reverts.
73+
* @param amount amount of tokens to burn
74+
* @param destinationDomain destination domain to receive message on
75+
* @param mintRecipient address of mint recipient on destination domain
76+
* @param burnToken token to burn `amount` of, on local domain
77+
* @param destinationCaller authorized caller on the destination domain, as bytes32. If equal to bytes32(0),
78+
* any address can broadcast the message.
79+
* @param maxFee maximum fee to pay on the destination domain, specified in units of burnToken
80+
* @param minFinalityThreshold the minimum finality at which a burn message will be attested to.
81+
*/
82+
function depositForBurn(
83+
uint256 amount,
84+
uint32 destinationDomain,
85+
bytes32 mintRecipient,
86+
address burnToken,
87+
bytes32 destinationCaller,
88+
uint256 maxFee,
89+
uint32 minFinalityThreshold
90+
) external;
91+
}
92+
6093
/**
6194
* A TokenMessenger stores a TokenMinter contract which extends the TokenController contract. The TokenController
6295
* contract has a burnLimitsPerMessage public mapping which can be queried to find the per-message burn limit

contracts/libraries/CircleCCTPAdapter.sol

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ library CircleDomainIds {
1414
uint32 public constant Base = 6;
1515
uint32 public constant Polygon = 7;
1616
uint32 public constant DoctorWho = 10;
17+
uint32 public constant Linea = 11; // TODO replace with actual domain once Circle publishes it.
1718
// Use this value for placeholder purposes only for adapters that extend this adapter but haven't yet been
1819
// assigned a domain ID by Circle.
1920
uint32 public constant UNINITIALIZED = type(uint32).max;
@@ -50,6 +51,13 @@ abstract contract CircleCCTPAdapter {
5051
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
5152
ITokenMessenger public immutable cctpTokenMessenger;
5253

54+
/**
55+
* @notice Indicates if the CCTP V2 TokenMessenger is being used.
56+
* @dev This is determined by checking if the feeRecipient() function exists and returns a non-zero address.
57+
*/
58+
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
59+
bool public immutable cctpV2;
60+
5361
/**
5462
* @notice intiailizes the CircleCCTPAdapter contract.
5563
* @param _usdcToken USDC address on the current chain.
@@ -59,12 +67,23 @@ abstract contract CircleCCTPAdapter {
5967
/// @custom:oz-upgrades-unsafe-allow constructor
6068
constructor(
6169
IERC20 _usdcToken,
70+
/// @dev This should ideally be an address but its kept as an ITokenMessenger to avoid rippling changes to the
71+
/// constructors for every SpokePool/Adapter.
6272
ITokenMessenger _cctpTokenMessenger,
6373
uint32 _recipientCircleDomainId
6474
) {
6575
usdcToken = _usdcToken;
6676
cctpTokenMessenger = _cctpTokenMessenger;
6777
recipientCircleDomainId = _recipientCircleDomainId;
78+
79+
// Only the CCTP V2 TokenMessenger has a feeRecipient() function, so we use it to
80+
// figure out if we are using CCTP V2 or V1. `success` can be true even if the contract doesn't
81+
// implement feeRecipient but it has a fallback function so to be extra safe, we check the return value
82+
// of feeRecipient() as well.
83+
(bool success, bytes memory feeRecipient) = address(cctpTokenMessenger).staticcall(
84+
abi.encodeWithSignature("feeRecipient()")
85+
);
86+
cctpV2 = (success && address(bytes20(feeRecipient)) != address(0));
6887
}
6988

7089
/**
@@ -101,7 +120,25 @@ abstract contract CircleCCTPAdapter {
101120
uint256 remainingAmount = amount;
102121
while (remainingAmount > 0) {
103122
uint256 partAmount = remainingAmount > burnLimit ? burnLimit : remainingAmount;
104-
cctpTokenMessenger.depositForBurn(partAmount, recipientCircleDomainId, to, address(usdcToken));
123+
if (cctpV2) {
124+
// Uses the CCTP V2 "standard transfer" speed and
125+
// therefore pays no additional fee for the transfer to be sped up.
126+
ITokenMessengerV2(address(cctpTokenMessenger)).depositForBurn(
127+
partAmount,
128+
recipientCircleDomainId,
129+
to,
130+
address(usdcToken),
131+
// The following parameters are new in this function from V2 to V1, can read more here:
132+
// https://developers.circle.com/stablecoins/evm-smart-contracts
133+
bytes32(0), // destinationCaller is set to bytes32(0) to indicate that anyone can call
134+
// receiveMessage on the destination to finalize the transfer
135+
0, // maxFee can be set to 0 for a "standard transfer"
136+
2000 // minFinalityThreshold can be set to 20000 for a "standard transfer",
137+
// https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/v2/FinalityThresholds.sol#L21
138+
);
139+
} else {
140+
cctpTokenMessenger.depositForBurn(partAmount, recipientCircleDomainId, to, address(usdcToken));
141+
}
105142
remainingAmount -= partAmount;
106143
}
107144
}

deploy/028_deploy_linea_adapter.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { L1_ADDRESS_MAP, WETH } from "./consts";
1+
import { L1_ADDRESS_MAP, WETH, USDCe } from "./consts";
22
import { DeployFunction } from "hardhat-deploy/types";
33
import { HardhatRuntimeEnvironment } from "hardhat/types";
44

@@ -14,7 +14,10 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
1414
WETH[chainId],
1515
L1_ADDRESS_MAP[chainId].lineaMessageService,
1616
L1_ADDRESS_MAP[chainId].lineaTokenBridge,
17-
L1_ADDRESS_MAP[chainId].lineaUsdcBridge,
17+
// TODO: USDC.e on Linea will be upgraded to USDC so eventually we should add a USDC entry for Linea in consts
18+
// and read from there instead of using the L1 USDC.e address.
19+
USDCe[chainId],
20+
L1_ADDRESS_MAP[chainId].cctpV2TokenMessenger,
1821
],
1922
});
2023
};

deploy/029_deploy_linea_spokepool.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { DeployFunction } from "hardhat-deploy/types";
22
import { HardhatRuntimeEnvironment } from "hardhat/types";
33
import { deployNewProxy, getSpokePoolDeploymentInfo } from "../utils/utils.hre";
4-
import { FILL_DEADLINE_BUFFER, L2_ADDRESS_MAP, QUOTE_TIME_BUFFER, WETH } from "./consts";
4+
import { FILL_DEADLINE_BUFFER, L2_ADDRESS_MAP, QUOTE_TIME_BUFFER, WETH, USDCe } from "./consts";
55

66
const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
77
const { hubPool } = await getSpokePoolDeploymentInfo(hre);
@@ -14,11 +14,18 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
1414
1_000_000,
1515
L2_ADDRESS_MAP[chainId].lineaMessageService,
1616
L2_ADDRESS_MAP[chainId].lineaTokenBridge,
17-
L2_ADDRESS_MAP[chainId].lineaUsdcBridge,
1817
hubPool.address,
1918
hubPool.address,
2019
];
21-
const constructorArgs = [WETH[chainId], QUOTE_TIME_BUFFER, FILL_DEADLINE_BUFFER];
20+
const constructorArgs = [
21+
WETH[chainId],
22+
QUOTE_TIME_BUFFER,
23+
FILL_DEADLINE_BUFFER,
24+
// TODO: USDC.e on Linea will be upgraded to USDC so eventually we should add a USDC entry for Linea in consts
25+
// and read from there instead of using the L1 USDC.e address.
26+
USDCe[chainId],
27+
L2_ADDRESS_MAP[chainId].cctpV2TokenMessenger,
28+
];
2229

2330
await deployNewProxy("Linea_SpokePool", constructorArgs, initArgs);
2431
};

deploy/consts.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ export const L1_ADDRESS_MAP: { [key: number]: { [contractName: string]: string }
3131
polygonRegistry: "0x33a02E6cC863D393d6Bf231B697b82F6e499cA71",
3232
polygonDepositManager: "0x401F6c983eA34274ec46f84D70b31C151321188b",
3333
cctpTokenMessenger: "0xBd3fa81B58Ba92a82136038B25aDec7066af3155",
34+
cctpV2TokenMessenger: "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d",
3435
cctpMessageTransmitter: "0x0a992d191deec32afe36203ad87d7d289a738f81",
3536
lineaMessageService: "0xd19d4B5d358258f05D7B411E21A1460D11B0876F",
3637
lineaTokenBridge: "0x051F1D88f0aF5763fB888eC4378b4D8B29ea3319",
37-
lineaUsdcBridge: "0x504a330327a089d8364c4ab3811ee26976d388ce",
3838
scrollERC20GatewayRouter: "0xF8B1378579659D8F7EE5f3C929c2f3E332E41Fd6",
3939
scrollMessengerRelay: "0x6774Bcbd5ceCeF1336b5300fb5186a12DDD8b367",
4040
scrollGasPriceOracle: "0x0d7E906BD9cAFa154b048cFa766Cc1E54E39AF9B",
@@ -53,7 +53,6 @@ export const L1_ADDRESS_MAP: { [key: number]: { [contractName: string]: string }
5353
usdc: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
5454
lineaMessageService: "0xd19d4B5d358258f05D7B411E21A1460D11B0876F", // No sepolia deploy address
5555
lineaTokenBridge: "0x051F1D88f0aF5763fB888eC4378b4D8B29ea3319", // No sepolia deploy address
56-
lineaUsdcBridge: "0x504a330327a089d8364c4ab3811ee26976d388ce", // No sepolia deploy address
5756
scrollERC20GatewayRouter: "0x13FBE0D0e5552b8c9c4AE9e2435F38f37355998a",
5857
scrollMessengerRelay: "0x50c7d3e7f7c656493D1D76aaa1a836CedfCBB16A",
5958
scrollGasPriceOracle: "0x247969F4fad93a33d4826046bc3eAE0D36BdE548",
@@ -217,7 +216,7 @@ export const L2_ADDRESS_MAP: { [key: number]: { [contractName: string]: string }
217216
},
218217
[CHAIN_IDs.LINEA]: {
219218
lineaMessageService: "0x508Ca82Df566dCD1B0DE8296e70a96332cD644ec",
220-
lineaUsdcBridge: "0xA2Ee6Fce4ACB62D95448729cDb781e3BEb62504A",
219+
cctpV2TokenMessenger: "0xunknown", // No official address from Circle yet.
221220
lineaTokenBridge: "0x353012dc4a9A6cF55c941bADC267f82004A8ceB9",
222221
},
223222
[CHAIN_IDs.SCROLL_SEPOLIA]: {

0 commit comments

Comments
 (0)