Platform: Code4rena
Start Date: 24/03/2023
Pot Size: $49,200 USDC
Total HM: 20
Participants: 246
Period: 6 days
Judge: Picodes
Total Solo HM: 1
Id: 226
League: ETH
Rank: 115/246
Findings: 2
Award: $28.81
🌟 Selected for report: 0
🚀 Solo Findings: 0
11.1318 USDC - $11.13
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L84 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L98
Complete loss of user funds when staking with no active derivatives
Relevant code snippet from the contract
uint256 totalStakeValueEth = 0; // total amount of derivatives worth of ETH in system for (uint i = 0; i < derivativeCount; i++) { uint256 weight = weights[i]; IDerivative derivative = derivatives[i]; // <--- // @audit skips all derivatives when all zero weights if (weight == 0) continue; // --> uint256 ethAmount = (msg.value * weight) / totalWeight; // This is slightly less than ethAmount because slippage uint256 depositAmount = derivative.deposit{value: ethAmount}(); uint derivativeReceivedEthValue = (derivative.ethPerDerivative( depositAmount ) * depositAmount) / 10 ** 18; totalStakeValueEth += derivativeReceivedEthValue; } // mintAmount represents a percentage of the total assets in the system uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice; _mint(msg.sender, mintAmount);
Relevant POC code snippet demonstrating user loss of funds
contract LossOfFunds is Test { SafEth safEth; SfrxEth sfrxEth; WstEth wstEth; Reth reth; address admin = address(0xcafe); address user = address(0xdeadbeef); function setUp() public { vm.createSelectFork("mainnet"); // deploy contracts safEth = SafEth(payable(address( new ERC1967Proxy(address( new SafEth()), abi.encodeWithSelector(SafEth.initialize.selector, "safEth", "seth"))))); safEth.transferOwnership(admin); sfrxEth = SfrxEth(payable(address( new ERC1967Proxy(address( new SfrxEth()), abi.encodeWithSelector(SfrxEth.initialize.selector, address(safEth)))))); reth = Reth(payable(address( new ERC1967Proxy(address( new Reth()), abi.encodeWithSelector(Reth.initialize.selector, address(safEth)))))); wstEth = WstEth(payable(address( new ERC1967Proxy(address( new WstEth()), abi.encodeWithSelector(WstEth.initialize.selector, address(safEth)))))); // equal weights uint weight = 1e27; vm.startPrank(admin); safEth.addDerivative(address(sfrxEth), weight); safEth.addDerivative(address(reth), weight); safEth.addDerivative(address(wstEth), weight); vm.stopPrank(); } function testStuckFunds() public { // admin accidentiall disables all derivatives for (uint i; i < safEth.derivativeCount(); ++i) { vm.prank(admin); safEth.adjustWeight(i, 0); } // ensure user balance of shares is zero uint balanceBefore = safEth.balanceOf(user); assertEq(balanceBefore, 0); // get some ETH uint value = 10 ether; vm.deal(user, value); // stake vm.prank(user); safEth.stake{value: value}(); // get user balance shares uint balanceAfter = safEth.balanceOf(user); // convince ourselves user did not get any shares assertEq(balanceAfter, 0); // and has no ETH assertEq(address(user).balance, 0); // and contract has ETH stuck in the contract assertEq(address(safEth).balance, 10 ether); } }
Full POC https://gist.github.com/alpeware/41123fa62461bd5192d4bd430bc29499#file-lossoffunds-t-sol
POC logs https://gist.github.com/alpeware/41123fa62461bd5192d4bd430bc29499#file-log-txt
Foundry
Add an additional require
statement to stake
function -
require(totalWeight > 0, "no active derivatives")
#0 - c4-pre-sort
2023-04-04T19:11:25Z
0xSorryNotSorry marked the issue as duplicate of #363
#1 - c4-judge
2023-04-21T16:29:01Z
Picodes changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-04-21T16:32:09Z
Picodes marked the issue as satisfactory
🌟 Selected for report: RaymondFam
Also found by: 7siech, LeoGold, Phantasmagoria, SunSec, adeolu, anodaram, aviggiano, chaduke, d3e4, eyexploit, jasonxiale, juancito, koxuan, mojito_auditor, n33k, neumo, rotcivegaf, yac
17.681 USDC - $17.68
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L88
Loss of precision can result in lost user funds during staking.
Depending on the weights
and msg.value
, not the total value of ETH sent to stake will be
deposited into derivatives and thus left over in the contract since there's no way to pull
ETH out of the contract.
Here's the relevant code snippet from the contract -
uint256 totalStakeValueEth = 0; // total amount of derivatives worth of ETH in system for (uint i = 0; i < derivativeCount; i++) { uint256 weight = weights[i]; IDerivative derivative = derivatives[i]; if (weight == 0) continue; // <--- // @audit loss of precision creates residue ETH stuck in the contract uint256 ethAmount = (msg.value * weight) / totalWeight; // ---> // This is slightly less than ethAmount because slippage uint256 depositAmount = derivative.deposit{value: ethAmount}(); uint derivativeReceivedEthValue = (derivative.ethPerDerivative( depositAmount ) * depositAmount) / 10 ** 18; totalStakeValueEth += derivativeReceivedEthValue; } // mintAmount represents a percentage of the total assets in the system uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice;
Relevant code snippet from POC
contract LostFunds is Test { SafEth safEth; SfrxEth sfrxEth; WstEth wstEth; Reth reth; address admin = address(0xcafe); address user = address(0xdeadbeef); function setUp() public { vm.createSelectFork("mainnet"); // deploy contracts safEth = SafEth(payable(address( new ERC1967Proxy(address( new SafEth()), abi.encodeWithSelector(SafEth.initialize.selector, "safEth", "seth"))))); safEth.transferOwnership(admin); sfrxEth = SfrxEth(payable(address( new ERC1967Proxy(address( new SfrxEth()), abi.encodeWithSelector(SfrxEth.initialize.selector, address(safEth)))))); reth = Reth(payable(address( new ERC1967Proxy(address( new Reth()), abi.encodeWithSelector(Reth.initialize.selector, address(safEth)))))); wstEth = WstEth(payable(address( new ERC1967Proxy(address( new WstEth()), abi.encodeWithSelector(WstEth.initialize.selector, address(safEth)))))); // equal weights uint weight = 1e27; vm.startPrank(admin); safEth.addDerivative(address(sfrxEth), weight); safEth.addDerivative(address(reth), weight); safEth.addDerivative(address(wstEth), weight); vm.stopPrank(); } function testStuckFunds() public { // ensure contract balance is zero uint balanceBefore = address(safEth).balance; assertEq(balanceBefore, 0); // example value leaving residue uint value = 111882089711632596292; // get some ETH vm.deal(user, value); // stake vm.prank(user); safEth.stake{value: value}(); // get contract balance uint balanceAfter = address(safEth).balance; // convince ourselves not all ETH has been deployed assertTrue(balanceAfter > 0); } }
Full POC here https://gist.github.com/alpeware/7bd7e481efab941934256b34a14c97fb#file-lostfunds-t-sol
Logs https://gist.github.com/alpeware/7bd7e481efab941934256b34a14c97fb#file-logs-txt
Foundry
if (msg.value
- totalStakeValueEth
) is > 0, add to a randomly chosen derivative
add a new external function stakeBalance
allowing anyone to distribute the contract's balance across all derivatives without the contract taking any shares (this would also address the issue with ETH directly sent to the contract for any other reason)
#0 - c4-pre-sort
2023-04-04T16:22:16Z
0xSorryNotSorry marked the issue as duplicate of #455
#1 - c4-judge
2023-04-24T21:24:05Z
Picodes marked the issue as satisfactory
#2 - c4-judge
2023-04-24T21:42:25Z
Picodes changed the severity to 2 (Med Risk)