hats-finance / Catalyst-Exchange-0x3026c1ea29bf1280f99b41934b2cb65d053c9db4

Other
1 stars 2 forks source link

Incentive mechanism doesn't work on Arbitrum #50

Open hats-bug-reporter[bot] opened 8 months ago

hats-bug-reporter[bot] commented 8 months ago

Github username: -- Twitter username: https://twitter.com/windhustler Submission hash (on-chain): 0x5affa9ba62c91d0fa1e633b336f88d94167b33dcca92ae9c5e0ab3e26af0f213 Severity: high

Description: Description\ The incentive structure is broken on Arbitrum. The relayer is getting underpaid for the work he's doing. As the main point of the codebase is to provide incentives and it doesn't work on Arbitrum, I'm marking this as high severity.

Attack Scenario\ Gas on Arbitrum is handled differently than on other chains.

Here is an explanation of how gas works on Arbitrum: https://docs.arbitrum.io/devs-how-tos/how-to-estimate-gas.

The summary is that the transaction fee is a function of:

This is the formula used to compute the transaction fee:

L1 Estimated Cost (L1C) = L1 price per byte of data (L1P) * Size of data to be posted in bytes (L1S)

Extra Buffer (B) = L1 Estimated Cost (L1C) / L2 Gas Price (P)

TXFEES = P * (L2G + ((L1P * L1S) / P))

What is important to note here gas used on Arbitrum is the same as you would use on Ethereum. It's the same EVM opcodes. What is different is the final transaction fee that includes the L1 portion.

Issue

The execution on the receiving side inside the processPacket starts by taking note of the gas at the beginning of the function execution: https://github.com/catalystdao/GeneralisedIncentives/blob/main/src/IncentivizedMessageEscrow.sol#L243 And at the end of the function, it computes the total gas used. This is being sent with the acknowledgment back to the sending chain so that the relayer can be paid from the Incentive deposited by the application: https://github.com/catalystdao/GeneralisedIncentives/blob/main/src/IncentivizedMessageEscrow.sol#L329

As maxGas is the same as for any other EVM chain, the sending application would need to figure out the price which takes into account all these additional factors that come into play on Arbitrum.

If we take an average Arbitrum transaction the majority of transaction fee belongs to the L1 portion of the transaction fee. https://arbiscan.io/tx/0x23c53172b6dbd6d34d66f70f8bf94e1a3b1038e89a5dc7d6b024506ff1092951#txninfo

With that kind of setup this is simply not going to work.

I'm just going to give a simple example (not real numbers but for illustration purposes):

maxGas = 100; gasPrice = 1; L1P * L1S = 5000;

TxFee = 1 (100 + ((5000 1) / 1)) = 5100

As the maxGas is 100, to fit this in the simple model we would need to set the gasPrice to 51.

When the actual execution happens on Arbitrum the gas used is 70 and all the other params remain the same:

We get the following:

TxFee = 1 (70 + ((5000 1) / 1)) = 5070

But when this is sent back to the sending chain the relayer only gets paid a smaller portion of the incentive, although his cost is practically the same as with maxGas.

This way applications can set high maxGas to trick the relayer and as the actual gas spent is smaller than then on ack they will pay less than they should.

In short it just doesn't work on Arbitrum.

The issue I have described exists on Arbitrum but applies to other rollups as well, e.g. Optimism.

Recommended Mitigation Steps\ The incentive structure needs to be adjusted to account for rollups.

windhustler commented 8 months ago

Dropping the transaction on Arbitrum I quoted above to showcase that the majority of the transaction fee is due to L1. See L1 vs L2 gas used row:

Screenshot 2024-01-26 at 16 38 50
reednaa commented 8 months ago

Is this an issue in regards to a relayer implementation or an application using Generalised Incentives?

windhustler commented 8 months ago

It's an issue concerning how the struct is defined:

struct IncentiveDescription {
        **uint48 maxGasDelivery**;     
        uint48 maxGasAck;          
        address refundGasTo;      
        **uint96 priceOfDeliveryGas**; 
        uint96 priceOfAckGas;    
        uint64 targetDelta;     
    }

And how when the acknowledgment message is received you calculate the delivery fee: https://github.com/catalystdao/GeneralisedIncentives/blob/main/src/IncentivizedMessageEscrow.sol#L421.

As it's described the transaction fee on Arbitrum is not simply gas * price. The gas * L2 gas price is just a smaller fraction of the total fee paid by someone who submits the transaction.

reednaa commented 8 months ago

This is the relayer's problem and not on-chain logic.

