Error codes
Look up a revert string here to find the contract that raises it and the condition behind it. Each table maps the revert strings of one contract to their causes. The tables cover the core, DAO, and net-pressure contracts of yb-core. They also cover the lending oracles YBLendingOracle, YBLendingOracleLL, and YBLendingOracleLLFactory, the Curve Twocrypto pools of markets 7 to 10, and the pool policies.
The four ERC-4626 cap strings share one row in the LiquidityGauge.vy and FastGauge.vy tables, written as erc4626: deposit/mint/withdraw/redeem more than maximum.
Some checks have no message and revert with empty data. For each contract that has them, a list after its table names the main ones.
A revert inside a Curve pool call reaches the caller of the YieldBasis function. For example, a pool policy revert inside add_liquidity reaches the caller of LT.deposit.
LT.deposit, LT.withdraw, and AMM.exchange end with the borrower-fee donation to the Curve pool. If that donation fails, the pool reverts with slippage or donation above cap!, and the whole call reverts. A VirtualPool route reverts in the same way, through AMM.exchange. For the donation, read distribute_borrower_fees in the LT contract reference.
Core
LT.vy
LT.vy reverts with these strings:
| String | Cause |
|---|---|
Access | Caller neither admin nor factory admin |
Already set | Second set_amm |
Slippage | min_shares / min_assets not met |
Debt too high | LEVAMM value after the deposit would exceed AMM.max_debt() / 2 (preview_deposit also raises it) |
Withdrawing nothing | withdraw(0) |
Withdraw to/from staker | The gauge (staker) is msg.sender or receiver |
We're dead. Use emergency_withdraw | Normal withdraw while the LEVAMM is killed |
Remainder too small | Supply would drop below MIN_SHARE_REMAINDER (1e6) |
Deposit to staker | receiver == staker in deposit |
N>2 | Constructor: the Curve pool has more than 2 coins (coins(2) did not revert) |
receiver | emergency_withdraw from the emergency admin or the admin with receiver other than owner |
owner | emergency_withdraw from any other caller with owner other than msg.sender |
Not killed | A caller other than the position owner calls emergency_withdraw while the LEVAMM is live |
Need factory | withdraw_admin_fees called when admin is not a factory contract |
Killed | Admin-fee withdrawal while the LEVAMM is killed |
No fee_receiver | Factory fee receiver is unset |
Staker=fee_receiver | The fee receiver is the gauge (staker) |
Loss made admin fee negative | Admin-fee withdrawal attempted while admin bucket is negative |
Staker already set | Second set_staker |
GAS | Gauge checkpoint retry path: _checkpoint_gauge falls back to a fixed-gas call (200k) and requires the caller to supply at least that much. This guards against gas-griefing of reward checkpoints |
For the caller rules of emergency_withdraw, read emergency_withdraw() in the LT contract reference.
AMM.vy
AMM.vy, the LEVAMM contract, reverts with these strings:
| String | Cause |
|---|---|
Fee too high | Constructor: fee > MAX_FEE (10%) |
Unsafe min | debt < coll_value · MIN_SAFE_DEBT / 1e18 |
Unsafe max | debt > coll_value · MAX_SAFE_DEBT / 1e18 |
Access | set_rate / set_fee / set_killed from non-LT |
Access violation | _deposit / _withdraw from non-LT |
Rate too high | rate > MAX_RATE (100% APR) |
Amount too large | get_dy(i=0, in_amount > debt) |
Slippage | exchange with out < min_out |
Empty AMM | exchange with collateral_amount == 0 |
Bad final state | Post-swap x0 < x0_pre |
This call reverts without a message:
set_feewithfee > MAX_FEE, the same check asFee too high.
VirtualPool.vy
VirtualPool.vy reverts with these strings:
| String | Cause |
|---|---|
Wrong caller | onFlashLoan not from FACTORY.flash() |
Slippage | Output below min_out |
These calls revert without a message:
onFlashLoanwithinitiator != selfortoken != STABLECOIN.get_dyorexchangewith(i, j)other than(0, 1)or(1, 0).
Factory.vy
Factory.vy reverts with these strings:
| String | Cause |
|---|---|
Access | Non-admin on admin function |
Wrong stablecoin | Pool's coin 0 ≠ factory STABLECOIN |
Only set once | set_mint_factory re-called |
Minter | set_allocator targets mint_factory |
Already set | set_gauge_controller re-called |
Admin fee too high | min_admin_fee > 1e18 |
Bad aggregator | Init / set_agg with agg.price() ∉ (0.9e18, 1.1e18) (open interval, both ends) |
Too many markets | market_count ≥ 50000 |
Nonexistent market | fill_staker_vpool(i ≥ market_count) |
HybridVault.vy
HybridVault.vy reverts with these strings:
| String | Cause |
|---|---|
Already initialized | A second initialize call |
Access | Generic non-owner caller on owner-only function (deposit, withdraw, emergency_withdraw, stake, unstake, redeem_crvusd, withdraw_scrvusd, recover_tokens) |
Only admin | set_personal_limit from non-vault-factory-admin |
Only owner | Owner-gated call from non-owner (set_crvusd_vault) |
Vault not allowed | New crvusd_vault not in the allowlist |
Has active positions | set_crvusd_vault while positions exist |
Bad pool_id | market.lt == 0 |
Not enough crvUSD | Insufficient backing without deposit_stablecoins flag |
Beyond pool limit | Deposit exceeds cap |
Debt made too high | Sanity: debt > 1.1 · additional_crvusd |
No liquidity given | lt_shares == 0 from deposit |
Cannot withdraw stables | Oracle broken, not opting into stablecoin path |
Not enough crvUSD left | Backing would drop below requirement |
Token not allowed | recover_tokens targets protected token |
Oracle is broken | Global and per-pool oracle both fail |
Zero shares | emergency_withdraw(0) |
HybridVaultFactory.vy
HybridVaultFactory.vy reverts with these strings:
| String | Cause |
|---|---|
Already created | User already has a vault |
Vault not allowed | crvusd_vault not in the allowlist |
Vault impl not set | vault_impl == 0 |
Access | Non-ADMIN on admin function |
Only vaults can call | update_* from non-registered vault |
Vault wind-down | Requirement increase on disallowed vault |
Beyond vault limit | Increase exceeds crvusd_vault_limits |
HybridFactoryOwner.vy
HybridFactoryOwner.vy reverts with these strings:
| String | Cause |
|---|---|
Access | Caller is not ADMIN (the YB DAO). For lt_allocate_stablecoins on an enabled LT, the caller is neither ADMIN nor a limit setter |
Not enough reserves | Deallocation of a disabled LT would drop the allocation below 3/4 of the LEVAMM oracle value |
MigrationFactoryOwner.vy
MigrationFactoryOwner.vy reverts with these strings:
| String | Cause |
|---|---|
Access | Caller is not ADMIN |
Not disabled | A non-admin calls lt_allocate_stablecoins(lt, 0) on an LT that is not disabled |
Not enough reserves | Deallocation would drop the allocation below 3/4 of the LEVAMM oracle value |
LTMigrator.vy
LTMigrator.vy reverts with these strings:
| String | Cause |
|---|---|
same LT | lt_from and lt_to are the same LT |
lt_to deprecated | Migration target is disabled in the factory owner (disabled_lts(lt_to) is true) |
lt_from not in factory | Source LT is not a factory market |
lt_to superseded | lt_to is not the newest market for the asset. The deployed LTMigrator can run an older version without this rule; its address is in Contract addresses |
not enough out | shares_out < min_out in migrate_staked |
YBLendingOracle.vy
YBLendingOracle.vy reverts with these strings:
| String | Cause |
|---|---|
Zero | Constructor: the factory or the proxy implementation is the zero address |
No market | create_oracles with market_id equal to or above Factory.market_count(), or for a market whose LT address is zero |
AMM reentrancy | A price read while the market's LEVAMM holds its reentrancy lock (the check_nonreentrant() probe fails) |
YBLendingOracleLL.vy
YBLendingOracleLL.vy reverts with these strings:
| String | Cause |
|---|---|
Initialized | Second initialize |
Zero | initialize with the zero address as LT or factory |
ema_time | initialize or set_ema_time with ema_time 0 or above MAX_EMA_TIME (10**9 s) |
Only factory | set_ema_time from an address other than the factory |
AMM reentrancy | A price read while the market's LEVAMM holds its reentrancy lock |
YBLendingOracleLLFactory.vy
YBLendingOracleLLFactory.vy reverts with these strings:
| String | Cause |
|---|---|
Zero | Constructor with a zero factory, implementation, USD blueprint or DAO address; set_dao with the zero address |
ema_time | Constructor or set_default_ema_time with default_ema_time 0 or above MAX_EMA_TIME |
No market | create_oracles with market_id equal to or above Factory.market_count(), or for a market whose LT address is zero |
Not DAO | set_dao, set_default_ema_time or set_ema_time from an address other than the DAO |
No oracles | set_ema_time for a market that has no oracles |
LTSwapZap.vy
LTSwapZap has no custom revert strings. Its owner-only functions revert with ownable: caller is not the owner (snekmate ownable).
DAO
YB.vy
YB.vy reverts with this string:
| String | Cause |
|---|---|
erc20: access is denied | emit not from a minter |
VotingEscrow.vy
VotingEscrow.vy reverts with these strings:
| String | Cause |
|---|---|
Min value | create_lock value < UMAXTIME |
Withdraw old tokens first | create_lock with existing lock |
Can only lock until time in the future | unlock_time ≤ block time |
Voting lock can be 4 years max | The new lock end is more than 4 years after the block time |
No existing lock found | increase_amount on empty |
Cannot add to expired lock. Withdraw | Expired lock |
Nothing is locked | increase_unlock_time on empty |
Lock expired | Expired lock |
Can only increase lock duration | New end ≤ current end |
Not allowed | infinite_lock_toggle blocked by clearance checker |
The lock didn't expire | withdraw before expiry |
Not supported | delegate / delegateBySig |
erc721: caller is not token owner or approved | Transfer auth fail |
Wrong token ID | Token ID mismatches owner's address |
Need max veLock | Transfer with non-max or non-infinite locks |
erc721: transfer to non-IERC721Receiver implementer | safeTransferFrom to a contract that does not accept the veYB NFT |
This call reverts without a message:
increase_amountwith value < UMAXTIME, the same check asMin value.
GaugeController.vy
GaugeController.vy reverts with these strings:
| String | Cause |
|---|---|
Gauge already added | Duplicate gauge registration |
Gauge not alive | checkpoint, or emit from a gauge, for a gauge that the GaugeController has not added (time_weight(gauge) == 0). Killing a gauge does not raise it |
Vote checker not set | veYB's transfer_clearance_checker ≠ this |
Mismatch in lengths | Array-length mismatch |
Expired | User's veYB lock expired |
Weight too large | Single weight > 10,000 bp |
Killed | Voting for killed gauge |
Gauge not added | Unregistered gauge (vote_for_gauge_weights or set_killed) |
Cannot vote so often | Same-gauge re-vote within 10 days |
Used too much power | Sum of veYB vote weights across gauges exceeds 10,000 bp (100%) after this vote |
LiquidityGauge.vy
LiquidityGauge.vy reverts with these strings:
| String | Cause |
|---|---|
YB | add_reward / deposit_reward targets YB |
LP_TOKEN | add_reward targets LT |
Already added | Duplicate add_reward |
Not added | change_reward_distributor for a token that was not added |
No rewards | deposit_reward(0) |
Finishes in the past | New finish time ≤ block time |
Rate unknown | Extending past old finish time |
No reward | Reward token has no distributor (it was not added); raised in reward checkpoints |
erc4626: deposit/mint/withdraw/redeem more than maximum | The amount of deposit, mint, withdraw, or redeem is above the ERC-4626 maximum for that call, for example maxWithdraw(owner) |
erc4626: leave MIN_SHARES | Gauge supply after the operation would be between 0 and MIN_SHARES (1e12) |
ownable: caller is not the owner | Owner-only function from another address (snekmate ownable) |
receiver | Kill-mode redeem with receiver ≠ owner |
erc4626: transferFrom operation did not succeed | The LT-share transfer into the gauge failed in deposit or mint |
erc4626: transfer operation did not succeed | The LT-share transfer out of the gauge failed in withdraw or redeem |
FeeDistributor.vy
FeeDistributor.vy reverts with these strings:
| String | Cause |
|---|---|
Ded | fill_epochs when killed |
Might be a vest | Fee claim routed through a vesting escrow when direct receiver rules do not allow it |
InflationaryVest.vy
InflationaryVest.vy reverts with these strings:
| String | Cause |
|---|---|
Admin required | start / set_recipient from non-owner |
Already started | Second start() |
VestingEscrow.vy
VestingEscrow.vy reverts with these strings:
| String | Cause |
|---|---|
Lengths mismatch | fund with mismatched arrays |
Rugged | The call targets a recipient whose tokens rug_disabled already reclaimed |
Disabled | fund for a recipient that is disabled |
Cannot disable | toggle_disable when can_disable = False |
Not disabled | rug_disabled for a recipient that is not disabled |
CliffEscrow.vy
CliffEscrow.vy reverts with these strings:
| String | Cause |
|---|---|
Empty recipient | initialize(recipient=0) |
Already initialized | A second initialize call |
Cliff still applies | Transfer/approve before unlock_time |
Not authorized | Non-recipient ve op |
Cannot recover YB | recover_token(YB) |
SnapshotSplitter.vy
SnapshotSplitter.vy reverts with this string:
| String | Cause |
|---|---|
Already claimed | claim for an address that already claimed |
CallComparator.vy
CallComparator.vy reverts with these strings:
| String | Cause |
|---|---|
Not equal | check_equal |
Equal | check_nonequal |
Not greater | check_gt |
Not less | check_lt |
Timestamp not greater | check_timestamp_gt |
Timestamp not less | check_timestamp_lt |
Too early | check_called_after rate limit |
TokenSender, Multisend, StakeZap
These contracts have no custom revert strings. A call fails when a token transfer that it makes reverts.
Net pressure
Owner-only functions of FeeSplitter, MerklPIDDriver, and PID revert with ownable: caller is not the owner (snekmate ownable).
FeeSplitter.vy
FeeSplitter.vy reverts with this string:
| String | Cause |
|---|---|
fraction > 1 | Constructor or set_split_fraction with split_fraction above PRECISION (10**18, which is 100%) |
MerklPIDDriver.vy
MerklPIDDriver.vy reverts with these strings:
| String | Cause |
|---|---|
Not manager | A function for the owner or the manager (approve_merkl, accept_conditions, create_campaign, override_campaign, set_gains, set_execution_params) from another caller |
No wrapper | approve_merkl while no reward wrapper is set |
No pools | preview_target_apr while half_tvl is 0 |
PID.vy
PID.vy reverts with this string:
| String | Cause |
|---|---|
No pools | trigger or preview_signals while half_tvl is 0 |
FastGauge.vy
FastGauge.vy reverts with these strings:
| String | Cause |
|---|---|
Below min supply | Gauge supply after the operation would be above 0 and below MIN_TOTAL_SUPPLY (10 × 10**18) |
Only PID | set_reward_rate from an address other than pid |
ema_time | set_ema_time(0) |
No zap: LP is not a pool | add_liquidity, remove_liquidity or remove_liquidity_one_coin while ZAP_ENABLED is false |
Bad amounts length | add_liquidity with amounts of a length other than POOL_N_COINS |
Bad min_amounts length | remove_liquidity with min_amounts of a length other than POOL_N_COINS |
erc4626: deposit/mint/withdraw/redeem more than maximum | The amount of deposit, mint, withdraw, or redeem is above the ERC-4626 maximum for that call, for example maxWithdraw(owner) |
erc4626: transferFrom operation did not succeed | The LP-token transfer into the gauge failed in deposit or mint |
erc4626: transfer operation did not succeed | The LP-token transfer out of the gauge failed in withdraw or redeem |
YBNetPressure.vy
YBNetPressure.vy reverts with this string:
| String | Cause |
|---|---|
AMM reentrancy | A read (net_pressure_oracle, net_pressure_naive, net_pressure_and_tvl, half_tvl_oracle, crvusd_value_fraction) while the market's LEVAMM holds its reentrancy lock |
MarketRateGetter.vy
MarketRateGetter.vy reverts with this string:
| String | Cause |
|---|---|
Bad ssr | Constructor: susds.ssr() is below RAY (10**27) |
Curve pool (Twocrypto with policy hook)
These strings come from the Curve Twocrypto v3.0.0 pools of markets 7 to 10. The pools of markets 0 to 6 run older code. To check the version of a pool, call version() on it.
A v3.0.0 pool reverts with these strings:
| String | Cause |
|---|---|
!amounts | add_liquidity with both amounts 0 |
!wl | LP allowlist is on and msg.sender is not on it (not checked for donations) |
!init | First add_liquidity to a pool that is not initialized |
nothing minted | add_liquidity mints 0 LP |
nonzero receiver | Donation with a receiver other than the zero address |
donation above cap! | Donation shares would exceed donation_shares_max_ratio of supply |
slippage | Output below the caller's minimum in add_liquidity, remove_liquidity, remove_liquidity_fixed_out, remove_liquidity_one_coin or exchange |
!coins | exchange_received: the pool did not receive at least dx before the call |
same coin | exchange with i == j |
zero dx | exchange with zero input received |
negative donation | Internal check in tweak_price: boosted virtual price below virtual price |
tweaked supply must shrink | Internal check when the pool burns donation shares |
!amount | remove_liquidity_fixed_out or remove_liquidity_one_coin (and their calc_ views) with more LP than the total supply |
!tokens | Fixed-output or one-coin removal that computes both amounts as 0 |
!receiver | LP-token transfer to the pool itself or to the zero address |
!transferFrom | The token transfer into the pool failed |
!transfer | The token transfer out of the pool failed |
!balance | After the operation, a balance is 0, or the scaled balances differ by 1,000:1 or more |
only owner | Admin function (set_policy_contract, change_allowlist, set_fee_parameters, parameter changes) not from the Curve factory admin |
These calls revert without a message:
- A balanced removal, if the best-effort policy callback fails and less than 500,000 gas remained before the call (
assert gas_before_policy_call >= 500_000). - An admin call that fails a parameter check.
pool.get_dy and pool.get_dx run in the pool's view contract. They can revert with coin index out of range, do not exchange 0 coins, do not exchange out 0 coins, or unsafe value for y.
Pool policies
These strings come from the policy contract attached to a Curve pool (pool.POLICY()). For the checks behind them, read Pool policy.
YBOraclizedPolicy
These strings apply to a pool that runs the oracle-report policy. To see which policy a pool runs, call POLICY() on the pool or read the pool policy table in Contract addresses.
YBOraclizedPolicy reverts with these strings:
| String | Cause |
|---|---|
LT deviation | An LT.deposit debt differs from the proportional debt added_lp * AMM.get_debt() / AMM.collateral_amount() by more than ratio_tolerance. Raised inside the pool's add_liquidity. Compute debt as in Compute debt on a pool that runs the oracle-report policy |
auth! | update_pool_state caller is not the pool |
killed | deliver_oracle_report while the policy is killed |
pool uninitialized | deliver_oracle_report, sync or a revive (set_killed(False)) while the pool price_scale is 0. Inside a pool operation, sync and deliver_oracle_report revert on the pool's reentrancy lock instead |
report length | Report is shorter than 112 bytes |
signer | The verifier returned the zero address as signer |
payload length | Signed payload is not 41 bytes |
payload magic | Payload does not start with the Pyth Lazer magic 0x93c7d375 |
channel | Payload channel is not oracle.channel. Also raised by set_oracle_config for a channel outside 1 to 4 |
one feed | Payload has more or less than one feed |
feed | Feed id is not FEED_ID |
price property, exponent property, observation property | Payload properties are missing or in another order (required: price, exponent, feed update time) |
price/exponent | Price mantissa is not positive, or exponent is outside −18 to 0 |
report time | Observation time is 0, observation is after the message time, or the message time is more than future_window_us after the block |
report too old | Observation is more than past_window_us before the block |
observation not newer | The stored report (or the watermark after a kill) is at least as new |
price bounds | Report price is outside oracle.min_price to oracle.max_price. Also raised by set_oracle_config for bounds outside $1e-12 to $1e12 or min_price > max_price |
access | set_killed caller is not the owner or the emergency admin |
ownable: caller is not the owner | set_policy_config, set_oracle_config or an ownership function from a non-owner |
coin index out of range | get_dy_with_report* with i == j or an index above 1 |
not attached | get_dy_with_report* while pool.POLICY() is not this policy |
unsafe value for y | get_dy_with_report* with dx that drains the output coin |
base fee | set_policy_config: base_fee below 0.1 bp or above fallback_fee |
fallback fee | set_policy_config: fallback_fee above 10% |
capture | set_policy_config: capture above 100% |
relax time | set_policy_config: relax_time is 0 or above 3,600 s |
fast half-life | set_policy_config: fast half-life below 600 s or above the slow half-life |
slow half-life | set_policy_config: slow half-life above 604,800 s |
trend gate | set_policy_config: trend_gate above 10% |
step cap | set_policy_config: step_cap is 0 or above 60 bp |
report lifetime | set_policy_config: report_lifetime above 14,400 s |
ratio tolerance | set_policy_config: ratio_tolerance above 10 bp |
verifier | set_oracle_config: verifier is not a contract |
past window | set_oracle_config: past_window_us above 12,000,000 |
future window | set_oracle_config: future_window_us above 6,000,000 |
pool=0, feed id, emergency admin=0, amm collateral, amm stablecoin, pool precision, admin fee | Constructor checks |
The Pyth Pro (Pyth Lazer) verifier can revert deliver_oracle_report with its own errors, for example on a wrong msg.value or a bad signature. The deployed verifier (implementation 0xD8f4A467AbeC64Bc944eee1325B9007879B6555b, PythLazer 0.2.0) has these strings: Insufficient fee provided, input too short, invalid evm magic, invalid signature, invalid signer. input too short and invalid evm magic mean that the update is not in EVM format. invalid signature and invalid signer mean that the signature does not recover to a trusted Pyth signer.
These calls revert without a message:
- A call with a report longer than 112 bytes, in ABI decoding.
deliver_oracle_reporton verifier 0.2.0 with amsg.valueaboveverification_fee(), because the refund to the policy fails.
For the exact-fee rule, how a later verifier version differs, and how to check the version, read Verification fee in Oracle report arbitrage.
YBTwocryptoPolicy
YBTwocryptoPolicy sets only the price_scale target, from the pool's own prices. It reverts with these strings:
| String | Cause |
|---|---|
auth! | update_pool_state caller is not the pool |
pool=0, fast half-life, slow half-life, half-life order, kappa, deadband, max cap, min cap | Constructor checks |
Related
- Event catalog: the events of the same contracts, with topic0 hashes.
- Deposit and withdraw: the deposit and withdrawal reverts at the calls that raise them.
- Pool policy: the policy checks behind the policy strings.
- Oracle report arbitrage: the deliver-and-swap procedure, and the admission rules in the order a delivery checks them.
- LT contract reference: the LT functions that raise most core strings.