<!-- canonical: https://0xsimao.com/findings/spirit-protocol-rounding-staking-unstake-abused -->

# Wrong rounding direction in StakingPool::unstake() can be abused

Crit/High · 0xSimao · AI revenue sharing · 28th November 2025

Finding H-3 of the Spirit Protocol security review.

- Report: /reports/spirit-protocol
- Source: https://github.com/0xSimao/audits/blob/main/0xSimao/2025-11-28-spirit-protocol.pdf

---

**Description:**

[StakingPool::unstake()](https://github.com/0xPilou/spirit-contracts/blob/main/src/core/StakingPool.sol#L209) reduces the units of the user pro-rata to the unstaked amount:
```solidity
function unstake(uint256 amount) external {
    ...

    // Get current units and calculate units to remove proportionally
    // This maintains the exact proportional relationship between units and staked amount
    uint128 currentUnits = distributionPool.getUnits(msg.sender);
    uint128 unitsToRemove = uint128((amount * currentUnits) / userStakingInfo.stakedAmount);

    // Update member units
    distributionPool.decreaseMemberUnits(msg.sender, unitsToRemove);

    // Update staking info
    _stakingInfo[msg.sender].stakedAmount -= amount;

    // If all tokens are unstaked, reset the staking info
    if (_stakingInfo[msg.sender].stakedAmount == 0) {
        delete _stakingInfo[msg.sender];
    }

    // Transfer staked tokens back to user
    child.transfer(msg.sender, amount);

    emit Unstaked(msg.sender, amount);
}
```
The issue is that, as it rounds the units down, a staker is able to abuse it by unstaking many times, each time rounding it down, withdrawing all the stake, but keeping many units. Since 1000e18 stake is 1000 units, if they withdraw 1.9999e18, they remove only 1 unit, since `1.9999e18*1000/1000e18 = 1`. By repeating this, they can unstake the full stake, and keep 50% staked.

**Impact:**

Stolen rewards.

**Recommended Mitigation:**

Round up. Also needs to make sure it doesn't underflow when reducing units.

**0xSimao:**

Fixed in PR [#5](https://github.com/0xPilou/spirit-contracts/pull/5).

---

Related findings:

- [`testReturnJumps` asserts the wrong ratio direction](https://0xsimao.com/findings/morpho-midnight-test-jumps-asserts-direction): Morpho Midnight
- [Users cannot unstake from YiedlETHStakingEtherfi.sol, because YieldAccount.sol is incompatible with ether.fi's WithdrawRequestNFT.sol](https://0xsimao.com/findings/benddao-unstake-eth-staking-yield): BendDAO
- [Rounding error in Aave Lending Pool](https://0xsimao.com/findings/gaib-rounding-error-aave-lending): GAIB Pre-Vaults
- [A malicious user may unlock instantly all the funds from the `FluidLocker` when no one is staking in the Tax pool](https://0xsimao.com/findings/superfluid-locker-system-unlock-instantly-staking-tax): Superfluid Locker System