The issue with implementing any of this on chain is that it has to be implemented on the sending chain. We cannot possibly ensure that the gas scheme works for every single chain. (We actually know it does not work on all CosmWasm chains and Solana) BUT it is a very good start.

The scheme is designed to be permissionless, so how would you propose determining which chains (from the source chain) are rollups / require some other gas evaluation? It is not reasonable to expect the applications to keep track of this.

What the scheme does is:

  1. Very strictly tell the relayer: This is the incentive associated with relaying the package.
  2. Define how applications should interface cross-chain.
  3. Increase the security assumptions of relaying.

The goals are outlined (not great but okay) in the beginning of the readme: https://github.com/catalystdao/GeneralisedIncentives#idea

Additional transaction costs are not included in the scheme, it is expected the relayers are aware of these. For example: processPacket is payable. Why would the relayer pay to relay a message?

Because there may be a cost associated with sending the package: https://github.com/catalystdao/GeneralisedIncentives/blob/2448d77e412216283ed75d8c3cbaa1270657f7b5/src/apps/wormhole/IncentivizedWormholeEscrow.sol#L68-L73 Which goes into https://github.com/catalystdao/GeneralisedIncentives/blob/2448d77e412216283ed75d8c3cbaa1270657f7b5/src/IncentivizedMessageEscrow.sol#L254.

windhustler commented 8 months ago

I’ve been thinking about the exact impact here, these are my thoughts.

Gas on Arbitrum consists of L1 and L2 fees which comprise the total transaction cost. L1 fees make up +90% percent of the total transaction cost.

Let’s take the following example:

An application wants to send a message from Ethereum → Arbitrum. The receiving app has the logic, i.e. ICrossChainReceiver(toApplication).receiveMessage(..) to invoke a swap through a DEX aggregator. I’m giving this example since gas spent there can vary by ~30%. But this can be anything where it’s hard to estimate gas with 100% accuracy.

Now the sending app needs to place an incentive for the relayer to deliver this to Arbitrum.

Since you’re only giving the option to compute the incentive with gas * price the sending application does the following (This needs to be done off-chain):

1) It first roughly estimates the maxGasDelivery on Arbitrum. Let’s take the estimate to be 200k gas.

2) Then it figures out what will be the final transaction cost taking into account the Arbitrum gas model. For simplicity reasons, it consists of L1 and L2 fees which comprise the total transaction cost. L1 fees make up +90% percent of the total transaction cost. So we can say:

Transaction cost = L2_cost + L1_cost = 200k * 0.1 + 100k = 120k

3) Since the only way of expressing this 120k is using two variables we arrive at the following values:

maxGasDelivery = 200k

price = 0.6

When multiplied yield the same incentive ⇒ 200k * 0.6 = 120k

What is the issue here?

First, the 200k gas is a maximum estimate. If the actual gas spent on the destination is 150k, the incentive the relayer picks is:

150k * 0.6 ⇒ 90k

When in reality his cost is:

150k * 0.1 + 100k ⇒ 115k

So he doesn’t deliver the message.

Another side of the coin is when the L1 fixed cost decreases massively. Then the application will be overpaying for the delivery of their transaction.

What is the impact here?

If the application cannot set the incentive properly it:

I don’t see a solution being an easy fix. Let me think a bit more.

reednaa commented 8 months ago

Thanks for the walkthrough. Would a potential fix to use all of the gas on arbitrum?: Not recording the gas spent and instead telling the source chain that all of the allowed gas used was actually used?

From the docs: If a chain is incapable of measuring gas, it should return maxGas as gasSpent. https://github.com/catalystdao/GeneralisedIncentives?tab=readme-ov-file#definition-of-gas

windhustler commented 8 months ago

You could do that for Arbitrum/Optimism and any other chain with a different gas model. That would also mean there is no gas refund functionality for these chains as the relayer eats up all the incentives.

Another option is to define a gas * price + fixed_cost as the whole incentive schema. I'm just afraid this would bring too much complexity, as I see no way of determining how much the Relayer spent for the transaction.

reednaa commented 8 months ago

Okay. Thanks for the documentation. I am leaning no or low.

You didn't make it into our triage session so I will schedule for next time. Feel free to add more information in an attempt to sway us into higher severity.

windhustler commented 7 months ago

From the docs: If a chain is incapable of measuring gas, it should return maxGas as gasSpent. https://github.com/catalystdao/GeneralisedIncentives?tab=readme-ov-file#definition-of-gas

I agree with the proposed fix. On chains that have a different gas model return maxGas as spent. As this is not in place right now I believe this is a valid issue.

reednaa commented 7 months ago

We have decided to classify this as won't fix. This is based on the following arguments:

Based on these arguments, we have decided to classify this as won't fix.