Skip to content

Collateral Gateway

The CollateralGateway contract turns an underlying balance into collateral in one call. It works on the BNB Chain Core pool, through a Liquidity Hub and its vh market (the Core market that lists the Hub’s share token, such as vvhUSDT), and on hub-funded spoke pools. It also withdraws a Hub position back to the wallet in one call.

This page matches the CollateralGateway source on develop. The gateway is deployed without a proxy, at 0xd0F7B94BF06675D13e8bA9a2546835a1c32a80b8 on BNB Chain and 0x1B6D1ff76beb57CF7C9B2e05BE16cb9529DCa2F9 on BNB Chain testnet.

The grants the gateway needs are AccessControlManager and Comptroller state, not bytecode: read them at the block an integration uses. On BNB Chain, VIP-672 made the Core pool grants and VIP-670 the spoke pool grant.

A Liquidity Hub share token (for example vhUSDT) is listed as its own Core market (vvhUSDT), so a Hub position can also be used as Core collateral. Doing that by hand takes four transactions: approve the Hub, deposit, approve the market, supply. The market then still has to be entered as collateral.

The gateway does all of it in one call, and the market is entered as collateral in the same call. It also covers two cases a user cannot easily do alone:

  • Moving an existing Core position into the Hub. If redeeming the old collateral leaves no shortfall, the gateway redeems it directly. If it would leave the account in shortfall, the gateway supplies the replacement collateral before the old collateral leaves, so a leveraged position moves in full.
  • Supplying collateral to a spoke pool. In a spoke pool, entering a market as collateral is a separate transaction. The gateway supplies and enters in one call, across several markets and several pools.

The gateway is a single shared deployment per chain. It holds no funds between calls. The owner (the Normal Timelock on a live network) can only sweep tokens sent to the contract by mistake. The Core Comptroller and the SpokePoolRegistry are fixed at construction.

Before the first call, the user grants the gateway what that call needs:

FunctionWhat the user does first
supplyFromWalletasset.approve(gateway, assets)
supplyFromCollateralcomptroller.updateDelegate(gateway, true)
withdrawPositionhub.approve(gateway, shares) for shares in the wallet, and comptroller.updateDelegate(gateway, true) to have any shares come out of the vh market. Without the grant the vh market is left alone and only the wallet shares are paid out; with no wallet shares either, the call reverts NothingReceived
supplyAndEnterSpokeMarketsunderlying.approve(gateway, amount) for each market

Interaction with the CollateralGateway contract

Section titled “Interaction with the CollateralGateway contract”
function supplyFromWallet(uint256 assets, address vhMarket, uint256 minShares) external returns (uint256 shares);
  • The user approves the gateway for assets of the Hub’s underlying.
  • supplyFromWallet is called, and the execution proceeds as follows:
    • The Hub is read from vhMarket.underlying(), and vhMarket must be listed on the Core Comptroller.
    • vhMarket is entered as the user’s collateral.
    • The underlying is deposited into the Hub, which mints Hub shares.
    • The shares are supplied to vhMarket, with the market tokens credited to the user.
  • minShares is the slippage guard on the Hub deposit. A full supply cap on vhMarket reverts the whole call, and the user keeps their underlying.

Move a Core position into the Hub (supplyFromCollateral)

Section titled “Move a Core position into the Hub (supplyFromCollateral)”
function supplyFromCollateral(address vToken, uint256 vTokenAmount, address vhMarket, uint256 minShares)
external returns (uint256 shares);
  • The user grants the gateway delegate rights with updateDelegate.
  • supplyFromCollateral is called with the Core market to move out of (vToken), the amount of market tokens to move, and the vh market to move into. vTokenAmount = type(uint256).max moves the whole balance.
  • vToken.underlying() must be the Hub’s asset, and both markets must be listed on the Core Comptroller.
  • The gateway picks one of two paths:
PathWhenWhat happens
DirectvToken is not entered as collateral, or removing it leaves no shortfallenter vhMarket, redeem the old position, deposit into the Hub, supply to vhMarket
Flash loanvToken is entered and removing it would leave a shortfallenter vhMarket, borrow the position’s value from Core, supply the replacement collateral first, then redeem the old position and repay the loan
  • The flash loan path needs the gateway to be allowlisted for Core flash loans, flash loans not paused system wide, and vToken flash-enabled. The loan is taken from vToken itself. Before redeeming the old position from that market, the gateway deposits the borrowed funds into the Hub. Cash needed therefore depends on where the Hub routes that deposit: if it returns to the same Core market, it replenishes the cash available for redemption, so roughly one position’s value can suffice. If it goes entirely elsewhere, the market needs cash for both the loan and the redemption, roughly twice the position’s value. Partial routing back falls between those cases; queue order, capacity and pauses determine the route, so simulate the full migration against current state. The flash loan premium is paid by the user: the loan is sized so the redeemed collateral covers principal plus premium, so the user’s position moves in slightly smaller than it left.
  • minShares means slightly different things on the two paths. The direct path deposits the redeem proceeds. The flash loan path deposits a lower bound on them and returns the difference to the user as underlying.
