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 (theassetsinput) of the asset token (for example, WETH or WBTC). - Before
withdraw_and_unstake, approve the zap to spend the gauge shares that you unstake (thesharesinput).
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
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).
- The pool has no policy:
- A pool that runs a live
YBOraclizedPolicyon a LEVAMM that holds collateral: computedebtfrom 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:
- Reads the LT from
gauge.LP_TOKEN()and the asset fromLT.ASSET_TOKEN(). - Pulls
assetsof the underlying asset from the caller. - Calls
LT.deposit(assets, debt, min_shares). The zap receives the LT shares. - 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:
| Input | Type | Description |
|---|---|---|
gauge | LiquidityGauge | The gauge of the market. The zap reads the LT from it. |
assets | uint256 | Amount of the underlying asset, in raw units at the asset's decimals. |
debt | uint256 | crvUSD 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_shares | uint256 | Minimum LT shares (18 decimals) from LT.deposit. The check applies to LT shares, not to gauge shares. |
receiver | address | Address 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:
| String | Raised by | Condition |
|---|---|---|
LT deviation | The pool policy, inside LT.deposit | The pool runs a live YBOraclizedPolicy and debt differs from the proportional debt by more than ratio_tolerance. |
Debt too high | LT.deposit | The LEVAMM value after the deposit exceeds AMM.max_debt() / 2. |
Unsafe min, Unsafe max | The LEVAMM, inside LT.deposit | The LEVAMM debt after the deposit is outside its safe range. |
Remainder too small | LT.deposit | The LT supply after the mint is below MIN_SHARE_REMAINDER (10^6). |
Slippage | LT.deposit | The minted LT shares are fewer than min_shares. |
erc4626: leave MIN_SHARES | LiquidityGauge.deposit | The gauge supply after the deposit is between 0 and MIN_SHARES (10^12). |
Gauge not alive | GaugeController.emit(), inside LiquidityGauge.deposit | The 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:
- 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. - Calls
LT.withdraw(lt_tokens, min_assets, receiver), wherelt_tokensis 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:
| Input | Type | Description |
|---|---|---|
gauge | LiquidityGauge | The gauge of the market. |
shares | uint256 | Gauge shares to unstake (18 decimals). |
min_assets | uint256 | Minimum asset amount, in raw units at the asset's decimals. |
receiver | address | Address 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:
| String | Raised by | Condition |
|---|---|---|
erc4626: redeem more than maximum | LiquidityGauge.redeem | shares is more than the caller's gauge balance. |
erc20: insufficient allowance | LiquidityGauge.redeem | The zap's allowance over the caller's gauge shares is below shares. |
erc4626: leave MIN_SHARES | LiquidityGauge.redeem | The gauge supply after the redeem is between 0 and MIN_SHARES (10^12). |
Gauge not alive | GaugeController.emit(), inside LiquidityGauge.redeem | The GaugeController has not added gauge. |
Withdrawing nothing | LT.withdraw | The redeem returns 0 LT shares. |
Withdraw to/from staker | LT.withdraw | receiver is LT.staker(). |
We're dead. Use emergency_withdraw | LT.withdraw | The LEVAMM is killed. |
Remainder too small | LT.withdraw | The LT supply after the withdrawal is below MIN_SHARE_REMAINDER (10^6) and not 0. |
Slippage | LT.withdraw | The asset amount is below min_assets. |
Related
- LiquidityGauge: the ERC-4626 gauge that the zap stakes into and redeems from.
- Deposit and withdraw: both debt rules in full, and the LT error tables.
- Stake and unstake flow: the same flow as separate gauge calls.