-
Notifications
You must be signed in to change notification settings - Fork 75
feat(linea): add adapter and spokepool contracts #382
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
Conversation
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.
Looks great! A few comments
| * @param message Data to send to target. | ||
| */ | ||
| function relayMessage(address target, bytes calldata message) external payable override { | ||
| l1MessageService.sendMessage{ value: msg.value }(target, 0, message); |
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.
Typically, we don't require the dataworker to send in the ETH on every execution. This has a few problems:
- Hard to compute this offchain.
- Can't use the in-contract multicall function because it doesn't allow msg.value and all the value would be used up in the first call.
The way we typically do this is shown in the arbitrum adapter: https://github.com/across-protocol/contracts-v2/blob/866d09fcae4c3257c9759fd150c2b8c6f10034d7/contracts/chain-adapters/Arbitrum_Adapter.sol#L187-L190. We pre-load the dataworker with ETH regularly and compute the value needed in relayMessage in the contract.
Note: remember this code is run via delegatecall, so it's as if it's running within the HubPool contract. msg.value, msg.sender, etc are all the same as they are when code is executing within the HubPool.
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.
Got it! Yes, because we need to finalize the relayed messages on Linea ourselves anyways, we are not required to send any value. So in our case, msg.value would be 0. Should I still remove the forwarding of msg.value?
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.
Yes I think we should remove any msg.value and just use any ETH held in the HubPool account if we need any. If we don't need any, then just set to 0?
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.
Agreed. Remember that these adapters are easy to replace if the requirements change.
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.
Bump on this, we should drop the msg.value here
| @@ -0,0 +1,354 @@ | |||
| // SPDX-License-Identifier: BUSL-1.1 | |||
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.
I would suggest adding a few simple tests, like we have for the other adapters.
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.
done here 32d72c0
Linea_Adapter.sol and goerli deployment|
New dependencies detected. Learn more about Socket for GitHub ↗︎
|
| * @param message Data to send to target. | ||
| */ | ||
| function relayMessage(address target, bytes calldata message) external payable override { | ||
| l1MessageService.sendMessage{ value: msg.value }(target, 0, message); |
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 are required to include a msg.value to pay for the message then you should grab eth from the hub pools balance like in Arbitrum. This way the caller isn't forced to add msg.value
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.
Because Linea (currently at least) does not support auto-claiming of messages that contain calldata, we need to finalize messages ourselves. Which means we can omit fees for now. I will set the msg.value to 0
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.
ok and in the future if we want to omit fees we should still grab balance from address(this).balance, so that the caller doesn't have to send value on each txn
| // via the Canoncial Message Service. | ||
| if (l1Token == address(l1Weth)) { | ||
| l1Weth.withdraw(amount); | ||
| l1MessageService.sendMessage{ value: amount }(to, 0, ""); |
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.
We only have to pay amount? No extra fees to send a message?
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.
Because Linea (currently at least) does not support auto-claiming messages containing calldata, we need to finalize most messages ourselves. In this case, though, we could set a fee because the calldata is empty. But as we need to finalize all other messages anyways, I am wondering if it makes sense to do it or not 🤔
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.
i think its ideal if we can get someone else to finalize this for us if we can set the fee correctly
contracts/Linea_SpokePool.sol
Outdated
| // send ETH directly via the Canonical Message Service. | ||
| if (l2TokenAddress == address(wrappedNativeToken)) { | ||
| WETH9Interface(l2TokenAddress).withdraw(amountToReturn); // Unwrap into ETH. | ||
| IMessageService(l2MessageService).sendMessage{ value: amountToReturn }(hubPool, 0, ""); |
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.
Are there no fees that have to be paid here?
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.
Yea, auto-claiming for L2->L1 seems not to be supported yet. So again we need to do that ourselves.
contracts/Linea_SpokePool.sol
Outdated
| // If the l1Token is USDC, then we need sent it via the USDC Bridge. | ||
| else if (l2TokenAddress == l2UsdcBridge.usdc()) { | ||
| IERC20(l2TokenAddress).safeIncreaseAllowance(address(l2UsdcBridge), amountToReturn); | ||
| l2UsdcBridge.depositTo(amountToReturn, hubPool); |
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.
This function is payable, does it need to include any fees?
contracts/Linea_SpokePool.sol
Outdated
| // For other tokens, we can use the Canonical Token Bridge. | ||
| else { | ||
| IERC20(l2TokenAddress).safeIncreaseAllowance(address(l2TokenBridge), amountToReturn); | ||
| l2TokenBridge.bridgeToken(l2TokenAddress, amountToReturn, hubPool); |
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.
| } | ||
|
|
||
| function _requireAdminSender() internal view override { | ||
| require(IMessageService(l2MessageService).sender() == crossDomainAdmin, "ONLY_COUNTERPART_GATEWAY"); |
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.
Have you tested that no one else can call a function protected by this internal func?
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.
crossDomainAdmin is set to the HubPool address, right?
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.
Have you tested that no one else can call a function protected by this internal func?
Yes, I tried to call a few functions with an EOA for example.
crossDomainAdmin is set to the HubPool address, right?
Yes, it is set to the address of the HubPool
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.
Yes, I tried to call a few functions with an EOA for example.
Did you also test calling over the L1->L2 bridge from an address that isn't the HubPool?
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.
Did you also test calling over the L1->L2 bridge from an address that isn't the HubPool?
yes I also tested that
| // If the l1Token is USDC, then we need sent it via the USDC Bridge. | ||
| else if (l1Token == l1UsdcBridge.usdc()) { | ||
| IERC20(l1Token).safeIncreaseAllowance(address(l1UsdcBridge), amount); | ||
| l1UsdcBridge.depositTo(amount, to); |
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.
Do we need to include any fees here?
| // For other tokens, we can use the Canonical Token Bridge. | ||
| else { | ||
| IERC20(l1Token).safeIncreaseAllowance(address(l1TokenBridge), amount); | ||
| l1TokenBridge.bridgeToken(l1Token, amount, to); |
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.
scripts/claimLineaL2Message.ts
Outdated
|
|
||
| const uniqueTxHashes = new Set(events.map((event) => event.transactionHash)); | ||
| for (const txHash of uniqueTxHashes) { | ||
| const messageSentEvents = await l1MessageService.getMessagesByTransactionHash(txHash); |
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.
Does this handle well sending multiple messages from the same txHash? In the finalizer we've sometimes had issues using the native SDK for a chain to handle the multiple message case so we implemented a helper function getUniqueLogIndex here in use in the Arbitrum finalizer to handle this case
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.
Good question 🤔 I will take a look at the source code
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.
I looked into the code and the implementation looks like
async getMessagesByTransactionHash(transactionHash) {
const receipt = await this.provider.getTransactionReceipt(transactionHash);
if (!receipt) {
return null;
}
return receipt.logs
.filter((log) => log.address === this.contractAddress && log.topics[0] === constants_1.MESSAGE_SENT_EVENT_SIGNATURE)
.map((log) => this.contract.interface.parseLog(log))
.map(mappers_1.mapMessageSentEventOrLogToMessage);
}So it should correctly handle multiple MessageSent event logs in a single tx
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.
Looks great! Only a few minor comments/questions
| } | ||
|
|
||
| function _requireAdminSender() internal view override { | ||
| require(IMessageService(l2MessageService).sender() == crossDomainAdmin, "ONLY_COUNTERPART_GATEWAY"); |
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.
crossDomainAdmin is set to the HubPool address, right?
| * @param message Data to send to target. | ||
| */ | ||
| function relayMessage(address target, bytes calldata message) external payable override { | ||
| l1MessageService.sendMessage{ value: msg.value }(target, 0, message); |
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.
Agreed. Remember that these adapters are easy to replace if the requirements change.
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.
Looks great! Only minor comments
| } | ||
|
|
||
| function _requireAdminSender() internal view override { | ||
| require(IMessageService(l2MessageService).sender() == crossDomainAdmin, "ONLY_COUNTERPART_GATEWAY"); |
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.
Yes, I tried to call a few functions with an EOA for example.
Did you also test calling over the L1->L2 bridge from an address that isn't the HubPool?
| * @param message Data to send to target. | ||
| */ | ||
| function relayMessage(address target, bytes calldata message) external payable override { | ||
| l1MessageService.sendMessage{ value: msg.value }(target, 0, message); |
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.
Bump on this, we should drop the msg.value here
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.
Some suggestions on reducing the fee we pay on L2 and making it easier for dataworker to know what to pay
contracts/Linea_SpokePool.sol
Outdated
| function _bridgeTokensToHubPool(uint256 amountToReturn, address l2TokenAddress) internal override { | ||
| // Linea's L2 Canonical Message Service, requires a minimum fee to be set. | ||
| uint256 minFee = IMessageService(l2MessageService).minimumFeeInWei(); | ||
| require(msg.value >= minFee, "MESSAGE_FEE_TOO_LOW"); |
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.
Should we require msg.value is exactly the min fee so the caller doesn't burn extra fees?
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.
Ok yea, that makes sense.
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.
Changed here a29b3a0
contracts/Linea_SpokePool.sol
Outdated
|
|
||
| function _bridgeTokensToHubPool(uint256 amountToReturn, address l2TokenAddress) internal override { | ||
| // Linea's L2 Canonical Message Service, requires a minimum fee to be set. | ||
| uint256 minFee = IMessageService(l2MessageService).minimumFeeInWei(); |
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.
Can we add an external method that returns the minder so dataworker can more easily know how much value it should send on each call?
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.
Sure will do!
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.
contracts/Linea_SpokePool.sol
Outdated
|
|
||
| function _bridgeTokensToHubPool(uint256 amountToReturn, address l2TokenAddress) internal override { | ||
| // Linea's L2 Canonical Message Service, requires a minimum fee to be set. | ||
| uint256 minFee = IMessageService(l2MessageService).minimumFeeInWei(); |
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.
nit: should we use the public minimumFeeInwei function now?
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.
Changed here d76364c
| contract Linea_SpokePool is SpokePool { | ||
| using SafeERC20 for IERC20; | ||
|
|
||
| IMessageService public l2MessageService; |
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.
Need comments above public vars
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.
| event SetL2UsdcBridge(address indexed newUsdcBridge, address oldUsdcBridge); | ||
|
|
||
| /// @custom:oz-upgrades-unsafe-allow constructor | ||
| constructor( |
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.
Need Natspec on public functions
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.
| ITokenBridge public l2TokenBridge; | ||
| IUSDCBridge public l2UsdcBridge; | ||
|
|
||
| event LineaTokensBridged(address indexed l2Token, address target, uint256 numberOfTokensBridged); |
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.
Please add an EVENTS section, or just follow all the commenting rules James implemented in https://github.com/across-protocol/contracts-v2/pull/381/files
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.
| function _bridgeTokensToHubPool(uint256 amountToReturn, address l2TokenAddress) internal override { | ||
| // Linea's L2 Canonical Message Service, requires a minimum fee to be set. | ||
| uint256 minFee = minimumFeeInWei(); | ||
| require(msg.value == minFee, "MESSAGE_FEE_MISMATCH"); |
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.
Can you add a comment here about why we require that the caller pass in msg.value versus pulling ETH out of this contract's balance which would require a separate accounting system to keep LP funds separated from system funds used to pay for L2 to L1 messages?
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.
| USSSpokePoolInterface.USSRelayerRefundLeaf calldata relayerRefundLeaf, | ||
| bytes32[] calldata proof | ||
| ) public virtual override nonReentrant { | ||
| ) public payable virtual override nonReentrant { |
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.
So this doesn;'t compile if we leave this change out?
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.
yea exactly
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.
This mostly looks good to go but needs more comments
Closes ACX-1759, ACX-1760 and ACX-1768
Test Contracts
Linea_AdapterTokensRelayerLinea_SpokePoolLinea_MockSpokePoolL1->L2
relayMessage- viasetDepositRoutesetDepositRoutetxclaimMessagetxrelayTokens- WETHrelayTokenstxclaimMessagetxrelayTokens- USDCrelayTokenstxclaimMessagetxrelayTokens- USDTrelayTokenstxclaimMessagetxL2->L1
_bridgeTokensToHubPool- WETHbridgeTokensToHubPooltxclaimMessagetx_bridgeTokensToHubPool- USDCbridgeTokensToHubPooltxclaimMessagetx_bridgeTokensToHubPool- USDTbridgeTokensToHubPooltxclaimMessagetx