# Contracts (https://docs.helico.site/docs/contracts)

HelicoVault in one page. What it checks, its three doors, and its known limits.

`HelicoVault` enforces a user's committed mandate on an agent that re-centres their Uniswap v4
position. The user keeps the position NFT and approves the vault to act on it. The vault holds
nothing across a transaction, and asserts at the end of every re-centre that it kept none of
what passed through.

## What a rogue agent can do
Re-range a position **whose owner committed a mandate**, into a band of the committed width,
containing the current price, closer to it than before, no more often than the cooldown allows,
before the mandate expires. The new NFT and every token go to the owner; those are the only
destinations the contract writes. No path pays an agent.

## The checks
| Check                                                               | Rejection                                                  |
| ------------------------------------------------------------------- | ---------------------------------------------------------- |
| owner committed a mandate and still owns the position               | `MandateInactive`, `NotPositionOwner`                      |
| position is in the committed pool                                   | `PoolNotPermitted`                                         |
| ticks ordered and aligned to spacing                                | `TicksNotOrdered`, `TicksNotSpaced`                        |
| range exactly `rangeWidthTicks` wide                                | `RangeWidthMismatch`                                       |
| range contains the current tick                                     | `RangeOffMarket`                                           |
| closer by at least `minImprovementBps`                              | `NotEnoughImprovement`                                     |
| cooldown elapsed                                                    | `CooldownNotElapsed`                                       |
| liquidity within `maxLiquidity`                                     | `LiquidityTooLarge`                                        |
| delivered liquidity at least `minRetainedBps` of what was withdrawn | `LiquidityNotRetained`                                     |
| not expired, not revoked                                            | `MandateExpired`, `MandateInactive`                        |
| caller holds `AGENT_ROLE`, or one signed for it                     | `AccessControlUnauthorizedAccount`, `SignerLacksAgentRole` |

Afterwards: the position asked for is the one that exists, `PositionNotDelivered`,
`RangeNotDelivered`. The range rules are one function,
[`_checkRange`](https://github.com/0xHelico/helico/blob/049f7d848d680a17cca5f4c1ba664ebffd7ea69d/contracts/src/HelicoVault.sol#L827-L851).

## Three doors
| Door                    | Caller                            | Authority                                                                             |
| ----------------------- | --------------------------------- | ------------------------------------------------------------------------------------- |
| `recenter`              | an `AGENT_ROLE` holder            | the role and the mandate                                                              |
| `recenterWithSignature` | anyone, relaying                  | an EIP-712 signature by an `AGENT_ROLE` holder, single use, bound to the mandate hash |
| `onReport`              | the Chainlink `KeystoneForwarder` | `msg.sender == forwarder`; the DON's signatures were verified by the forwarder        |

The door decides who may propose, never what they may propose.

## How a re-centre runs
`recenter` takes numbers, not calldata. Inside its own `unlock` callback it burns the old
position, swaps the excess side through the position's own pool with a price limit at the edge
of the committed range, then mints the new range to the owner. The tick is re-read after the
swap, `amountIn` is capped at what the burn returned, and the mint's maxima are what the vault
holds. Re-centring changes the token id, so accounts are keyed by owner; one address, one
position at a time; `setMandate` on a second position reverts with `MandateAlreadyActive`.

## Roles and upgrades
`AGENT_ROLE` proposes, `GUARDIAN_ROLE` pauses actions but not exits, `UPGRADER_ROLE`
schedules upgrades. Upgrades wait two days, commit the implementation's `codehash`, and expire
seven days after becoming ready. `revoke` is never gated.

## Known limits
* The agent picks the withdrawal's slippage bounds; a dishonest one can pick weak ones.
* `DEFAULT_ADMIN_ROLE` can grant itself `AGENT_ROLE` in one transaction; hold it in a multisig.
* `maxLiquidity` caps the whole position.
* Stray native sent to the vault is stuck, by design.
* The mocks do not model the price curve; swap claims are asserted on a fork or not at all.

## Run
```bash
cd contracts
forge install --no-git --shallow foundry-rs/forge-std@v1.16.2 \
  OpenZeppelin/openzeppelin-contracts@v5.7.0 OpenZeppelin/openzeppelin-contracts-upgradeable@v5.7.0
forge build && forge test
ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc forge test
```

CI also runs `scripts/check-no-payable.py` (`multicall` stays non-payable) and
`scripts/check-storage-layout.py` (the upgradeable layout stays at 9 slots). The full
reasoning is in [`contracts/README.md`](https://github.com/0xHelico/helico/blob/main/contracts/README.md).
