Solidity API for smart contract functions
Deployments
Ethereum Mainnet
| Contract | Address |
|---|---|
| UniswapHook | 0x0A71509dfF50940945538B6c9137d26B1D4468A8 |
| SLP | 0xda6a067D9435a28549D75Dd459C0016911c26E54 |
| Chainlink SLP/USD feed | 0x0e095dD47bdE0d7cdb57a72bB98B6419dfBB505b |
| AccessManager | 0xf01dB5B12E573cd9A06B8384f75b153345690e49 |
| SlpOracle (deprecated) | 0xd346e20420b980A8F31B2De716C156057C366B26 — legacy operator-published feed, no longer read by the protocol |
The hook's live pricing source is always UniswapHook.oracle(); integrators should read it rather than hard-code a feed address.
Sepolia Testnet
| Contract | Address |
|---|---|
| UniswapHook | 0x71E924F968797857675ebb21394d644f041868A8 |
| SLP | 0x104f7443e53Ff3d1Af8858e2A95E1fc1661d6EB3 |
| SlpOracle (test feed) | 0x73d7624688CD146F477C00d7ca4dcbbc0E243444 — there is no Chainlink SLP/USD feed on Sepolia, so the testnet hook reads this operator-published feed |
| AccessManager | 0x686b23264e1f3cb439C42930E4eB1739B0409BD7 |
| Uniswap v4 PoolManager | 0xE03A1074c86CFeDd5C142C4F04F1a1536e203543 |
Overview
This page documents Sivo's on-chain architecture for tokenizing receivables. The protocol is built around the SLP model, which replaces the legacy ERC-4626/7540 Vault and Share contracts (the retired contracts remain deployed on-chain, but the protocol no longer uses them).
The system has three primary contracts:
- SLP is the Sivo Liquidity Provider token: a plain, upgradeable ERC-20 that represents a share of Sivo's liquidity. Unlike the retired Share token, SLP carries no on-chain accrual logic — all value appreciation is reflected in its Chainlink SLP/USD NAV price. For what backs SLP, who issues it, how the NAV is computed, and risk disclosures, see the SLP Token guide.
- UniswapHook is a single Uniswap v4 hook serving both SLP pools (SLP/USDC and SLP/USDT). It prices every interaction at the exact Chainlink NAV — gated on each stablecoin's own Chainlink USD feed holding a tight peg band — mints SLP on deposits, and runs an asynchronous FIFO withdrawal queue for exits.
- Chainlink SLP/USD feed is the NAV pricing source: a Chainlink Data Feed (
AggregatorV3Interface, 8 decimals, descriptionSLP / USD Exchange Rate) fed from Sivo's portfolio and liquidity data after independent verification by Accountable. The hook reads it throughOracleGate, so every deposit and fill clears at the Chainlink answer. The operator-published SlpOracle that priced the initial mainnet release is deprecated: the hook was re-pointed to the Chainlink feed viasetOracle, and SlpOracle is abandoned in place.
All privileged operations across the system are restricted through a central OpenZeppelin AccessManager.
Mainnet parameters
| Parameter | Value |
|---|---|
| SLP/USD NAV feed | Chainlink SLP / USD Exchange Rate, 8 decimals, sourced from Accountable-verified NAV data (SlpOracle deprecated) |
NAV staleness limit (maxStaleness) | 90,000 s (25 h) — deposits and fills revert if the feed is older |
NAV sanity band (priceMin / priceMax) | 0.90 – 1.50 USD per SLP (ratcheted upward via setOracle) |
| Stablecoin peg band (USDC, USDT) | 0.995 – 1.005 against each asset's Chainlink USD feed, 90,000 s staleness |
Minimum withdrawal request (minRequestShares) | 1 SLP (1000000, 6 decimals) |
Auto-fill per deposit (AUTO_FILL_MAX) | 5 queue entries |
| Pool fee tier | 0 (no swap fee); no protocol deposit or withdrawal fee |
flowchart TD
subgraph SLP protocol
feed["Chainlink SLP/USD feed"]
assetFeeds["Chainlink USDC/USD + USDT/USD feeds"]
hook["UniswapHook (v4 pools SLP/USDC + SLP/USDT)"]
slp["SLP token"]
queue[/"withdrawal queue"\]
end
lp["LP"] -->|deposit USDC/USDT| hook
hook -->|mints SLP at oracle price| slp
feed -->|NAV price| hook
assetFeeds -->|stablecoin peg gate| hook
lp -->|requestWithdraw SLP| queue
queue -->|filled at oracle price| lp
hook -->|sweep free stablecoin| sivo["Sivo → RWA"]
sivo -->|replenish| queue
Architecture
Oracle-priced pools with zero curve liquidity
The two SLP pools are ordinary Uniswap v4 pools in the canonical PoolManager, but they hold zero curve liquidity by construction. The hook's beforeSwap callback fully absorbs every swap via BeforeSwapDelta custom accounting, so the AMM curve never prices anything: every swap clears at the exact Chainlink NAV price. This gives the protocol native access to Uniswap router and aggregator flow while keeping pricing entirely oracle-driven.
The hook enforces the pool shape at every layer:
beforeInitializerejects any pool other than the two sanctioned SLP/stablecoin pools (fee 0, tick spacing 60).beforeAddLiquidityalways reverts — third-party liquidity can never be added.beforeDonatealways reverts — donations are blocked.- Only the stablecoin → SLP direction is a swap. The SLP → stablecoin direction reverts with
UniswapHookExitViaQueue; exits must go through the asynchronous withdrawal queue.
Like every Uniswap v4 hook, the hook's proxy address encodes its permission flags in its low 14 bits (0x28a8: BEFORE_INITIALIZE | BEFORE_ADD_LIQUIDITY | BEFORE_SWAP | BEFORE_DONATE | BEFORE_SWAP_RETURNS_DELTA), which is why the address is CREATE2-mined at deployment.
Deposits (synchronous)
Deposits are instant and can arrive through two equivalent paths:
- Router / aggregator flow — a regular Uniswap swap of USDC or USDT into SLP.
beforeSwaptakes the input stablecoin, reads a fresh oracle price, mints exactly the corresponding amount of SLP, and settles it to the PoolManager. Both exact-input and exact-output swaps are supported. - Direct
deposit()— a convenience entrypoint on the hook that is economically identical to a pool swap: it pulls the stablecoin (requires a prior ERC-20 approval), mints SLP to the receiver at the same oracle read, and emits the sameDepositevent.
Conversion rounding always favors the protocol: SLP out rounds down, stablecoin in rounds up.
Conversions value the deposited stablecoin at par, so deposits and fills are additionally gated on the stablecoin's own Chainlink USD feed (M001): each asset carries a tight peg band ([0.995, 1.005] in deployment) and staleness limit, and deposits and fills for it revert while its feed is unset, stale, or outside the band — mirroring the legacy Vault's per-asset oracle gate. The feed never enters the conversion arithmetic; it only decides whether the par assumption is currently safe, so a depegged stablecoin can neither mint SLP at par nor drain the other stablecoin's reserve, and the extractable spread inside the band is capped at the band's width.
The stablecoin received from deposits accumulates in the hook and is sweepable by Sivo for RWA deployment — except the portion reserved for filled-but-unclaimed withdrawals, which can never be swept.
Withdrawals (asynchronous FIFO queue)
Exits mirror the ERC-7540 request → claimable → claim lifecycle of the legacy Vault, implemented as a per-asset FIFO queue inside the hook:
-
Request —
requestWithdraw(asset, shares)pulls SLP into the hook and appends a queue entry. No price is locked at request time; a minimum request size (minRequestShares) guards against dust-request griefing. -
Fill — queued requests are filled FIFO from the hook's free stablecoin at the oracle price current at fill time. The filled SLP is burned and the stablecoin is moved into a claim reserve. Fills happen three ways:
- automatically inside every deposit (a bounded pass of at most
AUTO_FILL_MAX = 5queue entries, keeping router gas estimates predictable), - by Sivo operators via
fill(asset, maxIterations), which fills from stablecoin already held by the hook, - by Sivo operators via
replenish(asset, assets, maxIterations), which pulls stablecoin back from RWA deployment and fills the queue with it.
Partial fills are supported, and each filled portion locks its price at fill time.
- automatically inside every deposit (a bounded pass of at most
-
Claim —
claimWithdraw(asset, receiver)pays out the controller's full claimable balance. Claims are deliberately not gated on oracle health, because the redemption price was already locked when the request was filled.
Unfilled requests can be cancelled at any time with cancelWithdraw, which returns the SLP newest-request-first so older requests keep their FIFO priority. A partial cancellation may not leave a live request below minRequestShares (M002); cancel less, or the request's full remainder. cancelWithdrawByIds(asset, requestIds) cancels explicitly listed requests in full, so the work is proportional to the ids listed rather than to the caller's request history and a cancellation can always be sized to fit one transaction (L004).
Queue state is aggregated per (asset, controller), so a controller's requests stay fungible — analogous to requestId 0 in the legacy ERC-7540 vault.
Operator delegation
Controllers can delegate queue management with setOperator(operator, approved) (the ERC-7540 operator pattern). An approved operator can cancel and claim on the controller's behalf; cancelled SLP always returns to the controller, and claims are paid to the receiver the operator names.
Oracle circuit breaker
Pricing and gating share one component, OracleGate, extracted from the audited price-gating logic of the legacy Vault:
- Every price read enforces Chainlink round completeness, a configurable staleness limit, and a configurable min/max price band.
- Deposits, swaps, and queue fills all require a fresh, in-range price;
maxDeposit(asset)returns 0 while the contract is paused or either price is out of range. - Unlike the legacy Vault, the band is not required to straddle parity — the SLP NAV trends upward over time, so the band is ratcheted around the current NAV operationally via
setOracle, which atomically updates the feed address, staleness limit, and band. priceStatus()exposes the evaluation result (InRange,IncompleteRound,Stale,OutOfRange,OracleError) for off-chain monitoring.- The same checks gate each stablecoin's own USD feed (M001):
setAssetOracle(asset, feed, staleness, pegMin, pegMax)atomically configures a feed and its peg band per asset, andassetOracleConfig(asset)exposes the configuration. An asset with an unset feed cannot be peg-checked — deposits and fills for it revert rather than fall back to par. - Monitoring whether the protocol is blocked by a depeg or feed outage:
oracleStatus()returns the status of all three feeds (SLP NAV, USDC, USDT) in one call,assetPriceStatus(asset)mirrorspriceStatus()per stablecoin, andmaxDeposit(asset)returns 0 whenever deposits for that asset are blocked. Anything butInRangeon a stablecoin means its deposits and fills currently revert.
Contract Interfaces
IUniswapHook
The hook's user-facing surface.
| Function | Description |
|---|---|
deposit(asset, assets, receiver) | Deposits USDC/USDT and mints SLP to receiver at the oracle price. Requires a prior ERC-20 approval. |
requestWithdraw(asset, shares) | Queues SLP for asynchronous redemption into asset. Returns the queue position (requestId). |
cancelWithdraw(asset, shares[, controller]) | Cancels unfilled queued SLP and returns it to the controller. A partial cancel may not leave a request below minRequestShares. |
cancelWithdrawByIds(asset, requestIds[, controller]) | Cancels the full unfilled remainder of each listed request; bounded by the ids listed rather than the request history. |
claimWithdraw(asset, receiver[, controller]) | Pays out the controller's full claimable stablecoin balance. |
fill(asset, maxIterations) | Operator-only: fills the queue from the hook's free stablecoin. |
replenish(asset, assets, maxIterations) | Operator-only: pulls stablecoin from the operator and fills the queue. |
sweep(asset, to) | Operator-only: sweeps free (unreserved) stablecoin to Sivo for RWA deployment. |
setOperator(operator, approved) / isOperator(controller, operator) | ERC-7540-style operator delegation for cancels and claims. |
View functions: pendingWithdraw(asset, controller), claimableWithdraw(asset, controller), totalPendingShares(asset), totalClaimableAssets(asset), sweepable(asset), queueLength(asset), maxDeposit(asset), poolKey(asset), plus the configuration getters slp(), supportedAssets(), minRequestShares(), oracle(), priceMin(), priceMax(), maxStaleness(), priceStatus(), assetOracleConfig(asset), assetPriceStatus(asset), and oracleStatus().
Key events: Deposit, RedeemRequest, RedeemCancel, RedeemClaimable (a fill locking the redemption price), Withdraw (a claim), OperatorSet, UniswapHookReplenish, UniswapHookSweep, UniswapHookMinRequestChange, UniswapHookAssetOracleChange.
SLP
An upgradeable (UUPS) ERC-20 with burn, pause, and ERC-2612 permit support. It has 6 decimals, matching the paired stablecoins so hook conversions against the oracle price are a single mulDiv without extra scaling. Minting is restricted through the AccessManager: the UniswapHook mints on deposits and burns from its own balance when filling the withdrawal queue; Sivo operations hold the same role.
Chainlink SLP/USD feed
The live NAV source is a standard Chainlink Data Feed proxy (AggregatorV3Interface, 8 decimals, description SLP / USD Exchange Rate). Read it exactly as any other Chainlink feed: latestRoundData() for the current answer, getRoundData(roundId) for history, decimals() and description() for metadata. The published value is derived from Sivo's portfolio, collections, and liquidity data after independent verification by Accountable, so the on-chain NAV is independently checked rather than self-reported. The hook consumes it through OracleGate, which still enforces round completeness, maxStaleness, and the [priceMin, priceMax] band on every read, so a stale or implausible answer halts deposits and fills rather than mispricing them.
SlpOracle (deprecated)
The operator-published AggregatorV3Interface feed (8 decimals, "SLP / USD") that priced the initial mainnet release. Every setPrice opened and answered a new round with Chainlink round semantics, and past rounds remain queryable via getRoundData for historical reference. With the Chainlink feed live, the hook was re-pointed via setOracle and SlpOracle is no longer read by the protocol; it is abandoned in place and its setPrice is no longer called in production. On Sepolia, where no Chainlink SLP/USD feed exists, the testnet hook still reads an SlpOracle instance.
Access control
All privileged functions route through a central OpenZeppelin AccessManager:
| Role | Grants | Held by |
|---|---|---|
ADMIN (0) | AccessManager administration: role grants and revocations, and any function not explicitly mapped to another role. | Safe multisig (2-of-3) |
MANAGER (1) | UniswapHook.setOracle, UniswapHook.setAssetOracle, UniswapHook.setMinRequestShares. | Safe multisig (2-of-3) |
OPERATOR (2) | UniswapHook.fill, UniswapHook.replenish, UniswapHook.sweep (and SlpOracle.setPrice on the deprecated feed). | AWS KMS-backed service accounts |
PAUSER (3) | pause / unpause on the hook and the SLP token. | Safe multisig (2-of-3) |
MINTER (4) | SLP.mint (held by the UniswapHook and Sivo operations). | Protocol contracts |
UPGRADER (5) | Contract upgrades (upgradeToAndCall) on the hook and the SLP token, mapped explicitly so upgrades can be timelocked independently of ADMIN. | Safe multisig (2-of-3) |
Pausing the hook halts swaps, deposits, and the entire queue lifecycle; pausing SLP halts all token transfers, mints, and burns. The hook additionally uses transient-storage reentrancy guards on every state-changing entrypoint, and both the hook and SLP are UUPS-upgradeable behind ERC-1967 proxies with ERC-7201 namespaced storage.
Governance roles are held by Safe multisigs so no single individual can upgrade a contract, change configuration, or grant a role. Operator keys are generated and held inside AWS KMS hardware security modules and can never be exported; their on-chain authority is limited to filling the queue and replenishing and sweeping liquidity; the NAV itself is published by Chainlink, not by Sivo's keys. AccessManager execution delays (timelocks) on upgrades, configuration, and role administration are being introduced in phases, starting with a 24-hour delay on upgrades; scheduled operations emit public on-chain events before they take effect.
Migration from the legacy Vault/Share stack
| Legacy (ERC-4626/7540) | SLP model |
|---|---|
| Per-asset Vault contracts (USDC, USDT, Fiat) sharing a Share token via ERC-7575 | One UniswapHook serving both SLP pools (SLP/USDC and SLP/USDT) |
Share token (formerly sivoUSDX) with on-chain discount accrual (accrue()) | SLP token with no accrual — appreciation is reflected in the Chainlink NAV price |
Synchronous deposit/mint on the vault | Oracle-priced Uniswap v4 swap or direct deposit() on the hook |
ERC-7540 async redemption (requestRedeem → claimable → redeem) | requestWithdraw → FIFO fill at oracle price → claimWithdraw |
deploy() / recall() moving assets to RWA systems | sweep() / replenish() on the hook |
| Vault price gate straddling parity | OracleGate band ratcheted around the drifting NAV via setOracle |

