Skip to content

TokenBuyback Contract

TokenBuyback is the contract that holds protocol income arriving from ProtocolShareReserve (PSR) and converts it into a configured BASE_ASSET via on-chain DEX swaps. Each instance is deployed as a Transparent Proxy bound to a (DESTINATION, BASE_ASSET) pair at construction time; both addresses are immutable, so retargeting either requires a new deployment.

Swaps are initiated by an ACM-authorized finance-team cron, not by external community members. The cron builds the router calldata off-chain using DEX aggregators and submits it to executeBuyback. The contract validates a minimum output, pulls the actual tokenIn delta consumed by the router, forwards the resulting BASE_ASSET directly to DESTINATION, and applies two on-chain safety rails (USD daily cap + abnormal-slippage event) using a ResilientOracle reference price.

TokenBuyback implements IIncomeDestination, the same interface as the legacy converters, so PSR rewires to the new instances via a governance VIP without any PSR contract changes.

TokenBuyback architecture and PSR distribution flow
PSR distribution across TokenBuyback instances and their destinations (example: BNB Chain — 10 instances; percentages from PSR schema 0 — interest reserves)

Per-instance state:

SlotPurpose
DESTINATION (immutable)Address that receives BASE_ASSET after each swap or forward
BASE_ASSET (immutable)Output token of every buyback — every swap converts into this
PROTOCOL_SHARE_RESERVE (immutable)Only address permitted to call updateAssetsState — prevents spoofed AssetsReceived events
RESILIENT_ORACLE (immutable)USD-pricing source for the daily cap and the abnormal-slippage signal — not used for swap pricing
allowedRoutersOn-chain allowlist of DEX routers (governance-managed)
assetsReserves[token]Balance watermark used to derive the inflow delta on each PSR call
dailyCapUsdRolling 24h USD cap on tokenIn consumption (1e18-scaled)
slippageEventUsdAbsolute USD threshold above which AbnormalSlippage fires (1e18-scaled)
usdConsumedInWindow, lastUpdateLeaky-bucket accumulator state for the daily cap
  1. PSR transfers underlying tokens to TokenBuyback and calls updateAssetsState(comptroller, asset).
  2. The contract reads balanceOf(this), compares against the assetsReserves[asset] watermark, emits AssetsReceived(comptroller, asset, delta) when the delta is non-zero, and resyncs the watermark to the current balance.
  3. The cron monitors AssetsReceived events to know which token landed and from which pool.
  4. When the cron decides to convert, it calls executeBuyback with off-chain-built router calldata.
  5. executeBuyback swaps tokenIn → BASE_ASSET through the allowlisted router, validates minAmountOut, forwards the output to DESTINATION, pushes fresh oracle prices, enforces the daily cap, and emits AbnormalSlippage if the USD delta crosses the threshold.
  6. BASE_ASSET that lands without needing a swap (e.g. PSR delivers BASE_ASSET directly) is sent on via forwardBaseAsset, which partitions the balance by caller-supplied amount so each portion can be attributed to a different comptroller via separate events.

executeBuyback enforces a rolling 24h USD cap on tokenIn consumption. The accumulator decays linearly between calls:

elapsed = block.timestamp - lastUpdate
decayed = elapsed >= WINDOW ? 0 : usdConsumedInWindow * (WINDOW - elapsed) / WINDOW
newUsage = decayed + usdIn
require(newUsage <= dailyCapUsd)
usdConsumedInWindow = newUsage
lastUpdate = block.timestamp

Where WINDOW = 24 hours and usdIn = actualAmountIn * priceIn / 1e18. The accumulator usdConsumedInWindow is bounded by dailyCapUsd at every moment, which caps sustainable throughput at dailyCapUsd / 24h plus a one-time burst of up to dailyCapUsd (so worst-case cumulative consumption over any rolling 24h interval is up to ~2 × dailyCapUsd). Reverts with DailyCapExceeded(attempted, cap) past the cap.

Default: $30,000 cap at deploy. Tunable via setDailyCapUsd (ACM-restricted, rejects zero).

After settling the swap, executeBuyback USD-prices both legs and emits AbnormalSlippage(tokenIn, actualAmountIn, amountOut, usdIn, usdOut) when usdIn − usdOut > slippageEventUsd. The event is informational only — it does not revert. Used by off-chain monitoring to flag swaps routed through hostile pools or stale-aggregator outputs.

Default: $500 absolute. Tunable via setSlippageEventUsd (ACM-restricted, rejects zero).

The slippage check uses the same oracle snapshot as the cap, refreshed via oracle.updateAssetPrice(tokenIn) and oracle.updateAssetPrice(BASE_ASSET) immediately before the read.

updateAssetsState(address comptroller, address asset)

Section titled “updateAssetsState(address comptroller, address asset)”

PSR-only. Records the balance delta against the assetsReserves[asset] watermark and emits AssetsReceived(comptroller, asset, delta) when non-zero. Restricted by the PROTOCOL_SHARE_RESERVE immutable. The reported amount is a balance delta, not an authenticated source-of-funds record — tokens transferred directly to the contract outside the PSR flow are merged into the next event under whichever comptroller PSR happens to be processing.

executeBuyback(tokenIn, amountIn, minAmountOut, deadline, router, routerCalldata, comptroller)

Section titled “executeBuyback(tokenIn, amountIn, minAmountOut, deadline, router, routerCalldata, comptroller)”

ACM-restricted. Performs the swap via the supplied router (must be on the allowlist) and forwards the output to DESTINATION.

