🤓 Smart Contracts

Solidity API for smart contract functions

Deployments

Ethereum Mainnet

ContractAddress
UniswapHook0x0A71509dfF50940945538B6c9137d26B1D4468A8
SLP0xda6a067D9435a28549D75Dd459C0016911c26E54
Chainlink SLP/USD feed0x0e095dD47bdE0d7cdb57a72bB98B6419dfBB505b
AccessManager0xf01dB5B12E573cd9A06B8384f75b153345690e49
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

ContractAddress
UniswapHook0x71E924F968797857675ebb21394d644f041868A8
SLP0x104f7443e53Ff3d1Af8858e2A95E1fc1661d6EB3
SlpOracle (test feed)0x73d7624688CD146F477C00d7ca4dcbbc0E243444 — there is no Chainlink SLP/USD feed on Sepolia, so the testnet hook reads this operator-published feed
AccessManager0x686b23264e1f3cb439C42930E4eB1739B0409BD7
Uniswap v4 PoolManager0xE03A1074c86CFeDd5C142C4F04F1a1536e203543

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, description SLP / USD Exchange Rate) fed from Sivo's portfolio and liquidity data after independent verification by Accountable. The hook reads it through OracleGate, 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 via setOracle, and SlpOracle is abandoned in place.

All privileged operations across the system are restricted through a central OpenZeppelin AccessManager.

Mainnet parameters

ParameterValue
SLP/USD NAV feedChainlink 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 tier0 (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:

  • beforeInitialize rejects any pool other than the two sanctioned SLP/stablecoin pools (fee 0, tick spacing 60).
  • beforeAddLiquidity always reverts — third-party liquidity can never be added.
  • beforeDonate always 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:

  1. Router / aggregator flow — a regular Uniswap swap of USDC or USDT into SLP. beforeSwap takes 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.
  2. 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 same Deposit event.

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:

  1. RequestrequestWithdraw(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.

  2. 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 = 5 queue 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.

  3. ClaimclaimWithdraw(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, and assetOracleConfig(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) mirrors priceStatus() per stablecoin, and maxDeposit(asset) returns 0 whenever deposits for that asset are blocked. Anything but InRange on a stablecoin means its deposits and fills currently revert.

Contract Interfaces

IUniswapHook

The hook's user-facing surface.

FunctionDescription
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:

RoleGrantsHeld 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-7575One 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 vaultOracle-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 systemssweep() / replenish() on the hook
Vault price gate straddling parityOracleGate band ratcheted around the drifting NAV via setOracle