Skip to main content

StakeZap

StakeZap combines a deposit to LT with gauge staking, or gauge unstaking with a withdrawal from LT, in one transaction. LT is the market's vault contract, and the gauge (LiquidityGauge) is the market's staking contract. The LEVAMM (contract AMM) is the market's leveraged AMM.

The zap is a helper, not a separate accounting layer. LT still mints the yb-LP shares, which this page calls LT shares, and the gauge still holds the staked position. StakeZap has no revert strings of its own. Each function section lists the revert strings of the contracts that it calls.

Approvals​

You give the zap two approvals, one before each function:

  • Before deposit_and_stake, approve the zap to spend the deposit amount (the assets input) of the asset token (for example, WETH or WBTC).
  • Before withdraw_and_unstake, approve the zap to spend the gauge shares that you unstake (the shares input).

StakeZap also caches max approvals from the zap to the LT and the gauge. The first deposit_and_stake call for a gauge approves the LT to pull the underlying asset. It also approves the gauge to pull LT shares. Later calls for that gauge use those approvals again. These approvals do not replace the approvals that you give to the zap.

Functions​

deposit_and_stake​

The contract source gives this signature:

def deposit_and_stake(
gauge: LiquidityGauge,
assets: uint256,
debt: uint256,
min_shares: uint256,
receiver: address = msg.sender
) -> uint256
Debt on a pool that runs the oracle-report policy

StakeZap forwards debt unchanged to LT.deposit. Read POLICY() on the market's Curve pool to choose the rule:

  • Use the price rule, debt = assets * pool.price_oracle() // 10**asset_decimals, in each of these cases:
    • The pool has no policy: POLICY() reverts on markets 0 to 6, or returns the zero address.
    • The pool runs YBTwocryptoPolicy.
    • The pool runs a killed YBOraclizedPolicy.
    • The pool runs a live YBOraclizedPolicy, and the LEVAMM book is empty (AMM.collateral_amount() == 0).
  • A pool that runs a live YBOraclizedPolicy on a LEVAMM that holds collateral: compute debt from the LEVAMM book, as in Compute debt on a pool that runs the oracle-report policy. For a flowchart of the rule, read Choose the debt rule.

On a pool with a live YBOraclizedPolicy, any other debt reverts the zap call with LT deviation. preview_deposit does not run this check, so simulate the zap call before you send it.

The zap runs these steps in order:

  1. Reads the LT from gauge.LP_TOKEN() and the asset from LT.ASSET_TOKEN().
  2. Pulls assets of the underlying asset from the caller.
  3. Calls LT.deposit(assets, debt, min_shares). The zap receives the LT shares.
  4. Stakes the LT shares into the gauge for receiver.

The function returns the gauge shares that the gauge mints to receiver, at 18 decimals.

The function takes these inputs:

InputTypeDescription
gaugeLiquidityGaugeThe gauge of the market. The zap reads the LT from it.
assetsuint256Amount of the underlying asset, in raw units at the asset's decimals.
debtuint256crvUSD that the LEVAMM lends to the deposit, in raw units at 18 decimals. Compute it with the rule in the warning before the steps.
min_sharesuint256Minimum LT shares (18 decimals) from LT.deposit. The check applies to LT shares, not to gauge shares.
receiveraddressAddress that gets the gauge shares. Defaults to the caller.

This callable example sets min_shares from preview_deposit, approves the zap, simulates the deposit, and then sends it. It is a fragment: it assumes a web3.py setup as in the Developer quickstart. It also assumes that these names are defined: w3, user, zap (StakeZap), gauge, asset, LT_ABI, assets, and a debt from the rule in the warning. The Quickstart defines all of these names except zap and gauge. Bind zap to the StakeZap address in Contract addresses, with an ABI that has deposit_and_stake and withdraw_and_unstake. Bind gauge to the market's gauge, the staker field of Factory.markets(i), with an ABI that has LP_TOKEN, previewRedeem, and approve.

lt = w3.eth.contract(gauge.functions.LP_TOKEN().call(), abi=LT_ABI)
expected = lt.functions.preview_deposit(assets, debt).call()
min_shares = expected * 9_990 // 10_000 # 10 bp tolerance; set your own
asset.functions.approve(zap.address, assets).transact({"from": user})
args = (gauge.address, assets, debt, min_shares, user)
gauge_shares = zap.functions.deposit_and_stake(*args).call({"from": user})
zap.functions.deposit_and_stake(*args).transact({"from": user})

