DittoETH

Ditto
DeFiFoundryOracle
55,000 USDC
View results
Submission Details
Severity: medium
Invalid

No slippage protection in unstakeEth()

Summary

If a user deposits STETH tokens he/she receives ZETH tokens equivalent to the amount of deposited STETH. The deposit is combined with deposits of RETH tokens in the Carbon vault. If the price of RETH drops more than the price of ETH and the yield becomes negative, when users unstake they will receive less STETH tokens than the ZETH tokens that they burn. In volatile market conditions the price of RETH token can drop with
5%, and the price of ETH may drop only with 1%. Although this is a mechanism of the protocol to protect itself against volatile market conditions, users are not protected from burning all their ZETH tokens and receiving less STETH tokens than expected when calling the unstakeEth() function. Also there is no deadline specified so even if when the user created the transaction the yield was not negative, by the time it is picked and executed by the node market condition may have changed, and the yield may have became negative which results in users loosing funds.

Vulnerability Details

In BridgeSteth.sol import:

import {console} from "forge-std/Test.sol";

and add:

console.log("Number of stETH represented by 1 NFT: ", amount);

to the unstake() function in order to the the amount of STETH that will be represented by the NFT

POC

In BridgeRouter.t.sol add the following

Create user alice:

address public alice = address(12);

In setUp add the following:

vm.startPrank(alice);
steth.approve(
_bridgeSteth,
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
);
vm.stopPrank();

Add the following test:

function test_minAmountOutUnstake() public {
console.log("");
console.log("Balances of the carbon vault, alice and sender before deposits");
console.log("Here is the value in ETH of the staked RETH and STETH tokens in the carbon vault", diamond.getZethTotal(Vault.CARBON));
console.log("Here is the sender zeth tokens for the carbon vault: ", diamond.getVaultUserStruct(Vault.CARBON, sender).ethEscrowed);
console.log("Here is the begginer zeth tokens for the carbon vault: ", diamond.getVaultUserStruct(Vault.CARBON, alice).ethEscrowed);
deal(_reth, sender, 100 ether);
deal(_steth, alice, 100 ether);
console.log("");
console.log("Balances of alice and sender after dealing them RETH and STETH tokens");
console.log("reth balance of sender: ", reth.balanceOf(sender));
console.log("steth balance of begginer: ", steth.balanceOf(alice));
/// INFO: Sender deposits 100 RETH
vm.startPrank(sender);
uint88 deposit1 = 100 ether;
diamond.deposit(_bridgeReth, deposit1);
console.log("");
console.log("Sender deposits 100 RETH");
console.log("Here is the sender zeth tokens for the carbon vault: ", diamond.getVaultUserStruct(Vault.CARBON, sender).ethEscrowed);
console.log("Here is the value in ETH of the staked RETH and STETH tokens in the carbon vault: ", diamond.getZethTotal(Vault.CARBON));
vm.stopPrank();
/// INFO: Alice deposits 100 STETH
vm.startPrank(alice);
uint88 deposit2 = 100 ether;
diamond.deposit(_bridgeSteth, deposit2);
console.log("");
console.log("Alice deposits 100 STETH");
console.log("Here is Alice zeth tokens for the carbon vault: ", diamond.getVaultUserStruct(Vault.CARBON, alice).ethEscrowed);
console.log("Here is the value in ETH of the staked RETH and STETH tokens in the carbon vault: ", diamond.getZethTotal(Vault.CARBON));
vm.stopPrank();
/// INFO: Simulate a market condition where the price of RETH falls down
uint256 _ethSupply = 0.8 ether;
uint256 _rethSupply = 1 ether;
address reth_address = bridgeReth.getBaseCollateral();
IRocketTokenRETH rocketTokenRETH = IRocketTokenRETH(reth_address);
rocketTokenRETH.submitBalances(_ethSupply, _rethSupply);
/// INFO: Get the Steth bridge unstake fee
console.log("");
console.log("Withdraw fee of the Steth bridge: ", diamond.getBridgeStruct(_bridgeSteth).unstakeFee);
/// INFO: Alice unstakes all of her staked STETH
vm.startPrank(alice);
diamond.unstakeEth(_bridgeSteth, deposit2);
console.log("");
console.log("Alice unstakes 100 STETH");
console.log("Here is the amount of ZETH tokens in the carbon vault: ", diamond.getVaultStruct(Vault.CARBON).zethTotal);
console.log("Here is the alice zeth tokens for the carbon vault: ", diamond.getVaultUserStruct(Vault.CARBON, alice).ethEscrowed);
console.log("Balance of unsteth of alice: ", unsteth.balanceOf(alice));
vm.stopPrank();
}

Output:

Balances of the carbon vault, alice and sender before deposits
Here is the value in ETH of the staked RETH and STETH tokens in the carbon vault 0
Here is the sender zeth tokens for the carbon vault: 0
Here is the begginer zeth tokens for the carbon vault: 0
Balances of alice and sender after dealing them RETH and STETH tokens
reth balance of sender: 100000000000000000000
steth balance of begginer: 100000000000000000000
Sender deposits 100 RETH
Here is the sender zeth tokens for the carbon vault: 100000000000000000000
Here is the value in ETH of the staked RETH and STETH tokens in the carbon vault: 100000000000000000000
Alice deposits 100 STETH
Here is Alice zeth tokens for the carbon vault: 100000000000000000000
Here is the value in ETH of the staked RETH and STETH tokens in the carbon vault: 200000000000000000000
Withdraw fee of the Steth bridge: 0
Number of stETH represented by 1 NFT: 90000000000000000000
Alice unstakes 100 STETH
Here is the amount of ZETH tokens in the carbon vault: 100000000000000000000
Here is the alice zeth tokens for the carbon vault: 0
Balance of unsteth of alice: 1

To run the test use: forge test -vvv --mt test_minAmountOutUnstake

Impact

When a user decides to unstake his/her STETH tokens, he/she may receive less STETH tokens, than he/she wants and already burned ZETH tokens for. Resulting in user losing funds.

Tools Used

Manual review

Recommendations

Add a minAmountOut parameter that a user can set to the unstakeEth() function, and revert if the amount returned from the _ethCOnversion() function is less.

Updates

Lead Judging Commences

0xnevi Lead Judge
about 2 years ago
0xnevi Lead Judge about 2 years ago
Submission Judgement Published
Invalidated
Reason: Other
dimulski Auditor
about 2 years ago
0xnevi Lead Judge
about 2 years ago
dimulski Auditor
about 2 years ago
0xnevi Lead Judge
about 2 years ago
dimulski Auditor
about 2 years ago
0xnevi Lead Judge about 2 years ago
Submission Judgement Published
Invalidated
Reason: Other

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.