# BoostStaking

`contracts/src/BoostStaking.sol` — optional staking for the **2x odds boost**.
Multi-tenant: one contract serves every registered tax token. It records checkpointed
balances so the off-chain weights script can compute an exact time-weighted average
balance (TWAB) for any window — staked TWAB counts double in the draw.

Inherits `ReentrancyGuard`; uses `SafeERC20`.

## Accounting model

Simplified port of PoolTogether V5's TwabController idea: each stake/unstake appends a
checkpoint carrying the running `cumulative` of balance × seconds:

```solidity
struct Checkpoint {
    uint64 timestamp;
    uint192 balance;    // balance after the action
    uint256 cumulative; // ∑ balance × elapsed-seconds up to `timestamp`
}
```

TWAB over `[start, end)` is then an O(log n) lookup, no iteration over history:

```
twab = (cumulativeAt(end) − cumulativeAt(start)) / (end − start)
```

`cumulativeAt(t)` binary-searches for the last checkpoint at or before `t` and
extrapolates: `cp.cumulative + cp.balance × (t − cp.timestamp)`. Same-second updates
overwrite the last checkpoint's balance (zero elapsed time ⇒ cumulative unchanged).

Checkpoints are kept per `(token, user)` and per token globally, so both
`twab(token, account, start, end)` and `totalTwab(token, start, end)` are exact.

## Fee-on-transfer-safe staking

$POT (a Pons v1 token) is a plain untaxed ERC-20, but `stake()` still measures
the contract's balance **before and after** the transfer and credits the delta
actually received — not the caller's argument — as a defensive invariant
against any fee-on-transfer token:

```solidity
uint256 before = erc20.balanceOf(address(this));
erc20.safeTransferFrom(msg.sender, address(this), amount);
uint256 received = erc20.balanceOf(address(this)) - before;
```

If the stake transfer is taxed, your staked balance reflects the post-tax amount, and
fee-on-transfer behavior can never inflate the accounting.

## Functions

| Function | Access | Notes |
|---|---|---|
| `stake(token, amount)` | anyone, `nonReentrant` | Credits the received delta; reverts on zero amount or zero received |
| `unstake(token, amount)` | staker, `nonReentrant` | Checkpoints **before** the transfer out (checks-effects-interactions); reverts on insufficient stake |
| `stakedBalance(token, account) → uint256` | view | Latest checkpoint balance |
| `totalStaked(token) → uint256` | view | Latest global checkpoint balance |
| `twab(token, account, start, end) → uint256` | view | Reverts on invalid windows (`end <= start` or `end > block.timestamp`) |
| `totalTwab(token, start, end) → uint256` | view | Global counterpart |

## Events & errors

```solidity
event Staked(address indexed token, address indexed account, uint256 amount, uint256 newBalance);
event Unstaked(address indexed token, address indexed account, uint256 amount, uint256 newBalance);
error ZeroAmount();
error InsufficientStake();
error WindowInvalid();
```

## How the boost enters the draw

The weights script queries `twab(token, account, windowStart, windowEnd)` for every
staker and computes:

```
effectiveWeight = walletTwab + 2 × stakedTwab
```

Staked tokens sit in this contract (not your wallet), so their base weight moves from
the wallet term into the doubled staked term. See
[Eligibility & Odds](/protocol/eligibility).
