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

feat(protocol): improve Bridge gasleft() validation #17422

Merged
merged 21 commits into from
Jun 2, 2024
Merged

Conversation

dantaik
Copy link
Contributor

@dantaik dantaik commented May 29, 2024

Resolving L-23 Diff-Audit: Inaccurate Gas Limit on Message Invocation


Users can send cross-chain messages by calling sendMessage on the Bridge contract. An optional gas limit can be defined for this message. In that case, if a user wants their message to be executed with at least A gas, message.gasLimit has to be at least A + minGas, where minGas depends on the message length plus a flat gas amount.

When a message is processed by a third party, the gas remaining before invoking the user's message is validated to be greater than 64 / 63 * A. This is because of EIP-150, which silently caps the amount of gas sent in external calls to 63 / 64 * gasleft(). If such a check was not present, it would be possible for the gas to be capped by EIP-150, executing the call with less than A gas.

However, while the intention was right, a gas limit of A is not guaranteed. This is due to the gas expenses that accumulate between the EIP-150 check and the actual message execution on the target:

costs of memory expansion
account access
transfers
opcode costs
Because 1 / 64 * gasleft() has to be enough to execute the rest of the processMessage function without running out of gas, a gas limit below A on the target is only possible for high gas limits. A proof of concept shows the issue in practice.

Consider addressing the above to build a more predictable bridge for users and projects sending cross-chain messages. What follows is an example of how this could be achieved.

The goal of the computation is to ensure that EIP-150 does not silently cap the amount of gas sent with the external call to be less than A. We note:

memory_cost: the amount of gas needed to expand the memory when storing the inputs and outputs of the external call.
access_gas_cost: the gas cost of accessing the message.to account. This currently corresponds to 2600 gas if the account is cold, and 100 otherwise.
transfer_gas_cost: the cost of transferring a nonzero msg.value. This cost is currently 9000, but provides a 2300 gas stipend to the called contract.
create_gas_cost: the cost of creating a new account, currently 25000. This only applies if message.value != 0, message.to.nounce == 0, message.to.code == b"" and message.to.balance == 0. Because message.to is checked to have code, this cost can be ignored here.
We thus want to check that the following:

63 / 64 * (gasleft() - memory_cost - access_gas_cost - transfer_gas_cost)
is at least as much as A (reference implementation). The sum of access_gas_cost and transfer_gas_cost can be upper bounded with 2_600 + 9_000 - 2_300 = 9_300. The memory_cost is cumbersome to compute in practice, but by estimating the costs through tests, we can upper bound the cost of memory expansion for up to 10_000 bytes of message.data in the context of the call with the formula 1_200 + 3 * message.data.length / 32.

Thus, it would be possible to validate that the call will have enough gas by checking the following condition right before the external call is made:

63 * gasleft() >= 64 * A + 63 * (9_300 + 1_200 + 3 * message.data.length / 32) + small_buffer
This would be accurate for messages with up to 10_000 bytes of message.data. Any message above this limit could be required to have a gas limit of zero. This would force it to be processed by its destOwner. A similar approach, without a constraint on the data size, has been adopted by Optimism and can be used for inspiration.


Note that I didn't put a hard limit as suggested to message.data.length as we don't know yet in practice how large message.data can be.

@dantaik dantaik marked this pull request as ready for review May 29, 2024 16:49
@dantaik dantaik requested a review from KorbinianK May 29, 2024 16:51
Copy link

openzeppelin-code bot commented May 29, 2024

feat(protocol): improve Bridge gasleft() validation

Generated at commit: a6ddc8792e7d5b6df56e9b83d4422c3b603a2cda

🚨 Report Summary

Severity Level Results
Contracts Critical
High
Medium
Low
Note
Total
2
2
0
8
42
54
Dependencies Critical
High
Medium
Low
Note
Total
0
0
0
0
0
0

For more details view the full report in OpenZeppelin Code Inspector

@dantaik dantaik marked this pull request as draft May 30, 2024 02:34
@dantaik dantaik marked this pull request as ready for review May 30, 2024 13:00
@dantaik dantaik requested a review from cyberhorsey May 30, 2024 13:19
@dantaik dantaik added this pull request to the merge queue Jun 2, 2024
Merged via the queue into main with commit 0febafe Jun 2, 2024
5 checks passed
@dantaik dantaik deleted the improve_gas_check branch June 2, 2024 06:26
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