# First xERC4626 deposit exploit can break share calculation

First depositor of a fresh xERC4626 vault mints 1 share then donates tokens to inflate share price, forcing later depositors to receive near-zero shares.

- Payout: Audit contest
- Severity: medium
- Program: Tribe
- Found by: VAD37
- Disclosed: 2022-07-14
- Write-up quality: 6/10
- Original: https://code4rena.com/reports/2022-04-xtribe#m-02
- Library page: https://bugbountylibrary.com/r/c4%3A2022-04-xtribe%3AM-02
- Tags: area:web3 domain:web3 impact:funds-loss kind:contest platform:code4rena sev:medium tech:evm tech:solidity vuln:accounting-error vuln:precision-loss vuln:share-inflation

**Target:** xERC4626 vault share accounting (Solmate ERC4626)
**Root cause:** convertToShares uses assetBalanceBeforeDeposit as denominator; with totalSupply=1 an attacker can donate tokens to inflate totalAssets, so subsequent deposits round down to near-zero shares.
**Steps:**
1. Call `xToken.deposit(1, address(this))` to set totalSupply=1.
2. Transfer `100e18` tokens directly to the xToken contract to inflate totalAssets().
3. Call `xToken.syncRewards()` and warp 1 block so totalAssets updates.
4. convertToAssets(1) now ~1e18; subsequent `deposit(1e18)` returns only 9 shares.
5. Small deposits below share price revert with `ZERO_SHARES`.
**How found:** Reading Solmate's convertToShares formula and reasoning about the totalSupply=1 initial state combined with direct token transfers inflating totalAssets before rewards accrue.
**Exploitation:** Attacker seeds 1 wei of shares, donates tokens to the vault to inflate share price, then later depositors' deposits are converted to dust shares, diluting them and capturing value for the attacker's single share.
**Insight:** Any ERC4626 fork using balanceOf-based totalAssets is vulnerable at totalSupply=0/1; check whether the vault mints dead shares or uses stored accounting rather than live balance.

## Proof of concept (copied exactly from the original report)

**Baseline deposit test showing 1:1 share price**

```
function testExploitNormalCase() public {
 token.mint(address(this), 1e24); // funding
 token.approve(address(xToken), type(uint256).max);
 xToken.syncRewards();
 hevm.warp(1); // skip 1 block from sync rewards. to update new TotalAsset() value
 emit log_named_uint("share price ", xToken.convertToAssets(1));
 emit log_named_uint("deposit 1e18 ", xToken.deposit(1e18, address(this)));
 emit log_named_uint("share price ", xToken.convertToAssets(1));
 emit log_named_uint("deposit 500e18", xToken.deposit(500e18, address(this)));
 emit log_named_uint("share price ", xToken.convertToAssets(1));
 emit log_named_uint("deposit 500e18", xToken.deposit(500e18, address(this)));
 emit log_named_uint("share price ", xToken.convertToAssets(1));
 emit log_string("fast forward 1 hour to new rewards cycle");
 hevm.warp(3601); // new cycle
 emit log_named_uint("deposit 2e18 ", xToken.deposit(2e18, address(this)));
 emit log_named_uint("share price ", xToken.convertToAssets(1));
 emit log_named_uint("deposit 500e18", xToken.deposit(500e18, address(this)));
 emit log_named_uint("share price ", xToken.convertToAssets(1));
 // Share price stay the same 1:1. Due to no rewards have been given.
 }
```