function withdrawPosition(address vhMarket, uint256 shares, uint256 minAssets) external returns (uint256 assets);
  • A Hub position can sit in two places: Hub shares in the wallet, and market tokens in vhMarket.
  • withdrawPosition takes one total share amount, and the execution proceeds as follows:
    • Shares in the wallet are used first. A withdraw the wallet covers alone never touches Core, so the user’s borrow position is not affected.
    • The rest is freed out of vhMarket on the user’s behalf, but only when the user holds market tokens there and has granted the delegate. Without either, the Core leg is skipped and the wallet leg alone pays out. When it does run, the Comptroller runs its usual liquidity check against the user, so a withdraw that would leave them in shortfall reverts.
    • All shares are redeemed from the Hub, and the underlying is sent to the user.
  • The Core leg frees at most the market tokens the user holds. A market that charges a redeem fee frees fewer shares than asked, and the payout is smaller. A Core market without enough cash for the redeem reverts inside the market with the string "math error", as it does when the redeem would leave the user in shortfall; the error-code path that would surface VTokenRedeemFailed does not run for either case.
  • Holding market tokens is not on its own enough to run the Core leg, because mintBehalf is permissionless: anyone can mint a dust amount to another account, and that must not be able to force a Core redeem on a user who never granted the delegate.
  • shares = type(uint256).max redeems the whole wallet balance, plus every vhMarket market token when the delegate grant is set. minAssets is the floor on the assets the Hub pays out, whichever legs ran.

Supply to a spoke pool (supplyAndEnterSpokeMarkets, enterSpokeMarkets)

Section titled “Supply to a spoke pool (supplyAndEnterSpokeMarkets, enterSpokeMarkets)”
function supplyAndEnterSpokeMarkets(address[] calldata vTokens, uint256[] calldata amounts) external;
function enterSpokeMarkets(address[] calldata vTokens) external;
  • supplyAndEnterSpokeMarkets supplies each amount to the matching spoke market, credits the market tokens to the user, and enters every market as the user’s collateral. The market’s supply allowlist applies to the user, not the gateway, because the allowlist checks the account the market tokens are minted to. In the BNB Chain bStock index pool the USDT, USDC and U markets have it on, with the Liquidity Hub’s Spoke Sources on the list, so an ordinary user’s supply there reverts with SupplyNotAllowed; the bStock collateral markets have no allowlist.
  • enterSpokeMarkets only enters the markets, for a user who already holds the market tokens. Entering a market the user is already in does nothing and does not revert, unless ENTER_MARKET is paused on it: the pause is checked before membership.
  • Each market’s Comptroller is read from the market itself, so one call can cover several spoke pools. Each market must be the one SpokePoolRegistry holds for its pool and underlying, and still listed.
  • Every market is supplied and entered, or the whole call reverts.
  • The amount supplied is measured from the balance that actually arrived, so a token with a transfer fee supplies only what arrived.
function sweepToken(IERC20 token) external;
  • Owner only. It sends the whole balance of token to the owner, and takes no recipient argument.
  • No leg of the gateway leaves a balance behind, so this only recovers tokens sent to the contract by mistake. A zero balance reverts ZeroAmount.

Each function reverts until the grant it needs has been made.

Needed forActionTarget
every Core supplythe MarketFacet with enterMarketForAccount(address,address) is added to the Core ComptrollerCore Comptroller
every Core supplygiveCallPermission(comptroller, "enterMarketForAccount(address,address)", gateway)AccessControlManager
supplyFromCollateral, flash loan pathsetWhiteListFlashLoanAccount(gateway, true)Core Comptroller
spoke functions, once per spoke poolgiveCallPermission(spokeComptroller, "enterMarketForAccount(address,address)", gateway)AccessControlManager
  • The gateway only acts for its caller. Every mintBehalf and enterMarketForAccount is passed msg.sender. Holding the enterMarketForAccount role does not let the gateway enter a market for anyone else.
  • Nothing is held between calls. Market tokens go straight to the user, and each call either completes or reverts in full. On supplyFromCollateral, a call that ends with less of the underlying in the gateway than it started with reverts (BalanceSpent), so a hostile market or Hub cannot spend a stray balance awaiting a sweep.
  • Amounts are measured, not assumed. Market tokens credited and shares freed from vhMarket are read as balance changes. Shares minted and assets paid out are the return values of IHub.deposit and IHub.redeem.
  • Markets are checked, not trusted. Core markets must be listed on the fixed Core Comptroller, and spoke markets must match the SpokePoolRegistry.
EventEmitted when
SuppliedFromWallet(user, hub, vhMarket, assets, shares, vTokens)a wallet balance is deposited and supplied
SuppliedFromCollateral(user, vToken, hub, vhMarket, vTokenAmount, shares, vTokens)a Core position is moved into the Hub
PositionWithdrawn(user, hub, vhMarket, walletShares, freedShares, assets)a Hub position is withdrawn to the wallet
SuppliedToSpoke(user, vToken, assets, vTokens)a spoke market is supplied and entered
TokenSwept(token, recipient, amount)the owner sweeps tokens sent to the contract by mistake
ErrorMeaning
ZeroAddress() / ZeroAmount()a required address or amount was zero
InvalidArrayLength()an empty market list, or vTokens and amounts of different lengths
MarketNotListed(market)the market is not listed on its Comptroller
MarketNotRegistered(vToken)the spoke market is not the one SpokePoolRegistry holds for its pool and underlying
AssetMismatch(vTokenUnderlying, hubAsset)the source market’s underlying is not the Hub’s asset
InsufficientShares(shares, minShares)the Hub deposit minted fewer shares than minShares
InsufficientAssets(assets, minAssets)the withdraw paid out less than minAssets
InsufficientReceipts(held, requested)the user holds fewer market tokens than they asked to move
VTokenMintFailed(market, errorCode) / VTokenRedeemFailed(vToken, errorCode)a market returned a failure code
EnterMarketFailed(vhMarket, errorCode)the Comptroller refused to enter vhMarket as collateral
NothingMinted()a mint credited no market tokens
NothingReceived()a redeem produced no underlying, or a withdraw freed nothing from either leg
LiquidityCheckFailed(errorCode)the Comptroller could not price the user’s position
UnexpectedCallback()the flash loan callback did not match the move in progress, or the callback credited no market tokens
BalanceSpent(balanceBefore, balanceAfter)the call ended with less underlying in the gateway than it started with