The .call() line simulates the deposit and returns the gauge shares. A revert at that line names the failed check.

The zap's checks on the asset approve and transferFrom calls have no message. A failing token call reverts with the token's own error, if it has one. For the LT conditions in full, read Error modes in Deposit and withdraw.

deposit_and_stake also reverts with these strings from the contracts that it calls:

StringRaised byCondition
LT deviationThe pool policy, inside LT.depositThe pool runs a live YBOraclizedPolicy and debt differs from the proportional debt by more than ratio_tolerance.
Debt too highLT.depositThe LEVAMM value after the deposit exceeds AMM.max_debt() / 2.
Unsafe min, Unsafe maxThe LEVAMM, inside LT.depositThe LEVAMM debt after the deposit is outside its safe range.
Remainder too smallLT.depositThe LT supply after the mint is below MIN_SHARE_REMAINDER (10^6).
SlippageLT.depositThe minted LT shares are fewer than min_shares.
erc4626: leave MIN_SHARESLiquidityGauge.depositThe gauge supply after the deposit is between 0 and MIN_SHARES (10^12).
Gauge not aliveGaugeController.emit(), inside LiquidityGauge.depositThe GaugeController has not added gauge.

withdraw_and_unstake​

The contract source gives this signature:

def withdraw_and_unstake(
gauge: LiquidityGauge,
shares: uint256,
min_assets: uint256,
receiver: address = msg.sender
) -> uint256

The zap runs these steps in order:

  1. Calls gauge.redeem(shares, zap, caller). The gauge burns the caller's gauge shares and sends the LT shares to the zap. This spends the zap's allowance over the caller's gauge shares.
  2. Calls LT.withdraw(lt_tokens, min_assets, receiver), where lt_tokens is the amount of LT shares from step 1.

The function returns the underlying asset amount that receiver gets, in raw units at the asset's decimals.

The function takes these inputs:

InputTypeDescription
gaugeLiquidityGaugeThe gauge of the market.
sharesuint256Gauge shares to unstake (18 decimals).
min_assetsuint256Minimum asset amount, in raw units at the asset's decimals.
receiveraddressAddress that gets the asset. Defaults to the caller.

This callable example sets min_assets from previewRedeem and preview_withdraw, approves the zap for the gauge shares, simulates the withdrawal, and then sends it. It is a fragment: it uses the names from the deposit_and_stake example, plus shares.

lt_shares = gauge.functions.previewRedeem(shares).call()
expected = lt.functions.preview_withdraw(lt_shares).call()
min_assets = expected * 9_990 // 10_000 # 10 bp tolerance; set your own
gauge.functions.approve(zap.address, shares).transact({"from": user})
args = (gauge.address, shares, min_assets, user)
received = zap.functions.withdraw_and_unstake(*args).call({"from": user})
zap.functions.withdraw_and_unstake(*args).transact({"from": user})

The .call() line simulates the withdrawal and returns the asset amount that receiver gets.

withdraw_and_unstake reverts with these strings from the contracts that it calls:

StringRaised byCondition
erc4626: redeem more than maximumLiquidityGauge.redeemshares is more than the caller's gauge balance.
erc20: insufficient allowanceLiquidityGauge.redeemThe zap's allowance over the caller's gauge shares is below shares.
erc4626: leave MIN_SHARESLiquidityGauge.redeemThe gauge supply after the redeem is between 0 and MIN_SHARES (10^12).
Gauge not aliveGaugeController.emit(), inside LiquidityGauge.redeemThe GaugeController has not added gauge.
Withdrawing nothingLT.withdrawThe redeem returns 0 LT shares.
Withdraw to/from stakerLT.withdrawreceiver is LT.staker().
We're dead. Use emergency_withdrawLT.withdrawThe LEVAMM is killed.
Remainder too smallLT.withdrawThe LT supply after the withdrawal is below MIN_SHARE_REMAINDER (10^6) and not 0.
SlippageLT.withdrawThe asset amount is below min_assets.