# [Bug] A critical bug in bps function

Sherlock's bps() reads the token address from the last 32 bytes of calldata, letting attackers append an extra address so unstake computes on one token but transfers another.

- Payout: Audit contest
- Severity: high
- Program: Sherlock
- Found by: hrkrshnn
- Disclosed: 2021-09-16
- Write-up quality: 8/10
- Original: https://github.com/code-423n4/2021-07-sherlock-findings/issues/90
- Library page: https://bugbountylibrary.com/r/c4%3A2021-07-sherlock%3AH-02
- Tags: area:web3 domain:web3 impact:funds-loss kind:contest platform:code4rena sev:high tech:evm tech:solidity technique:source-review vuln:accounting-error vuln:business-logic

**Target:** PoolBase.sol unstake / bps() calldata parsing
**Root cause:** bps() derives the token from the last 32 bytes of msg.data instead of the declared parameter, so trailing calldata bytes override the intended token.
**Steps:**
1. Observe bps() loads mload(add(msg.data, msg.data.length)) masked to 20 bytes, i.e. the last word of calldata.
2. Call unstake(_id, _receiver, Token1) but append an extra address argument Token2: `abi.encodeWithSignature(PoolBase.unstake.selector, uint256(_id), address(_receiver), address(Token1), address(Token2))`.
3. bps() returns Token2 while the declared _token parameter is Token1.
4. All stakeBalance/lockToken math uses Token2, but `_token.safeTransfer(_receiver, amount)` transfers Token2 to receiver.
**How found:** Code review of the inline-assembly bps() helper: noticed it reads the last calldata word rather than the ABI-decoded parameter, then constructed a staticcall PoC showing address(1) returned when address(0) was passed.
**Exploitation:** Attacker initiates unstake with a cheap token, then calls unstake with a crafted payload appending a more expensive token as the trailing calldata word; accounting is done on the expensive token and the expensive token is transferred out, yielding profit.
**Escalation:** Same calldata-tail pattern applies to many other functions using bps(), enabling repeated token-confusion drains.
**Insight:** Inline-assembly helpers that read msg.data by offset/length are attacker-controllable via extra trailing calldata; always verify assembly-derived values match the ABI-decoded parameter.

