-
Notifications
You must be signed in to change notification settings - Fork 0
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
Exploiting gas limit manipulation in staking incentive distribution on L2 #48
Comments
We cannot revert on the side of L2 since this has an immediate loss of data effect. That's why we do the call instead of a regular high level interface call. Hence, for us there is no difference if the verification function has failed due to the lack of gas or not. Since cross-chain bridging interaction depends on off-chain computation for some networks, we have to accept that even without attacks there could be scenarios of wrongly supplied bridging parameters (if things are done outside of our SDK usage). However, there is a |
kupermind (sponsor) disputed |
Hey @kupermind
|
We have done tests such that there is no excessive gas spending on L1 side. This turned out to have at most 11 staking contracts sent from L1 to L2. This is a relatively small amount of contracts to perform checks on, and we always have enough gas on L2. |
Got it, given that the likelihood of the function requiring so much gas and the impact not being very high, I'm marking this as low |
0xA5DF changed the severity to QA (Quality Assurance) |
0xA5DF marked the issue as grade-b |
0xA5DF marked the issue as grade-a |
For awarding purposes, C4 staff have marked as 3rd place. |
Lines of code
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L160
Vulnerability details
Impact
An attacker can provide specific amount of gas during claiming staking incentives, so that on L2, the distribution to the staking target would be unsuccessful and the OLAS amount will be held as
withheldAmount
.Proof of Concept
During claiming the staking incentives, the parameter
bridgePayload
is provided by the user. The flow of function calls is as follows (assuming the bridging protocol is Optimism):https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/Dispenser.sol#L1041
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/Dispenser.sol#L423-L431
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/DefaultDepositProcessorL1.sol#L150
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/OptimismTargetDispenserL2.sol#L63
It shows that the amount of gas limit provided as parameter by the user will be forwarded to the destination contract on L2. Note that the minimum allowed value is 300_000.
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L67
On L2, the flow of function calls is as follows:
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/OptimismTargetDispenserL2.sol#L96
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L242
https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L160
https://github.com/code-423n4/2024-05-olas/blob/main/registries/contracts/staking/StakingFactory.sol#L294
This shows that the amount of gas provided by the user (as input parameter on L1) will be forwarded to the function
OptimismTargetDispenserL2::receiveMessage
. Suppose that the remaining gas before calling the functionStakingFactory::verifyInstanceAndGetEmissionsAmount
(before line 161) isX
.https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L161
So,
(63/64) * X
will be forwarded to theStakingFactory
.If, the amount of gas used in
StakingFactory::verifyInstanceAndGetEmissionsAmount
is bigger than(63/64) * X
, then the booleansuccess
would befalse
and almost(1/64) * X
would be left for execution of the remaining code.Running a simple test shows that the amount of gas usage from line 162 to 213 if
success = false
would be around 14_000. This amount of gas is calculated in case the two storage variablesstakingBatchNonce
andwithheldAmount
are nonzero already (for sure changing their state from zero to nonzero would consume more gas).Moreover, the amount of gas consumed during
StakingFactory::verifyInstanceAndGetEmissionsAmount
is unknown because it externally calls theinstance
that it can have customized implementation.This provides an opportunity for an attacker to force the distribution to the staking targets (that are heavy gas consumer during the verification) to be unsuccessful, so always the transferred OLAS amount would be held in
DefaultTargetDispenserL2
aswithheldAmount
. The attack is as follows:63 * 14_000 = 882_000
gas. So, the attacker calls the functionclaimStakingIncentives
with parameterbridgePayload
equal to:Where
1500
is the required gas until line 161.https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L161
StakingFactory::verifyInstanceAndGetEmissionsAmount
at line 161 would be almost equal toX = 64 * 14_000
.(63/64) * X = 882_000
would be forwarded to theStakingFactory
. If the amount of gas consumed there is more than that, then the low-level call would be unsuccessful, andsuccess = false
. Thus,limitAmount = 0
, and the OLAS amount would not be transferred to the target, and it would be held aswithholdAmount
.https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L163-L177
(1/64) * X = 14_000
would be left for the rest of the code, which is enough to be executed properly.In summary, the attacker by watching the required gas for verification of a staking target can set the gas limit to a value, so that the staking incentives distribution would be unsuccessful, and the OLAS token amount would be held as
withheldAmount
.Test Case
In the following test case:
withheldAmount
andstakingBatchNonce
increases to100
and1
, respectively. This is not part of the attack scenario, it is just to make those storage variables nonzero (to make the scenario more simple and realistic).(900_000 - 1500) * (63/64)
. By doing so, on L2, the distribution would be unsuccessful, and the transferred OLAS would be held in thewithheldAmount
. Note that, if the attacker provides 1_100_000 gas, it would distribute the OLAS amount successfullysuccess = true
.withheldAmount
is increased from 100 to 200 meaning that sending the message with 900_000 gas leads to the situation that the OLAS token transfer to the staking target is unsuccessful, and it is indeed added towithheldAmount
.The function
sendMessage
in the contractBridgeRelayer
should be modified as follows.https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/test/BridgeRelayer.sol#L286
The function
verifyInstanceAndGetEmissionsAmount
in the contractMockStakingFactory
should be modified as follows to emulate a case where the verification consumes high gas.https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/test/MockStakingFactory.sol#L39
The result is:
Tools Used
Recommended Mitigation Steps
It is recommended that when
StakingFactory::verifyInstanceAndGetEmissionsAmount
is called, in case of failure, it checks the gas forwarded was enough or not. If it was not enough, then it should revert and the message should be retried on L2 again.https://github.com/code-423n4/2024-05-olas/blob/main/tokenomics/contracts/staking/DefaultTargetDispenserL2.sol#L160
Assessed type
Context
The text was updated successfully, but these errors were encountered: