# Lack of Balance Validation

Shell Protocol's depositGivenInputAmount and withdrawGivenOutputAmount skip _checkBalances, letting deposits/withdrawals push the pool y/x ratio outside [MIN_M, MAX_M).

- Payout: Contest reward
- Severity: high
- Program: Shell Protocol
- Credited: Mirror
- Disclosed: 2023-10-04
- Write-up quality: 6/10
- Original: https://github.com/code-423n4/2023-08-shell-findings/issues/57
- Library page: https://bugbountylibrary.com/r/c4%3A2023-08-shell%3AH-01
- Tags: domain:web3 impact:integrity kind:contest platform:code4rena sev:high tech:evm tech:solidity technique:fuzzing technique:source-review vuln:accounting-error vuln:business-logic

**Target:** EvolvingProteus AMM pool contract (depositGivenInputAmount/withdrawGivenOutputAmount)
**Root cause:** _reserveTokenSpecified() is called by depositGivenInputAmount and withdrawGivenOutputAmount but never invokes _checkBalances(), unlike _swap() and _lpTokenSpecified(), so the y/x ratio invariant [MIN_M, MAX_M) is not enforced on those paths.
**Steps:**
1. Identify that _checkBalances() is only called in _swap() and _lpTokenSpecified(), not in _reserveTokenSpecified().
2. Write a Foundry fuzz test in EvolvingProteusProperties.t.sol assuming y0/(x0+depositedAmount) < MIN_M (or (y0-withdrawnAmount)/x0 < MIN_M).
3. Call `DUT.depositGivenInputAmount(x0, y0, s0, depositedAmount, SpecifiedToken.X)` and expect a revert.
4. Observe the call does not revert, proving the ratio invariant is broken.
5. Fix: add `_checkBalances(xi + specifiedAmount, yi)` and `_checkBalances(xi, yi + specifiedAmount)` after the reserve updates in _reserveTokenSpecified().
**How found:** Code reading: noticed _checkBalances() was applied in _swap() and _lpTokenSpecified() but omitted from the _reserveTokenSpecified() path used by deposit/withdraw, then confirmed with a fuzz test asserting the invariant can be violated.
**Exploitation:** Not described beyond breaking the pool ratio invariant via deposit/withdraw; no concrete value-extraction path given.
**Insight:** When an invariant is enforced by a helper called from some entry points, enumerate every external function that reaches the underlying math and check whether each path passes through the guard; fuzz the invariant directly rather than the function.
