Skip to the content
TAPEHOUSE

Contracts · Lending

Supply vault

The ERC-4626 vault where lenders deposit USDG and earn the interest the margin accounts pay, with its functions, rates, roles and SDK calls.

SupplyVault holds the USDG that the margin accounts borrow. Lenders deposit USDG and receive thUSDG shares, an ERC-4626 vault built on OpenZeppelin Contracts, and earn all the interest the borrower pays. You can deposit, read the rates and withdraw what the vault holds.

Address#

Robinhood Chain testnet, chain 46630, from deployments/46630.json, under tapehouse.SupplyVault.

NameAddress
Supply vault0xDa62E570B6d1A038C1b3b011555BdA1fFd82038F

Testnet, not audited.

Deposit and read the market#

TypeScript
import { readFileSync } from 'node:fs'
import { createPublicClient, http } from 'viem'
import { parseDeployments, supply } from '@tapehouse/sdk'

const deployments = parseDeployments(JSON.parse(readFileSync('deployments/46630.json', 'utf8')))
const client = createPublicClient({ transport: http('https://rpc.testnet.chain.robinhood.com') })

const market = await supply.market(client, deployments)
// { supplyRate, borrowRate, utilization, idle, debt, totalAssets, open }

const call = supply.deposit(deployments, { assets: 100_000_000n, receiver: '0x...' })

supply.deposit returns a viem contract call for simulateContract and writeContract. The amount is in USDG base units. The vault takes no deposit until its borrower is set, and open is false while deposits are stopped.

Functions#

ERC-4626 and the vault's own reads#

FunctionWho may callWhat it doesKey reverts
deposit(uint256 assets, address receiver)AnyoneDeposits USDG for shares to receiver.ERC4626ExceededMaxDeposit while deposits are closed
mint(uint256 shares, address receiver)AnyoneDeposits the USDG that shares cost.ERC4626ExceededMaxMint
depositWithPermit(uint256 assets, address receiver, uint256 deadline, uint8 v, bytes32 r, bytes32 s)AnyoneDeposits with an EIP-2612 permit for USDG. A permit already used, as by a front-runner, does not stop the deposit if the allowance stands.As deposit
withdraw(uint256 assets, address receiver, address owner)Owner of the shares, or an approved spenderSends USDG to receiver for the owner's shares, at most what the vault holds.ERC4626ExceededMaxWithdraw
redeem(uint256 shares, address receiver, address owner)Owner of the shares, or an approved spenderRedeems shares for USDG, at most what the vault holds.ERC4626ExceededMaxRedeem
totalAssets()Anyone, viewThe USDG the vault holds by its own count plus what the borrower owes.None
maxDeposit, maxMint, maxWithdraw, maxRedeemAnyone, viewmaxDeposit and maxMint are zero until the borrower is set. All four are zero while USDG is paused, the vault is frozen or its balance is below its count. maxRedeem is also capped by the USDG the vault holds, and maxWithdraw follows it.None
previewDeposit, previewMint, previewWithdraw, previewRedeem, convertToShares, convertToAssets, asset()Anyone, viewThe ERC-4626 views.None
debt()Anyone, viewWhat the borrower owes now, interest included, rounded up.None
idle()Anyone, viewThe USDG the vault holds for lenders and the borrower, by its own count.None
utilization()Anyone, viewThe share of the vault's assets lent, with 18 decimals.None
borrowRate(), supplyRate()Anyone, viewThe rates a year, with 18 decimals. Lenders earn the borrow rate on the share lent.None
borrowIndex(), scaledDebt(), lastAccrual()Anyone, viewThe borrow index with 27 decimals, the borrower's debt divided by the index at which it was taken, and when interest last accrued.None
rateModel()Anyone, viewThe RateModel struct: optimal, base, slope1, slope2, in basis points.None
borrower()Anyone, viewThe one address that may borrow and write debt off.None
sync()AnyoneBrings the vault's count down to its balance, as after the issuer wipes a frozen vault. It never counts USDG sent to the vault directly. Emits Sync.None

Shares have 6 more decimals than USDG, OpenZeppelin's virtual offset, so a first depositor cannot inflate the share price against the next.

Borrower and owner#

