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

Attackers can conduct a permanent DoS on others' snapshots. #30

Closed
c4-bot-3 opened this issue Jul 30, 2024 · 10 comments
Closed

Attackers can conduct a permanent DoS on others' snapshots. #30

c4-bot-3 opened this issue Jul 30, 2024 · 10 comments
Labels
bug Something isn't working downgraded by judge Judge downgraded the risk level of this issue grade-b primary issue Highest quality submission among a set of duplicates QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax 🤖_29_group AI based duplicate group recommendation sufficient quality report This report is of sufficient quality

Comments

@c4-bot-3
Copy link
Contributor

Lines of code

https://github.com/code-423n4/2024-07-karak/blob/main/src/NativeVault.sol#L126-L162
https://github.com/code-423n4/2024-07-karak/blob/main/src/NativeVault.sol#L448-L474
https://github.com/code-423n4/2024-07-karak/blob/main/src/NativeVault.sol#L476-L495
https://github.com/code-423n4/2024-07-karak/blob/main/src/entities/NativeVaultLib.sol#L110-L144

Vulnerability details

Impact

Attackers can conduct a permanent DoS on others' snapshots.

Proof of Concept

Let's consider the following scenario:

  1. Alice last called the startSnapshot() function 8 days ago and is about to invoke it again.

  2. Bob, the attacker, executes the following two functions in succession by front-running Alice within the same block:

    • The validateExpiredSnapshot() function, using nodeOwner set to Alice.
    • The validateSnapshotProofs() function, with nodeOwner as Alice and balanceProofs containing all of Alice's validators.

    As a result:

  3. Finally, Alice's transaction to call the startSnapshot() function succeeds, as node.currentSnapshotTimestamp == 0. So, node.currentSnapshotTimestamp is set to block.timestamp at L470, which results in validatorDetails.lastBalanceUpdateTimestamp == node.currentSnapshotTimestamp == block.timestamp for all the active validators.

    https://github.com/code-423n4/2024-07-karak/blob/main/src/NativeVault.sol#L470

            node.currentSnapshotTimestamp = uint64(block.timestamp);

From this point forward, Alice's call to validateSnapshotProofs() will always revert at L149, as the values of validatorDetails.lastBalanceUpdateTimestamp and node.currentSnapshotTimestamp are now identical. As a result, the validators can never be proven and snapshot.remainingProofs is forever stuck at a non-zero value, leading to a permanent DoS on Alice's snapshot.

