Closed howlbot-integration[bot] closed 1 month ago
Picodes marked the issue as not a duplicate
Picodes marked the issue as duplicate of #8
Picodes marked the issue as satisfactory
Hi @Picodes , could you consider selecting this issue for the report instead of #8 ? I believe it is better due to the following reasons:
baseStake
value).That being said, I am aware that a report being "better" is extremely subjective, and will respect your final decision as a judge.
Thanks!
Hi @Ch-301 ,
@Ch-301 thanks for your comment, that I totally understand given how arbitrary this is.
I went for #8 considering that:
Lines of code
https://github.com/code-423n4/2024-05-arbitrum-foundation/blob/main/src/rollup/RollupAdminLogic.sol#L223-L226
Vulnerability details
Description
All the validators that have lost a challenge in the past can steal funds from The honest validators in
RollupUserLogic.sol
Here are quick 7 steps POC (check below for coded POC): Note: Alice and Bob are playing under the same entity.1- The adversary validators Bob lose a challenge. Bob loses 10 Wei here.
2- The honest validator creates the next assertion.
3- The admin call setBaseStake() to decrease the
baseStake
state variable. e.g: from 10 Wei to 8 Wei.4- Alice invoke newStakeOnNewAssertion() to create the first child of Bob assertion Alice stake (technically it's a loss at this point) 10 Wei for this. However, the
requiredStake
in Alice's assertionconfigHash
is only 8 Wei (Check it here).5- Bob trigger reduceDeposit() to reduce his amount staked to only 8 Wei.
6- Now, Bob invoke stakeOnNewAssertion() to create the first child of Alice assertion.
7- Alice invoke returnOldDeposit() to withdraw her 10 Wei and Bob withdraw his 2 Wei by calling withdrawStakerFunds().
Deep dive
Key points:
In the
RollupUserLogic.sol
, When an assertion has two children or more, The protocol only keeps 1 stake per challenge, so the loser stakes are already sent to theloserStakeEscrow
(check here). So, the only funds will be available inRollupUserLogic
is from the honest validators and they can withdraw the funds when are not active staker.In the RollupCore system, it is possible to create multiple assertions with only one stack. It uses a structure where assertions are linked, and each new assertion is created based on the state of the previous one. This is why when an adversary validator loses a challenge his values in _stakerMap[stakerAddress] are not updated.
The
setBaseStake()
function in theRollupAdminLogic.sol
contract is used to update thebaseStake
state variable, which is required for posting a claim (aka an assertion) in theRollupUserLogic.sol
contract. This function is part of the administrative controls of the rollup system, allowing the rollup administrator to adjust the staking requirements as needed.The
baseStake
state variable is used in createNewAssertion() this values will be defined how much the next assertion should stake (the next assertion should get created based previous one).Step by step:
When Bob loses the challenge to the honest validator he also loses the 10 wei (as a stake amount). However, His values in
_stakerMap[stakerAddress]
don’t get updated this is intended by the protocol because the loser’s stake would not be able to be withdrawn unless someone else put down a stake to unlock the loser's stake, but that someone’s stake would be locked, effectively he is paying the original loser.On the other hand, the honest validator will keep up his good work by posting the next and the next assertions.
Until one day the rollup owner decides to decrease the baseStake state variable (More on how the logic handle it HERE)
Note: At this point, any validator who has lost a challenge in the past can do the same as Bob will do.
Back to Bob, His assertion state is still Pending and let's call it the assertion X Alice will invoke newStakeOnNewAssertion() to create the first child of Bob assertion X and let's call Alice's assertion Y. Of course, she will stake 10 Wei and Bob will be able to withdraw his 10 Wei (But Bob will not withdraw his 10 Wei now)
The reason why we need to create assertion Y. is because it will use the new value of
baseStake
in theconfigHash
of assertion Y (this will help the adversary validator to unlock Alice's stake).Now, Bob will trigger reduceDeposit() to reduce his amount staked to only 8 Wei (This 8 Wei he will use it to unlock 10 Wei of Alice). The other 2 Wei are unstacked and he can withdraw them anytime.
Bob will call stakeOnNewAssertion() to create the first child of Alice assertion Y BUT, this time he will stake only 8 Wei (Why? Check this again)
As a result of this: 1- Alice will withdraw her 10 Wei 2- Bob will withdraw the stolen funds which is 2 Wei in this POC Note: The 2 Wei are from the honest validators staked funds.
Impact
Any validator that has lost a challenge in the past can steal part of the honest validator's staked funds.
Proof of Concept
Foundry PoC:
Please copy the following POC in
Rollup.t.sol
2-
forge test --match-test testRun_Me_POC
Tools Used
Docs Wolf - Manual Review
Recommended Mitigation Steps
Make sure that any adversary validator is not able to come back and recover his funds by triggering
RollupCore.sol#deleteStaker()
However, I can't find a way with the current tracking system to find and delete the pending assertions after callingRollupUserLogic.sol#confirmAssertion()
Assessed type
Invalid Validation