Sequence:

  1. Validate deadline, tokenIn != BASE_ASSET, router allowlisted, amountIn > 0 and within balanceOf(this).
  2. Snapshot tokenIn and BASE_ASSET balances on this contract.
  3. forceApprove(router, amountIn), router.functionCall(routerCalldata), forceApprove(router, 0).
  4. Compute actualAmountIn = tokenInBefore - tokenInAfter and amountOut = baseAssetAfter - baseAssetBefore.
  5. Revert with SlippageExceeded if amountOut < minAmountOut.
  6. Transfer amountOut of BASE_ASSET to DESTINATION and resync both watermarks.
  7. Push fresh oracle prices, enforce the daily USD cap, emit AbnormalSlippage if applicable.
  8. Emit BuybackExecuted(tokenIn, actualAmountIn, amountOut, router, comptroller).

The reported amountIn in BuybackExecuted is the actual router consumption, not the caller-supplied parameter — the event reflects what the router actually pulled.

forwardBaseAsset(address comptroller, uint256 amount)

Section titled “forwardBaseAsset(address comptroller, uint256 amount)”

ACM-restricted. Forwards a caller-specified amount of accumulated BASE_ASSET to DESTINATION without a swap. The amount parameter is exposed so the operator can partition multi-pool BASE_ASSET inflows and attribute each portion via a separate BaseAssetForwarded(comptroller, amount) event. No-op when amount == 0. Resyncs the BASE_ASSET watermark after the transfer.

setAllowedRouter(address router, bool allowed)

Section titled “setAllowedRouter(address router, bool allowed)”

Governance-only. Adds or removes a DEX router from the on-chain allowlist. Emits RouterAllowlisted(router, allowed).

ACM-restricted. Updates the rolling 24h USD cap (1e18-scaled). Reverts with ZeroValueNotAllowed if newCap is zero — the cap is not a kill switch; revoke ACM permissions to fully disable buybacks. Emits DailyCapUpdated(oldCap, newCap).

ACM-restricted. Updates the absolute USD slippage-event threshold (1e18-scaled). Reverts with ZeroValueNotAllowed if newThreshold is zero. Emits SlippageEventUsdUpdated(oldThreshold, newThreshold).

sweepToken(address token, address to, uint256 amount)

Section titled “sweepToken(address token, address to, uint256 amount)”

Governance-only. Emergency token recovery. Also the canonical recovery path for tokens transferred directly to the contract outside the PSR flow, since on-chain accounting cannot distinguish donations from authenticated inflows. Resyncs assetsReserves[token] to the post-transfer balance. Emits SweepToken(token, to, amount).

EventWhen emitted
AssetsReceived(comptroller, asset, amount)PSR delivers tokens (non-zero delta)
BuybackExecuted(tokenIn, amountIn, amountOut, router, comptroller)Successful DEX swap; amountIn is the on-chain router consumption
BaseAssetForwarded(comptroller, amount)BASE_ASSET forwarded to DESTINATION without swap
RouterAllowlisted(router, allowed)Router added or removed from allowlist
SweepToken(token, to, amount)Emergency token sweep
AbnormalSlippage(tokenIn, actualAmountIn, amountOut, usdIn, usdOut)Swap returned less USD value than input by more than slippageEventUsd
DailyCapUpdated(oldCap, newCap)setDailyCapUsd succeeded
SlippageEventUsdUpdated(oldThreshold, newThreshold)setSlippageEventUsd succeeded
ErrorCause
UnauthorizedCaller(caller)updateAssetsState called by anyone other than PROTOCOL_SHARE_RESERVE
RouterNotAllowed(router)executeBuyback with a router not on the allowlist
InvalidTokenIn(tokenIn)executeBuyback with tokenIn == BASE_ASSET
InsufficientBalance(token, requested, available)executeBuyback or forwardBaseAsset requested more than the contract holds
SlippageExceeded(expected, actual)Swap output fell below minAmountOut
DeadlineExpired(deadline, blockTimestamp)executeBuyback after the supplied deadline
DailyCapExceeded(attempted, cap)executeBuyback would push cumulative USD usage past dailyCapUsd
ZeroAddressNotAllowedConstructor / setAllowedRouter / sweepToken with zero address
ZeroValueNotAllowedSetters or sweepToken invoked with zero

The (DESTINATION, BASE_ASSET) pair is fixed per instance. Common destinations across deployments:

  • VTreasury — accumulates protocol-revenue tokens (U, BTCB, ETH, USDT, USDC, XVS) bought back from non-Prime, non-RiskFund income.
  • PrimeLiquidityProvider — accumulates Prime rewards in USDT and U; later distributed to Prime users according to per-token speeds configured via VIP. See Prime tokens.
  • RiskFundV2 — receives the risk-fund route’s converted USDT on BNB Chain mainnet. Per-pool accounting was removed alongside the migration, and Shortfall auction starts and restarts are paused; use of the global raw balance is governance-controlled rather than an automatic auction workflow.
  • XVSVaultTreasury — accumulates XVS that funds XVSVault rewards via VIP.

See deployed contracts for per-chain proxy addresses and their (base asset, destination) mapping.

  • Conversions run on a defined schedule rather than on every inflow. The cron decides when to call executeBuyback; the contract has no internal trigger.
  • The oracle is consulted only for the cap and slippage signal — not for swap pricing. Swap economics are determined by the off-chain-built router calldata.
  • The daily cap bounds blast radius if the operator key is compromised. It is not intended to throttle normal operation.
  • Routers must be added to the allowlist by governance before the cron can route through them. Removing a router via setAllowedRouter(router, false) immediately blocks future swaps through it.
  • Donations to the contract (tokens sent directly, outside the PSR flow) are not distinguished from authenticated PSR inflows on-chain. Use sweepToken to recover them if needed.