Skip to content

Conversation

@dohaki
Copy link
Contributor

@dohaki dohaki commented Dec 18, 2023

Closes ACX-1759, ACX-1760 and ACX-1768

Test Contracts

Contract Address Chain
Linea_Adapter 0x84e14730cf11d7e2a0ac448b8a88d8e02ef296fd 5
TokensRelayer 0x33e76207af8AeAc2072213C9Bc33eB844b0e26e4 5
Linea_SpokePool 0x9FC7700c4Fd1e689f0c9cd25a1BC9B82328fe0C5 59140
Linea_MockSpokePool 0x24fE232B87E6bba1865ddAD8AF4faA2C8B5323B4 59140

L1->L2

Method Goerli Linea Goerli
relayMessage - via setDepositRoute setDepositRoute tx claimMessage tx
relayTokens - WETH relayTokens tx claimMessage tx
relayTokens - USDC relayTokens tx claimMessage tx
relayTokens - USDT relayTokens tx claimMessage tx

L2->L1

Method Linea Goerli Goerli
_bridgeTokensToHubPool - WETH bridgeTokensToHubPool tx claimMessage tx
_bridgeTokensToHubPool - USDC bridgeTokensToHubPool tx claimMessage tx
_bridgeTokensToHubPool - USDT bridgeTokensToHubPool tx claimMessage tx

@linear
Copy link

linear bot commented Dec 18, 2023

Copy link
Contributor

@mrice32 mrice32 left a 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);
Copy link
Contributor

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:

  1. Hard to compute this offchain.
  2. 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.

Copy link
Contributor Author

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?

Copy link
Member

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?

Copy link
Contributor

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.

Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done here 32d72c0

@dohaki dohaki changed the title feat: add Linea_Adapter.sol and goerli deployment feat(linea): add adapter and spokepool contracts Dec 26, 2023
@socket-security
Copy link

New dependencies detected. Learn more about Socket for GitHub ↗︎

Packages Version New capabilities Transitives Size Publisher
@consensys/linea-sdk 0.1.6 None +27 11.6 MB victorien-gauch

@linear
Copy link

linear bot commented Dec 26, 2023

@dohaki dohaki requested a review from mrice32 December 27, 2023 08:56
* @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);
Copy link
Member

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

Copy link
Contributor Author

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

Copy link
Member

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, "");
Copy link
Member

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?

Copy link
Contributor Author

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 🤔

Copy link
Member

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

// 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, "");
Copy link
Member

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?

Copy link
Contributor Author

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.

// 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);
Copy link
Member

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?

// For other tokens, we can use the Canonical Token Bridge.
else {
IERC20(l2TokenAddress).safeIncreaseAllowance(address(l2TokenBridge), amountToReturn);
l2TokenBridge.bridgeToken(l2TokenAddress, amountToReturn, hubPool);
Copy link
Member

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");
Copy link
Member

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?

Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

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?

Copy link
Contributor Author

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);
Copy link
Member

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


const uniqueTxHashes = new Set(events.map((event) => event.transactionHash));
for (const txHash of uniqueTxHashes) {
const messageSentEvents = await l1MessageService.getMessagesByTransactionHash(txHash);
Copy link
Member

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

Copy link
Contributor Author

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

Copy link
Contributor Author

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

Copy link
Contributor

@mrice32 mrice32 left a 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");
Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Contributor

@mrice32 mrice32 left a 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");
Copy link
Contributor

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);
Copy link
Contributor

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

@dohaki dohaki requested a review from nicholaspai January 3, 2024 18:36
Copy link
Member

@nicholaspai nicholaspai left a 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

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");
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed here a29b3a0


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();
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure will do!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added here 916f600. New deployment can be tested here

@dohaki dohaki requested a review from nicholaspai January 4, 2024 14:40

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();
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed here d76364c

@dohaki dohaki requested a review from nicholaspai January 4, 2024 15:34
contract Linea_SpokePool is SpokePool {
using SafeERC20 for IERC20;

IMessageService public l2MessageService;
Copy link
Member

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

Copy link
Contributor Author

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(
Copy link
Member

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

Copy link
Contributor Author

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);
Copy link
Member

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

Copy link
Contributor Author

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");
Copy link
Member

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?

Copy link
Contributor Author

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 {
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea exactly

Copy link
Member

@nicholaspai nicholaspai left a 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

@dohaki dohaki merged commit 339bafa into master Jan 7, 2024
@dohaki dohaki deleted the dong-ha/acx-1759-implement-linea_adapter branch January 7, 2024 11:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants