TokenBuyback Contract
Overview
Token Converter Phase 2 replaces the original community-driven Token Converter system with a single, lightweight contract class — TokenBuyback — deployed as one instance per (destination, base asset) pair. Conversions are now executed by an ACM-authorized finance-team cron job using DEX aggregators at market rate, eliminating the dependency on external community participation entirely.
Protocol revenues, sourced from reserve interests and liquidations, are processed through the Automatic Income Allocation module. Once allocated, these underlying tokens are sent to the appropriate TokenBuyback instances, which swap them on a defined schedule and forward the output to the configured destination.
Problem with the Original System
The original Token Converter relied on external community members to manually trigger token swaps. In practice:
Tokens sat idle for hours or days waiting for someone to act
When conversions did happen, the protocol paid up to 50% above market price as an incentive to attract participants
The system spanned 5 contract classes across 8 deployed instances (~2,000+ lines of Solidity), making it expensive to audit and slow to iterate on
No guaranteed conversion cadence — protocol income accumulation was unpredictable
Solution: TokenBuyback
A single upgradeable contract (TokenBuyback) is deployed as a Transparent Proxy per (destination, base asset) pair. Each instance:
Passively accumulates any token sent by
ProtocolShareReserve(PSR)Swaps on demand via
executeBuyback, which is ACM-restricted to the finance-team cron jobForwards the
BASE_ASSETdirectly to itsDESTINATIONafter each swapEnforces a rolling 24h USD cap on per-token consumption (
executeBuybackreverts past the cap) to bound blast radius if the operator key is compromisedUses
ResilientOracleto USD-pricetokenInandBASE_ASSETfor the cap and for an event-only abnormal-slippage signal
No community. No premium. Conversions happen on a defined schedule at market rate using off-chain-built DEX calldata. Oracle is used only for safety rails (cap + slippage signal), not for swap pricing.
Key Design Decisions
DESTINATION & BASE_ASSET
Constructor immutables — changing either requires a new deployment
Swap routing
Off-chain (cron builds DEX calldata); router must be on the on-chain allowlist
updateAssetsState caller
Pinned to PROTOCOL_SHARE_RESERVE immutable — prevents spoofed AssetsReceived events
Pool attribution
No per-pool accounting on-chain; comptroller is echoed in events for off-chain attribution
Upgradeability
Transparent Proxy — upgradeable without migrating accumulated funds
Daily USD cap
Rolling 24h leaky bucket (dailyCapUsd); linear decay rate dailyCapUsd / 24h; executeBuyback reverts with DailyCapExceeded past the cap. Default $30,000.
Abnormal slippage detection
Event-only — executeBuyback emits AbnormalSlippage when usdIn − usdOut > slippageEventUsd (default $500). Does not revert.
Oracle dependency
RESILIENT_ORACLE constructor immutable — used only to USD-price the cap and the slippage signal, not for swap pricing. executeBuyback calls updateAssetPrice(tokenIn) and updateAssetPrice(BASE_ASSET) before reading prices, so the cap and slippage check use a freshly pushed oracle snapshot rather than a stale value.
Contract Architecture
PSR requires no contract changes — TokenBuyback implements IIncomeDestination, the same interface used by the original converters. The migration is a governance VIP that rewires PSR's distributionTargets rows to point to the new instances.
Key Functions
updateAssetsState(address comptroller, address asset)
Called by PSR after transferring tokens. Records the balance delta and emits AssetsReceived for off-chain tracking. Only callable by PROTOCOL_SHARE_RESERVE. AssetsReceived is only emitted when the balance delta is non-zero. The reported amount is the observed balanceOf(this) delta against the previous watermark — tokens transferred directly to the contract outside the PSR flow are merged into the next event under whatever comptroller PSR is processing at the time.
executeBuyback(address tokenIn, uint256 amountIn, uint256 minAmountOut, uint256 deadline, address router, bytes calldata routerCalldata, address comptroller)
ACM-restricted. Swaps amountIn of tokenIn to BASE_ASSET via the specified router (must be allowlisted). Validates slippage against minAmountOut. Forwards the output directly to DESTINATION. Emits BuybackExecuted. The reported amountIn in BuybackExecuted is the actual on-chain tokenIn delta consumed by the router (balanceBefore − balanceAfter), not the caller-supplied amountIn parameter, so the event is honest about what the router actually pulled. After the swap settles, the call pushes a fresh oracle price for both tokenIn and BASE_ASSET (updateAssetPrice), then enforces the rolling 24h USD cap on tokenIn consumption (reverts with DailyCapExceeded if exceeded) and emits AbnormalSlippage if usdIn − usdOut exceeds slippageEventUsd.
forwardBaseAsset(address comptroller, uint256 amount)
ACM-restricted. Forwards a caller-specified amount of accumulated BASE_ASSET to DESTINATION without a swap. The amount parameter lets the operator partition multi-pool BASE_ASSET inflows so each portion is attributed separately via BaseAssetForwarded events.
setAllowedRouter(address router, bool allowed)
Governance-only. Adds or removes a DEX router from the allowlist.
setDailyCapUsd(uint256 newCap)
ACM-restricted. Updates the rolling 24h USD cap on tokenIn consumption (1e18-scaled). Reverts with ZeroValueNotAllowed if newCap is zero (the cap cannot be used to fully disable buybacks — use ACM revocation for that). Emits DailyCapUpdated.
setSlippageEventUsd(uint256 newThreshold)
ACM-restricted. Updates the absolute USD threshold above which AbnormalSlippage fires (1e18-scaled). Reverts with ZeroValueNotAllowed if newThreshold is zero. Emits SlippageEventUsdUpdated.
sweepToken(address token, address to, uint256 amount)
Governance-only. Emergency token recovery from the contract. Also the canonical recovery path for tokens transferred directly to the contract outside the PSR flow.
Events
AssetsReceived(comptroller, asset, amount)
PSR deposits tokens
BuybackExecuted(tokenIn, amountIn, amountOut, router, comptroller)
Successful DEX swap
BaseAssetForwarded(comptroller, amount)
BASE_ASSET forwarded without swap
RouterAllowlisted(router, allowed)
Router added/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 succeeds
SlippageEventUsdUpdated(oldThreshold, newThreshold)
setSlippageEventUsd succeeds
BSC: Before & After
Before (8 contracts)
RiskFundConverter
RiskFundV2
USDTPrimeConverter
PrimeLiquidityProvider
USDCPrimeConverter
PrimeLiquidityProvider
BTCBPrimeConverter
PrimeLiquidityProvider
ETHPrimeConverter
PrimeLiquidityProvider
XVSVaultConverter
XVSVaultTreasury
WBNBBurnConverter
Retired (was burn path)
ConverterNetwork
Retired (registry)
After (10 TokenBuyback instances, deployed on BSC mainnet)
WBNBBurnConverter and ConverterNetwork are retired. The 6 TreasuryBuyback instances are new — treasury previously accepted arbitrary tokens without conversion.
RiskFundV2 Changes
RiskFundV2 is upgraded as part of this migration:
poolAssetsFundsmapping removed — per-pool accounting deprecated (isolated pools wound down; core pool does not auction via Shortfall)preSweepTokensimplified — plain balance check replaces thegetPools()proportional looptransferReserveForAuctiondraws against contract balance;comptrollerretained only for ABI parity and event attributiongetPoolsBaseAssetReservesreturns 0 for ABI parity withShortfall.sol
Migration
BSC mainnet migration executed across two governance proposals — VIP-620 and VIP-621 (vips PR #708). The work was originally proposed as a single transaction in VIP-618, which became unexecutable when BSC's Osaka hardfork enforced a hard per-tx gas cap of 2^24 = 16,777,216: the single-call helper.execute() required ~17.5M gas, driven primarily by the converter drain (6 converters × 47 core-pool tokens) and the router allowlist (10 buybacks × 9 routers). Splitting the drain + router allowlist into a separate execute2() entrypoint drops both halves comfortably under the cap.
Pre-VIP (deploy-script setup):
10 new
TokenBuybackproxies redeployed (the proxies originally deployed for VIP-618 are abandoned), each initialized with its(DESTINATION, BASE_ASSET, PROTOCOL_SHARE_RESERVE, RESILIENT_ORACLE)immutables andpendingOwner = TokenBuybackMigrationHelperNew
RiskFundV2implementation deployed (see RiskFundV2 Changes above)TokenBuybackMigrationHelperredeployed with three one-shot entrypoints —execute1(),executeSwap()andexecute2()— each gated toNormalTimelock
VIP-620 (Part 1) — non-drain, non-allowlist phase plus the May 2026 Prime Rewards Allocation:
Grant
DEFAULT_ADMIN_ROLEon ACM to the helper (renounced insideexecute1()so the helper retains no residual ACM privilege between the two VIPs)Transfer ownership of the 6 timelock-owned legacy converters to the helper
helper.execute1()accepts all 16 ownerships, pauses every timelock-owned converter andShortfallauctions, repoints PSR'sdistributionTargetsto the new buybacks (18 rows added, 12 stale rows zeroed, in a sequence that respects PSR'smaxLoopsLimitand the per-schema percentage invariant at every checkpoint), grants the cron operator persistentexecuteBuyback+forwardBaseAssetACM permissions on every buyback, and renouncesDEFAULT_ADMIN_ROLE. The helper retains ownership of all 16 contracts across the gap between the two VIPs but holds no ACM privileges.May 2026 Prime Rewards Allocation, driven directly from
NormalTimelock(the helper only wraps the swap leg):Prime.addMarket(coreComptroller, vU, ...),PrimeLiquidityProvider.initializeTokens([U]),PrimeLiquidityProvider.setMaxTokensDistributionSpeed([U], [1e18]),PrimeLiquidityProvider.sweepToken(USDC, helper, 10_000e18),helper.executeSwap()(single soft-failing USDC → USDT → U multihop on PancakeSwap V3 with a 1% slippage floor; the deep USDT/U pool is required because the direct USDC/U pool is too thin),PrimeLiquidityProvider.setTokensDistributionSpeed([USDT, U], [...]).Upgrade
RiskFundV2to the new implementation. Safe becauseRiskFundConverterwas paused insideexecute1(), so no in-flightconvertExactTokenscallback can reach the removedupdatePoolStateselector.
Between Part 1 and Part 2: the 6 legacy converters are paused, PSR no longer routes revenue to them, and Shortfall auctions are paused — so converter balances are frozen and there is no economic surface from them while the helper still holds ownership.
VIP-621 (Part 2) — router allowlist, converter drain, ownership handback:
helper.execute2()allowlists 9 swap routers on every buyback (PancakeSwap V2 / V3 / Smart / Universal, Uniswap V2 SwapRouter02 / V3 SwapRouter02 / V4 / Universal, 1inch v5), drains every non-zero core-pool ERC20 balance off each timelock-owned converter into its replacement buyback, and transfers ownership of all 16 contracts (10 buybacks + 6 converters) back toNormalTimelock.NormalTimelockaccepts ownership of all 16 contracts.
Post-VIP:
The 6 timelock-owned legacy converters (
RiskFundConverter,USDTPrimeConverter,USDCPrimeConverter,BTCBPrimeConverter,ETHPrimeConverter,XVSVaultConverter) remain deployed but are paused and emptyWBNBBurnConverteris Guardian-owned and was not handed to the helper; its sub-dollar residual is drained in a separate multisig transaction, and its PSR distribution row was already removed insideexecute1()ConverterNetworkis unreferenced — no contract changes were needed to itAll 10 new buybacks are owned by
NormalTimelockand ready to receive PSR distributionsThe finance-team cron operator can now call
executeBuybackandforwardBaseAsseton each buyback via its ACM permissionsThe migration helper holds no privileges, no balances, and no ownership over any contract; all three entrypoints revert
AlreadyExecutedon any subsequent callPre-existing ACM grants on legacy converter functions are not explicitly revoked; conversions are paused, which renders those permissions inert
Impact Summary
Solidity lines
~2,160 across 5 contracts
~425 lines, single contract class
Deployed instances (BSC)
8
10 (all TokenBuyback)
Conversion trigger
External community (voluntary)
Finance cron (scheduled)
Pricing
Oracle + up to 50% premium
DEX market rate
Community dependency
Required
None
Last updated