https://github.com/code-423n4/2024-07-karak/blob/main/src/NativeVault.sol#L149-L151

    function validateSnapshotProofs(
        ...
            if (validatorDetails.lastBalanceUpdateTimestamp >= node.currentSnapshotTimestamp) {
                revert ValidatorAlreadyProved();
            }
        ...

If this occurs, stakers won't be able to withdraw all ETH staked to the validators. This is because the value of withdrawableCreditedNodeETH will never increase since it only increases by snapshot. As a result, the withdrawableCreditedNodeETH can't include all withdrawn ethers from validators to nodeAddress. So, the withdrawableWei() function doesn't return proper value.

    function withdrawableWei(address nodeOwner) public view nodeExists(nodeOwner) returns (uint256) {
        return
            Math.min(convertToAssets(balanceOf(nodeOwner)), _state().ownerToNode[nodeOwner].withdrawableCreditedNodeETH);
    }

This attack is available even when the nodeOwner calls startSnapshot() with the parameter revertIfNoBalanceChange as true. The attacker can circumvent a revert by donating 1 wei to nodeAddress between steps 2 and 3. (Donations are feasible because the NativeNode inherits from the Ownable contract, which includes the payable function cancelOwnershipHandover(), allowing anyone to call it. Even in the absence of the payable function cancelOwnershipHandover(), there are alternative methods available.)

Tools Used

Manual review

Recommended Mitigation Steps

There should be a mechanism in place to prevent double snapshots from occurring within the same block.

Assessed type

DoS

@c4-bot-3 c4-bot-3 added 3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working labels Jul 30, 2024
c4-bot-5 added a commit that referenced this issue Jul 30, 2024
@c4-bot-11 c4-bot-11 added the 🤖_29_group AI based duplicate group recommendation label Jul 30, 2024
@howlbot-integration howlbot-integration bot added sufficient quality report This report is of sufficient quality duplicate-59 labels Aug 1, 2024
@c4-judge
Copy link
Contributor

c4-judge commented Aug 5, 2024

MiloTruck marked the issue as not a duplicate

@c4-judge c4-judge reopened this Aug 5, 2024
@c4-judge
Copy link
Contributor

c4-judge commented Aug 5, 2024

MiloTruck marked the issue as primary issue

@c4-judge c4-judge added the primary issue Highest quality submission among a set of duplicates label Aug 5, 2024
@MiloTruck
Copy link

MiloTruck commented Aug 5, 2024

The crux of this issue is an attacker front-running a user's call to startSnapshot() to call validateExpiredSnapshot() followed by validateSnapshotProofs(), causing two snapshots to be started in the same block:

Bob, the attacker, executes the following two functions in succession by front-running Alice within the same block:

  • The validateExpiredSnapshot() function, using nodeOwner set to Alice.
  • The validateSnapshotProofs() function, with nodeOwner as Alice and balanceProofs containing all of Alice's validators.

However, I don't believe it is possible to start a snapshot and call validateSnapshotProofs() to prove validators in the same block. This is because it should be impossible to generate proofs for a block root returned by _getParentBlockRoot() in a future block, so an attacker wouldn't know what proofs to call validateSnapshotProofs() with.

Will check with the sponsor though.

A simple fix would be to revert in _startSnapshot() if node.lastSnapshotTimestamp == block.timestamp.

@c4-judge
Copy link
Contributor

c4-judge commented Aug 5, 2024

MiloTruck marked the issue as unsatisfactory:
Invalid

@c4-judge c4-judge closed this as completed Aug 5, 2024
@c4-judge c4-judge added the unsatisfactory does not satisfy C4 submission criteria; not eligible for awards label Aug 5, 2024
@karan-andalusia
Copy link

Fixed this by updating _startSnapshot to include the following check:

if (node.lastSnapshotTimestamp == uint64(block.timestamp)) revert AttemptedSnapshotInSameBlock();

@KupiaSecAdmin
Copy link

@MiloTruck

This issue, along with H-7 from the previous audit, presents identical possibilities and impacts.
It is difficult but possible to generate the proofs for a block root returned by _getParentBlockRoot() in a future block, particularly for miners.
It’s important to note that 'difficult' does not equate to 'unlikely' but reflects the computational complexity involved. The likelihood of this issue occurring cannot exceed that of a sandwich attack.
Moreover, attackers can explore numerous potential sets of proofs. If one of these proofs is valid, the attack can succeed.

Therefore, I think this issue is indeed valid.

@MiloTruck
Copy link

MiloTruck commented Aug 20, 2024

If you're referring to this line in H-7:

it is difficult to generate proofs for a block root returned by _getParentBlockRoot() in a future block.

It was written by me.

It's not possible because an attacker does not know which block his and the victim's transactions will get included in, and therefore, he will not know which parent block root to generate proofs for.

Even if the attacker was a validator, he would have to:

  • Somehow be able to propose a block with his transaction and the victim's transaction.
  • Compute proofs with the previous block's root.
  • Submit his attack transaction.
  • Build and propose his block.

All under 12 seconds.

Moreover, attackers can explore numerous potential sets of proofs. If one of these proofs is valid, the attack can succeed.

The number of potential sets of proofs is infinite, it's not possible for one of them to randomly be valid.

Given the numerous unlikely conditions required for this to ever occur, I believe QA is appropriate.

If you believe validators can pull this off, you need to be able to explain in detail what occurs on the beacon chain at the consensus level. Otherwise, this is just hypothetical speculation.

@c4-judge
Copy link
Contributor

MiloTruck removed the grade

@c4-judge c4-judge reopened this Aug 20, 2024
@c4-judge c4-judge added downgraded by judge Judge downgraded the risk level of this issue QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax and removed unsatisfactory does not satisfy C4 submission criteria; not eligible for awards 3 (High Risk) Assets can be stolen/lost/compromised directly labels Aug 20, 2024
@c4-judge
Copy link
Contributor

MiloTruck changed the severity to QA (Quality Assurance)

@c4-judge
Copy link
Contributor

MiloTruck marked the issue as grade-b

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working downgraded by judge Judge downgraded the risk level of this issue grade-b primary issue Highest quality submission among a set of duplicates QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax 🤖_29_group AI based duplicate group recommendation sufficient quality report This report is of sufficient quality
Projects
None yet
Development

No branches or pull requests

7 participants