FunctionWho may callWhat it doesKey reverts
borrow(uint256 assets, address receiver)The borrowerLends USDG from the vault.NotBorrower, InsufficientLiquidity(requested, available)
repay(uint256 assets)The borrowerRepays up to assets from the borrower and returns what it repaid.NotBorrower
writeOff(uint256 assets)The borrowerWrites off up to assets of debt. Every share loses its part.NotBorrower
setBorrower(address newBorrower)Owner, onceSets the only borrower, which is the margin accounts.BorrowerAlreadySet, InvalidBorrower
setRateModel(RateModel model)OwnerSets the rate model within its bounds, at once, after accruing at the old rate.InvalidRateModel
transferOwnership(address), acceptOwnership()Owner; pending owner for acceptOwnershipOwnable2Step behaviour.OwnableUnauthorizedAccount
renounceOwnership()NobodyAlways reverts.OwnershipCannotBeRenounced

The borrower is the margin accounts. A person never calls borrow, repay or writeOff directly: they go through the accounts.

Rates#

The borrow rate rises with utilisation in two straight lines. The rate model gives base at zero utilisation, base + slope1 at optimal and base + slope1 + slope2 at full utilisation, in basis points a year, and the rate is linear in between. The rate model's bounds are:

BoundValue
MIN_OPTIMAL, MAX_OPTIMAL100 and 9,900 basis points
slope1At most slope2
Highest rate, MAX_RATE100,000 basis points a year, 1,000%

The model deployed on testnet is read with rateModel().

Events#

EventEmitted when
Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares)A deposit or mint.
Withdraw(address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares)A withdrawal or redemption.
Transfer, ApprovalThe share token's ERC-20 events.
BorrowerSet(address indexed borrower)The owner sets the borrower.
RateModelSet(uint16 optimal, uint32 base, uint32 slope1, uint32 slope2)The rate model changes.
InterestAccrued(uint256 interest, uint256 index)Interest accrues on the debt.
Borrow(address indexed receiver, uint256 assets)The borrower borrows.
Repay(address indexed payer, uint256 assets)The borrower repays.
WriteOff(uint256 assets)The borrower writes debt off, a loss to lenders.
Sync(uint256 idle)The vault's count falls to its balance.
OwnershipTransferStarted, OwnershipTransferredOwnership moves in two steps.

Roles and admin powers#

  • The borrower, the margin accounts, is set once by the owner and can never change. No lender's USDG can be lent to a borrower chosen after it was deposited.
  • The owner can set the rate model at once, within its bounds, and move ownership. It cannot move funds.
  • USDG's issuer can pause USDG or freeze an address, and then wipe a frozen address's balance. While USDG is paused, the vault is frozen or its balance is below its count, every max* view returns zero, so deposits and withdrawals revert with ERC-4626's ERC4626ExceededMax... errors. After a wipe the vault stays closed until sync() brings its count down to its balance, and lenders' shares lose what was wiped.

Invariants#

  • The vault's assets are its own count of the USDG it holds plus what the borrower owes. USDG sent to it directly changes no share's value.
  • The debt grows by a borrow index with 27 decimals, compounded at every deposit, withdrawal, loan and repayment, and always rounded up, in lenders' favour.
  • A lender can withdraw only what the vault holds. The rest returns as the borrower repays.
  • Lenders bear last whatever the borrower cannot repay and the gap backstop does not cover. Until a write-off, shares keep their price, so a lender who withdraws before one loses nothing and those who stay bear it all.

SDK#

TypeScript, module supply in @tapehouse/sdk.

Contract functionSDK call
depositsupply.deposit(deployments, { assets, receiver })
depositWithPermitsupply.depositWithPermit(deployments, { assets, receiver, deadline, signature }), with the permit signature as one hex string
withdrawsupply.withdraw(deployments, { assets, receiver, owner })
redeemsupply.redeem(deployments, { shares, receiver, owner })
supplyRate, borrowRate, utilization, idle, debt, totalAssets, maxDepositsupply.market(client, deployments), all read at one block
A lender's shares and what they are worthsupply.lender(client, deployments, owner)
mint, rateModel, borrowIndex, sync, setRateModelNo wrapper. Use supplyVaultAbi, which the package exports, with viem.

Next#