# Curve Knowledge Hub
> Everything you need to know about Curve
This file contains all documentation content in a single document following the llmstxt.org standard.
## Cryptoswap: In Depth
Cryptoswap is an automated market maker (AMM) pool developed by Curve for swapping between **uncorrelated assets**, such as `ETH` and `USDT`, where the exchange rate between these assets changes.
To understand Cryptoswap, it's helpful to first understand Stableswap, Curve's original AMM. Stableswap was designed to swap **pegged assets** (e.g., `USDC/USDT`) by concentrating liquidity around a fixed price (1 `USDT` = 1 `USDC`). While these products have different use cases, they are built on similar underlying technology.
:::info[Cryptoswap Names]
- **Cryptopools:** This was the original name used in the whitepaper. You might still find it in outdated resources.
- **Cryptoswap-NG:** NG stands for **New Generation**, referring to an optimized version of the original implementation.
- **Curve v2:** An unofficial name used simply because it was the second product released by Curve. This term is misleading, and we recommend always using the Cryptoswap name instead.
:::
## Understanding Stableswap
Stableswap was designed for pools of similarly priced assets, like stablecoins, to **concentrate liquidity** around their pegged price (e.g., 1 `USDC` = 1 `USDT`). This allows for large swaps with very low slippage, even when the pool is imbalanced.
Let's look at an example with a `crvUSD/USDC` pool, where each block represents $1M in tokens:

Stableswap pools are designed to function effectively even when heavily imbalanced. Depending on the **Amplification Coefficient** (`A`), pools can maintain close to 1:1 pricing even when significantly imbalanced. If the imbalance becomes large enough to cause a price deviation from the 1:1 peg, it creates an arbitrage opportunity. This incentivizes traders to rebalance the pool, with each swap generating fees for liquidity providers (LPs).
While the blocks offer a helpful visual, Stableswap's liquidity is more accurately represented by a bonding curve:

The shape of this liquidity bonding curve and how imbalanced a pool can become before price deviates from 1:1 is controlled by a parameter called `A`, the **Amplification Coefficient**:
- A **higher `A`** (e.g., 1,000–20,000) concentrates liquidity more tightly around the peg. This provides deeper liquidity for swaps and allows pools to become very imbalanced before the price deviates significantly from 1:1. The trade-off is that if an asset moves far from the peg, liquidity and pricing can drop off sharply.
- A **lower `A`** (e.g., 50–200) distributes liquidity more evenly. The price will deviate more gradually from the peg as the pool becomes imbalanced, avoiding sharp jumps.
## Understanding Cryptoswap
Cryptoswap pools build upon the core Stableswap algorithm, but with a key innovation: *where* liquidity is concentrated. Instead of targeting a fixed peg, Cryptoswap automatically concentrates and rebalances liquidity around the pool's **recent average price**. This allows it to efficiently support **volatile asset pairs** (e.g., `crvUSD/ETH`) while making the entire process **fully passive for liquidity providers**.
## Parameters
This article from Nagaking goes into detail about each of Cryptoswap's parameters: [Deep Dive: Curve v2 Parameters](https://nagaking.substack.com/p/deep-dive-curve-v2-parameters).
The shape of the Liquidity Bonding Curve is governed by two parameters: `A` which is also present in Stableswap, as well as a new parameter called `gamma`:
- **`A`**: controls liquidity concentration in the center of the bonding curve
- **`gamma`**: controls whether liquidity drops off gradually or sharply away from the center of the bonding curve
Here is how they affect the curve in practice (note that orange curve are equal in both charts):

As the image shows, a higher `A` means more liquidity is concentrated around the price at which it's balanced, called the `price_scale`. Whereas a higher `gamma` means liquidity is spread wider.
## Rebalancing
Assets within Cryptoswap pools are volatile, so prices and exchange rates are constantly changing. Cryptoswap's goal is to center most liquidity close to the current price, which allows more trading volume, and therefore more profit for LPs. So, as prices move, the algorithm must re-center or "rebalance" its liquidity to follow it. This process is handled carefully, because **rebalancing realizes impermanent loss**. To protect LPs, Cryptoswap only rebalances when two conditions are met:
1. The internal price must move beyond a minimum threshold, known as the **adjustment step**.
2. The cost of rebalancing must be less than 50% of the trading fees earned by LPs. **This core safeguard ensures that impermanent loss is only realized when it is sufficiently offset by trading profits**, helping to prevent the erosion of LP deposits from rebalancing fees over time.
:::info[Internal EMA Price Oracle]
For added safety, rebalances are triggered not by the last price of the pool, but by an **Exponential Moving Average** (EMA) of all recent prices. This internal price oracle helps prevent manipulation of rebalances.
In the following examples, the EMA Price and current price are assumed to be the same. In reality, the EMA will lag the current price slightly.
:::
Let's look at an example using a forex pool trading Euros (EUR) against US Dollars (USD):

In this scenario, the pool performs a rebalance once the price hits the adjustment step, using up to 50% of its collected fees to cover the cost.
## Why Does Rebalancing Cost?
Rebalancing costs because you are offering your assets to be swapped in return for trading fees. As price increases, you are selling your assets. When you rebalance, you are rebuying your assets, but at a higher price, causing a loss. Let's look at a very simple example of a concentrated liquidity range AMM:

This example highlights two important takeaways about rebalancing:
* If the user had not rebalanced, they would have kept their initial asset value.
* If the price continued to increase without a rebalance, the user's impermanent loss would have been larger, as they would need to buy back assets at an even higher price to re-enter the liquidity range.
:::info[Cryptoswap Rebalancing]
Rebalancing is necessary, but both frequent and infrequent rebalancing can lead to significant losses.
Cryptoswap automates this process to strike a balance, only rebalancing when two conditions are met:
1. The price has changed more than the minimum amount
2. The rebalance costs less than 50% of the profit earned from swaps
This ensures LPs remain profitable and minimizes rebalances, while maintaining high liquidity depth for swappers.
:::
## Dynamic Fees
Cryptoswap and all new Stableswap pools feature **dynamic fees** that adjust to increase returns for LPs when their liquidity is in high demand.
For Cryptoswap pools, this works as follows:
## Cryptoswap Benefits
**1. Passive LPing and Decentralization**
Cryptoswap was built on the original cypherpunk ethos of DeFi: that anyone should be able to provide liquidity easily, passively, and profitably. Compared to protocols that require LPs to become active managers, Cryptoswap's design allows for broader participation, increasing the resilience of the ecosystem.
**2. Automatic Impermanent Loss Management**
The algorithm is designed to protect LPs from rebalancing losses (as much as possible). By only rebalancing when the fees earned are **more than double the cost**, it ensures that the act of locking in impermanent loss is itself profitable. This prevents the pool from "chasing" the price at a loss to LPs.
**3. Capital Efficiency**
This efficiency stands in contrast to classic AMMs with the `x*y=k` invariant, which use the $x \cdot y = k$ formula. In those models, liquidity is spread thinly across all possible prices (from zero to infinity). By concentrating liquidity around the current market price, Cryptoswap offers significantly lower slippage for traders and generates more fees for LPs from the same amount of capital.
## Stale Pools - How Cryptoswap Pools Can Become Stuck
A Cryptoswap pool's main safety feature is its refusal to rebalance at a loss to LPs. However, this can sometimes cause a pool to become **stuck**, meaning it has a **stale liquidity concentration** because the last rebalance price (`price_scale`) is very different from the current price. This can trigger a negative feedback loop during periods of high volatility:
As the market price moves away from the pool's last rebalance price, the available liquidity for traders decreases. This leads to fewer swaps and, consequently, lower fee generation. Without enough profit from fees, the pool cannot afford to rebalance and follow the price, leaving its liquidity stranded.

## Monitoring Liquidity Balance within Pools
To see how balanced liquidity is within a pool, navigate to the pool's page, for example, the [EURe/USDC pool on Arbitrum](https://www.curve.finance/dex/arbitrum/pools/factory-twocrypto-89/deposit). At the bottom of the pool details, click the `Advanced` tab. You will then see the following details:

In the image above, you can see two key parameters:
- **Price Scale**: This is the price at which liquidity was last rebalanced.
- **Price Oracle**: This is an Exponential Moving Average (EMA) of recent market prices. Rebalances are triggered by this price, not the current market price, which helps prevent manipulation of the rebalancing mechanism.
## How to Prevent Stale Liquidity within Pools
The best prevention for stale liquidity is **proper parameterization**.
Choosing a higher `gamma` and a lower `A` spreads liquidity across a wider price range. This makes a pool more resilient to volatility in two ways:
1. It ensures the pool can continue to facilitate trades and earn fees even during large price swings.
2. It makes the eventual rebalance cheaper because the liquidity is less concentrated.
For assistance with simulations to find reasonable parameters for your pool, refer to [Llamarisk](https://www.llamarisk.com/).
## Help! My Pool's Liquidity is Stale!
If your pool's liquidity becomes stale, you have three primary options:
1. **Change Pool Parameters:** Through a DAO vote, parameters can be gradually changed (a process called "ramping"). Reducing `A` and adjusting `gamma` will spread out liquidity, adding depth at the current price. If parameterization was not the root cause, these parameters can be ramped back to their original values once the pool recovers.
2. **Seed a New Pool:** This option is typically only viable for protocols that own most of the pool's liquidity (POL). It involves deploying a new pool with better parameters and "killing" the old gauge, if applicable.
3. **Wash Trade the Pool:** This involves generating high trading volume (often via flash loans) to create enough fee profit for the pool to rebalance. This requires careful coding and simulation, and is performed at a loss with no guarantee of a lasting fix, as another market swing can immediately undo the rebalance. This strategy should only be considered as a last resort.
## Why Not Use Stableswap with an External Oracle?
Since Stableswap is highly efficient around a single price and can be guided by an external oracle, many developers have considered using this design to price volatile asset pairs against each other.
While some protocols have attempted this, and it can work (e.g., [Spectra](https://spectra.finance/)'s pools), there are a few important considerations:
* **Rebalancing Costs:** Every time the oracle pushes a new price, the pool is forced to rebalance. For volatile assets, these frequent rebalances can accumulate into significant losses for LPs. Profitability depends on trading fees being high enough, or LPs being subsidized in other ways, such as with token emissions. However, for low-volatility assets (even USD/EUR volatility is too high), this technique can work well.
* **Oracle Dependency:** This design requires a high-quality oracle. A malfunctioning, manipulated, or delayed oracle could report an incorrect price, leading to substantial losses for LPs.
In contrast, Cryptoswap's **profit-aware rebalancing mechanisms** are designed specifically to mitigate these risks for highly volatile asset pairs.
---
## Curve AMM
Curve's Automated Market Maker (AMM) is built around two core invariants — **StableSwap** for assets that trade near parity, and **CryptoSwap** for volatile asset pairs. Both algorithms have gone through multiple iterations, with the current generation ("-NG") contracts offering significant gas optimizations, built-in LP tokens, and improved oracle support.
For the mathematical foundations, see the [StableSwap whitepaper](../assets/pdf/whitepaper_stableswap.pdf) and the [CryptoSwap whitepaper](../assets/pdf/whitepaper_cryptoswap.pdf).
---
## Current Implementations
Next-generation StableSwap pools for stablecoins and pegged assets. Supports plain pools and metapools with multiple asset types (standard ERC-20, rebasing, ERC-4626).
Optimized 2-coin CryptoSwap pools for volatile asset pairs with auto-rebalancing, built-in ERC-20 LP tokens, and a hardcoded 50% admin fee.
Optimized 3-coin CryptoSwap pools with native transfer support. Used for major volatile pairs like ETH/BTC/USD.
Permissionless deployment of liquidity pools, gauges, and LP tokens across all pool types and chains.
On-chain router that finds optimal swap routes across Curve pools, supporting up to five tokens in a single transaction.
Earlier implementations of StableSwap and CryptoSwap pools, factory contracts, LP tokens, and deposit contracts. Superseded by the NG versions above.
---
## Pool Factory Overview
A Pool Factory enables the permissionless deployment of liquidity pools, gauges, and LP tokens.
:::deploy[Contract Source & Deployment]
Factories are deployed on the Ethereum Mainnet, as well as on sidechains and Layer-2 networks. Note that some pool types may not yet be supported on these networks. A comprehensive list of all deployed contracts is available [here](../../deployments.md).
The source code for each specific Factory contract can be found on GitHub in the respective section.
:::
Each Factory contract includes **built-in functions designed to populate the [MetaRegistry](../../integration/registry/meta-registry-api.md)**with details about the created pools. These functions are not documented in this section. For more information, please refer to the [MetaRegistry documentation](../../integration/registry/overview.md).
*Note: The methods described below may vary slightly depending on the specific Factory contract. Any anomalies or noteworthy features will be detailed as accurately as possible in the relevant section.*
---
## Available Factories
Curve Factories facilitate the deployment of pools containing almost any combination of assets, whether they are stable or volatile, rebasing or not. Note that some variations (e.g., cryptoswap pool) might not yet be supported on sidechains or Layer 2 networks.
*For a straightforward, non-technical explanation of pool variations, visit: https://resources.curve.fi/pools/overview/*
Factory for deploying new-generation plain- and metapools for pegged assets (e.g., `crvUSD <> USDC`).
Factory for deploying two-coin volatile asset pools (e.g., `CRV <> ETH`).
Factory for deploying three-coin volatile asset pools (e.g., `crvUSD <> ETH <> BTC`).
Factories for older stableswap, twocrypto, or tricrypto pools.
---
## Implementations
Liquidity pools, gauges, and LP token contracts are created based on their respective implementation contracts within the Factory. Newer implementations (NG pools) integrate both the liquidity pool and LP token, while older implementations require separate contracts.
:::warning[Upgradable Implementations]
**Implementation contracts are upgradable.**They can be replaced or supplemented with additional implementation contracts. Due to this, always ensure to check the most recent versions when working with these contracts.
:::
*There are two main methods for deploying contracts:*
- **`create_forwarder_to`**Traditional Factories such as the regular [stableswap](../legacy/factory/stableswap/deployer-api.md) or [cryptoswap](../legacy/factory/cryptoswap/deployer-api.md) utilize Vyper's [`create_forwarder_to`](https://docs.vyperlang.org/en/stable/built-in-functions.html?highlight=create_forwarder_to#chain-interaction) function (renamed to `create_minimal_proxy_to` in Vyper version 0.3.4) to deploy liquidity pools, LP tokens, and gauges.
- **`Blueprint Contracts`**Newer factories utilize blueprint contracts as outlined in [EIP-5202](https://eips.ethereum.org/EIPS/eip-5202). The corresponding contracts are directly created from their blueprint implementations, which has become the preferred method for all newly deployed factories.
---
## Fee Receiver
Users interacting with liquidity pools, such as for exchanging tokens, are required to pay fees. Each factory contains a universal `fee_receiver` variable, where all fees from pools deployed through that factory are collected. This address can usually be changed by the `owner` of the factory via a `set_fee_receiver` function, which is typically the Curve DAO. Therefore, to change the fee receiver address, an approved on-chain vote must pass.
### `fee_receiver`
::::description[`PoolFactory.fee_receiver() -> address: view`]
Getter for the address where the accrued admin fees are collected.
Returns: fee receiver (`address`).
```vyper
# fee receiver for all pools
fee_receiver: public(address)
```
```shell
>>> PoolFactory.fee_receiver()
'0xa2Bcd1a4Efbd04B63cd03f5aFf2561106ebCCE00'
```
::::
### `set_fee_receiver`
::::description[`PoolFactory.set_fee_receiver(_fee_receiver: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new fee receiver address.
| Input | Type | Description |
| --------------- | --------- | ----------- |
| `_fee_receiver` | `address` | Address set as the new fee receiver. |
```vyper
# fee receiver for all pools:
fee_receiver: public(address)
@external
def set_fee_receiver(_fee_receiver: address):
"""
@notice Set fee receiver
@param _fee_receiver Address that fees are sent to
"""
assert msg.sender == self.admin, "dev: admin only"
log UpdateFeeReceiver(self.fee_receiver, _fee_receiver)
self.fee_receiver = _fee_receiver
```
::::
---
## Contract Ownership
Each Factory is controlled by an `admin`, which is typically set to the DAO; thus, any changes to the contract require approval by the Curve DAO.
The contracts utilize the classic two-step ownership model found within Curve contracts. Ownership can be transferred by first committing to the transfer of ownership via `commit_transfer_ownership`. This transfer must then be accepted by the `future_admin` through the `accept_transfer_ownership` function.
Some Factory contracts are indirectly owned by the DAO through a proxy contract.
### `admin`
::::description[`PoolFactory.admin() -> address: view`]
Getter for the current admin of the Factory.
Returns: admin (`address`).
```vyper
admin: public(address)
```
```shell
>>> Factory.admin()
'0x40907540d8a6C65c637785e8f8B742ae6b0b9968'
```
::::
### `future_admin`
::::description[`PoolFactory.future_admin() -> address: view`]
Getter for the future admin of the Factory.
Returns: future admin (`address`).
```vyper
future_admin: public(address)
```
```shell
>>> PoolFactory.future_admin()
'0x0000000000000000000000000000000000000000'
```
::::
### `commit_transfer_ownership`
::::description[`PoolFactory.commit_transfer_ownership(_addr: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
This function commits a transfer of ownership by setting `_addr` as the `future_admin` of the contract. These changes must then be applied by the `future_admin` itself through the `accept_transfer_ownership` function.
| Input | Type | Description |
| -------- | --------- | ----------------------------------- |
| `_addr` | `address` | Address to transfer ownership to. |
```vyper
admin: public(address)
future_admin: public(address)
@external
def commit_transfer_ownership(_addr: address):
"""
@notice Transfer ownership of this contract to `addr`
@param _addr Address of the new owner
"""
assert msg.sender == self.admin, "dev: admin only"
self.future_admin = _addr
```
::::
### `accept_transfer_ownership`
::::description[`PoolFactory.accept_transfer_ownership():`]
:::guard[Guarded Method]
This function is only callable by the `future_admin` of the contract.
:::
Function to accept the ownership transfer.
Emits: `TransferOwnership`
```vyper
event TransferOwnership:
_old_owner: address
_new_owner: address
admin: public(address)
future_admin: public(address)
@external
def accept_transfer_ownership():
"""
@notice Accept a pending ownership transfer
@dev Only callable by the new owner
"""
assert msg.sender == self.future_admin, "dev: future admin only"
log TransferOwnership(self.admin, msg.sender)
self.admin = msg.sender
```
::::
---
## Stableswap-NG Factory: Deployer API
## Name and Symbol
The input values of `_name` or `_symbol` are obviously non-trivial for the performance of the pool. These parameters should visualize, what kind of tokens are included in the pool.
```shell
_name = "rETH/wETH Pool"
_symbol = "rETH/wETH"
```
---
## Coins`_coins` includes all tokens included in the pool as a `DynArray`.
```shell
_coins = ["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "0xae78736Cd615f374D3085123A210448E74Fc6393"]
```
---
## A, Fee, Off-Peg Fee Multiplier, and MA-Exp-Time
- `_A` represents the amplification coefficient of the pool, signifying its density.
- `_fee` is referred to as the "base fee."
- The `offpeg_fee_multiplier` parameter enables the system to dynamically adjust fees according to the pool's state.
- `ma_exp_time` denotes the time window for the moving average oracle.
*Recommended Parameters*:
| Parameter | Fiat-Redeemable Stablecoin | Crypto-Collateralized Stablecoin |
| :--------------------- | :------------------------: | :------------------------------: |
| `A` | 200 | 100 |
| `fee` | 0.04% | 0.04% |
| `offpeg_fee_multiplier`| 2 | 2 |
| `ma_exp_time` | 866 | 866 |
```shell
_A = 200
_fee = 4000000 # 0.0001 or 0.01%
_offpeg_fee_multiplier = 20000000000 # 5 or 500%
_ma_exp_time = 866 # ~600 seconds
```
:::note[Parameter Precision]
The precision of `_fee` and `_offpeg_fee_multiplier` is 1e10.
The time window of the moving average exponential oracle is calculated using `time_in_seconds / ln(2)`.
:::
---
## Implemention ID
Pools are **created from implementation contracts**(blueprints). These contracts are added to the Factory and must be choosen when deploying a pool.
:::warning
The Factory can have **multiple plain- and meta-pool implementations**. If there are multiple implementations for a plain or meta-pool, it's important to understand the differences and determine which one is suitable.
Additionally, implementation contracts are **upgradable**. They can either be replaced or have additional implementation contracts set. Please always make sure to check the most recent ones.
To query the factory-specific implementations:
```shell
>>> Factory.pool_implementation(0)
'0xDCc91f930b42619377C200BA05b7513f2958b202'
>>> Factory.metapool_implementation(0)
'0xede71F77d7c900dCA5892720E76316C6E575F0F7'
```
:::
---
## Assets Types
Stableswap-NG infrastructure supports pools with the following asset types:
| Asset Type | Description |
| :---------: | ---------------------- |
| `0` | **Standard ERC-20**token with no additional features |
| `1` | **Oracle**- token with rate oracle (e.g. wstETH) |
| `2` | **Rebasing**- token with rebase (e.g. stETH) |
| `3` | **ERC4626**- token with *`convertToAssets`* method (e.g. sDAI) |
*Consequently, supported tokens include:*
- ERC-20 support for return `True/revert`, `True/False` or `None`
- ERC-20 tokens can have *arbitrary decimals (≤18)*
- ERC-20 tokens that *rebase* (either positive or fee on transfer)
- ERC-20 tokens that have a *rate oracle* (e.g. wstETH, cbETH) Oracle precision must be $10^{18}$
- ERC-4626 tokens with *arbitrary percision* (≤18) of Vault token and underlying asset
:::warning
- **`ERC20:`**Users are advised to do careful due-diligence on ERC20 tokens that they interact with, as this contract **cannot differentiate between harmless and malicious**ERC20 tokens.
- **`Oracle:`**When using tokens with oracles, its important to know that they **may be controlled externally by an EOA**.
- **`Rebasing:`**Users and Integrators are advised to understand how the AMM contract works with rebasing balances.
- **`ERC4626:`**Some ERC4626 implementations **may be susceptible to Donation/Inflation attacks**. Users are advised to proceed with caution.
:::
Choosing asset types can sometimes be quite tricky. Asset types should be seen more as information for the AMM on **how to treat the assets under the hood**.
:::example
Let's consider the example of [rmETH/mETH](https://etherscan.io/address/0xdd4316c777a2295d899ba7ad92e0efa699865f43).
- [mETH](https://etherscan.io/address/0xd5F7838F5C461fefF7FE49ea5ebaF7728bB0ADfa) is a token with a rate oracle (the underlying asset is ETH). The rate can be fetched by reading the `mETHToETH` method within the [staking contract](https://etherscan.io/address/0xe3cBd06D7dadB3F4e6557bAb7EdD924CD1489E8f).
- rmETH is a rebasing token.
Because the deployer wants rmETH and mETH to trade as close to 1:1 as possible, they need to treat mETH like a regular ERC-20 token (asset type 0), instead of a rate oraclized token (asset type 1).
```shell
_asset_types = [0, 2] # coin(0) = asset type 0; coin(1) = asset type 2
```
:::
---
## Method IDs and Rate Oracles
`method_ids` and `_oracles` are required for rate oracles to function. ERC-4626 does not need either of these. The sole requirement for those is to have a `convertToAssets` method.
:::info
When deploying pools that include coins not requiring a rate oracle, **`b""`**or **`0x00000000`**should be included in the `_methods_id` array and the **`ZERO_ADDRESS`**should be used in the `_oracles` array as placeholders for each coin.
:::
- `_method_ids` is the first four bytes of the Keccak-256 hash of the function signatures of the oracle addresses that give rate oracles.
As an example, lets look at the [rETH](https://etherscan.io/token/0xae78736cd615f374d3085123a210448e74fc6393) token. The relevant function which returns the rate is `getExchangeRate`, the according first four bytes of the Keccak-256 hash of the functions signature is therefore `0xe6aa216c`. When calculating, its always important to include `"()"`, as they will change the bytes.
```shell
getExchangeRate -> "0xb2fc0e3e" # wrong
getExchangeRate() -> "0xe6aa216c" # correct
```
[Method ID Calculator](https://piyolab.github.io/playground/ethereum/getEncodedFunctionSignature/)
- `_oracles` is simply the contract address which provides the rate oracle function.
The input values are `DynArrays` with the length of tokens in the pool. Therefore, a rETH/wETH pool would have the following input values:
```shell
_method_id = [b"", "0xe6aa216c"]
_oracles = ["0x0000000000000000000000000000000000000000", "0xae78736cd615f374d3085123a210448e74fc6393"]
```
### Examples
| Pool | Asset Types | Method ID's | Rate Oracle |
| :---: | :--------: | :---------: | :---------: |
| [mkUSD/USDC](https://etherscan.io/tx/0xf980b4a4194694913af231de69ab4593f5e0fcdc) | `[0, 0]` | `['0x00000000', '0x00000000']` | `['0x0000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000']` |
| [FRAX/sDAI](https://etherscan.io/tx/0xce6431d21e3fb1036ce9973a3312368ed96f5ce7) | `[0, 3]` | `['0x00000000', '0x00000000']` | `['0x0000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000']` |
| [wETH/rETH](https://etherscan.io/tx/0x9efe1a1cbd6ca51ee8319afc4573d253c3b732af) | `[0 , 1]` | `['0x00000000', '0xe6aa216c']` | `['0x0000000000000000000000000000000000000000', '0xae78736Cd615f374D3085123A210448E74Fc6393']` |
| [rmETH/mETH](https://etherscan.io/address/0xdd4316c777a2295d899ba7ad92e0efa699865f43) | `[2 , 0]` | `['0x00000000', '0x00000000']` | `['0x0000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000']` |
---
## Deploying Plain- and Metapools
### `deploy_plain_pool`
*Parameter limitations when deploying a plain pool:*
- Minimum of 2 and maximum of 8 coins.
- All coin arrays should be the same length.
- `_fee` ≤ 100000000 (1%).
- `_offpeg_fee_multiplier` * `_fee` ≤ `MAX_FEE` * `FEE_DENOMINATOR`.
- Maximum of 18 decimals for a coin.
- No duplicate coins.
- Valid implementation index.
::::description[`Factory.deploy_plain_pool(_name: String[32], _symbol: String[10], _coins: DynArray[address, MAX_COINS], _A: uint256, _fee: uint256, _offpeg_fee_multiplier: uint256, _ma_exp_time: uint256, _implementation_idx: uint256, _asset_types: DynArray[uint8, MAX_COINS], _method_ids: DynArray[bytes4, MAX_COINS], _oracles: DynArray[address, MAX_COINS], ) -> address:`]
Function to deploy a stableswap-ng plain pool. The pool is created from a blueprint contract.
Returns: Deployed pool (`address`).
Emits: `PlainPoolDeployed`
| Input | Type | Description |
| -------------------- | ---------------------------- | ----------- |
| `_name` | `String[32]` | Name of the new plain pool |
| `_symbol` | `String[10]` | Symbol for the new pool's LP token; this value will be concatenated with the factory symbol |
| `_coins` | `DynArray[address, MAX_COINS]` | Array of addresses of the coins being used in the pool |
| `_A` | `uint256` | Amplification coefficient |
| `_fee` | `uint256` | Trade fee, given as an integer with `1e10` precision |
| `_offpeg_fee_multiplier` | `uint256` | Off-peg fee multiplier |
| `_ma_exp_time` | `uint256` | MA time; set as time_in_seconds / ln(2) |
| `_implementation_idx` | `uint256` | Index of the implementation to use |
| `_asset_types` | `DynArray[uint8, MAX_COINS]` | Asset type of the pool as an integer; more [here](../../stableswap-ng/overview.md#supported-assets) |
| `_method_ids` | `DynArray[bytes4, MAX_COINS]` | Array of first four bytes of the Keccak-256 hash of the function signatures of the oracle addresses that give rate oracles |
| `_oracles` | `DynArray[address, MAX_COINS]` | Array of rate oracle addresses |
:::info[Implementation ID]
There might be multiple pool implementations. To query all available ones, see [here](./overview.md#query-implementations). As of the current date (31.10.2023), there is only one pool implementation available. Since the `_implementation_idx` starts at 0, users need to input "0" when deploying a pool.
:::
```vyper
event PlainPoolDeployed:
coins: DynArray[address, MAX_COINS]
A: uint256
fee: uint256
deployer: address
MAX_COINS: constant(uint256) = 8
MAX_FEE: constant(uint256) = 5 * 10 **9
FEE_DENOMINATOR: constant(uint256) = 10 **10
@external
def deploy_plain_pool(
_name: String[32],
_symbol: String[10],
_coins: DynArray[address, MAX_COINS],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_implementation_idx: uint256,
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
) -> address:
"""
@notice Deploy a new plain pool
@param _name Name of the new plain pool
@param _symbol Symbol for the new plain pool - will be
concatenated with factory symbol
@param _coins List of addresses of the coins being used in the pool.
@param _A Amplification co-efficient - a lower value here means
less tolerance for imbalance within the pool's assets.
Suggested values include:
* Uncollateralized algorithmic stablecoins: 5-10
* Non-redeemable, collateralized assets: 100
* Redeemable assets: 200-400
@param _fee Trade fee, given as an integer with 1e10 precision. The
maximum is 1% (100000000). 50% of the fee is distributed to veCRV holders.
@param _ma_exp_time Averaging window of oracle. Set as time_in_seconds / ln(2)
Example: for 10 minute EMA, _ma_exp_time is 600 / ln(2) ~= 866
@param _implementation_idx Index of the implementation to use
@param _asset_types Asset types for pool, as an integer
@param _method_ids Array of first four bytes of the Keccak-256 hash of the function signatures
of the oracle addresses that gives rate oracles.
Calculated as: keccak(text=event_signature.replace(" ", ""))[:4]
@param _oracles Array of rate oracle addresses.
@return Address of the deployed pool
"""
assert len(_coins) >= 2 # dev: pool needs to have at least two coins!
assert len(_coins) == len(_method_ids) # dev: All coin arrays should be same length
assert len(_coins) == len(_oracles) # dev: All coin arrays should be same length
assert len(_coins) == len(_asset_types) # dev: All coin arrays should be same length
assert _fee <= 100000000, "Invalid fee"
assert _offpeg_fee_multiplier * _fee <= MAX_FEE * FEE_DENOMINATOR
n_coins: uint256 = len(_coins)
_rate_multipliers: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
decimals: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
for i in range(MAX_COINS):
if i == n_coins:
break
coin: address = _coins[i]
decimals.append(ERC20(coin).decimals())
assert decimals[i] < 19, "Max 18 decimals for coins"
_rate_multipliers.append(10 **(36 - decimals[i]))
for j in range(i, i + MAX_COINS):
if (j + 1) == n_coins:
break
assert coin != _coins[j+1], "Duplicate coins"
implementation: address = self.pool_implementations[_implementation_idx]
assert implementation != empty(address), "Invalid implementation index"
pool: address = create_from_blueprint(
implementation,
_name, # _name: String[32]
_symbol, # _symbol: String[10]
_A, # _A: uint256
_fee, # _fee: uint256
_offpeg_fee_multiplier, # _offpeg_fee_multiplier: uint256
_ma_exp_time, # _ma_exp_time: uint256
_coins, # _coins: DynArray[address, MAX_COINS]
_rate_multipliers, # _rate_multipliers: DynArray[uint256, MAX_COINS]
_asset_types, # _asset_types: DynArray[uint8, MAX_COINS]
_method_ids, # _method_ids: DynArray[bytes4, MAX_COINS]
_oracles, # _oracles: DynArray[address, MAX_COINS]
code_offset=3
)
length: uint256 = self.pool_count
self.pool_list[length] = pool
self.pool_count = length + 1
self.pool_data[pool].decimals = decimals
self.pool_data[pool].n_coins = n_coins
self.pool_data[pool].base_pool = empty(address)
self.pool_data[pool].implementation = implementation
self.pool_data[pool].asset_types = _asset_types
for i in range(MAX_COINS):
if i == n_coins:
break
coin: address = _coins[i]
self.pool_data[pool].coins.append(coin)
for j in range(i, i + MAX_COINS):
if (j + 1) == n_coins:
break
swappable_coin: address = _coins[j + 1]
key: uint256 = (convert(coin, uint256) ^ convert(swappable_coin, uint256))
length = self.market_counts[key]
self.markets[key][length] = pool
self.market_counts[key] = length + 1
log PlainPoolDeployed(_coins, _A, _fee, msg.sender)
return pool
```
```shell
>>> Factory.deploy_plain_pool(
"crvusd/USDT", # _name
"crvusd-usdt", # _symbol
[ # coins:
"0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E", # crvusd
"0xdAC17F958D2ee523a2206206994597C13D831ec7" # usdt
],
1500 # _A
1000000, # _fee
20000000000, # _offpeg_fee_multiplier
865, # _ma_exp_time
0, # _implementation_idx
[0, 0], # _asset_types
[b"", b""], # _method_ids
["0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000"] # _oracles
)
'returns address of the deployed plain pool'
```
::::
### `deploy_metapool`
*Parameter limitations when deploying a meta pool:*
- Cannot pair against a token that is included in the base pool.
- `_fee` ≤ 100000000 (1%).
- `_offpeg_fee_multiplier` * `_fee` ≤ `MAX_FEE` * `FEE_DENOMINATOR`.
- Valid implementation index.
- Maximum of 18 decimals for a coin.
::::description[`Factory.deploy_metapool(_base_pool: address, _name: String[32], _symbol: String[10], _coin: address, _A: uint256, _fee: uint256, _offpeg_fee_multiplier: uint256, _ma_exp_time: uint256, _implementation_idx: uint256, _asset_type: uint8, _method_id: bytes4, _oracle: address) -> address:`]
Function to deploy a stableswap-ng metapool.
Returns: Deployed metapool (`address`).
Emits: `MetaPoolDeployed`
| Input | Type | Description |
| -------------------- | ------------- | ----------- |
| `_base_pool` | `address` | Address of the base pool to pair the token with |
| `_name` | `String[32]` | Name of the new metapool |
| `_symbol` | `String[10]` | Symbol for the new metapool’s LP token - will be concatenated with the base pool symbol |
| `_coin` | `address` | Address of the coin being used in the metapool |
| `_A` | `uint256` | Amplification coefficient |
| `_fee` | `uint256` | Trade fee, given as an integer with `1e10` precision |
| `_offpeg_fee_multiplier` | `uint256` | Off-peg multiplier |
| `_ma_exp_time` | `uint256` | MA time; set as time_in_seconds / ln(2) |
| `_implementation_idx` | `uint256` | Index of the implementation to use |
| `_asset_type` | `uint8` | Asset type of the pool as an integer; more [here](../../stableswap-ng/overview.md#supported-assets) |
| `_method_id` | `bytes4` | First four bytes of the Keccak-256 hash of the function signatures of the oracle addresses that give rate oracles |
| `_oracle` | `address` | Rate oracle address |
:::info[Implementation ID]
There might be multiple metapool implementations. To query all available ones, see [here](./overview.md#query-implementations). As of the current date (31.10.2023), there is only one metapool implementation available. Since the **`_implementation_idx`**starts at 0, users need to input "0" when deploying a pool.
:::
```vyper
event MetaPoolDeployed:
coin: address
base_pool: address
A: uint256
fee: uint256
deployer: address
MAX_COINS: constant(uint256) = 8
MAX_FEE: constant(uint256) = 5 * 10 **9
FEE_DENOMINATOR: constant(uint256) = 10 **10
@external
def deploy_metapool(
_base_pool: address,
_name: String[32],
_symbol: String[10],
_coin: address,
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_implementation_idx: uint256,
_asset_type: uint8,
_method_id: bytes4,
_oracle: address,
) -> address:
"""
@notice Deploy a new metapool
@param _base_pool Address of the base pool to use
within the metapool
@param _name Name of the new metapool
@param _symbol Symbol for the new metapool - will be
concatenated with the base pool symbol
@param _coin Address of the coin being used in the metapool
@param _A Amplification co-efficient - a higher value here means
less tolerance for imbalance within the pool's assets.
Suggested values include:
* Uncollateralized algorithmic stablecoins: 5-10
* Non-redeemable, collateralized assets: 100
* Redeemable assets: 200-400
@param _fee Trade fee, given as an integer with 1e10 precision. The
the maximum is 1% (100000000).
50% of the fee is distributed to veCRV holders.
@param _ma_exp_time Averaging window of oracle. Set as time_in_seconds / ln(2)
Example: for 10 minute EMA, _ma_exp_time is 600 / ln(2) ~= 866
@param _implementation_idx Index of the implementation to use
@param _asset_type Asset type for token, as an integer
@param _method_id First four bytes of the Keccak-256 hash of the function signatures
of the oracle addresses that gives rate oracles.
Calculated as: keccak(text=event_signature.replace(" ", ""))[:4]
@param _oracle Rate oracle address.
@return Address of the deployed pool
"""
assert not self.base_pool_assets[_coin], "Invalid asset: Cannot pair base pool asset with base pool's LP token"
assert _fee <= 100000000, "Invalid fee"
assert _offpeg_fee_multiplier * _fee <= MAX_FEE * FEE_DENOMINATOR
base_pool_n_coins: uint256 = len(self.base_pool_data[_base_pool].coins)
assert base_pool_n_coins != 0, "Base pool is not added"
implementation: address = self.metapool_implementations[_implementation_idx]
assert implementation != empty(address), "Invalid implementation index"
# things break if a token has >18 decimals
decimals: uint256 = ERC20(_coin).decimals()
assert decimals < 19, "Max 18 decimals for coins"
# combine _coins's _asset_type and basepool coins _asset_types:
base_pool_asset_types: DynArray[uint8, MAX_COINS] = self.base_pool_data[_base_pool].asset_types
asset_types: DynArray[uint8, MAX_COINS] = [_asset_type, 0]
for i in range(0, MAX_COINS):
if i == base_pool_n_coins:
break
asset_types.append(base_pool_asset_types[i])
_coins: DynArray[address, MAX_COINS] = [_coin, self.base_pool_data[_base_pool].lp_token]
_rate_multipliers: DynArray[uint256, MAX_COINS] = [10 **(36 - decimals), 10 **18]
_method_ids: DynArray[bytes4, MAX_COINS] = [_method_id, empty(bytes4)]
_oracles: DynArray[address, MAX_COINS] = [_oracle, empty(address)]
pool: address = create_from_blueprint(
implementation,
_name, # _name: String[32]
_symbol, # _symbol: String[10]
_A, # _A: uint256
_fee, # _fee: uint256
_offpeg_fee_multiplier, # _offpeg_fee_multiplier: uint256
_ma_exp_time, # _ma_exp_time: uint256
self.math_implementation, # _math_implementation: address
_base_pool, # _base_pool: address
_coins, # _coins: DynArray[address, MAX_COINS]
self.base_pool_data[_base_pool].coins, # base_coins: DynArray[address, MAX_COINS]
_rate_multipliers, # _rate_multipliers: DynArray[uint256, MAX_COINS]
asset_types, # asset_types: DynArray[uint8, MAX_COINS]
_method_ids, # _method_ids: DynArray[bytes4, MAX_COINS]
_oracles, # _oracles: DynArray[address, MAX_COINS]
code_offset=3
)
# add pool to pool_list
length: uint256 = self.pool_count
self.pool_list[length] = pool
self.pool_count = length + 1
base_lp_token: address = self.base_pool_data[_base_pool].lp_token
self.pool_data[pool].decimals = [decimals, 18, 0, 0, 0, 0, 0, 0]
self.pool_data[pool].n_coins = 2
self.pool_data[pool].base_pool = _base_pool
self.pool_data[pool].coins = [_coin, self.base_pool_data[_base_pool].lp_token]
self.pool_data[pool].implementation = implementation
is_finished: bool = False
swappable_coin: address = empty(address)
for i in range(MAX_COINS):
if i < len(self.base_pool_data[_base_pool].coins):
swappable_coin = self.base_pool_data[_base_pool].coins[i]
else:
is_finished = True
swappable_coin = base_lp_token
key: uint256 = (convert(_coin, uint256) ^ convert(swappable_coin, uint256))
length = self.market_counts[key]
self.markets[key][length] = pool
self.market_counts[key] = length + 1
if is_finished:
break
log MetaPoolDeployed(_coin, _base_pool, _A, _fee, msg.sender)
return pool
```
```shell
>>> Factory.deploy_metapool(
"0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", # _base_pool
"crvusd/3CRV", # _name
"crvusd-3crv" # _symbol
"0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E", # _coin
1500 # _A
1000000, # _fee
20000000000, # _offpeg_fee_multiplier
865, # _ma_exp_time
0, # _implementation_idx
0, # _asset_type
"b""", # _method_id
"0x0000000000000000000000000000000000000000" # _oracle
)
'returns address of the deployed metapool'
```
::::
---
## Deploying Liquidity Gauges
Liquidity gauges for pools can also be deployed from this contract, but deploying gauges through a factory contract is only possible using the same factory contract that was used for deploying the pool. This feature is only available on the Ethereum mainnet, as liquidity gauges on sidechains need to be deployed through the [RootChainGaugeFactory](../../../gauges/xchain-gauges/root-gauge-factory.md).
### `deploy_gauge`
::::description[`Factory.deploy_gauge(_pool: address) -> address:`]
Function to deploy a gauge. The Factory utilizes the `gauge_implementation` to create the contract from a blueprint.
Returns: deployed gauge (`address`).
Emits: `LiquidityGaugeDeployed`
| Input | Type | Description |
| -------- | --------- | ------------------------------------- |
| `_pool` | `address` | Pool address to deploy the gauge for |
```vyper
event LiquidityGaugeDeployed:
pool: address
gauge: address
@external
def deploy_gauge(_pool: address) -> address:
"""
@notice Deploy a liquidity gauge for a factory pool
@param _pool Factory pool address to deploy a gauge for
@return Address of the deployed gauge
"""
assert self.pool_data[_pool].coins[0] != empty(address), "Unknown pool"
assert self.pool_data[_pool].liquidity_gauge == empty(address), "Gauge already deployed"
implementation: address = self.gauge_implementation
assert implementation != empty(address), "Gauge implementation not set"
gauge: address = create_from_blueprint(self.gauge_implementation, _pool, code_offset=3)
self.pool_data[_pool].liquidity_gauge = gauge
log LiquidityGaugeDeployed(_pool, gauge)
return gauge
```
```shell
>>> Factory.deploy_gauge("0x36DfE783603522566C046Ba1Fa403C8c6F569220")
'returns address of the deployed gauge'
```
::::
---
## Stableswap-NG Factory: Overview
The `CurveStableswapFactoryNG.vy` allows the permissionless deployment of up to eight-coin plain- and metapools, as well as gauges. **Liquidity pool and LP token share the same contract.**For more details, see here: [Stableswap-NG Documentation](../../stableswap-ng/overview.md).
:::github[GitHub]
The source code of the `CurveStableSwapFactoryNG.vy` can be found on [GitHub ](https://github.com/curvefi/stableswap-ng/blob/main/contracts/main/CurveStableSwapFactoryNG.vy).
A list of all deployments can be found [here](../../../deployments.md).
:::
---
## Asset Types
Stableswap-NG pools supports various tokens with different [asset types](../../stableswap-ng/overview.md#supported-assets). New asset types can be added by the `admin` of the contract via the `add_asset_type` method.
For a list of all supported assets, please see [below](#assets-types).
### `asset_types`
::::description[`CurveStableswapFactoryNG.asset_types(arg0: uint8) -> String[20]`]
Getter for name of the different asset types.
| Input | Type | Description |
| ------- | -------- | ------------------------------- |
| `arg0` | `uint8` | Index value of the asset type |
Returns: asset type (`String[20]`)
```vyper
asset_types: public(HashMap[uint8, String[20]])
```
```shell
>>> CurveStableswapFactoryNG.asset_types(0)
'Standard'
>>> CurveStableswapFactoryNG.asset_types(1)
'Oracle'
```
::::
### `add_asset_type`
::::description[`CurveStableSwapFactoryNG.add_asset_type(_id: uint8, _name: String[10])`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to add a new asset type.
| Input | Type | Description |
| ------- | ------------ | ------------------------ |
| `_id` | `uint8` | Asset type ID |
| `_name` | `String[10]` | Name of the new asset type |
```vyper
asset_types: public(HashMap[uint8, String[20]])
@external
def add_asset_type(_id: uint8, _name: String[10]):
"""
@notice Admin only method that adds a new asset type.
@param _id asset type id.
@param _name Name of the asset type.
"""
assert msg.sender == self.admin # dev: admin only
self.asset_types[_id] = _name
```
::::
---
## Base Pools
Stableswap pools also allow the deployment of metapools (an asset paired against a base pool). When deploying a new Factory, the existing base pools must be manually added to the contract for them to be used for metapools.
*Limitations when adding new base pools:*
- Rebasing tokens are not allowed in a base pool.
- Can not add a base pool that contains native tokens (e.g., ETH).
- As much as possible: Use standard `ERC20` tokens.
### `add_base_pool`
::::description[`CurveStableSwapFactoryNG.add_base_pool(_base_pool: address, _base_lp_token: address, _asset_types: DynArray[uint8, MAX_COINS], _n_coins: uint256):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to add a new base pool.
| Input | Type | Description |
| ---------------- | ---------------------------- | -------------------------------- |
| `_base_pool` | `address` | Pool address to add as a base pool |
| `_base_lp_token` | `address` | LP token address of the pool |
| `_asset_types` | `DynArray[uint8, MAX_COINS]` | Array of asset types of the pool |
| `_n_coins` | `uint256` | Number of coins in the base pool |
Emits: `BasePoolAdded`
```vyper
event BasePoolAdded:
base_pool: address
@external
def add_base_pool(
_base_pool: address,
_base_lp_token: address,
_asset_types: DynArray[uint8, MAX_COINS],
_n_coins: uint256,
):
"""
@notice Add a base pool to the registry, which may be used in factory metapools
@dev 1. Only callable by admin
1. Rebasing tokens are not allowed in the base pool.
2. Do not add base pool which contains native tokens (e.g. ETH).
3. As much as possible: use standard ERC20 tokens.
Should you choose to deviate from these recommendations, audits are advised.
@param _base_pool Pool address to add
@param _asset_types Asset type for pool, as an integer
"""
assert msg.sender == self.admin # dev: admin-only function
assert 2 not in _asset_types # dev: rebasing tokens cannot be in base pool
assert len(self.base_pool_data[_base_pool].coins) == 0 # dev: pool exists
assert _n_coins < MAX_COINS # dev: base pool can only have (MAX_COINS - 1) coins.
# add pool to pool_list
length: uint256 = self.base_pool_count
self.base_pool_list[length] = _base_pool
self.base_pool_count = length + 1
self.base_pool_data[_base_pool].lp_token = _base_lp_token
self.base_pool_data[_base_pool].n_coins = _n_coins
self.base_pool_data[_base_pool].asset_types = _asset_types
decimals: uint256 = 0
coins: DynArray[address, MAX_COINS] = empty(DynArray[address, MAX_COINS])
coin: address = empty(address)
for i in range(MAX_COINS):
if i == _n_coins:
break
coin = CurvePool(_base_pool).coins(i)
assert coin != 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE # dev: native token is not supported
self.base_pool_data[_base_pool].coins.append(coin)
self.base_pool_assets[coin] = True
decimals += (ERC20(coin).decimals() << i*8)
self.base_pool_data[_base_pool].decimals = decimals
log BasePoolAdded(_base_pool)
```
::::
### `base_pool_list`
::::description[`CurveStableSwapFactoryNG.base_pool_list(arg0: uint256) -> address: view`]
Getter for the base pool at index `arg0`.
| Input | Type | Description |
| ---------------- | ---------------------------- | -------------------------------- |
| `arg0` | `uint256` | Index of the base pool |
```vyper
base_pool_list: public(address[4294967296]) # list of base pools
```
```shell
>>> CurveStableSwapFactoryNG.base_pool_list(0)
'0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7'
```
::::
---
## Implementations
The Stableswap-NG Factory makes use of **blueprint contracts**to deploy its contracts from the implementations.
:::warning
**Implementation contracts are upgradable.**They can either be replaced, or additional implementation contracts can be added. Therefore, please always make sure to check the most recent ones.
:::
*It utilizes five different implementations:*
- **`pool_implementations`**, containing multiple blueprint contracts that are used to deploy plain pools.
- **`metapool_implementations`**, containing multiple blueprint contracts that are used to deploy metapools.
- **`math_implementation`**, containing math functions used in the AMM.
- **`gauge_implementation`**, containing a blueprint contract that is used when deploying gauges for pools.[^1]
- **`views_implementation`**, containing a view methods contract relevant for integrators and users looking to interact with the AMMs.
[^1]: The `gauge_implementation` is only relevant on Ethereum mainnet. Liquidity gauges on sidechains need to be deployed through the `RootChainGaugeFactory`.
*More on the [**Math Implementation**](../../stableswap-ng/utility-contracts/math.md) and [**Views Implementation**](../../stableswap-ng/utility-contracts/views.md).*
## Query Implementations
### `pool_implementations`
::::description[`CurveStableSwapFactoryNG.pool_implementations(arg0: uint256) -> address: view`]
Getter for the pool implementations. There might be multiple pool implementations base on various circumstances.
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `uint256` | index value of the implementation |
Returns: implementation (`address`).
```vyper
# index -> implementation address
pool_implementations: public(HashMap[uint256, address])
```
```shell
>>> CurveStableSwapFactoryNG.pool_implementation(0)
'0x3E3B5F27bbf5CC967E074b70E9f4046e31663181'
```
::::
### `metapool_implementations`
::::description[`CurveStableSwapFactoryNG.metapool_implementations(arg0: uint256) -> address: view`]
Getter for the pool implementations at index `arg0`. This variable can hold multiple implementations which may be tailored for specific setups.
| Input | Type | Description |
| ------ | --------- | -------------------------------- |
| `arg0` | `uint256` | Index of the pool implementation |
Returns: pool implementation (`address`).
```vyper
# index -> implementation address
metapool_implementations: public(HashMap[uint256, address])
```
```shell
>>> CurveStableSwapFactoryNG.metapool_implementation(0)
'0x19bd1AB34d6ABB584b9C1D5519093bfAA7f6c7d2'
```
::::
### `math_implementations`
::::description[`CurveStableSwapFactoryNG.math_implementations() -> address: view`]
Getter for the math implementation.
Returns: math implementation (`address`).
```vyper
# index -> implementation address
math_implementation: public(address)
```
```shell
>>> CurveStableSwapFactoryNG.math_implementation()
'0x20D1c021525C85D9617Ccc64D8f547d5f730118A'
```
::::
### `gauge_implementations`
::::description[`CurveStableSwapFactoryNG.gauge_implementations() -> address: view`]
Getter for the gauge implementation.
Returns: gauge implementation (`address`).
```vyper
# index -> implementation address
gauge_implementation: public(address)
```
```shell
>>> CurveStableSwapFactoryNG.gauge_implementation()
'0xF5617D4f7514bE35fce829a1C19AE7f6c9106979'
```
::::
### `views_implementation`
::::description[`CurveStableSwapFactoryNG.views_implementations() -> address: view`]
Getter for the views implementation.
Returns: views implementation (`address`).
```vyper
# index -> implementation address
views_implementation: public(address)
```
```shell
>>> CurveStableSwapFactoryNG.views_implementation()
'0x87DD13Dd25a1DBde0E1EdcF5B8Fa6cfff7eABCaD'
```
::::
## Setting New Implementations
New implementation can be by the `admin` of the contract using the following functions:
### `set_pool_implementations`
::::description[`CurveStableSwapFactoryNG.set_pool_implementations(_implementation_index: uint256, _implementation: address,):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set/add a new pool implementation. Existing implementations can be overritten or additionly implementations at another (not set index) can be added.[^2]
[^2]: This only works for `pool_implementations` and `metapool_implementations`. For other the implementations, only a single contract can be set.
| Input | Type | Description |
| ----------------------- | --------- | ------------------------------- |
| `_implementation_index` | `uint256` | Index value at which the new implementation is set |
| `_implementation` | `address` | Implementation contract address |
```vyper
# index -> implementation address
pool_implementations: public(HashMap[uint256, address])
@external
def set_pool_implementations(
_implementation_index: uint256,
_implementation: address,
):
"""
@notice Set implementation contracts for pools
@dev Only callable by admin
@param _implementation_index Implementation index where implementation is stored
@param _implementation Implementation address to use when deploying plain pools
"""
assert msg.sender == self.admin # dev: admin-only function
self.pool_implementations[_implementation_index] = _implementation
```
```shell
>>> CurveStableSwapFactoryNG.set_pool_implementations('todo')
```
::::
### `set_metapool_implementations`
::::description[`CurveStableSwapFactoryNG.set_pool_implementations(_implementation_index: uint256, _implementation: address,):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set/add a new metapool implementation. Existing implementations can be overritten or additionly implementations at another (not set index) can be added.[^3]
[^3]: This only works for `pool_implementations` and `metapool_implementations`. For other the implementations, only a single contract can be set.
| Input | Type | Description |
| ----------------------- | --------- | ------------------------------- |
| `_implementation_index` | `uint256` | Index value at which the new implementation is set |
| `_implementation` | `address` | Implementation contract address |
```vyper
# index -> implementation address
pool_implementations: public(HashMap[uint256, address])
metapool_implementations: public(HashMap[uint256, address])
math_implementation: public(address)
gauge_implementation: public(address)
views_implementation: public(address)
@external
def set_metapool_implementations(
_implementation_index: uint256,
_implementation: address,
):
"""
@notice Set implementation contracts for metapools
@dev Only callable by admin
@param _implementation_index Implementation index where implementation is stored
@param _implementation Implementation address to use when deploying meta pools
"""
assert msg.sender == self.admin # dev: admin-only function
self.metapool_implementations[_implementation_index] = _implementation
```
```shell
>>> CurveStableSwapFactoryNG.set_metapool_implementations('todo')
```
::::
### `set_math_implementation`
::::description[`CurveStableSwapFactoryNG.set_math_implementation(_math_implementation: address):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new math implementation. There can only be one math implementation.
| Input | Type | Description |
| ---------------------- | --------- | ---------------------------------------- |
| `_math_implementation` | `address` | New math implementation contract address |
```vyper
# index -> implementation address
pool_implementations: public(HashMap[uint256, address])
metapool_implementations: public(HashMap[uint256, address])
math_implementation: public(address)
gauge_implementation: public(address)
views_implementation: public(address)
@external
def set_math_implementation(_math_implementation: address):
"""
@notice Set implementation contracts for StableSwap Math
@dev Only callable by admin
@param _math_implementation Address of the math implementation contract
"""
assert msg.sender == self.admin # dev: admin-only function
self.math_implementation = _math_implementation
```
```shell
>>> CurveStableSwapFactoryNG.set_math_implementations('todo')
```
::::
### `set_gauge_implementations`
::::description[`CurveStableSwapFactoryNG.set_gauge_implementation(_gauge_implementation: address):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract. There can only be one gauge implementation.
:::
Function to set a new gauge implementation.
| Input | Type | Description |
| ----------------------- | --------- | ----------------------------------------- |
| `_gauge_implementation` | `address` | New gauge implementation contract address |
```vyper
# index -> implementation address
pool_implementations: public(HashMap[uint256, address])
metapool_implementations: public(HashMap[uint256, address])
math_implementation: public(address)
gauge_implementation: public(address)
views_implementation: public(address)
@external
def set_gauge_implementation(_gauge_implementation: address):
"""
@notice Set implementation contracts for liquidity gauge
@dev Only callable by admin
@param _gauge_implementation Address of the gauge blueprint implementation contract
"""
assert msg.sender == self.admin # dev: admin-only function
self.gauge_implementation = _gauge_implementation
```
```shell
>>> CurveStableSwapFactoryNG.set_gauge_implementations('todo')
```
::::
### `set_views_implementation`
::::description[`CurveStableSwapFactoryNG.set_views_implementation(_views_implementation: address):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract. There can only be one views implementation.
:::
Function to set a new views implementation.
| Input | Type | Description |
| ----------------------- | --------- | ----------------------------------------- |
| `_views_implementation` | `address` | New views implementation contract address |
```vyper
# index -> implementation address
pool_implementations: public(HashMap[uint256, address])
metapool_implementations: public(HashMap[uint256, address])
math_implementation: public(address)
gauge_implementation: public(address)
views_implementation: public(address)
@external
def set_views_implementation(_views_implementation: address):
"""
@notice Set implementation contracts for Views methods
@dev Only callable by admin
@param _views_implementation Implementation address of views contract
"""
assert msg.sender == self.admin # dev: admin-only function
self.views_implementation = _views_implementation
```
```shell
>>> CurveStableSwapFactoryNG.set_views_implementations('todo')
```
::::
---
## Deployer API
### Name and Symbol
The input values of `_name` or `_symbol` are obviously non-trivial for the performance of the pool. These parameters should visualize, what kind of tokens are included in the pool.
```shell
_name = "rETH/wETH Pool"
_symbol = "rETH/wETH"
```
---
### Coins`_coins` includes all tokens included in the pool as a `DynArray`.
```shell
_coins = ["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "0xae78736Cd615f374D3085123A210448E74Fc6393"]
```
---
### A, Fee, Off-Peg Fee Multiplier, and MA-Exp-Time
- `_A` represents the amplification coefficient of the pool, signifying its density.
- `_fee` is referred to as the "base fee."
- The `offpeg_fee_multiplier` parameter enables the system to dynamically adjust fees according to the pool's state.
- `ma_exp_time` denotes the time window for the moving average oracle.
*Recommended Parameters*:
| Parameter | Fiat-Redeemable Stablecoin | Crypto-Collateralized Stablecoin |
| :--------------------- | :------------------------: | :------------------------------: |
| `A` | 200 | 100 |
| `fee` | 0.04% | 0.04% |
| `offpeg_fee_multiplier`| 2 | 2 |
| `ma_exp_time` | 866 | 866 |
```shell
_A = 200
_fee = 4000000 # 0.0001 or 0.01%
_offpeg_fee_multiplier = 20000000000 # 5 or 500%
_ma_exp_time = 866 # ~600 seconds
```
:::note[Parameter Precision]
The precision of `_fee` and `_offpeg_fee_multiplier` is 1e10.
The time window of the moving average exponential oracle is calculated using `time_in_seconds / ln(2)`.
:::
---
### Implemention ID
Pools are **created from implementation contracts**(blueprints). These contracts are added to the Factory and must be choosen when deploying a pool.
:::warning
The Factory can have **multiple plain- and meta-pool implementations**. If there are multiple implementations for a plain or meta-pool, it's important to understand the differences and determine which one is suitable.
Additionally, implementation contracts are **upgradable**. They can either be replaced or have additional implementation contracts set. Please always make sure to check the most recent ones.
To query the factory-specific implementations:
```shell
>>> Factory.pool_implementation(0)
'0xDCc91f930b42619377C200BA05b7513f2958b202'
>>> Factory.metapool_implementation(0)
'0xede71F77d7c900dCA5892720E76316C6E575F0F7'
```
:::
---
### Assets Types
Stableswap-NG infrastructure supports pools with the following asset types:
| Asset Type | Description |
| :---------: | ---------------------- |
| `0` | **Standard ERC-20**token with no additional features |
| `1` | **Oracle**- token with rate oracle (e.g. wstETH) |
| `2` | **Rebasing**- token with rebase (e.g. stETH) |
| `3` | **ERC4626**- token with *`convertToAssets`* method (e.g. sDAI) |
*Consequently, supported tokens include:*
- ERC-20 support for return `True/revert`, `True/False` or `None`
- ERC-20 tokens can have *arbitrary decimals (≤18)*
- ERC-20 tokens that *rebase* (either positive or fee on transfer)
- ERC-20 tokens that have a *rate oracle* (e.g. wstETH, cbETH) Oracle precision must be $10^{18}$
- ERC-4626 tokens with *arbitrary percision* (≤18) of Vault token and underlying asset
:::warning
- **`ERC20:`**Users are advised to do careful due-diligence on ERC20 tokens that they interact with, as this contract **cannot differentiate between harmless and malicious**ERC20 tokens.
- **`Oracle:`**When using tokens with oracles, its important to know that they **may be controlled externally by an EOA**.
- **`Rebasing:`**Users and Integrators are advised to understand how the AMM contract works with rebasing balances.
- **`ERC4626:`**Some ERC4626 implementations **may be susceptible to Donation/Inflation attacks**. Users are advised to proceed with caution.
:::
Choosing asset types can sometimes be quite tricky. Asset types should be seen more as information for the AMM on **how to treat the assets under the hood**.
:::example
Let's consider the example of [rmETH/mETH](https://etherscan.io/address/0xdd4316c777a2295d899ba7ad92e0efa699865f43).
- [mETH](https://etherscan.io/address/0xd5F7838F5C461fefF7FE49ea5ebaF7728bB0ADfa) is a token with a rate oracle (the underlying asset is ETH). The rate can be fetched by reading the `mETHToETH` method within the [staking contract](https://etherscan.io/address/0xe3cBd06D7dadB3F4e6557bAb7EdD924CD1489E8f).
- rmETH is a rebasing token.
Because the deployer wants rmETH and mETH to trade as close to 1:1 as possible, they need to treat mETH like a regular ERC-20 token (asset type 0), instead of a rate oraclized token (asset type 1).
```shell
_asset_types = [0, 2] # coin(0) = asset type 0; coin(1) = asset type 2
```
:::
---
### Method IDs and Rate Oracles
`method_ids` and `_oracles` are required for rate oracles to function. ERC-4626 does not need either of these. The sole requirement for those is to have a `convertToAssets` method.
:::info
When deploying pools that include coins not requiring a rate oracle, **`b""`**or **`0x00000000`**should be included in the `_methods_id` array and the **`ZERO_ADDRESS`**should be used in the `_oracles` array as placeholders for each coin.
:::
- `_method_ids` is the first four bytes of the Keccak-256 hash of the function signatures of the oracle addresses that give rate oracles.
As an example, lets look at the [rETH](https://etherscan.io/token/0xae78736cd615f374d3085123a210448e74fc6393) token. The relevant function which returns the rate is `getExchangeRate`, the according first four bytes of the Keccak-256 hash of the functions signature is therefore `0xe6aa216c`. When calculating, its always important to include `"()"`, as they will change the bytes.
```shell
getExchangeRate -> "0xb2fc0e3e" # wrong
getExchangeRate() -> "0xe6aa216c" # correct
```
[Method ID Calculator](https://piyolab.github.io/playground/ethereum/getEncodedFunctionSignature/)
- `_oracles` is simply the contract address which provides the rate oracle function.
The input values are `DynArrays` with the length of tokens in the pool. Therefore, a rETH/wETH pool would have the following input values:
```shell
_method_id = [b"", "0xe6aa216c"]
_oracles = ["0x0000000000000000000000000000000000000000", "0xae78736cd615f374d3085123a210448e74fc6393"]
```
#### Examples
| Pool | Asset Types | Method ID's | Rate Oracle |
| :---: | :--------: | :---------: | :---------: |
| [mkUSD/USDC](https://etherscan.io/tx/0xf980b4a4194694913af231de69ab4593f5e0fcdc) | `[0, 0]` | `['0x00000000', '0x00000000']` | `['0x0000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000']` |
| [FRAX/sDAI](https://etherscan.io/tx/0xce6431d21e3fb1036ce9973a3312368ed96f5ce7) | `[0, 3]` | `['0x00000000', '0x00000000']` | `['0x0000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000']` |
| [wETH/rETH](https://etherscan.io/tx/0x9efe1a1cbd6ca51ee8319afc4573d253c3b732af) | `[0 , 1]` | `['0x00000000', '0xe6aa216c']` | `['0x0000000000000000000000000000000000000000', '0xae78736Cd615f374D3085123A210448E74Fc6393']` |
| [rmETH/mETH](https://etherscan.io/address/0xdd4316c777a2295d899ba7ad92e0efa699865f43) | `[2 , 0]` | `['0x00000000', '0x00000000']` | `['0x0000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000']` |
---
## Deploying Plain- and Metapools
### `deploy_plain_pool`
*Parameter limitations when deploying a plain pool:*
- Minimum of 2 and maximum of 8 coins.
- All coin arrays should be the same length.
- `_fee` ≤ 100000000 (1%).
- `_offpeg_fee_multiplier` * `_fee` ≤ `MAX_FEE` * `FEE_DENOMINATOR`.
- Maximum of 18 decimals for a coin.
- No duplicate coins.
- Valid implementation index.
::::description[`Factory.deploy_plain_pool(_name: String[32], _symbol: String[10], _coins: DynArray[address, MAX_COINS], _A: uint256, _fee: uint256, _offpeg_fee_multiplier: uint256, _ma_exp_time: uint256, _implementation_idx: uint256, _asset_types: DynArray[uint8, MAX_COINS], _method_ids: DynArray[bytes4, MAX_COINS], _oracles: DynArray[address, MAX_COINS], ) -> address:`]
Function to deploy a stableswap-ng plain pool. The pool is created from a blueprint contract.
| Input | Type | Description |
| -------------------- | ---------------------------- | ----------- |
| `_name` | `String[32]` | Name of the new plain pool |
| `_symbol` | `String[10]` | Symbol for the new pool's LP token; this value will be concatenated with the factory symbol |
| `_coins` | `DynArray[address, MAX_COINS]` | Array of addresses of the coins being used in the pool |
| `_A` | `uint256` | Amplification coefficient |
| `_fee` | `uint256` | Trade fee, given as an integer with `1e10` precision |
| `_offpeg_fee_multiplier` | `uint256` | Off-peg fee multiplier |
| `_ma_exp_time` | `uint256` | MA time; set as time_in_seconds / ln(2) |
| `_implementation_idx` | `uint256` | Index of the implementation to use |
| `_asset_types` | `DynArray[uint8, MAX_COINS]` | Asset type of the pool as an integer; more [here](../../stableswap-ng/overview.md#supported-assets) |
| `_method_ids` | `DynArray[bytes4, MAX_COINS]` | Array of first four bytes of the Keccak-256 hash of the function signatures of the oracle addresses that give rate oracles |
| `_oracles` | `DynArray[address, MAX_COINS]` | Array of rate oracle addresses |
Returns: Deployed pool (`address`).
Emits: `PlainPoolDeployed`
:::info[Implementation ID]
There might be multiple pool implementations. To query all available ones, see [above](#query-implementations). As of the current date (31.10.2023), there is only one pool implementation available. Since the `_implementation_idx` starts at 0, users need to input "0" when deploying a pool.
:::
```vyper
event PlainPoolDeployed:
coins: DynArray[address, MAX_COINS]
A: uint256
fee: uint256
deployer: address
MAX_COINS: constant(uint256) = 8
MAX_FEE: constant(uint256) = 5 * 10 **9
FEE_DENOMINATOR: constant(uint256) = 10 **10
@external
def deploy_plain_pool(
_name: String[32],
_symbol: String[10],
_coins: DynArray[address, MAX_COINS],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_implementation_idx: uint256,
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
) -> address:
"""
@notice Deploy a new plain pool
@param _name Name of the new plain pool
@param _symbol Symbol for the new plain pool - will be
concatenated with factory symbol
@param _coins List of addresses of the coins being used in the pool.
@param _A Amplification co-efficient - a lower value here means
less tolerance for imbalance within the pool's assets.
Suggested values include:
* Uncollateralized algorithmic stablecoins: 5-10
* Non-redeemable, collateralized assets: 100
* Redeemable assets: 200-400
@param _fee Trade fee, given as an integer with 1e10 precision. The
maximum is 1% (100000000). 50% of the fee is distributed to veCRV holders.
@param _ma_exp_time Averaging window of oracle. Set as time_in_seconds / ln(2)
Example: for 10 minute EMA, _ma_exp_time is 600 / ln(2) ~= 866
@param _implementation_idx Index of the implementation to use
@param _asset_types Asset types for pool, as an integer
@param _method_ids Array of first four bytes of the Keccak-256 hash of the function signatures
of the oracle addresses that gives rate oracles.
Calculated as: keccak(text=event_signature.replace(" ", ""))[:4]
@param _oracles Array of rate oracle addresses.
@return Address of the deployed pool
"""
assert len(_coins) >= 2 # dev: pool needs to have at least two coins!
assert len(_coins) == len(_method_ids) # dev: All coin arrays should be same length
assert len(_coins) == len(_oracles) # dev: All coin arrays should be same length
assert len(_coins) == len(_asset_types) # dev: All coin arrays should be same length
assert _fee <= 100000000, "Invalid fee"
assert _offpeg_fee_multiplier * _fee <= MAX_FEE * FEE_DENOMINATOR
n_coins: uint256 = len(_coins)
_rate_multipliers: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
decimals: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
for i in range(MAX_COINS):
if i == n_coins:
break
coin: address = _coins[i]
decimals.append(ERC20(coin).decimals())
assert decimals[i] < 19, "Max 18 decimals for coins"
_rate_multipliers.append(10 **(36 - decimals[i]))
for j in range(i, i + MAX_COINS):
if (j + 1) == n_coins:
break
assert coin != _coins[j+1], "Duplicate coins"
implementation: address = self.pool_implementations[_implementation_idx]
assert implementation != empty(address), "Invalid implementation index"
pool: address = create_from_blueprint(
implementation,
_name, # _name: String[32]
_symbol, # _symbol: String[10]
_A, # _A: uint256
_fee, # _fee: uint256
_offpeg_fee_multiplier, # _offpeg_fee_multiplier: uint256
_ma_exp_time, # _ma_exp_time: uint256
_coins, # _coins: DynArray[address, MAX_COINS]
_rate_multipliers, # _rate_multipliers: DynArray[uint256, MAX_COINS]
_asset_types, # _asset_types: DynArray[uint8, MAX_COINS]
_method_ids, # _method_ids: DynArray[bytes4, MAX_COINS]
_oracles, # _oracles: DynArray[address, MAX_COINS]
code_offset=3
)
length: uint256 = self.pool_count
self.pool_list[length] = pool
self.pool_count = length + 1
self.pool_data[pool].decimals = decimals
self.pool_data[pool].n_coins = n_coins
self.pool_data[pool].base_pool = empty(address)
self.pool_data[pool].implementation = implementation
self.pool_data[pool].asset_types = _asset_types
for i in range(MAX_COINS):
if i == n_coins:
break
coin: address = _coins[i]
self.pool_data[pool].coins.append(coin)
for j in range(i, i + MAX_COINS):
if (j + 1) == n_coins:
break
swappable_coin: address = _coins[j + 1]
key: uint256 = (convert(coin, uint256) ^ convert(swappable_coin, uint256))
length = self.market_counts[key]
self.markets[key][length] = pool
self.market_counts[key] = length + 1
log PlainPoolDeployed(_coins, _A, _fee, msg.sender)
return pool
```
```shell
>>> Factory.deploy_plain_pool(
"crvusd/USDT", # _name
"crvusd-usdt", # _symbol
[ # coins:
"0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E", # crvusd
"0xdAC17F958D2ee523a2206206994597C13D831ec7" # usdt
],
1500 # _A
1000000, # _fee
20000000000, # _offpeg_fee_multiplier
865, # _ma_exp_time
0, # _implementation_idx
[0, 0], # _asset_types
[b"", b""], # _method_ids
["0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000"] # _oracles
)
'returns address of the deployed plain pool'
```
::::
### `deploy_metapool`
*Parameter limitations when deploying a meta pool:*
- Cannot pair against a token that is included in the base pool.
- `_fee` ≤ 100000000 (1%).
- `_offpeg_fee_multiplier` * `_fee` ≤ `MAX_FEE` * `FEE_DENOMINATOR`.
- Valid implementation index.
- Maximum of 18 decimals for a coin.
::::description[`Factory.deploy_metapool(_base_pool: address, _name: String[32], _symbol: String[10], _coin: address, _A: uint256, _fee: uint256, _offpeg_fee_multiplier: uint256, _ma_exp_time: uint256, _implementation_idx: uint256, _asset_type: uint8, _method_id: bytes4, _oracle: address) -> address:`]
Function to deploy a stableswap-ng metapool.
| Input | Type | Description |
| -------------------- | ------------- | ----------- |
| `_base_pool` | `address` | Address of the base pool to pair the token with |
| `_name` | `String[32]` | Name of the new metapool |
| `_symbol` | `String[10]` | Symbol for the new metapool's LP token - will be concatenated with the base pool symbol |
| `_coin` | `address` | Address of the coin being used in the metapool |
| `_A` | `uint256` | Amplification coefficient |
| `_fee` | `uint256` | Trade fee, given as an integer with `1e10` precision |
| `_offpeg_fee_multiplier` | `uint256` | Off-peg multiplier |
| `_ma_exp_time` | `uint256` | MA time; set as time_in_seconds / ln(2) |
| `_implementation_idx` | `uint256` | Index of the implementation to use |
| `_asset_type` | `uint8` | Asset type of the pool as an integer; more [here](../../stableswap-ng/overview.md#supported-assets) |
| `_method_id` | `bytes4` | First four bytes of the Keccak-256 hash of the function signatures of the oracle addresses that give rate oracles |
| `_oracle` | `address` | Rate oracle address |
Returns: Deployed metapool (`address`).
Emits: `MetaPoolDeployed`
:::info[Implementation ID]
There might be multiple metapool implementations. To query all available ones, see [above](#query-implementations). As of the current date (31.10.2023), there is only one metapool implementation available. Since the **`_implementation_idx`**starts at 0, users need to input "0" when deploying a pool.
:::
```vyper
event MetaPoolDeployed:
coin: address
base_pool: address
A: uint256
fee: uint256
deployer: address
MAX_COINS: constant(uint256) = 8
MAX_FEE: constant(uint256) = 5 * 10 **9
FEE_DENOMINATOR: constant(uint256) = 10 **10
@external
def deploy_metapool(
_base_pool: address,
_name: String[32],
_symbol: String[10],
_coin: address,
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_implementation_idx: uint256,
_asset_type: uint8,
_method_id: bytes4,
_oracle: address,
) -> address:
"""
@notice Deploy a new metapool
@param _base_pool Address of the base pool to use
within the metapool
@param _name Name of the new metapool
@param _symbol Symbol for the new metapool - will be
concatenated with the base pool symbol
@param _coin Address of the coin being used in the metapool
@param _A Amplification co-efficient - a higher value here means
less tolerance for imbalance within the pool's assets.
Suggested values include:
* Uncollateralized algorithmic stablecoins: 5-10
* Non-redeemable, collateralized assets: 100
* Redeemable assets: 200-400
@param _fee Trade fee, given as an integer with 1e10 precision. The
the maximum is 1% (100000000).
50% of the fee is distributed to veCRV holders.
@param _ma_exp_time Averaging window of oracle. Set as time_in_seconds / ln(2)
Example: for 10 minute EMA, _ma_exp_time is 600 / ln(2) ~= 866
@param _implementation_idx Index of the implementation to use
@param _asset_type Asset type for token, as an integer
@param _method_id First four bytes of the Keccak-256 hash of the function signatures
of the oracle addresses that gives rate oracles.
Calculated as: keccak(text=event_signature.replace(" ", ""))[:4]
@param _oracle Rate oracle address.
@return Address of the deployed pool
"""
assert not self.base_pool_assets[_coin], "Invalid asset: Cannot pair base pool asset with base pool's LP token"
assert _fee <= 100000000, "Invalid fee"
assert _offpeg_fee_multiplier * _fee <= MAX_FEE * FEE_DENOMINATOR
base_pool_n_coins: uint256 = len(self.base_pool_data[_base_pool].coins)
assert base_pool_n_coins != 0, "Base pool is not added"
implementation: address = self.metapool_implementations[_implementation_idx]
assert implementation != empty(address), "Invalid implementation index"
# things break if a token has >18 decimals
decimals: uint256 = ERC20(_coin).decimals()
assert decimals < 19, "Max 18 decimals for coins"
# combine _coins's _asset_type and basepool coins _asset_types:
base_pool_asset_types: DynArray[uint8, MAX_COINS] = self.base_pool_data[_base_pool].asset_types
asset_types: DynArray[uint8, MAX_COINS] = [_asset_type, 0]
for i in range(0, MAX_COINS):
if i == base_pool_n_coins:
break
asset_types.append(base_pool_asset_types[i])
_coins: DynArray[address, MAX_COINS] = [_coin, self.base_pool_data[_base_pool].lp_token]
_rate_multipliers: DynArray[uint256, MAX_COINS] = [10 **(36 - decimals), 10 **18]
_method_ids: DynArray[bytes4, MAX_COINS] = [_method_id, empty(bytes4)]
_oracles: DynArray[address, MAX_COINS] = [_oracle, empty(address)]
pool: address = create_from_blueprint(
implementation,
_name, # _name: String[32]
_symbol, # _symbol: String[10]
_A, # _A: uint256
_fee, # _fee: uint256
_offpeg_fee_multiplier, # _offpeg_fee_multiplier: uint256
_ma_exp_time, # _ma_exp_time: uint256
self.math_implementation, # _math_implementation: address
_base_pool, # _base_pool: address
_coins, # _coins: DynArray[address, MAX_COINS]
self.base_pool_data[_base_pool].coins, # base_coins: DynArray[address, MAX_COINS]
_rate_multipliers, # _rate_multipliers: DynArray[uint256, MAX_COINS]
asset_types, # asset_types: DynArray[uint8, MAX_COINS]
_method_ids, # _method_ids: DynArray[bytes4, MAX_COINS]
_oracles, # _oracles: DynArray[address, MAX_COINS]
code_offset=3
)
# add pool to pool_list
length: uint256 = self.pool_count
self.pool_list[length] = pool
self.pool_count = length + 1
base_lp_token: address = self.base_pool_data[_base_pool].lp_token
self.pool_data[pool].decimals = [decimals, 18, 0, 0, 0, 0, 0, 0]
self.pool_data[pool].n_coins = 2
self.pool_data[pool].base_pool = _base_pool
self.pool_data[pool].coins = [_coin, self.base_pool_data[_base_pool].lp_token]
self.pool_data[pool].implementation = implementation
is_finished: bool = False
swappable_coin: address = empty(address)
for i in range(MAX_COINS):
if i < len(self.base_pool_data[_base_pool].coins):
swappable_coin = self.base_pool_data[_base_pool].coins[i]
else:
is_finished = True
swappable_coin = base_lp_token
key: uint256 = (convert(_coin, uint256) ^ convert(swappable_coin, uint256))
length = self.market_counts[key]
self.markets[key][length] = pool
self.market_counts[key] = length + 1
if is_finished:
break
log MetaPoolDeployed(_coin, _base_pool, _A, _fee, msg.sender)
return pool
```
```shell
>>> Factory.deploy_metapool(
"0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", # _base_pool
"crvusd/3CRV", # _name
"crvusd-3crv" # _symbol
"0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E", # _coin
1500 # _A
1000000, # _fee
20000000000, # _offpeg_fee_multiplier
865, # _ma_exp_time
0, # _implementation_idx
0, # _asset_type
"b""", # _method_id
"0x0000000000000000000000000000000000000000" # _oracle
)
'returns address of the deployed metapool'
```
::::
---
## Deploying Liquidity Gauges
Liquidity gauges for pools can also be deployed from this contract, but deploying gauges through a factory contract is only possible using the same factory contract that was used for deploying the pool. This feature is only available on the Ethereum mainnet, as liquidity gauges on sidechains need to be deployed through the [RootChainGaugeFactory](../../../gauges/xchain-gauges/root-gauge-factory.md).
### `deploy_gauge`
::::description[`Factory.deploy_gauge(_pool: address) -> address:`]
Function to deploy a gauge. The Factory utilizes the `gauge_implementation` to create the contract from a blueprint.
| Input | Type | Description |
| -------- | --------- | ------------------------------------- |
| `_pool` | `address` | Pool address to deploy the gauge for |
Returns: deployed gauge (`address`).
Emits: `LiquidityGaugeDeployed`
```vyper
event LiquidityGaugeDeployed:
pool: address
gauge: address
@external
def deploy_gauge(_pool: address) -> address:
"""
@notice Deploy a liquidity gauge for a factory pool
@param _pool Factory pool address to deploy a gauge for
@return Address of the deployed gauge
"""
assert self.pool_data[_pool].coins[0] != empty(address), "Unknown pool"
assert self.pool_data[_pool].liquidity_gauge == empty(address), "Gauge already deployed"
implementation: address = self.gauge_implementation
assert implementation != empty(address), "Gauge implementation not set"
gauge: address = create_from_blueprint(self.gauge_implementation, _pool, code_offset=3)
self.pool_data[_pool].liquidity_gauge = gauge
log LiquidityGaugeDeployed(_pool, gauge)
return gauge
```
```shell
>>> Factory.deploy_gauge("0x36DfE783603522566C046Ba1Fa403C8c6F569220")
'returns address of the deployed gauge'
```
::::
---
## Deployer Api
## Liquidity Pools:::warning
The transaction will revert if the following requirements are not met.
:::
### `deploy_pool`
The pool **deployment is permissionless**, but it must adhere to certain parameter limitations:
| Parameter | Limitation |
| -------------------- | ---------------------------------------------------- |
| `A` | A_min - 1 < A < A_max + 1 |
| `gamma` | gamma_min - 1 < gamma < gamma_max + 1 |
| `mid_fee` | mid_fee < fee_max - 1; (mid_fee can be 0) |
| `out_fee` | out_fee >= mid_fee AND out_fee < fee_max - 1 |
| `fee_gamma` | 0 < fee_gamma < 10^18 + 1 |
| `allowed_extra_profit` | allowed_extra_profit < 10^18 + 1 |
| `adjustment_step` | 0 < adjustment_step < 10^18 + 1 |
| `ma_exp_time` | 86 < ma_exp_time < 872542 |
| `initial_prices` | 10^6 < initial_prices[0] and initial_prices[1] < 10^30 |
- Three coins; no duplicate coins possible.
- **`implementation_id`**cannot be **`ZERO_ADDRESS`**.
*With:*
| Parameters | Value |
| ---------------- | ---------------------------------------- |
| n_coins | 3 |
| A_multiplier | 10000 |
| A_min | n_coins^n_coins * A_multiplier = 270000 |
| A_max | 1000 * A_multiplier * n_coins^n_coins = 270000000 |
| gamma_min | 10^10 = 10000000000 |
| gamma_max | 5 * 10^16 = 50000000000000000 |
| fee_max | 10 * 10^9 = 10000000000 |
::::description[`Factory.deploy_pool(_name: String[64], _symbol: String[32], _coins: address[N_COINS], _weth: address, implementation_id: uint256, A: uint256, gamma: uint256, mid_fee: uint256, out_fee: uint256, fee_gamma: uint256, allowed_extra_profit: uint256, adjustment_step: uint256, ma_exp_time: uint256, initial_prices: uint256[N_COINS-1],) -> address:`]
Function to deploy a tricrypto pool.
Returns: Deployed pool (`address`).
Emits event: `TricryptoPoolDeployed`
| Input | Type | Description |
| ------------------- | --------------------- | ----------- |
| `_name` | `String[64]` | Pool Name |
| `_symbol` | `String[32]` | Pool Symbol |
| `_coins` | `address[N_COINS]` | Included Coins |
| `_weth` | `address` | WETH Address |
| `implementation_id` | `uint256` | Index of Pool Implementation |
| `A` | `uint256` | Amplification Factor |
| `gamma` | `uint256` | Gamma |
| `mid_fee` | `uint256` | Mid Fee |
| `out_fee` | `uint256` | Out Fee |
| `fee_gamma` | `uint256` | Fee Gamma |
| `allowed_extra_profit` | `uint256` | Allowed Extra Profit |
| `adjustment_step` | `uint256` | Adjustment Step |
| `ma_exp_time` | `uint256` | Exponential Moving Average Time |
| `initial_prices` | `uint256[N_COINS-1]` | Initial Prices |
```vyper hl_lines="1"
event TricryptoPoolDeployed:
pool: address
name: String[64]
symbol: String[32]
weth: address
coins: address[N_COINS]
math: address
salt: bytes32
packed_precisions: uint256
packed_A_gamma: uint256
packed_fee_params: uint256
packed_rebalancing_params: uint256
packed_prices: uint256
deployer: address
N_COINS: constant(uint256) = 3
A_MULTIPLIER: constant(uint256) = 10000
MAX_FEE: constant(uint256) = 10 * 10 **9
MIN_GAMMA: constant(uint256) = 10 **10
MAX_GAMMA: constant(uint256) = 5 * 10**16
MIN_A: constant(uint256) = N_COINS **N_COINS * A_MULTIPLIER / 100
MAX_A: constant(uint256) = 1000 * A_MULTIPLIER * N_COINS**N_COINS
PRICE_SIZE: constant(uint128) = 256 / (N_COINS - 1)
PRICE_MASK: constant(uint256) = 2**PRICE_SIZE - 1
@external
def deploy_pool(
_name: String[64],
_symbol: String[32],
_coins: address[N_COINS],
_weth: address,
implementation_id: uint256,
A: uint256,
gamma: uint256,
mid_fee: uint256,
out_fee: uint256,
fee_gamma: uint256,
allowed_extra_profit: uint256,
adjustment_step: uint256,
ma_exp_time: uint256,
initial_prices: uint256[N_COINS-1],
) -> address:
"""
@notice Deploy a new pool
@param _name Name of the new plain pool
@param _symbol Symbol for the new plain pool - will be concatenated with factory symbol
@return Address of the deployed pool
"""
pool_implementation: address = self.pool_implementations[implementation_id]
assert pool_implementation != empty(address), "Pool implementation not set"
# Validate parameters
assert A > MIN_A-1
assert A < MAX_A+1
assert gamma > MIN_GAMMA-1
assert gamma < MAX_GAMMA+1
assert mid_fee < MAX_FEE-1 # mid_fee can be zero
assert out_fee >= mid_fee
assert out_fee < MAX_FEE-1
assert fee_gamma < 10**18+1
assert fee_gamma > 0
assert allowed_extra_profit < 10**18+1
assert adjustment_step < 10**18+1
assert adjustment_step > 0
assert ma_exp_time < 872542 # 7 * 24 * 60 * 60 / ln(2)
assert ma_exp_time > 86 # 60 / ln(2)
assert min(initial_prices[0], initial_prices[1]) > 10**6
assert max(initial_prices[0], initial_prices[1]) < 10**30
assert _coins[0] != _coins[1] and _coins[1] != _coins[2] and _coins[0] != _coins[2], "Duplicate coins"
decimals: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
for i in range(N_COINS):
d: uint256 = ERC20(_coins[i]).decimals()
assert d < 19, "Max 18 decimals for coins"
decimals[i] = d
precisions[i] = 10**(18 - d)
# pack precisions
packed_precisions: uint256 = self._pack(precisions)
# pack fees
packed_fee_params: uint256 = self._pack(
[mid_fee, out_fee, fee_gamma]
)
# pack liquidity rebalancing params
packed_rebalancing_params: uint256 = self._pack(
[allowed_extra_profit, adjustment_step, ma_exp_time]
)
# pack A_gamma
packed_A_gamma: uint256 = A << 128
packed_A_gamma = packed_A_gamma | gamma
# pack initial prices
packed_prices: uint256 = 0
for k in range(N_COINS - 1):
packed_prices = packed_prices << PRICE_SIZE
p: uint256 = initial_prices[N_COINS - 2 - k]
assert p < PRICE_MASK
packed_prices = p | packed_prices
# pool is an ERC20 implementation
_salt: bytes32 = block.prevhash
_math_implementation: address = self.math_implementation
pool: address = create_from_blueprint(
pool_implementation,
_name,
_symbol,
_coins,
_math_implementation,
_weth,
_salt,
packed_precisions,
packed_A_gamma,
packed_fee_params,
packed_rebalancing_params,
packed_prices,
code_offset=3
)
# populate pool data
length: uint256 = self.pool_count
self.pool_list[length] = pool
self.pool_count = length + 1
self.pool_data[pool].decimals = decimals
self.pool_data[pool].coins = _coins
# add coins to market:
self._add_coins_to_market(_coins[0], _coins[1], pool)
self._add_coins_to_market(_coins[0], _coins[2], pool)
self._add_coins_to_market(_coins[1], _coins[2], pool)
log TricryptoPoolDeployed(
pool,
_name,
_symbol,
_weth,
_coins,
_math_implementation,
_salt,
packed_precisions,
packed_A_gamma,
packed_fee_params,
packed_rebalancing_params,
packed_prices,
msg.sender,
)
return pool
```
```shell
>>> TricryptoFactory.deploy_pool(
_name: crv/weth/tbtc tripool,
_symbol: crv-weth-tbtc,
_coins: '0xD533a949740bb3306d119CC777fa900bA034cd52', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', '0x8dAEBADE922dF735c38C80C7eBD708Af50815fAa',
_weth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
implementation_id: 0,
A: 2700000,
gamma: 1300000000000,
mid_fee: 2999999,
out_fee: 80000000,
fee_gamma: 350000000000000,
allowed_extra_profit: 100000000000,
adjustment_step: 100000000000,
ma_exp_time: 600,
initial_prices: todo,
)
'returns address of the deployed pool'
```
::::
## Liquidity Gauge:::info
Liquidity gauges can only be successfully deployed from the same contract from which the pool was deployed!
:::
### `deploy_gauge`
::::description[`deploy_gauge(_pool: address) -> address`]
Deploy a liquidity gauge for a factory pool. The deployed gauge implementation is based on what the factory admin has set for `gauge_implementation`.
| Input | Type | Description |
| -------- | --------- | ------------------------------------ |
| `_pool` | `address` | Pool address to deploy a gauge for |
```vyper
@external
def deploy_gauge(_pool: address) -> address:
"""
@notice Deploy a liquidity gauge for a factory pool
@param _pool Factory pool address to deploy a gauge for
@return Address of the deployed gauge
"""
assert self.pool_data[_pool].coins[0] != ZERO_ADDRESS, "Unknown pool"
assert self.pool_data[_pool].liquidity_gauge == ZERO_ADDRESS, "Gauge already deployed"
implementation: address = self.gauge_implementation
assert implementation != ZERO_ADDRESS, "Gauge implementation not set"
gauge: address = create_forwarder_to(implementation)
LiquidityGauge(gauge).initialize(_pool)
self.pool_data[_pool].liquidity_gauge = gauge
log LiquidityGaugeDeployed(_pool, gauge)
return gauge
```
```shell
>>> Factory.deploy_gauge('0x...')
'returns address of the deployed gauge'
```
::::
---
## Pool Factory: Overview
The Tricrypto-NG Factory allows the permissionless deployment of two-coin volatile asset pools, as well as gauges. **The liquidity pool and LP token share the same contract.**Additionally, the Factory contract is the direct admin and fee receiver of all pools. In turn, the Factory is controlled by the CurveDAO.
:::deploy[Contract Source & Deployment]
Source code for this contract is available on [Github](https://github.com/curvefi/tricrypto-ng/blob/main/contracts/main/CurveTricryptoFactory.vy).
A list of all deployed contracts can be found [here](../../../deployments.md).
:::
---
## Implementations
**The Tricrypto-NG Factory makes use of blueprint contracts to deploy its contracts from the implementations.**
:::warning
**Implementation contracts are upgradable.** They can either be replaced, or additional implementation contracts can be added. Therefore, please always make sure to check the most recent ones.
:::
It utilizes four different implementations:
- `pool_implementations`, containing multiple blueprint contracts that are used to deploy the pools.
- `gauge_implementation`, containing a blueprint contract that is used when deploying gauges for pools.
- `views_implementation`, containing a view methods contract relevant for integrators and users looking to interact with the AMMs.
- `math_implementation`, containing math functions used in the AMM.
*More on the [**Math Implementation**](../../tricrypto-ng/utility-contracts/math.md) and [**Views Implementation**](../../tricrypto-ng/utility-contracts/views.md).*
## Query Implementations
### `pool_implementation`
::::description[`Factory.pool_implementations(arg0: uint256) -> address: view`]
Getter for the current pool implementation contract. This accounts for variations such as two-coin and three-pool pools.
| Input | Type | Description |
| ------- | --------- | ------------- |
| `arg0` | `uint256` | Index |
Returns: Pool blueprint contract (`address`).
```vyper
pool_implementations: public(HashMap[uint256, address])
```
```shell
>>> Factory.pool_implementation(0)
'0x66442B0C5260B92cAa9c234ECf2408CBf6b19a6f'
```
::::
### `gauge_implementation`
::::description[`Factory.gauge_implementation() -> address: view`]
Getter for the current gauge implementation contract.
Returns: Gauge blueprint contract (`address`).
```vyper
gauge_implementation: public(address)
```
```shell
>>> Factory.gauge_implementation()
'0x5fC124a161d888893529f67580ef94C2784e9233'
```
::::
### `views_implementation`
::::description[`Factory.views_implementation() -> address: view`]
Getter for the current views implementation contract.
Returns: Views blueprint contract (`address`).
```vyper
views_implementation: public(address)
```
```shell
>>> Factory.views_implementation()
'0x064253915b8449fdEFac2c4A74aA9fdF56691a31'
```
::::
### `math_implementation`
::::description[`Factory.math_implementation() -> address: view`]
Getter for the current pool implementation contract.
Returns: Math blueprint contract (`address`).
```vyper
math_implementation: public(address)
```
```shell
>>> Factory.math_implementation()
'0xcBFf3004a20dBfE2731543AA38599A526e0fD6eE'
```
::::
## Set New Implementations
*New implementations can be set via these admin-only functions:*
### `set_pool_implementation`
::::description[`Factory.set_pool_implementation(_pool_implementation: address, _implementation_index: uint256):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a `_pool_implementation` for `_implementation_index`.
| Input | Type | Description |
| ----------------------- | --------- | ------------------------- |
| `_pool_implementation` | `address` | New pool implementation |
| `_implementation_index` | `uint256` | Index |
Emits event: `UpdatePoolImplementation`
```vyper
event UpdatePoolImplementation:
_implemention_id: uint256
_old_pool_implementation: address
_new_pool_implementation: address
pool_implementations: public(HashMap[uint256, address])
@external
def set_pool_implementation(
_pool_implementation: address, _implementation_index: uint256
):
"""
@notice Set pool implementation
@dev Set to empty(address) to prevent deployment of new pools
@param _pool_implementation Address of the new pool implementation
@param _implementation_index Index of the pool implementation
"""
assert msg.sender == self.admin, "dev: admin only"
log UpdatePoolImplementation(
_implementation_index,
self.pool_implementations[_implementation_index],
_pool_implementation
)
self.pool_implementations[_implementation_index] = _pool_implementation
```
```shell
>>> Factory.set_pool_implementation("todo")
'todo'
```
::::
### `set_gauge_implementation`
::::description[`Factory.set_gauge_implementation(_gauge_implementation: address):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new `_gauge_implementation`.
| Input | Type | Description |
| ------------------------ | --------- | ------------------------- |
| `_gauge_implementation` | `address` | Gauge blueprint contract |
Emits event: `UpdateGaugeImplementation`
```vyper
event UpdateGaugeImplementation:
_old_gauge_implementation: address
_new_gauge_implementation: address
gauge_implementation: public(address)
@external
def set_gauge_implementation(_gauge_implementation: address):
"""
@notice Set gauge implementation
@dev Set to empty(address) to prevent deployment of new gauges
@param _gauge_implementation Address of the new token implementation
"""
assert msg.sender == self.admin, "dev: admin only"
log UpdateGaugeImplementation(self.gauge_implementation, _gauge_implementation)
self.gauge_implementation = _gauge_implementation
```
```shell
>>> Factory.set_gauge_implementation("todo")
'todo'
```
::::
### `set_views_implementation`
::::description[`Factory.set_views_implementation(_views_implementation: address):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new `_views_implementation`.
| Input | Type | Description |
| ------------------------- | --------- | ------------------------ |
| `_views_implementation` | `address` | Views blueprint contract |
Emits event: `UpdateViewsImplementation`
```vyper
event UpdateViewsImplementation:
_old_views_implementation: address
_new_views_implementation: address
views_implementation: public(address)
@external
def set_views_implementation(_views_implementation: address):
"""
@notice Set views contract implementation
@param _views_implementation Address of the new views contract
"""
assert msg.sender == self.admin, "dev: admin only"
log UpdateViewsImplementation(self.views_implementation, _views_implementation)
self.views_implementation = _views_implementation
```
```shell
>>> Factory.set_views_implementation("todo")
'todo'
```
::::
### `set_math_implementation`
::::description[`Factory.set_math_implementation(_math_implementation: address):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new `_math_implementation`.
| Input | Type | Description |
| ------------------------ | --------- | ------------------------ |
| `_math_implementation` | `address` | Math blueprint contract |
Emits event: `UpdateMathImplementation`
```vyper
event UpdateMathImplementation:
_old_math_implementation: address
_new_math_implementation: address
math_implementation: public(address)
@external
def set_math_implementation(_math_implementation: address):
"""
@notice Set math implementation
@param _math_implementation Address of the new math contract
"""
assert msg.sender == self.admin, "dev: admin only"
log UpdateMathImplementation(self.math_implementation, _math_implementation)
self.math_implementation = _math_implementation
```
```shell
>>> Factory.set_math_implementation("todo")
'todo'
```
::::
---
## Deploying Pools
:::warning
The transaction will revert if the following requirements are not met.
:::
### `deploy_pool`
The pool **deployment is permissionless**, but it must adhere to certain parameter limitations:
| Parameter | Limitation |
| -------------------- | ---------------------------------------------------- |
| `A` | A_min - 1 < A < A_max + 1 |
| `gamma` | gamma_min - 1 < gamma < gamma_max + 1 |
| `mid_fee` | mid_fee < fee_max - 1; (mid_fee can be 0) |
| `out_fee` | out_fee >= mid_fee AND out_fee < fee_max - 1 |
| `fee_gamma` | 0 < fee_gamma < 10^18 + 1 |
| `allowed_extra_profit` | allowed_extra_profit < 10^18 + 1 |
| `adjustment_step` | 0 < adjustment_step < 10^18 + 1 |
| `ma_exp_time` | 86 < ma_exp_time < 872542 |
| `initial_prices` | 10^6 < initial_prices[0] and initial_prices[1] < 10^30 |
- Three coins; no duplicate coins possible.
- **`implementation_id`**cannot be **`ZERO_ADDRESS`**.
*With:*
| Parameters | Value |
| ---------------- | ---------------------------------------- |
| n_coins | 3 |
| A_multiplier | 10000 |
| A_min | n_coins^n_coins * A_multiplier = 270000 |
| A_max | 1000 * A_multiplier * n_coins^n_coins = 270000000 |
| gamma_min | 10^10 = 10000000000 |
| gamma_max | 5 * 10^16 = 50000000000000000 |
| fee_max | 10 * 10^9 = 10000000000 |
::::description[`Factory.deploy_pool(_name: String[64], _symbol: String[32], _coins: address[N_COINS], _weth: address, implementation_id: uint256, A: uint256, gamma: uint256, mid_fee: uint256, out_fee: uint256, fee_gamma: uint256, allowed_extra_profit: uint256, adjustment_step: uint256, ma_exp_time: uint256, initial_prices: uint256[N_COINS-1],) -> address:`]
Function to deploy a tricrypto pool.
| Input | Type | Description |
| ------------------- | --------------------- | ----------- |
| `_name` | `String[64]` | Pool Name |
| `_symbol` | `String[32]` | Pool Symbol |
| `_coins` | `address[N_COINS]` | Included Coins |
| `_weth` | `address` | WETH Address |
| `implementation_id` | `uint256` | Index of Pool Implementation |
| `A` | `uint256` | Amplification Factor |
| `gamma` | `uint256` | Gamma |
| `mid_fee` | `uint256` | Mid Fee |
| `out_fee` | `uint256` | Out Fee |
| `fee_gamma` | `uint256` | Fee Gamma |
| `allowed_extra_profit` | `uint256` | Allowed Extra Profit |
| `adjustment_step` | `uint256` | Adjustment Step |
| `ma_exp_time` | `uint256` | Exponential Moving Average Time |
| `initial_prices` | `uint256[N_COINS-1]` | Initial Prices |
Returns: Deployed pool (`address`).
Emits event: `TricryptoPoolDeployed`
```vyper hl_lines="1"
event TricryptoPoolDeployed:
pool: address
name: String[64]
symbol: String[32]
weth: address
coins: address[N_COINS]
math: address
salt: bytes32
packed_precisions: uint256
packed_A_gamma: uint256
packed_fee_params: uint256
packed_rebalancing_params: uint256
packed_prices: uint256
deployer: address
N_COINS: constant(uint256) = 3
A_MULTIPLIER: constant(uint256) = 10000
MAX_FEE: constant(uint256) = 10 * 10 **9
MIN_GAMMA: constant(uint256) = 10 **10
MAX_GAMMA: constant(uint256) = 5 * 10**16
MIN_A: constant(uint256) = N_COINS **N_COINS * A_MULTIPLIER / 100
MAX_A: constant(uint256) = 1000 * A_MULTIPLIER * N_COINS**N_COINS
PRICE_SIZE: constant(uint128) = 256 / (N_COINS - 1)
PRICE_MASK: constant(uint256) = 2**PRICE_SIZE - 1
@external
def deploy_pool(
_name: String[64],
_symbol: String[32],
_coins: address[N_COINS],
_weth: address,
implementation_id: uint256,
A: uint256,
gamma: uint256,
mid_fee: uint256,
out_fee: uint256,
fee_gamma: uint256,
allowed_extra_profit: uint256,
adjustment_step: uint256,
ma_exp_time: uint256,
initial_prices: uint256[N_COINS-1],
) -> address:
"""
@notice Deploy a new pool
@param _name Name of the new plain pool
@param _symbol Symbol for the new plain pool - will be concatenated with factory symbol
@return Address of the deployed pool
"""
pool_implementation: address = self.pool_implementations[implementation_id]
assert pool_implementation != empty(address), "Pool implementation not set"
# Validate parameters
assert A > MIN_A-1
assert A < MAX_A+1
assert gamma > MIN_GAMMA-1
assert gamma < MAX_GAMMA+1
assert mid_fee < MAX_FEE-1 # mid_fee can be zero
assert out_fee >= mid_fee
assert out_fee < MAX_FEE-1
assert fee_gamma < 10**18+1
assert fee_gamma > 0
assert allowed_extra_profit < 10**18+1
assert adjustment_step < 10**18+1
assert adjustment_step > 0
assert ma_exp_time < 872542 # 7 * 24 * 60 * 60 / ln(2)
assert ma_exp_time > 86 # 60 / ln(2)
assert min(initial_prices[0], initial_prices[1]) > 10**6
assert max(initial_prices[0], initial_prices[1]) < 10**30
assert _coins[0] != _coins[1] and _coins[1] != _coins[2] and _coins[0] != _coins[2], "Duplicate coins"
decimals: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
for i in range(N_COINS):
d: uint256 = ERC20(_coins[i]).decimals()
assert d < 19, "Max 18 decimals for coins"
decimals[i] = d
precisions[i] = 10**(18 - d)
# pack precisions
packed_precisions: uint256 = self._pack(precisions)
# pack fees
packed_fee_params: uint256 = self._pack(
[mid_fee, out_fee, fee_gamma]
)
# pack liquidity rebalancing params
packed_rebalancing_params: uint256 = self._pack(
[allowed_extra_profit, adjustment_step, ma_exp_time]
)
# pack A_gamma
packed_A_gamma: uint256 = A << 128
packed_A_gamma = packed_A_gamma | gamma
# pack initial prices
packed_prices: uint256 = 0
for k in range(N_COINS - 1):
packed_prices = packed_prices << PRICE_SIZE
p: uint256 = initial_prices[N_COINS - 2 - k]
assert p < PRICE_MASK
packed_prices = p | packed_prices
# pool is an ERC20 implementation
_salt: bytes32 = block.prevhash
_math_implementation: address = self.math_implementation
pool: address = create_from_blueprint(
pool_implementation,
_name,
_symbol,
_coins,
_math_implementation,
_weth,
_salt,
packed_precisions,
packed_A_gamma,
packed_fee_params,
packed_rebalancing_params,
packed_prices,
code_offset=3
)
# populate pool data
length: uint256 = self.pool_count
self.pool_list[length] = pool
self.pool_count = length + 1
self.pool_data[pool].decimals = decimals
self.pool_data[pool].coins = _coins
# add coins to market:
self._add_coins_to_market(_coins[0], _coins[1], pool)
self._add_coins_to_market(_coins[0], _coins[2], pool)
self._add_coins_to_market(_coins[1], _coins[2], pool)
log TricryptoPoolDeployed(
pool,
_name,
_symbol,
_weth,
_coins,
_math_implementation,
_salt,
packed_precisions,
packed_A_gamma,
packed_fee_params,
packed_rebalancing_params,
packed_prices,
msg.sender,
)
return pool
```
```shell
>>> TricryptoFactory.deploy_pool(
_name: crv/weth/tbtc tripool,
_symbol: crv-weth-tbtc,
_coins: '0xD533a949740bb3306d119CC777fa900bA034cd52', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', '0x8dAEBADE922dF735c38C80C7eBD708Af50815fAa',
_weth: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
implementation_id: 0,
A: 2700000,
gamma: 1300000000000,
mid_fee: 2999999,
out_fee: 80000000,
fee_gamma: 350000000000000,
allowed_extra_profit: 100000000000,
adjustment_step: 100000000000,
ma_exp_time: 600,
initial_prices: todo,
)
'returns address of the deployed pool'
```
::::
---
## Deploying Gauges
:::info
Liquidity gauges can only be successfully deployed from the same contract from which the pool was deployed!
:::
### `deploy_gauge`
::::description[`deploy_gauge(_pool: address) -> address`]
Deploy a liquidity gauge for a factory pool. The deployed gauge implementation is based on what the factory admin has set for `gauge_implementation`.
| Input | Type | Description |
| -------- | --------- | ------------------------------------ |
| `_pool` | `address` | Pool address to deploy a gauge for |
```vyper
@external
def deploy_gauge(_pool: address) -> address:
"""
@notice Deploy a liquidity gauge for a factory pool
@param _pool Factory pool address to deploy a gauge for
@return Address of the deployed gauge
"""
assert self.pool_data[_pool].coins[0] != ZERO_ADDRESS, "Unknown pool"
assert self.pool_data[_pool].liquidity_gauge == ZERO_ADDRESS, "Gauge already deployed"
implementation: address = self.gauge_implementation
assert implementation != ZERO_ADDRESS, "Gauge implementation not set"
gauge: address = create_forwarder_to(implementation)
LiquidityGauge(gauge).initialize(_pool)
self.pool_data[_pool].liquidity_gauge = gauge
log LiquidityGaugeDeployed(_pool, gauge)
return gauge
```
```shell
>>> Factory.deploy_gauge('0x...')
'returns address of the deployed gauge'
```
::::
---
## Fee Receiver
### `fee_receiver`
::::description[`Factory.fee_receiver() -> address: view`]
Getter for the fee receiver.
Returns: fee receiver (`address`).
```vyper
fee_receiver: public(address)
```
```shell
>>> Factory.fee_receiver()
'0xeCb456EA5365865EbAb8a2661B0c503410e9B347'
```
::::
### `set_fee_receiver`
::::description[`Factory.set_fee_receiver(_fee_receiver: address):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new `fee_receiver` address.
| Input | Type | Description |
| ----------- | -------| ----|
| `_fee_receiver` | `address` | new fee receiver address |
Emits event: `UpdateFeeReceiver`
```vyper
event UpdateFeeReceiver:
_old_fee_receiver: address
_new_fee_receiver: address
admin: public(address)
fee_receiver: public(address)
@external
def set_fee_receiver(_fee_receiver: address):
"""
@notice Set fee receiver
@param _fee_receiver Address that fees are sent to
"""
assert msg.sender == self.admin, "dev: admin only"
log UpdateFeeReceiver(self.fee_receiver, _fee_receiver)
self.fee_receiver = _fee_receiver
```
```shell
>>> Factory.set_fee_receiver("todo")
'todo'
```
::::
---
## Pool Factory: Deployer API
### `deploy_pool`
::::description[`Factory.deploy_pool(_name: String[64], _symbol: String[32], _coins: address[N_COINS], implementation_id: uint256, A: uint256, gamma: uint256, mid_fee: uint256, out_fee: uint256, fee_gamma: uint256, allowed_extra_profit: uint256, adjustment_step: uint256, ma_exp_time: uint256, initial_price: uint256) -> address:`]
Function to deploy a Twocrypto-NG liquidity pool.
Returns: deployed pool (`address`).
Emits: `TwocryptoPoolDeployed`
| Input | Type | Description |
|-----------------------|---------------------|----------------------------------------------------|
| `_name` | `String[64]` | Pool name |
| `_symbol` | `String[32]` | Pool symbol |
| `_coins` | `address[N_COINS]` | Coins |
| `implementation_id` | `uint256` | Implementation index of `Factory.poolImplementations()` |
| `A` | `uint256` | Amplification Coefficient |
| `gamma` | `uint256` | Gamma |
| `mid_fee` | `uint256` | Mid Fee |
| `out_fee` | `uint256` | Out Fee |
| `fee_gamma` | `uint256` | Fee Gamma |
| `allowed_extra_profit`| `uint256` | Allowed Extra Profit |
| `adjustment_step` | `uint256` | Adjustment Step |
| `ma_exp_time` | `uint256` | Moving Average Time Period |
| `initial_price` | `uint256` | Initial Prices |
*Limitations when deploying liquidity pools:*
- pool and math implementation must not be empty
- no duplicate coins
- maximum 18 decimal coins
| Parameter | Limitation |
| -------------------- | ---------------------------------------------------- |
| `mid_fee` | mid_fee < MAX_FEE - 1; mid_fee can be 0 |
| `out_fee` | mid_fee <= out_fee < MAX_FEE - 1 |
| `fee_gamma` | 0 < fee_gamma < 10^18 + 1 |
| `allowed_extra_profit` | allowed_extra_profit < 10^18 + 1 |
| `adjustment_step` | 0 < adjustment_step < 10^18 + 1 |
| `ma_exp_time` | 86 < ma_exp_time < 872542 |
| `initial_prices` | 10^6 < initial_prices[0] and initial_prices[1] < 10^30 |
```vyper
event TwocryptoPoolDeployed:
pool: address
name: String[64]
symbol: String[32]
coins: address[N_COINS]
math: address
salt: bytes32
precisions: uint256[N_COINS]
packed_A_gamma: uint256
packed_fee_params: uint256
packed_rebalancing_params: uint256
packed_prices: uint256
deployer: address
@external
def deploy_pool(
_name: String[64],
_symbol: String[32],
_coins: address[N_COINS],
implementation_id: uint256,
A: uint256,
gamma: uint256,
mid_fee: uint256,
out_fee: uint256,
fee_gamma: uint256,
allowed_extra_profit: uint256,
adjustment_step: uint256,
ma_exp_time: uint256,
initial_price: uint256,
) -> address:
"""
@notice Deploy a new pool
@param _name Name of the new plain pool
@param _symbol Symbol for the new plain pool - will be concatenated with factory symbol
@return Address of the deployed pool
"""
pool_implementation: address = self.pool_implementations[implementation_id]
_math_implementation: address = self.math_implementation
assert pool_implementation != empty(address), "Pool implementation not set"
assert _math_implementation != empty(address), "Math implementation not set"
assert mid_fee < MAX_FEE-1 # mid_fee can be zero
assert out_fee >= mid_fee
assert out_fee < MAX_FEE-1
assert fee_gamma < 10**18+1
assert fee_gamma > 0
assert allowed_extra_profit < 10**18+1
assert adjustment_step < 10**18+1
assert adjustment_step > 0
assert ma_exp_time < 872542 # 7 * 24 * 60 * 60 / ln(2)
assert ma_exp_time > 86 # 60 / ln(2)
assert initial_price > 10**6 and initial_price < 10**30 # dev: initial price out of bound
assert _coins[0] != _coins[1], "Duplicate coins"
decimals: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
for i in range(N_COINS):
d: uint256 = ERC20(_coins[i]).decimals()
assert d < 19, "Max 18 decimals for coins"
decimals[i] = d
precisions[i] = 10 **(18 - d)
# pack precision
packed_precisions: uint256 = self._pack_2(precisions[0], precisions[1])
# pack fees
packed_fee_params: uint256 = self._pack_3(
[mid_fee, out_fee, fee_gamma]
)
# pack liquidity rebalancing params
packed_rebalancing_params: uint256 = self._pack_3(
[allowed_extra_profit, adjustment_step, ma_exp_time]
)
# pack gamma and A
packed_gamma_A: uint256 = self._pack_2(gamma, A)
# pool is an ERC20 implementation
_salt: bytes32 = block.prevhash
pool: address = create_from_blueprint(
pool_implementation, # blueprint: address
_name, # String[64]
_symbol, # String[32]
_coins, # address[N_COINS]
_math_implementation, # address
_salt, # bytes32
packed_precisions, # uint256
packed_gamma_A, # uint256
packed_fee_params, # uint256
packed_rebalancing_params, # uint256
initial_price, # uint256
code_offset=3,
)
# populate pool data
self.pool_list.append(pool)
self.pool_data[pool].decimals = decimals
self.pool_data[pool].coins = _coins
self.pool_data[pool].implementation = pool_implementation
# add coins to market:
self._add_coins_to_market(_coins[0], _coins[1], pool)
log TwocryptoPoolDeployed(
pool,
_name,
_symbol,
_coins,
_math_implementation,
_salt,
precisions,
packed_gamma_A,
packed_fee_params,
packed_rebalancing_params,
initial_price,
msg.sender,
)
return pool
```
```shell
>>> Factory.deploy_pool(
_name: CRV/ETH,
_symbol: crv-eth,
_coins: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', '0xD533a949740bb3306d119CC777fa900bA034cd52',
implementation_id: 0,
A: 2700000,
gamma: 1300000000000,
mid_fee: 2999999,
out_fee: 80000000,
fee_gamma: 350000000000000,
allowed_extra_profit: 100000000000,
adjustment_step: 100000000000,
ma_exp_time: 600,
initial_prices: 0.00023684735380012821,
)
'returns address of the deployed pool'
```
::::
### `deploy_gauge`
::::description[`Factory.deploy_gauge(_pool: address) -> address:`]
:::warning
Deploying a liquidity gauge through the Factory is only possible on Ethereum Mainnet. Gauge deployments on sidechains must be done via the [`RootChainGaugeFactory`](../../../gauges/xchain-gauges/root-gauge-factory.md).
:::
Function to deploy a liquidity gauge on Ethereum mainnet. This function can only be used on pools deployed from this Factory contract.
Returns: deployed gauge (`address`).
Emits: `LiquidityGaugeDeployed`
| Input | Type | Description |
| ---------- | --------- | ----------- |
| `_pool` | `address` | Pool to deploy a gauge for |
```vyper
event LiquidityGaugeDeployed:
pool: address
gauge: address
@external
def deploy_gauge(_pool: address) -> address:
"""
@notice Deploy a liquidity gauge for a factory pool
@param _pool Factory pool address to deploy a gauge for
@return Address of the deployed gauge
"""
assert self.pool_data[_pool].coins[0] != empty(address), "Unknown pool"
assert self.pool_data[_pool].liquidity_gauge == empty(address), "Gauge already deployed"
assert self.gauge_implementation != empty(address), "Gauge implementation not set"
gauge: address = create_from_blueprint(self.gauge_implementation, _pool, code_offset=3)
self.pool_data[_pool].liquidity_gauge = gauge
log LiquidityGaugeDeployed(_pool, gauge)
return gauge
```
```shell
>>> Factory.deploy_gauge('pool address')
'returns address of the deployed gauge'
```
::::
---
## Pool Factory: Overview(Twocrypto-ng)
The Twocrypto-NG Factory allows the permissionless deployment of two-coin volatile asset pools, as well as gauges. **The liquidity pool and LP token share the same contract.**Additionally, the Factory contract is the direct admin and fee receiver of all pools. In turn, the Factory is controlled by the CurveDAO.
:::deploy[Contract Source & Deployment]
Source code for the Factory is available on [Github](https://github.com/curvefi/twocrypto-ng/blob/main/contracts/main/CurveTwocryptoFactory.vy).
A full list of all deployments can be found [here](../../../deployments.md).
:::
---
## Implementations
The Twocrypto-NG Factory makes use of **blueprint contracts**([EIP-5202](https://eips.ethereum.org/EIPS/eip-5202)) to deploy liquidity pools and gauges.
:::warning
**Implementation contracts are upgradable.**They can either be replaced, or additional implementation contracts can be added. Therefore, always make sure to check the most recent ones.
:::
It utilizes four different implementations:
- **`pool_implementations`**, containing multiple blueprint contracts that are used to deploy the pools.
- **`gauge_implementation`**, containing a blueprint contract that is used when deploying gauges for pools. This is only available on Ethereum Mainnet.
- **`views_implementation`**, containing a view methods contract relevant for integrators and users looking to interact with the AMMs.
- **`math_implementation`**, containing math functions used in the AMM.
*More on the [**Math Implementation**](../../twocrypto-ng/utility-contracts/math.md) and [**Views Implementation**](../../twocrypto-ng/utility-contracts/views.md).*
## Query Implementations
### `pool_implementations`
::::description[`Factory.pool_implementations(arg0: uint256) -> address: view`]
Getter for the pool implementation at index `arg0`.
| Input | Type | Description |
| ------ | --------- | -------------------------- |
| `arg0` | `uint256` | Index of pool implementation |
Returns: Pool implementation (`address`).
```vyper
pool_implementations: public(HashMap[uint256, address])
```
```shell
>>> Factory.pool_implementations(0)
'0x04Fd6beC7D45EFA99a27D29FB94b55c56dD07223'
```
::::
### `gauge_implementation`
::::description[`Factory.gauge_implementation() -> address: view`]
Getter for the current gauge implementation. Only Ethereum mainnet has a valid gauge implementation; on other chains, the implementation is set to `ZERO_ADDRESS`, as sidechain gauges need to be deployed via the `RootChainGaugeFactory`.
Returns: Gauge implementation (`address`).
```vyper
gauge_implementation: public(address)
```
```shell
>>> Factory.gauge_implementation()
'0x38D9BdA812da2C68dFC6aDE85A7F7a54E77F8325'
```
::::
### `views_implementation`
::::description[`Factory.views_implementation() -> address: view`]
Getter for the current views contract implementation.
Returns: Views contract implementation (`address`).
```vyper
views_implementation: public(address)
```
```shell
>>> Factory.views_implementation()
'0x07CdEBF81977E111B08C126DEFA07818d0045b80'
```
::::
### `math_implementation`
::::description[`Factory.math_implementation() -> address: view`]
Getter for the current math contract implementation.
Returns: Math contract implementation (`address`).
```vyper
math_implementation: public(address)
```
```shell
>>> Factory.math_implementation()
'0x2005995a71243be9FB995DaB4742327dc76564Df'
```
::::
## Set New Implementations
*New implementations can be set via the following admin-only functions:*
### `set_pool_implementation`
::::description[`Factory.set_pool_implementation(_pool_implementation: address, _implementation_index: uint256):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new pool implementation at a certain index. The Factory allows multiple pool implementations as some pools might require a different one.
| Input | Type | Description |
| ------------------------ | --------- | ----------------------------- |
| `_pool_implementation` | `address` | New pool implementation |
| `_implementation_index` | `uint256` | Index for the implementation |
Emits: `UpdatePoolImplementation`
```vyper
event UpdatePoolImplementation:
_implemention_id: uint256
_old_pool_implementation: address
_new_pool_implementation: address
pool_implementations: public(HashMap[uint256, address])
@external
def set_pool_implementation(
_pool_implementation: address, _implementation_index: uint256
):
"""
@notice Set pool implementation
@dev Set to empty(address) to prevent deployment of new pools
@param _pool_implementation Address of the new pool implementation
@param _implementation_index Index of the pool implementation
"""
assert msg.sender == self.admin, "dev: admin only"
log UpdatePoolImplementation(
_implementation_index,
self.pool_implementations[_implementation_index],
_pool_implementation
)
self.pool_implementations[_implementation_index] = _pool_implementation
```
```shell
>>> soon
```
::::
### `set_gauge_implementation`
::::description[`Factory.set_gauge_implementation(_gauge_implementation: address):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new gauge implementation (blueprint contract). This implementation is only available on Ethereum mainnet. To deploy a gauge on a sidechain, this needs to be done through the `RootChainGaugeFactory`.
| Input | Type | Description |
| -------------------------- | --------- | -------------------------- |
| `_gauge_implementation` | `address` | New gauge implementation |
Emits: `UpdateGaugeImplementation`
```vyper
event UpdateGaugeImplementation:
_old_gauge_implementation: address
_new_gauge_implementation: address
gauge_implementation: public(address)
@external
def set_gauge_implementation(_gauge_implementation: address):
"""
@notice Set gauge implementation
@dev Set to empty(address) to prevent deployment of new gauges
@param _gauge_implementation Address of the new token implementation
"""
assert msg.sender == self.admin, "dev: admin only"
log UpdateGaugeImplementation(self.gauge_implementation, _gauge_implementation)
self.gauge_implementation = _gauge_implementation
```
```shell
>>> soon
```
::::
### `set_views_implementation`
::::description[`Factory.set_views_implementation(_views_implementation: address):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new views contract.
| Input | Type | Description |
| ----------------------- | --------- | -------------------------------------- |
| `_views_implementation` | `address` | New views contract implementation |
Emits: `UpdateViewsImplementation`
```vyper
event UpdateViewsImplementation:
_old_views_implementation: address
_new_views_implementation: address
views_implementation: public(address)
@external
def set_views_implementation(_views_implementation: address):
"""
@notice Set views contract implementation
@param _views_implementation Address of the new views contract
"""
assert msg.sender == self.admin, "dev: admin only"
log UpdateViewsImplementation(self.views_implementation, _views_implementation)
self.views_implementation = _views_implementation
```
```shell
>>> soon
```
::::
### `set_math_implementation`
::::description[`Factory.set_math_implementation(_math_implementation: address):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new math contract.
| Input | Type | Description |
| ----------------------- | --------- | ------------------------------------ |
| `_math_implementation` | `address` | New math contract implementation |
Emits: `UpdateMathImplementation`
```vyper
event UpdateMathImplementation:
_old_math_implementation: address
_new_math_implementation: address
math_implementation: public(address)
@external
def set_math_implementation(_math_implementation: address):
"""
@notice Set math implementation
@param _math_implementation Address of the new math contract
"""
assert msg.sender == self.admin, "dev: admin only"
log UpdateMathImplementation(self.math_implementation, _math_implementation)
self.math_implementation = _math_implementation
```
```shell
>>> soon
```
::::
## Fee Receiver
### `fee_receiver`
::::description[`Factory.fee_receiver() -> address: view`]
Getter for the fee receiver address of the admin fee. The fee receiver is initially set by calling the `initialize_ownership` function. It can later be changed via the `set_fee_receiver` method.
Returns: fee receiver (`address`).
```vyper
fee_receiver: public(address)
@external
def initialise_ownership(_fee_receiver: address, _admin: address):
assert msg.sender == self.deployer
assert self.admin == empty(address)
self.fee_receiver = _fee_receiver
self.admin = _admin
log UpdateFeeReceiver(empty(address), _fee_receiver)
log TransferOwnership(empty(address), _admin)
```
```shell
>>> Factory.fee_receiver()
'0xeCb456EA5365865EbAb8a2661B0c503410e9B347'
```
::::
### `set_fee_receiver`
::::description[`Factory.set_fee_receiver(_fee_receiver: address):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new fee receiver address.
| Input | Type | Description |
| ------------------ | --------- | ---------------------------- |
| `_fee_receiver` | `uint256` | New fee receiver address |
Emits: `UpdateFeeReceiver`
```vyper
event UpdateFeeReceiver:
_old_fee_receiver: address
_new_fee_receiver: address
fee_receiver: public(address)
@external
def set_fee_receiver(_fee_receiver: address):
"""
@notice Set fee receiver
@param _fee_receiver Address that fees are sent to
"""
assert msg.sender == self.admin, "dev: admin only"
log UpdateFeeReceiver(self.fee_receiver, _fee_receiver)
self.fee_receiver = _fee_receiver
```
```shell
>>> soon
```
::::
---
## Deploying Pools
### `deploy_pool`
::::description[`Factory.deploy_pool(_name: String[64], _symbol: String[32], _coins: address[N_COINS], implementation_id: uint256, A: uint256, gamma: uint256, mid_fee: uint256, out_fee: uint256, fee_gamma: uint256, allowed_extra_profit: uint256, adjustment_step: uint256, ma_exp_time: uint256, initial_price: uint256) -> address:`]
Function to deploy a Twocrypto-NG liquidity pool.
| Input | Type | Description |
|-----------------------|---------------------|----------------------------------------------------|
| `_name` | `String[64]` | Pool name |
| `_symbol` | `String[32]` | Pool symbol |
| `_coins` | `address[N_COINS]` | Coins |
| `implementation_id` | `uint256` | Implementation index of `Factory.poolImplementations()` |
| `A` | `uint256` | Amplification Coefficient |
| `gamma` | `uint256` | Gamma |
| `mid_fee` | `uint256` | Mid Fee |
| `out_fee` | `uint256` | Out Fee |
| `fee_gamma` | `uint256` | Fee Gamma |
| `allowed_extra_profit`| `uint256` | Allowed Extra Profit |
| `adjustment_step` | `uint256` | Adjustment Step |
| `ma_exp_time` | `uint256` | Moving Average Time Period |
| `initial_price` | `uint256` | Initial Prices |
Returns: deployed pool (`address`).
Emits: `TwocryptoPoolDeployed`
*Limitations when deploying liquidity pools:*
- pool and math implementation must not be empty
- no duplicate coins
- maximum 18 decimal coins
| Parameter | Limitation |
| -------------------- | ---------------------------------------------------- |
| `mid_fee` | mid_fee < MAX_FEE - 1; mid_fee can be 0 |
| `out_fee` | mid_fee <= out_fee < MAX_FEE - 1 |
| `fee_gamma` | 0 < fee_gamma < 10^18 + 1 |
| `allowed_extra_profit` | allowed_extra_profit < 10^18 + 1 |
| `adjustment_step` | 0 < adjustment_step < 10^18 + 1 |
| `ma_exp_time` | 86 < ma_exp_time < 872542 |
| `initial_prices` | 10^6 < initial_prices[0] and initial_prices[1] < 10^30 |
```vyper
event TwocryptoPoolDeployed:
pool: address
name: String[64]
symbol: String[32]
coins: address[N_COINS]
math: address
salt: bytes32
precisions: uint256[N_COINS]
packed_A_gamma: uint256
packed_fee_params: uint256
packed_rebalancing_params: uint256
packed_prices: uint256
deployer: address
@external
def deploy_pool(
_name: String[64],
_symbol: String[32],
_coins: address[N_COINS],
implementation_id: uint256,
A: uint256,
gamma: uint256,
mid_fee: uint256,
out_fee: uint256,
fee_gamma: uint256,
allowed_extra_profit: uint256,
adjustment_step: uint256,
ma_exp_time: uint256,
initial_price: uint256,
) -> address:
"""
@notice Deploy a new pool
@param _name Name of the new plain pool
@param _symbol Symbol for the new plain pool - will be concatenated with factory symbol
@return Address of the deployed pool
"""
pool_implementation: address = self.pool_implementations[implementation_id]
_math_implementation: address = self.math_implementation
assert pool_implementation != empty(address), "Pool implementation not set"
assert _math_implementation != empty(address), "Math implementation not set"
assert mid_fee < MAX_FEE-1 # mid_fee can be zero
assert out_fee >= mid_fee
assert out_fee < MAX_FEE-1
assert fee_gamma < 10**18+1
assert fee_gamma > 0
assert allowed_extra_profit < 10**18+1
assert adjustment_step < 10**18+1
assert adjustment_step > 0
assert ma_exp_time < 872542 # 7 * 24 * 60 * 60 / ln(2)
assert ma_exp_time > 86 # 60 / ln(2)
assert initial_price > 10**6 and initial_price < 10**30 # dev: initial price out of bound
assert _coins[0] != _coins[1], "Duplicate coins"
decimals: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
for i in range(N_COINS):
d: uint256 = ERC20(_coins[i]).decimals()
assert d < 19, "Max 18 decimals for coins"
decimals[i] = d
precisions[i] = 10 **(18 - d)
# pack precision
packed_precisions: uint256 = self._pack_2(precisions[0], precisions[1])
# pack fees
packed_fee_params: uint256 = self._pack_3(
[mid_fee, out_fee, fee_gamma]
)
# pack liquidity rebalancing params
packed_rebalancing_params: uint256 = self._pack_3(
[allowed_extra_profit, adjustment_step, ma_exp_time]
)
# pack gamma and A
packed_gamma_A: uint256 = self._pack_2(gamma, A)
# pool is an ERC20 implementation
_salt: bytes32 = block.prevhash
pool: address = create_from_blueprint(
pool_implementation, # blueprint: address
_name, # String[64]
_symbol, # String[32]
_coins, # address[N_COINS]
_math_implementation, # address
_salt, # bytes32
packed_precisions, # uint256
packed_gamma_A, # uint256
packed_fee_params, # uint256
packed_rebalancing_params, # uint256
initial_price, # uint256
code_offset=3,
)
# populate pool data
self.pool_list.append(pool)
self.pool_data[pool].decimals = decimals
self.pool_data[pool].coins = _coins
self.pool_data[pool].implementation = pool_implementation
# add coins to market:
self._add_coins_to_market(_coins[0], _coins[1], pool)
log TwocryptoPoolDeployed(
pool,
_name,
_symbol,
_coins,
_math_implementation,
_salt,
precisions,
packed_gamma_A,
packed_fee_params,
packed_rebalancing_params,
initial_price,
msg.sender,
)
return pool
```
```shell
>>> Factory.deploy_pool(
_name: CRV/ETH,
_symbol: crv-eth,
_coins: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', '0xD533a949740bb3306d119CC777fa900bA034cd52',
implementation_id: 0,
A: 2700000,
gamma: 1300000000000,
mid_fee: 2999999,
out_fee: 80000000,
fee_gamma: 350000000000000,
allowed_extra_profit: 100000000000,
adjustment_step: 100000000000,
ma_exp_time: 600,
initial_prices: 0.00023684735380012821,
)
'returns address of the deployed pool'
```
::::
---
## Deploying Gauges
### `deploy_gauge`
::::description[`Factory.deploy_gauge(_pool: address) -> address:`]
:::warning
Deploying a liquidity gauge through the Factory is only possible on Ethereum Mainnet. Gauge deployments on sidechains must be done via the [`RootChainGaugeFactory`](../../../gauges/xchain-gauges/root-gauge-factory.md).
:::
Function to deploy a liquidity gauge on Ethereum mainnet. This function can only be used on pools deployed from this Factory contract.
| Input | Type | Description |
| ---------- | --------- | ----------- |
| `_pool` | `address` | Pool to deploy a gauge for |
Returns: deployed gauge (`address`).
Emits: `LiquidityGaugeDeployed`
```vyper
event LiquidityGaugeDeployed:
pool: address
gauge: address
@external
def deploy_gauge(_pool: address) -> address:
"""
@notice Deploy a liquidity gauge for a factory pool
@param _pool Factory pool address to deploy a gauge for
@return Address of the deployed gauge
"""
assert self.pool_data[_pool].coins[0] != empty(address), "Unknown pool"
assert self.pool_data[_pool].liquidity_gauge == empty(address), "Gauge already deployed"
assert self.gauge_implementation != empty(address), "Gauge implementation not set"
gauge: address = create_from_blueprint(self.gauge_implementation, _pool, code_offset=3)
self.pool_data[_pool].liquidity_gauge = gauge
log LiquidityGaugeDeployed(_pool, gauge)
return gauge
```
```shell
>>> Factory.deploy_gauge('pool address')
'returns address of the deployed gauge'
```
::::
---
## Lp Token V5
The LP token and exchange contract for two-coin CryptoSwap pools are two separate contracts from each other. Newer versions, like Tricrypto-NG, combine both the LP token and exchange contract into a single contract.
The LP token contract is created from the **`token_implementation`**using the [**`create_forwarder_to()`**](https://docs.vyperlang.org/en/stable/built-in-functions.html?highlight=create_forwarder_to#chain-interaction) function, which is a built-in function in Vyper.
After deployment, the LP token contract is then initialized through the **`initialize()`**function.
:::info
Newer deployments might make use of blueprint contracts ([EIP-5202](https://eips.ethereum.org/EIPS/eip-5202)), eliminating the need for an **`initialize()`**function.
:::
*To query the currently implemented LP token contract:*
```shell
>>> Factory.token_implementation()
'0xc08550A4cc5333f40e593eCc4C4724808085D304'
```
## LP Token Info Methods
### `name`
::::description[`LPTokenV5.name() -> String[64]: view`]
Getter for the name of the LP token.
Returns: name (`String[64]`).
```vyper
name: public(String[64])
@external
def initialize(_name: String[64], _symbol: String[32], _pool: address):
assert self.minter == ZERO_ADDRESS # dev: check that we call it from factory
self.name = _name
self.symbol = _symbol
self.minter = _pool
self.DOMAIN_SEPARATOR = keccak256(
_abi_encode(EIP712_TYPEHASH, keccak256(_name), keccak256(VERSION), chain.id, self)
)
# fire a transfer event so block explorers identify the contract as an ERC20
log Transfer(ZERO_ADDRESS, msg.sender, 0)
```
```shell
>>> LPTokenV5.name()
'Curve.fi Factory Crypto Pool: LDO/ETH'
```
::::
### `symbol`
::::description[`LPTokenV5.symbol() -> String[32]: view`]
Getter for the symbol of the LP token.
Returns: symbol (`String[32]`).
```vyper
symbol: public(String[32])
@external
def initialize(_name: String[64], _symbol: String[32], _pool: address):
assert self.minter == ZERO_ADDRESS # dev: check that we call it from factory
self.name = _name
self.symbol = _symbol
self.minter = _pool
self.DOMAIN_SEPARATOR = keccak256(
_abi_encode(EIP712_TYPEHASH, keccak256(_name), keccak256(VERSION), chain.id, self)
)
# fire a transfer event so block explorers identify the contract as an ERC20
log Transfer(ZERO_ADDRESS, msg.sender, 0)
```
```shell
>>> LPTokenV5.symbol()
'LDOETH-f'
```
::::
### `decimals`
::::description[`LPTokenV5.decimals() -> uint8`]
Getter for the decimals of the LP token.
Returns: decimals (`uint8`).
```vyper
@view
@external
def decimals() -> uint8:
"""
@notice Get the number of decimals for this token
@dev Implemented as a view method to reduce gas costs
@return uint8 decimal places
"""
return 18
```
```shell
>>> LPTokenV5.decimals()
18
```
::::
### `version`
::::description[`LPTokenV5.version() -> String[8]:`]
Getter for the version of the LP token.
Returns: version (`String[8]`).
```vyper
VERSION: constant(String[8]) = "v5.0.0"
@view
@external
def version() -> String[8]:
"""
@notice Get the version of this token contract
"""
return VERSION
```
```shell
>>> LPTokenV5.version()
'v5.0.0'
```
::::
### `balanceOf`
::::description[`LPTokenV5.balanceOf(arg0: address) -> uint256: view`]
Getter for the LP token balance of an address.
Returns: token balance (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `address` | Address to get the balance for |
```vyper
balanceOf: public(HashMap[address, uint256])
```
```shell
>>> LPTokenV5.balanceOf("0xe5d5Aa1Bbe72F68dF42432813485cA1Fc998DE32")
74284034901658384235023
```
::::
### `totalSupply`
::::description[`LPTokenV5.totalSupply() -> uint256: view`]
Getter for the total supply of the LP token.
Returns: total supply (`uint256`).
```vyper
totalSupply: public(uint256)
```
```shell
>>> LPTokenV5.totalSupply()
74357443715423884544842
```
::::
### `minter`
::::description[`LPTokenV5.totalSupply() -> uint256: view`]
Getter for the minter contract of the LP token. Minter contract address is the liquidity pool itself.
Returns: minter (`address`).
```vyper
minter: public(address)
@external
def __init__():
self.minter = 0x0000000000000000000000000000000000000001
@external
def initialize(_name: String[64], _symbol: String[32], _pool: address):
assert self.minter == ZERO_ADDRESS # dev: check that we call it from factory
self.name = _name
self.symbol = _symbol
self.minter = _pool
self.DOMAIN_SEPARATOR = keccak256(
_abi_encode(EIP712_TYPEHASH, keccak256(_name), keccak256(VERSION), chain.id, self)
)
# fire a transfer event so block explorers identify the contract as an ERC20
log Transfer(ZERO_ADDRESS, msg.sender, 0)
```
```shell
>>> LPTokenV5.minter()
'0x9409280DC1e6D33AB7A8C6EC03e5763FB61772B5'
```
::::
## Allowance and Transfer Methods
### `transfer`
::::description[`LPTokenV5.transfer(_to: address, _value: uint256) -> bool`]
Function to transfer `_value` token from `msg.sender` to `_to`.
Returns: True (`bool`).
Emits: `Transfer`
| Input | Type | Description |
| ----------- | -------| ----|
| `_to` | `address` | Address to transfer to |
| `_value` | `uint256` | Amount to transfer |
```vyper
event Transfer:
_from: indexed(address)
_to: indexed(address)
_value: uint256
@external
def transfer(_to: address, _value: uint256) -> bool:
"""
@dev Transfer token for a specified address
@param _to The address to transfer to.
@param _value The amount to be transferred.
"""
# NOTE: vyper does not allow underflows
# so the following subtraction would revert on insufficient balance
self.balanceOf[msg.sender] -= _value
self.balanceOf[_to] += _value
log Transfer(msg.sender, _to, _value)
return True
```
```shell
>>> LPToken.transfer("0xbabe61887f1de2713c6f97e567623453d3C79f67", 100)
True
```
::::
### `transferFrom`
::::description[`LPTokenV5.transfer(_to: address, _value: uin256) -> bool:`]
Function to transfer `_value` token from `msg.sender` to `_to`. Needs [`allowance`](#allowance) to successfully transfer on behalf of someone else.
Returns: True or False (`bool`).
Emits: `Transfer`
| Input | Type | Description |
| ----------- | -------| ----|
| `_to` | `address` | Address to transfer to |
| `_value` | `uint256` | Amount to transfer |
```vyper
event Transfer:
_from: indexed(address)
_to: indexed(address)
_value: uint256
@external
def transferFrom(_from: address, _to: address, _value: uint256) -> bool:
"""
@dev Transfer tokens from one address to another.
@param _from address The address which you want to send tokens from
@param _to address The address which you want to transfer to
@param _value uint256 the amount of tokens to be transferred
"""
self.balanceOf[_from] -= _value
self.balanceOf[_to] += _value
_allowance: uint256 = self.allowance[_from][msg.sender]
if _allowance != MAX_UINT256:
self.allowance[_from][msg.sender] = _allowance - _value
log Transfer(_from, _to, _value)
return True
```
```shell
>>> LPToken.transferFrom("0xbabe61887f1de2713c6f97e567623453d3C79f67", 100)
True
```
::::
### `approve`
::::description[`LPTokenV5.approve(_spender: address, _value: uint256) -> bool:`]
Function to approve `_spender` to transfer `_value` on behalf of msg.sender.
Returns: True (`bool`).
Emits: `Approval`
| Input | Type | Description |
| ----------- | -------| ----|
| `_spender` | `address` | Address approved to spend funds |
| `_value` | `uint256` | Amount of tokens allowed to spend |
```vyper
event Approval:
_owner: indexed(address)
_spender: indexed(address)
_value: uint256
@external
def approve(_spender: address, _value: uint256) -> bool:
"""
@notice Approve the passed address to transfer the specified amount of
tokens on behalf of msg.sender
@dev Beware that changing an allowance via this method brings the risk
that someone may use both the old and new allowance by unfortunate
transaction ordering. This may be mitigated with the use of
{increaseAllowance} and {decreaseAllowance}.
https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
@param _spender The address which will transfer the funds
@param _value The amount of tokens that may be transferred
@return bool success
"""
self.allowance[msg.sender][_spender] = _value
log Approval(msg.sender, _spender, _value)
return True
```
```shell
>>> LPToken.approve("todo")
"todo"
```
::::
### `permit`
::::description[`LPTokenV5.permit(_owner: address, _spender: address, _value: uint256, _deadline: uint256, _v: uint8, _r: bytes32, _s: bytes32) -> bool::`]
Function to approve the spender by the owner's signature to expend the owner's tokens.
Returns: True (`bool`).
Emits: `Approval`
| Input | Type | Description |
| ----------- | -------| ----|
| `_to` | `address` | Address to transfer to |
| `_value` | `uint256` | Amount to transfer |
```vyper
event Approval:
_owner: indexed(address)
_spender: indexed(address)
_value: uint256
@external
def permit(
_owner: address,
_spender: address,
_value: uint256,
_deadline: uint256,
_v: uint8,
_r: bytes32,
_s: bytes32
) -> bool:
"""
@notice Approves spender by owner's signature to expend owner's tokens.
See https://eips.ethereum.org/EIPS/eip-2612.
@dev Inspired by https://github.com/yearn/yearn-vaults/blob/main/contracts/Vault.vy#L753-L793
@dev Supports smart contract wallets which implement ERC1271
https://eips.ethereum.org/EIPS/eip-1271
@param _owner The address which is a source of funds and has signed the Permit.
@param _spender The address which is allowed to spend the funds.
@param _value The amount of tokens to be spent.
@param _deadline The timestamp after which the Permit is no longer valid.
@param _v The bytes[64] of the valid secp256k1 signature of permit by owner
@param _r The bytes[0:32] of the valid secp256k1 signature of permit by owner
@param _s The bytes[32:64] of the valid secp256k1 signature of permit by owner
@return True, if transaction completes successfully
"""
assert _owner != ZERO_ADDRESS
assert block.timestamp <= _deadline
nonce: uint256 = self.nonces[_owner]
digest: bytes32 = keccak256(
concat(
b"\x19\x01",
self.DOMAIN_SEPARATOR,
keccak256(_abi_encode(PERMIT_TYPEHASH, _owner, _spender, _value, nonce, _deadline))
)
)
if _owner.is_contract:
sig: Bytes[65] = concat(_abi_encode(_r, _s), slice(convert(_v, bytes32), 31, 1))
# reentrancy not a concern since this is a staticcall
assert ERC1271(_owner).isValidSignature(digest, sig) == ERC1271_MAGIC_VAL
else:
assert ecrecover(digest, convert(_v, uint256), convert(_r, uint256), convert(_s, uint256)) == _owner
self.allowance[_owner][_spender] = _value
self.nonces[_owner] = nonce + 1
log Approval(_owner, _spender, _value)
return True
```
```shell
>>> LPTokenV5.permit()
"todo"
```
::::
### `allowance`
::::description[`LPTokenV5.allowance(arg0: address, arg1: address) -> uint256: view`]
Getter method to check the allowance of `arg0` for funds of `arg1`.
Returns: allowed amount (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `address` | Address of the spender |
| `arg0` | `address` | Address to the token owner |
```vyper
allowance: public(HashMap[address, HashMap[address, uint256]])
```
```shell
>>> LPTokenV5.allowance("todo")
"todo"
```
::::
### `increaseAllowance`
::::description[`LPTokenV5.increaseAllowance(_spender: address, _added_value: uint256) -> bool:`]
Function to increase the allowance granted to `_spender`.
Returns: True or False (`bool`).
Emits: `Approval`
| Input | Type | Description |
| ----------- | -------| ----|
| `_spender` | `address` | Address to increase the allowance of |
| `_added_value` | `uint256` | Amount ot increase the allowance by |
```vyper
event Approval:
_owner: indexed(address)
_spender: indexed(address)
_value: uint256
allowance: public(HashMap[address, HashMap[address, uint256]])
@external
def increaseAllowance(_spender: address, _added_value: uint256) -> bool:
"""
@notice Increase the allowance granted to `_spender` by the caller
@dev This is alternative to {approve} that can be used as a mitigation for
the potential race condition
@param _spender The address which will transfer the funds
@param _added_value The amount of to increase the allowance
@return bool success
"""
allowance: uint256 = self.allowance[msg.sender][_spender] + _added_value
self.allowance[msg.sender][_spender] = allowance
log Approval(msg.sender, _spender, allowance)
return True
```
```shell
>>> LPTokenV5.increaseAllowance("todo")
"todo"
```
::::
### `decreaseAllowance`
::::description[`LPTokenV5.decreaseAllowance(_spender: address, _subtracted_value: uint256) -> bool:`]
Function to decrease the allowance granted to `_spender`.
Returns: True or False (`bool`).
| Input | Type | Description |
| ----------- | -------| ----|
| `_spender` | `address` | Address to decrease the allowance of |
| `_subtracted_value` | `uint256` | Amount ot decrease the allowance by |
```vyper
event Approval:
_owner: indexed(address)
_spender: indexed(address)
_value: uint256
allowance: public(HashMap[address, HashMap[address, uint256]])
@external
def decreaseAllowance(_spender: address, _subtracted_value: uint256) -> bool:
"""
@notice Decrease the allowance granted to `_spender` by the caller
@dev This is alternative to {approve} that can be used as a mitigation for
the potential race condition
@param _spender The address which will transfer the funds
@param _subtracted_value The amount of to decrease the allowance
@return bool success
"""
allowance: uint256 = self.allowance[msg.sender][_spender] - _subtracted_value
self.allowance[msg.sender][_spender] = allowance
log Approval(msg.sender, _spender, allowance)
return True
```
```shell
>>> LPTokenV5.allowance("todo")
"todo"
```
::::
## Minting and Burning
LP Tokens are minted when users deposit funds into the liquidity pool. Upon calling the `add_liquidity` function on the pool, it triggers the `mint` function of the LP Token to mint the corresponding tokens.
When liquidity is withdrawn using `remove_liquidity`, the LP tokens are burned through the `burnFrom` method.
The logic for both minting and burning the tokens resides in the pool contract.
### `mint`
::::description[`LPTokenV5.mint(_to: address, _value: uint256) -> bool:`]
:::guard[Guarded Method]
This function is only callable by the `minter` of the contract, which is the liquidity pool.
:::
Function to mint `_value` LP Tokens and transfer them to `_to`.
Returns: True (`bool`).
Emits: `Transfer`
| Input | Type | Description |
| ----------- | -------| ----|
| `_spender` | `address` | Address to decrease the allowance of |
| `_subtracted_value` | `uint256` | Amount ot decrease the allowance by |
```vyper
event Transfer:
_from: indexed(address)
_to: indexed(address)
_value: uint256
@external
def mint(_to: address, _value: uint256) -> bool:
"""
@dev Mint an amount of the token and assigns it to an account.
This encapsulates the modification of balances such that the
proper events are emitted.
@param _to The account that will receive the created tokens.
@param _value The amount that will be created.
"""
assert msg.sender == self.minter
self.totalSupply += _value
self.balanceOf[_to] += _value
log Transfer(ZERO_ADDRESS, _to, _value)
return True
```
```shell
>>> LPTokenV5.mint("todo")
"todo"
```
::::
### `burnFrom`
::::description[`LPTokenV5.burnFrom(_to: address, _value: uint256) -> bool:`]
Function to burn `_value` LP Tokens from `_to` and transfer them to `ZERO_ADDRESS`.
Returns: True (`bool`).
Emits: `Transfer`
| Input | Type | Description |
| ----------- | -------| ----|
| `_spender` | `address` | Address to decrease the allowance of |
| `_subtracted_value` | `uint256` | Amount ot decrease the allowance by |
```vyper
event Transfer:
_from: indexed(address)
_to: indexed(address)
_value: uint256
@external
def burnFrom(_to: address, _value: uint256) -> bool:
"""
@dev Burn an amount of the token from a given account.
@param _to The account whose tokens will be burned.
@param _value The amount that will be burned.
"""
assert msg.sender == self.minter
self.totalSupply -= _value
self.balanceOf[_to] -= _value
log Transfer(_to, ZERO_ADDRESS, _value)
return True
```
```shell
>>> LPTokenV5.burnFrom("todo")
"todo"
```
::::
## Initialize Method
### `initialize`
::::description[`LPTokenV5.initialize(_name: String[64], _symbol: String[32], _pool: address):`]
Function to initialize the LP Token and setting name (`_name`), symbol (`_symbol`) and the corresponding liquidity pool (`_pool`).
This function triggers a transfer event, enabling block explorers to recognize the contract as an ERC20.
Emits: `Transfer`
| Input | Type | Description |
| ----------- | -------| ----|
| `_name` | `String[64]` | name of the lp token |
| `_symbol` | `String[32]` | symbol of the lp token |
| `_pool` | `address` | liquidity pool address |
```vyper
event Transfer:
_from: indexed(address)
_to: indexed(address)
_value: uint256
@external
def initialize(_name: String[64], _symbol: String[32], _pool: address):
assert self.minter == ZERO_ADDRESS # dev: check that we call it from factory
self.name = _name
self.symbol = _symbol
self.minter = _pool
self.DOMAIN_SEPARATOR = keccak256(
_abi_encode(EIP712_TYPEHASH, keccak256(_name), keccak256(VERSION), chain.id, self)
)
# fire a transfer event so block explorers identify the contract as an ERC20
log Transfer(ZERO_ADDRESS, msg.sender, 0)
```
```shell
>>> LPTokenV5.initialize("todo")
```
::::
---
## Overview
In exchange for depositing coins into a Curve pool, liquidity providers receive pool LP (liquidity pool) tokens. A Curve pool LP token is an ERC20 contract specific to the Curve pool. Hence, LP tokens are transferrable. Holders of pool LP tokens may deposit and stake the token into a pool’s liquidity gauge in order to receive CRV token rewards.
:::info
There might be some variations of LP Tokens, but the two listed above are the most recent implementations.
For older Curve pools the token attribute **`version`**is not always public and a getter has not been explicitly implemented.
:::
---
## **The following functions are admin-only functions.**Pools created through the Factory are 'owned' by the factory **`admin`**(DAO). Ownership can be transferred only within the Factory contract, and this is done through the use of the **`commit_transfer_ownership()`**and **`accept_transfer_ownership()`**functions.
#
**The following functions are admin-only functions.**Pools created through the Factory are 'owned' by the factory **`admin`**(DAO). Ownership can be transferred only within the Factory contract, and this is done through the use of the **`commit_transfer_ownership()`**and **`accept_transfer_ownership()`**functions.
## Parameter Controls
More informations about the parameters [here](../../cryptoswap-overview.md).
The appropriate value for `A` and `gamma` is dependent upon the type of coin being used within the pool, and is subject to optimisation and pool-parameter update based on the market history of the trading pair. It is possible to modify the parameters for a pool after it has been deployed. However, it requires a vote within the Curve DAO and must reach a 15% quorum.
### `ramp_A_gamma`
::::description[`CryptoSwap.ramp_A_gamma(future_A: uint256, future_gamma: uint256, future_time: uint256):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the factory contract.
:::
Function to ramp A and gamma parameter values linearly.
Emits: `RampAgamma`
| Input | Type | Description |
| ----------- | -------| ----|
| `future_A` | `uint256` | future A value |
| `future_gamma` | `uint256` | future gamma value |
| `future_time` | `uint256` | timestamp at which the parameters are fully ramped |
```vyper
event RampAgamma:
initial_A: uint256
future_A: uint256
initial_gamma: uint256
future_gamma: uint256
initial_time: uint256
future_time: uint256
@external
def ramp_A_gamma(future_A: uint256, future_gamma: uint256, future_time: uint256):
assert msg.sender == Factory(self.factory).admin() # dev: only owner
assert block.timestamp > self.initial_A_gamma_time + (MIN_RAMP_TIME-1)
assert future_time > block.timestamp + (MIN_RAMP_TIME-1) # dev: insufficient time
A_gamma: uint256[2] = self._A_gamma()
initial_A_gamma: uint256 = shift(A_gamma[0], 128)
initial_A_gamma = bitwise_or(initial_A_gamma, A_gamma[1])
assert future_A > MIN_A-1
assert future_A < MAX_A+1
assert future_gamma > MIN_GAMMA-1
assert future_gamma < MAX_GAMMA+1
ratio: uint256 = 10**18 * future_A / A_gamma[0]
assert ratio < 10**18 * MAX_A_CHANGE + 1
assert ratio > 10**18 / MAX_A_CHANGE - 1
ratio = 10**18 * future_gamma / A_gamma[1]
assert ratio < 10**18 * MAX_A_CHANGE + 1
assert ratio > 10**18 / MAX_A_CHANGE - 1
self.initial_A_gamma = initial_A_gamma
self.initial_A_gamma_time = block.timestamp
future_A_gamma: uint256 = shift(future_A, 128)
future_A_gamma = bitwise_or(future_A_gamma, future_gamma)
self.future_A_gamma_time = future_time
self.future_A_gamma = future_A_gamma
log RampAgamma(A_gamma[0], future_A, A_gamma[1], future_gamma, block.timestamp, future_time)
```
```shell
>>> CryptoSwap.ramp_A_gamma(2700000, 1300000000000, 1693674492)
```
::::
### `stop_ramp_A_gamma`
::::description[`CryptoSwap.stop_ramp_A_gamma():`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the factory contract.
:::
Function to immediately stop ramping A and gamma parameters and set them to the current value.
Emits: `StopRampA`
```vyper
event StopRampA:
current_A: uint256
current_gamma: uint256
time: uint256
@external
def stop_ramp_A_gamma():
assert msg.sender == Factory(self.factory).admin() # dev: only owner
A_gamma: uint256[2] = self._A_gamma()
current_A_gamma: uint256 = shift(A_gamma[0], 128)
current_A_gamma = bitwise_or(current_A_gamma, A_gamma[1])
self.initial_A_gamma = current_A_gamma
self.future_A_gamma = current_A_gamma
self.initial_A_gamma_time = block.timestamp
self.future_A_gamma_time = block.timestamp
# now (block.timestamp < t1) is always False, so we return saved A
log StopRampA(A_gamma[0], A_gamma[1], block.timestamp)
```
```shell
>>> CryptoSwap.stop_ramp_A_gamma()
```
::::
### `commit_new_parameters`
::::description[`CryptoSwap.commit_new_parameters(_new_mid_fee: uint256, _new_out_fee: uint256, _new_fee_gamma: uint256, _new_allowed_extra_profit: uint256, _new_adjustment_step: uint256, _new_ma_time: uint256):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the factory contract.
:::
Function to commit new parameters. The new parameters do not take immedaite effect, they need to be applied first.
Emits: `CommitNewParameters`
| Input | Type | Description |
| ----------- | -------| ----|
| `_new_mid_fee` | `uint256` | new mid fee value |
| `_new_out_fee` | `uint256` | new out fee value |
| `_new_admin_fee` | `uint256` | new admin fee value |
| `_new_fee_gamma` | `uint256` | new fee-gamma value |
| `_new_allowed_extra_profit` | `uint256` | new allowed_extra_profit value |
| `_new_adjustment_step` | `uint256` |new adjustment_step value |
| `_new_ma_time` | `uint256` | new ma_time value |
```vyper
event CommitNewParameters:
deadline: indexed(uint256)
admin_fee: uint256
mid_fee: uint256
out_fee: uint256
fee_gamma: uint256
allowed_extra_profit: uint256
adjustment_step: uint256
ma_half_time: uint256
@external
def commit_new_parameters(
_new_mid_fee: uint256,
_new_out_fee: uint256,
_new_admin_fee: uint256,
_new_fee_gamma: uint256,
_new_allowed_extra_profit: uint256,
_new_adjustment_step: uint256,
_new_ma_half_time: uint256,
):
assert msg.sender == Factory(self.factory).admin() # dev: only owner
assert self.admin_actions_deadline == 0 # dev: active action
new_mid_fee: uint256 = _new_mid_fee
new_out_fee: uint256 = _new_out_fee
new_admin_fee: uint256 = _new_admin_fee
new_fee_gamma: uint256 = _new_fee_gamma
new_allowed_extra_profit: uint256 = _new_allowed_extra_profit
new_adjustment_step: uint256 = _new_adjustment_step
new_ma_half_time: uint256 = _new_ma_half_time
# Fees
if new_out_fee < MAX_FEE+1:
assert new_out_fee > MIN_FEE-1 # dev: fee is out of range
else:
new_out_fee = self.out_fee
if new_mid_fee > MAX_FEE:
new_mid_fee = self.mid_fee
assert new_mid_fee <= new_out_fee # dev: mid-fee is too high
if new_admin_fee > MAX_ADMIN_FEE:
new_admin_fee = self.admin_fee
# AMM parameters
if new_fee_gamma < 10**18:
assert new_fee_gamma > 0 # dev: fee_gamma out of range [1 .. 10**18]
else:
new_fee_gamma = self.fee_gamma
if new_allowed_extra_profit > 10**18:
new_allowed_extra_profit = self.allowed_extra_profit
if new_adjustment_step > 10**18:
new_adjustment_step = self.adjustment_step
# MA
if new_ma_half_time < 7*86400:
assert new_ma_half_time > 0 # dev: MA time should be longer than 1 second
else:
new_ma_half_time = self.ma_half_time
_deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY
self.admin_actions_deadline = _deadline
self.future_admin_fee = new_admin_fee
self.future_mid_fee = new_mid_fee
self.future_out_fee = new_out_fee
self.future_fee_gamma = new_fee_gamma
self.future_allowed_extra_profit = new_allowed_extra_profit
self.future_adjustment_step = new_adjustment_step
self.future_ma_half_time = new_ma_half_time
log CommitNewParameters(_deadline, new_admin_fee, new_mid_fee, new_out_fee,
new_fee_gamma,
new_allowed_extra_profit, new_adjustment_step,
new_ma_half_time)
```
```shell
>>> CryptoSwap.commit_new_parameters(20000000, 45000000, 50000000, 350000000000000, 100000000000, 100000000000, 1800)
```
::::
### `apply_new_parameters`
::::description[`CryptoSwap.apply_new_parameters()`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the factory contract.
:::
Function to apply the parameters from [`commit_new_parameters`](#commit_new_parameters).
Emits: `NewParameters`
```vyper
event NewParameters:
admin_fee: uint256
mid_fee: uint256
out_fee: uint256
fee_gamma: uint256
allowed_extra_profit: uint256
adjustment_step: uint256
ma_half_time: uint256
@external
@nonreentrant('lock')
def apply_new_parameters():
assert msg.sender == Factory(self.factory).admin() # dev: only owner
assert block.timestamp >= self.admin_actions_deadline # dev: insufficient time
assert self.admin_actions_deadline != 0 # dev: no active action
self.admin_actions_deadline = 0
admin_fee: uint256 = self.future_admin_fee
if self.admin_fee != admin_fee:
self._claim_admin_fees()
self.admin_fee = admin_fee
mid_fee: uint256 = self.future_mid_fee
self.mid_fee = mid_fee
out_fee: uint256 = self.future_out_fee
self.out_fee = out_fee
fee_gamma: uint256 = self.future_fee_gamma
self.fee_gamma = fee_gamma
allowed_extra_profit: uint256 = self.future_allowed_extra_profit
self.allowed_extra_profit = allowed_extra_profit
adjustment_step: uint256 = self.future_adjustment_step
self.adjustment_step = adjustment_step
ma_half_time: uint256 = self.future_ma_half_time
self.ma_half_time = ma_half_time
log NewParameters(admin_fee, mid_fee, out_fee,
fee_gamma,
allowed_extra_profit, adjustment_step,
ma_half_time)
```
```shell
>>> CryptoSwap.apply_new_parameters()
```
::::
### `revert_new_parameters`
::::description[`CryptoSwap.revert_new_parameters() -> address: view`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the factory contract.
:::
Function to revert the parameters changes.
```vyper
@external
def revert_new_parameters():
assert msg.sender == Factory(self.factory).admin() # dev: only owner
self.admin_actions_deadline = 0
```
```shell
>>> CryptoSwap.revert_new_parameters()
```
::::
## Admin Control Info Methods
### `admin_actions_deadline`
::::description[`CryptoSwap.admin_actions_deadline() -> uint256: view`]
Getter for the admin actions deadline. This is the deadline after which new parameter changes can be applied. When committing new changes, there is a three-day timespan after being able to apply them (`ADMIN_ACTIONS_DELAY`), otherwise the call will revert.
Returns: timestamp (`uint256`).
```vyper
admin_actions_deadline: public(uint256)
ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400
```
```shell
>>> CryptoSwap.admin_actions_deadline()
0
```
::::
### `initial_A_gamma`
::::description[`CryptoSwap.initial_A_gamma() -> uint256:`]
Getter for the initial A/gamma.
Returns: A/gamma (`uint256`).
```vyper
initial_A_gamma: public(uint256)
```
```shell
>>> CryptoSwap.initial_A_gamma()
581076037942835227425498917514114728328226821
```
::::
### `initial_A_gamma_time`
::::description[`CryptoSwap.initial_A_gamma_time() -> uint256:`]
Getter for the initial A/gamma time.
Returns: A/gamma time (`uint256`).
```vyper
initial_A_gamma_time: public(uint256)
```
```shell
>>> CryptoSwap.initial_A_gamma_time()
0
```
::::
### `future_A_gamma`
::::description[`CryptoSwap.future_A_gamma() -> uint256:`]
Getter for the future A/gamma.
Returns: future A/gamma (`uint256`).
```vyper
future_A_gamma: public(uint256)
```
```shell
>>> CryptoSwap.future_A_gamma()
581076037942835227425498917514114728328226821
```
::::
### `future_A_gamma_time`
::::description[`CryptoSwap.future_A_gamma_time() -> uint256:`]
Getter for the future A/gamma time.
Returns: future A/gamma time (`uint256`).
```vyper
future_A_gamma_time: public(uint256)
```
```shell
>>> CryptoSwap.future_A_gamma_time()
0
```
::::
### `future_allowed_extra_profit`
::::description[`CryptoSwap.future_allowed_extra_profit() -> uint256:`]
Getter for the future allowed extra profit.
Returns: future allowed extra profit (`uint256`).
```vyper
future_allowed_extra_profit: public(uint256)
```
```shell
>>> CryptoSwap.future_allowed_extra_profit()
0
```
::::
### `future_adjustment_step`
::::description[`CryptoSwap.future_adjustment_step() -> uint256:`]
Getter for the future adjustment step.
Returns: future adjustment step (`uint256`).
```vyper
future_adjustment_step: public(uint256)
```
```shell
>>> CryptoSwap.future_adjustment_step()
0
```
::::
---
## Crypto Pool
**Crypto-Pools are exchange contracts containing two volatile (non-pegged) assets.**These exchange contracts are deployed via the [CryptoSwap Factory](../../factory/cryptoswap/overview.md). Unlike newer Factory contracts, which utilize blueprint contracts, the earlier versions did not have this feature at the time of their deployments. Instead, in these earlier versions, the exchange contract is created using the Vyper built-in **`create_forwarder_to()`**function.
The pool is then initialized via the **`initialize()`**function of the pool implementation contract, which sets all the relevant variables, such as paired tokens, prices, and parameters.
```vyper
@external
def initialize(
A: uint256,
gamma: uint256,
mid_fee: uint256,
out_fee: uint256,
allowed_extra_profit: uint256,
fee_gamma: uint256,
adjustment_step: uint256,
admin_fee: uint256,
ma_half_time: uint256,
initial_price: uint256,
_token: address,
_coins: address[N_COINS],
_precisions: uint256,
):
assert self.mid_fee == 0 # dev: check that we call it from factory
self.factory = msg.sender
# Pack A and gamma:
# shifted A + gamma
A_gamma: uint256 = shift(A, 128)
A_gamma = bitwise_or(A_gamma, gamma)
self.initial_A_gamma = A_gamma
self.future_A_gamma = A_gamma
self.mid_fee = mid_fee
self.out_fee = out_fee
self.allowed_extra_profit = allowed_extra_profit
self.fee_gamma = fee_gamma
self.adjustment_step = adjustment_step
self.admin_fee = admin_fee
self.price_scale = initial_price
self._price_oracle = initial_price
self.last_prices = initial_price
self.last_prices_timestamp = block.timestamp
self.ma_half_time = ma_half_time
self.xcp_profit_a = 10**18
self.token = _token
self.coins = _coins
self.PRECISIONS = _precisions
```
## Exchange Methods
### `exchange`
::::description[`CryptoSwap.exchange(i: uint256, j: uint256, dx: uint256, min_dy: uint256, use_eth: bool = False, receiver: address = msg.sender) -> uint256:`]
Function to exchange `dx` amount of coin `i` for coin `j` and receive a minimum amount of `min_dy`.
Returns: output amount (`uint256`).
Emits: `TokenExchange`
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint256` | Index value for the input coin |
| `j` | `uint256` | Index value for the output coin |
| `dx` | `uint256` | Amount of input coin being swapped in |
| `min_dy` | `uint256` | Minimum amount of output coin to receive |
| `use_eth` | `bool` | whether to use plain ETH; defaults to `False` (uses wETH instead) |
| `receiver` | `address` | Address to send output coin to. Deafaults to `msg.sender` |
```vyper
event TokenExchange:
buyer: indexed(address)
sold_id: uint256
tokens_sold: uint256
bought_id: uint256
tokens_bought: uint256
@payable
@external
@nonreentrant('lock')
def exchange(i: uint256, j: uint256, dx: uint256, min_dy: uint256,
use_eth: bool = False, receiver: address = msg.sender) -> uint256:
"""
Exchange using WETH by default
"""
return self._exchange(msg.sender, msg.value, i, j, dx, min_dy, use_eth, receiver, ZERO_ADDRESS, EMPTY_BYTES32)
@internal
def _exchange(sender: address, mvalue: uint256, i: uint256, j: uint256, dx: uint256, min_dy: uint256,
use_eth: bool, receiver: address, callbacker: address, callback_sig: bytes32) -> uint256:
assert i != j # dev: coin index out of range
assert i < N_COINS # dev: coin index out of range
assert j < N_COINS # dev: coin index out of range
assert dx > 0 # dev: do not exchange 0 coins
A_gamma: uint256[2] = self._A_gamma()
xp: uint256[N_COINS] = self.balances
p: uint256 = 0
dy: uint256 = 0
in_coin: address = self.coins[i]
out_coin: address = self.coins[j]
y: uint256 = xp[j]
x0: uint256 = xp[i]
xp[i] = x0 + dx
self.balances[i] = xp[i]
price_scale: uint256 = self.price_scale
precisions: uint256[2] = self._get_precisions()
xp = [xp[0] * precisions[0], xp[1] * price_scale * precisions[1] / PRECISION]
prec_i: uint256 = precisions[0]
prec_j: uint256 = precisions[1]
if i == 1:
prec_i = precisions[1]
prec_j = precisions[0]
# In case ramp is happening
t: uint256 = self.future_A_gamma_time
if t > 0:
x0 *= prec_i
if i > 0:
x0 = x0 * price_scale / PRECISION
x1: uint256 = xp[i] # Back up old value in xp
xp[i] = x0
self.D = self.newton_D(A_gamma[0], A_gamma[1], xp)
xp[i] = x1 # And restore
if block.timestamp >= t:
self.future_A_gamma_time = 1
dy = xp[j] - self.newton_y(A_gamma[0], A_gamma[1], xp, self.D, j)
# Not defining new "y" here to have less variables / make subsequent calls cheaper
xp[j] -= dy
dy -= 1
if j > 0:
dy = dy * PRECISION / price_scale
dy /= prec_j
dy -= self._fee(xp) * dy / 10**10
assert dy >= min_dy, "Slippage"
y -= dy
self.balances[j] = y
# Do transfers in and out together
# XXX coin vs ETH
if use_eth and in_coin == WETH20:
assert mvalue == dx # dev: incorrect eth amount
else:
assert mvalue == 0 # dev: nonzero eth amount
if callback_sig == EMPTY_BYTES32:
response: Bytes[32] = raw_call(
in_coin,
_abi_encode(
sender, self, dx, method_id=method_id("transferFrom(address,address,uint256)")
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool) # dev: failed transfer
else:
b: uint256 = ERC20(in_coin).balanceOf(self)
raw_call(
callbacker,
concat(slice(callback_sig, 0, 4), _abi_encode(sender, receiver, in_coin, dx, dy))
)
assert ERC20(in_coin).balanceOf(self) - b == dx # dev: callback didn't give us coins
if in_coin == WETH20:
WETH(WETH20).withdraw(dx)
if use_eth and out_coin == WETH20:
raw_call(receiver, b"", value=dy)
else:
if out_coin == WETH20:
WETH(WETH20).deposit(value=dy)
response: Bytes[32] = raw_call(
out_coin,
_abi_encode(receiver, dy, method_id=method_id("transfer(address,uint256)")),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
y *= prec_j
if j > 0:
y = y * price_scale / PRECISION
xp[j] = y
# Calculate price
if dx > 10**5 and dy > 10**5:
_dx: uint256 = dx * prec_i
_dy: uint256 = dy * prec_j
if i == 0:
p = _dx * 10**18 / _dy
else: # j == 0
p = _dy * 10**18 / _dx
self.tweak_price(A_gamma, xp, p, 0)
log TokenExchange(sender, i, dx, j, dy)
return dy
```
```shell
>>> CryptoSwap.exchange()
1696841675
```
::::
### `exchange_underlying`
::::description[`CryptoSwap.exchange_underlying(i: uint256, j: uint256, dx: uint256, min_dy: uint256, receiver: address = msg.sender) -> uint256:`]
:::note
`exchange_underlying` exchanges tokens by using the 'underlying' ETH instead of wETH.
:::
Function to exchange `dx` amount of coin `i` for coin `j` and receive a minimum amount of `min_dy`.
Returns: output amount (`uint256`).
Emits: `TokenExchange`
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint256` | index value for the input coin |
| `j` | `uint256` | index value for the output coin |
| `dx` | `uint256` | amount of input coin being swapped in |
| `min_dy` | `uint256` | minimum amount of output coin to receive |
| `use_eth` | `bool` | whether to use plain ETH; defaults to `False` (uses wETH instead) |
| `receiver` | `address` | address to send output coin to; deafaults to `msg.sender` |
```vyper
event TokenExchange:
buyer: indexed(address)
sold_id: uint256
tokens_sold: uint256
bought_id: uint256
tokens_bought: uint256
@payable
@external
@nonreentrant('lock')
def exchange_underlying(i: uint256, j: uint256, dx: uint256, min_dy: uint256,
receiver: address = msg.sender) -> uint256:
"""
Exchange using ETH
"""
return self._exchange(msg.sender, msg.value, i, j, dx, min_dy, True, receiver, ZERO_ADDRESS, EMPTY_BYTES32)
@internal
def _exchange(sender: address, mvalue: uint256, i: uint256, j: uint256, dx: uint256, min_dy: uint256,
use_eth: bool, receiver: address, callbacker: address, callback_sig: bytes32) -> uint256:
assert i != j # dev: coin index out of range
assert i < N_COINS # dev: coin index out of range
assert j < N_COINS # dev: coin index out of range
assert dx > 0 # dev: do not exchange 0 coins
A_gamma: uint256[2] = self._A_gamma()
xp: uint256[N_COINS] = self.balances
p: uint256 = 0
dy: uint256 = 0
in_coin: address = self.coins[i]
out_coin: address = self.coins[j]
y: uint256 = xp[j]
x0: uint256 = xp[i]
xp[i] = x0 + dx
self.balances[i] = xp[i]
price_scale: uint256 = self.price_scale
precisions: uint256[2] = self._get_precisions()
xp = [xp[0] * precisions[0], xp[1] * price_scale * precisions[1] / PRECISION]
prec_i: uint256 = precisions[0]
prec_j: uint256 = precisions[1]
if i == 1:
prec_i = precisions[1]
prec_j = precisions[0]
# In case ramp is happening
t: uint256 = self.future_A_gamma_time
if t > 0:
x0 *= prec_i
if i > 0:
x0 = x0 * price_scale / PRECISION
x1: uint256 = xp[i] # Back up old value in xp
xp[i] = x0
self.D = self.newton_D(A_gamma[0], A_gamma[1], xp)
xp[i] = x1 # And restore
if block.timestamp >= t:
self.future_A_gamma_time = 1
dy = xp[j] - self.newton_y(A_gamma[0], A_gamma[1], xp, self.D, j)
# Not defining new "y" here to have less variables / make subsequent calls cheaper
xp[j] -= dy
dy -= 1
if j > 0:
dy = dy * PRECISION / price_scale
dy /= prec_j
dy -= self._fee(xp) * dy / 10**10
assert dy >= min_dy, "Slippage"
y -= dy
self.balances[j] = y
# Do transfers in and out together
# XXX coin vs ETH
if use_eth and in_coin == WETH20:
assert mvalue == dx # dev: incorrect eth amount
else:
assert mvalue == 0 # dev: nonzero eth amount
if callback_sig == EMPTY_BYTES32:
response: Bytes[32] = raw_call(
in_coin,
_abi_encode(
sender, self, dx, method_id=method_id("transferFrom(address,address,uint256)")
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool) # dev: failed transfer
else:
b: uint256 = ERC20(in_coin).balanceOf(self)
raw_call(
callbacker,
concat(slice(callback_sig, 0, 4), _abi_encode(sender, receiver, in_coin, dx, dy))
)
assert ERC20(in_coin).balanceOf(self) - b == dx # dev: callback didn't give us coins
if in_coin == WETH20:
WETH(WETH20).withdraw(dx)
if use_eth and out_coin == WETH20:
raw_call(receiver, b"", value=dy)
else:
if out_coin == WETH20:
WETH(WETH20).deposit(value=dy)
response: Bytes[32] = raw_call(
out_coin,
_abi_encode(receiver, dy, method_id=method_id("transfer(address,uint256)")),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
y *= prec_j
if j > 0:
y = y * price_scale / PRECISION
xp[j] = y
# Calculate price
if dx > 10**5 and dy > 10**5:
_dx: uint256 = dx * prec_i
_dy: uint256 = dy * prec_j
if i == 0:
p = _dx * 10**18 / _dy
else: # j == 0
p = _dy * 10**18 / _dx
self.tweak_price(A_gamma, xp, p, 0)
log TokenExchange(sender, i, dx, j, dy)
return dy
```
```shell
>>> CryptoSwap.exchange_underlying(todo)
```
::::
### `exchange_extended`
::::description[`CryptoSwap.exchange_extended(i: uint256, j: uint256, dx: uint256, min_dy: uint256, use_eth: bool, sender: address, receiver: address, cb: bytes32) -> uint256:`]
:::note
This method does not allow swapping in native token, but does allow swaps that transfer out native token from the pool.
:::
Function to exchange `dx` amount of coin `i` for coin `j` and receive a minimum amount of `min_dy` with using a callback method.
Returns: output amount (`uint256`).
Emits: `TokenExchange`
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint256` | index value for the input coin |
| `j` | `uint256` | index value for the output coin |
| `dx` | `uint256` | amount of input coin being swapped in |
| `min_dy` | `uint256` | minimum amount of output coin to receive |
| `use_eth` | `bool` | whether to use plain ETH; defaults to `False` (uses wETH instead) |
| `receiver` | `address` | address to send output coin to; deafaults to `msg.sender` |
| `cb` | `bytes32` | callback signature |
```vyper
event TokenExchange:
buyer: indexed(address)
sold_id: uint256
tokens_sold: uint256
bought_id: uint256
tokens_bought: uint256
@payable
@external
@nonreentrant('lock')
def exchange_extended(i: uint256, j: uint256, dx: uint256, min_dy: uint256,
use_eth: bool, sender: address, receiver: address, cb: bytes32) -> uint256:
assert cb != EMPTY_BYTES32 # dev: No callback specified
return self._exchange(sender, msg.value, i, j, dx, min_dy, use_eth, receiver, msg.sender, cb)
@internal
def _exchange(sender: address, mvalue: uint256, i: uint256, j: uint256, dx: uint256, min_dy: uint256,
use_eth: bool, receiver: address, callbacker: address, callback_sig: bytes32) -> uint256:
assert i != j # dev: coin index out of range
assert i < N_COINS # dev: coin index out of range
assert j < N_COINS # dev: coin index out of range
assert dx > 0 # dev: do not exchange 0 coins
A_gamma: uint256[2] = self._A_gamma()
xp: uint256[N_COINS] = self.balances
p: uint256 = 0
dy: uint256 = 0
in_coin: address = self.coins[i]
out_coin: address = self.coins[j]
y: uint256 = xp[j]
x0: uint256 = xp[i]
xp[i] = x0 + dx
self.balances[i] = xp[i]
price_scale: uint256 = self.price_scale
precisions: uint256[2] = self._get_precisions()
xp = [xp[0] * precisions[0], xp[1] * price_scale * precisions[1] / PRECISION]
prec_i: uint256 = precisions[0]
prec_j: uint256 = precisions[1]
if i == 1:
prec_i = precisions[1]
prec_j = precisions[0]
# In case ramp is happening
t: uint256 = self.future_A_gamma_time
if t > 0:
x0 *= prec_i
if i > 0:
x0 = x0 * price_scale / PRECISION
x1: uint256 = xp[i] # Back up old value in xp
xp[i] = x0
self.D = self.newton_D(A_gamma[0], A_gamma[1], xp)
xp[i] = x1 # And restore
if block.timestamp >= t:
self.future_A_gamma_time = 1
dy = xp[j] - self.newton_y(A_gamma[0], A_gamma[1], xp, self.D, j)
# Not defining new "y" here to have less variables / make subsequent calls cheaper
xp[j] -= dy
dy -= 1
if j > 0:
dy = dy * PRECISION / price_scale
dy /= prec_j
dy -= self._fee(xp) * dy / 10**10
assert dy >= min_dy, "Slippage"
y -= dy
self.balances[j] = y
# Do transfers in and out together
# XXX coin vs ETH
if use_eth and in_coin == WETH20:
assert mvalue == dx # dev: incorrect eth amount
else:
assert mvalue == 0 # dev: nonzero eth amount
if callback_sig == EMPTY_BYTES32:
response: Bytes[32] = raw_call(
in_coin,
_abi_encode(
sender, self, dx, method_id=method_id("transferFrom(address,address,uint256)")
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool) # dev: failed transfer
else:
b: uint256 = ERC20(in_coin).balanceOf(self)
raw_call(
callbacker,
concat(slice(callback_sig, 0, 4), _abi_encode(sender, receiver, in_coin, dx, dy))
)
assert ERC20(in_coin).balanceOf(self) - b == dx # dev: callback didn't give us coins
if in_coin == WETH20:
WETH(WETH20).withdraw(dx)
if use_eth and out_coin == WETH20:
raw_call(receiver, b"", value=dy)
else:
if out_coin == WETH20:
WETH(WETH20).deposit(value=dy)
response: Bytes[32] = raw_call(
out_coin,
_abi_encode(receiver, dy, method_id=method_id("transfer(address,uint256)")),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
y *= prec_j
if j > 0:
y = y * price_scale / PRECISION
xp[j] = y
# Calculate price
if dx > 10**5 and dy > 10**5:
_dx: uint256 = dx * prec_i
_dy: uint256 = dy * prec_j
if i == 0:
p = _dx * 10**18 / _dy
else: # j == 0
p = _dy * 10**18 / _dx
self.tweak_price(A_gamma, xp, p, 0)
log TokenExchange(sender, i, dx, j, dy)
return dy
```
```shell
>>> CryptoSwap.exchange_extended(todo)
```
::::
### `get_dy`
::::description[`CryptoSwap.get_dy(i: uint256, j: uint256, dx: uint256) -> uint256:`]
Getter for the received amount of coin `j` for swapping in `dx` amount of coin `i`.
Returns: output amount (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint256` | index value for the input coin |
| `j` | `uint256` | index value for the output coin |
| `dx` | `uint256` | amount of input coin being swapped in |
:::note
This method takes fees into consideration.
:::
```vyper
@external
@view
def get_dy(i: uint256, j: uint256, dx: uint256) -> uint256:
assert i != j # dev: same input and output coin
assert i < N_COINS # dev: coin index out of range
assert j < N_COINS # dev: coin index out of range
precisions: uint256[2] = self._get_precisions()
price_scale: uint256 = self.price_scale * precisions[1]
xp: uint256[N_COINS] = self.balances
A_gamma: uint256[2] = self._A_gamma()
D: uint256 = self.D
if self.future_A_gamma_time > 0:
D = self.newton_D(A_gamma[0], A_gamma[1], self.xp())
xp[i] += dx
xp = [xp[0] * precisions[0], xp[1] * price_scale / PRECISION]
y: uint256 = self.newton_y(A_gamma[0], A_gamma[1], xp, D, j)
dy: uint256 = xp[j] - y - 1
xp[j] = y
if j > 0:
dy = dy * PRECISION / price_scale
else:
dy /= precisions[0]
dy -= self._fee(xp) * dy / 10**10
return dy
```
```shell
>>> CryptoSwap.get_dy(0, 1, 1e18) # get_dy: 1 ETH for dy PRISMA
2244836869048665161301
```
::::
## Adding/Removing Liquidity
### `add_liquidity`
::::description[`CryptoSwap.add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256, use_eth: bool = False, receiver: address = msg.sender) -> uint256:`]
Function to add liquidity to the pool and mint the corresponding lp tokens.
Returns: amount of lp tokens received (`uint256`).
Emits: `AddLiquidity`
| Input | Type | Description |
| ----------- | -------| ----|
| `amounts` | `uint256[N_COINS]` | list of amounts to add of each coin |
| `min_mint_amount` | `uint256` | minimum amount of lp tokens to mint |
| `use_eth` | `bool` | `True` if native token is being added to the pool; default to `False` |
| `receiver` | `address` | receiver of the lp tokens; deaults to `msg.sender` |
```vyper
event AddLiquidity:
provider: indexed(address)
token_amounts: uint256[N_COINS]
fee: uint256
token_supply: uint256
N_COINS: constant(int128) = 2
@payable
@external
@nonreentrant('lock')
def add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256,
use_eth: bool = False, receiver: address = msg.sender) -> uint256:
assert amounts[0] > 0 or amounts[1] > 0 # dev: no coins to add
A_gamma: uint256[2] = self._A_gamma()
xp: uint256[N_COINS] = self.balances
amountsp: uint256[N_COINS] = empty(uint256[N_COINS])
xx: uint256[N_COINS] = empty(uint256[N_COINS])
d_token: uint256 = 0
d_token_fee: uint256 = 0
old_D: uint256 = 0
xp_old: uint256[N_COINS] = xp
for i in range(N_COINS):
bal: uint256 = xp[i] + amounts[i]
xp[i] = bal
self.balances[i] = bal
xx = xp
precisions: uint256[2] = self._get_precisions()
price_scale: uint256 = self.price_scale * precisions[1]
xp = [xp[0] * precisions[0], xp[1] * price_scale / PRECISION]
xp_old = [xp_old[0] * precisions[0], xp_old[1] * price_scale / PRECISION]
if not use_eth:
assert msg.value == 0 # dev: nonzero eth amount
for i in range(N_COINS):
coin: address = self.coins[i]
if use_eth and coin == WETH20:
assert msg.value == amounts[i] # dev: incorrect eth amount
if amounts[i] > 0:
if (not use_eth) or (coin != WETH20):
response: Bytes[32] = raw_call(
coin,
_abi_encode(
msg.sender,
self,
amounts[i],
method_id=method_id("transferFrom(address,address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool) # dev: failed transfer
if coin == WETH20:
WETH(WETH20).withdraw(amounts[i])
amountsp[i] = xp[i] - xp_old[i]
t: uint256 = self.future_A_gamma_time
if t > 0:
old_D = self.newton_D(A_gamma[0], A_gamma[1], xp_old)
if block.timestamp >= t:
self.future_A_gamma_time = 1
else:
old_D = self.D
D: uint256 = self.newton_D(A_gamma[0], A_gamma[1], xp)
lp_token: address = self.token
token_supply: uint256 = CurveToken(lp_token).totalSupply()
if old_D > 0:
d_token = token_supply * D / old_D - token_supply
else:
d_token = self.get_xcp(D) # making initial virtual price equal to 1
assert d_token > 0 # dev: nothing minted
if old_D > 0:
d_token_fee = self._calc_token_fee(amountsp, xp) * d_token / 10**10 + 1
d_token -= d_token_fee
token_supply += d_token
CurveToken(lp_token).mint(receiver, d_token)
# Calculate price
# p_i * (dx_i - dtoken / token_supply * xx_i) = sum{k!=i}(p_k * (dtoken / token_supply * xx_k - dx_k))
# Simplified for 2 coins
p: uint256 = 0
if d_token > 10**5:
if amounts[0] == 0 or amounts[1] == 0:
S: uint256 = 0
precision: uint256 = 0
ix: uint256 = 0
if amounts[0] == 0:
S = xx[0] * precisions[0]
precision = precisions[1]
ix = 1
else:
S = xx[1] * precisions[1]
precision = precisions[0]
S = S * d_token / token_supply
p = S * PRECISION / (amounts[ix] * precision - d_token * xx[ix] * precision / token_supply)
if ix == 0:
p = (10**18)**2 / p
self.tweak_price(A_gamma, xp, p, D)
else:
self.D = D
self.virtual_price = 10**18
self.xcp_profit = 10**18
CurveToken(lp_token).mint(receiver, d_token)
assert d_token >= min_mint_amount, "Slippage"
log AddLiquidity(receiver, amounts, d_token_fee, token_supply)
return d_token
```
```shell
>>> CryptoSwap.add_liquidity(todo)
```
::::
### `calc_token_amount`
::::description[`CryptoSwap.calc_token_amount(amounts: uint256[N_COINS]) -> uint256:`]
Function to calculate the amount of tokens to deposit/withdraw to get `amounts`.
Returns amount of LP tokens (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `_amount` | `uint256[N_COINS]` | amount of coins to withdraw/deposit |
```vyper
@view
@external
def calc_token_amount(amounts: uint256[N_COINS]) -> uint256:
token_supply: uint256 = CurveToken(self.token).totalSupply()
precisions: uint256[2] = self._get_precisions()
price_scale: uint256 = self.price_scale * precisions[1]
A_gamma: uint256[2] = self._A_gamma()
xp: uint256[N_COINS] = self.xp()
amountsp: uint256[N_COINS] = [
amounts[0] * precisions[0],
amounts[1] * price_scale / PRECISION]
D0: uint256 = self.D
if self.future_A_gamma_time > 0:
D0 = self.newton_D(A_gamma[0], A_gamma[1], xp)
xp[0] += amountsp[0]
xp[1] += amountsp[1]
D: uint256 = self.newton_D(A_gamma[0], A_gamma[1], xp)
d_token: uint256 = token_supply * D / D0 - token_supply
d_token -= self._calc_token_fee(amountsp, xp) * d_token / 10**10 + 1
return d_token
```
```shell
>>> CryptoSwap.calc_token_amount(todo)
```
::::
### `remove_liquidity`
::::description[`CryptoSwap.remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS], use_eth: bool = False, receiver: address = msg.sender):`]
Function to remove liquidity from the pool and burn the lp tokens. When removing liquidity via this function, no fees are charged as the coins are withdrawin in balanced proportions.
Emits: `RemoveLiquidity`
| Input | Type | Description |
| ----------- | -------| ----|
| `_amount` | `uint256[N_COINS]` | amount of lp tokens to burn |
| `min_amounts` | `uint256[N_COINS]` | minimum amounts of token to withdraw |
| `use_eth` | `bool` | True = withdraw ETH, False = withdraw wETH |
| `receiver` | `address` | receiver of the coins; defaults to msg.sender |
```vyper
event RemoveLiquidity:
provider: indexed(address)
token_amounts: uint256[N_COINS]
token_supply: uint256
N_COINS: constant(int128) = 2
@external
@nonreentrant('lock')
def remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS],
use_eth: bool = False, receiver: address = msg.sender):
"""
This withdrawal method is very safe, does no complex math
"""
lp_token: address = self.token
total_supply: uint256 = CurveToken(lp_token).totalSupply()
CurveToken(lp_token).burnFrom(msg.sender, _amount)
balances: uint256[N_COINS] = self.balances
amount: uint256 = _amount - 1 # Make rounding errors favoring other LPs a tiny bit
for i in range(N_COINS):
d_balance: uint256 = balances[i] * amount / total_supply
assert d_balance >= min_amounts[i]
self.balances[i] = balances[i] - d_balance
balances[i] = d_balance # now it's the amounts going out
coin: address = self.coins[i]
if use_eth and coin == WETH20:
raw_call(receiver, b"", value=d_balance)
else:
if coin == WETH20:
WETH(WETH20).deposit(value=d_balance)
response: Bytes[32] = raw_call(
coin,
_abi_encode(receiver, d_balance, method_id=method_id("transfer(address,uint256)")),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
D: uint256 = self.D
self.D = D - D * amount / total_supply
log RemoveLiquidity(msg.sender, balances, total_supply - _amount)
```
```shell
>>> CryptoSwap.remove_liquidity(todo)
```
::::
### `remove_liquidity_one_coin`
::::description[`CryptoSwap.remove_liquidity_one_coin(token_amount: uint256, i: uint256, min_amount: uint256, use_eth: bool = False, receiver: address = msg.sender) -> uint256:`]
Funtion to withdraw liquidity in a single token.
Returns: amount of withdrawn coin (`uint256`).
Emits: `RemoveLiquidityOne`
| Input | Type | Description |
| ----------- | -------| ----|
| `token_amount` | `uint256` | amount of lp tokens to burn |
| `i` | `uint256` | index of the token to withdraw |
| `min_amount` | `uint256` | minimum amount of token to withdraw |
| `use_eth` | `bool` | True = withdraw ETH, False = withdraw wETH |
| `receiver` | `address` | receiver of the coins; defaults to msg.sender |
```vyper
event RemoveLiquidityOne:
provider: indexed(address)
token_amount: uint256
coin_index: uint256
coin_amount: uint256
@external
@nonreentrant('lock')
def remove_liquidity_one_coin(token_amount: uint256, i: uint256, min_amount: uint256,
use_eth: bool = False, receiver: address = msg.sender) -> uint256:
A_gamma: uint256[2] = self._A_gamma()
dy: uint256 = 0
D: uint256 = 0
p: uint256 = 0
xp: uint256[N_COINS] = empty(uint256[N_COINS])
future_A_gamma_time: uint256 = self.future_A_gamma_time
dy, p, D, xp = self._calc_withdraw_one_coin(A_gamma, token_amount, i, (future_A_gamma_time > 0), True)
assert dy >= min_amount, "Slippage"
if block.timestamp >= future_A_gamma_time:
self.future_A_gamma_time = 1
self.balances[i] -= dy
CurveToken(self.token).burnFrom(msg.sender, token_amount)
coin: address = self.coins[i]
if use_eth and coin == WETH20:
raw_call(receiver, b"", value=dy)
else:
if coin == WETH20:
WETH(WETH20).deposit(value=dy)
response: Bytes[32] = raw_call(
coin,
_abi_encode(receiver, dy, method_id=method_id("transfer(address,uint256)")),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
self.tweak_price(A_gamma, xp, p, D)
log RemoveLiquidityOne(msg.sender, token_amount, i, dy)
return dy
```
```shell
>>> CryptoSwap.remove_liquidity_one_coin()
```
::::
### `calc_withdraw_one_coin`
::::description[`CryptoSwap.calc_withdraw_one_coin(token_amount: uint256, i: uint256) -> uint256:`]
Method to calculate the amount of output token `i` when burning `token_amount` of lp tokens, taking fees into condsideration.
Returns: amount of token received (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `token_amount` | `uint256` | amount of lp tokens burned |
| `i` | `uint256` | index of the coin to withdraw |
:::note
This method takes fees into consideration.
:::
```vyper
N_COINS: constant(int128) = 2
@view
@external
def calc_withdraw_one_coin(token_amount: uint256, i: uint256) -> uint256:
return self._calc_withdraw_one_coin(self._A_gamma(), token_amount, i, True, False)[0]
@internal
@view
def _calc_withdraw_one_coin(A_gamma: uint256[2], token_amount: uint256, i: uint256, update_D: bool,
calc_price: bool) -> (uint256, uint256, uint256, uint256[N_COINS]):
token_supply: uint256 = CurveToken(self.token).totalSupply()
assert token_amount <= token_supply # dev: token amount more than supply
assert i < N_COINS # dev: coin out of range
xx: uint256[N_COINS] = self.balances
D0: uint256 = 0
precisions: uint256[2] = self._get_precisions()
price_scale_i: uint256 = self.price_scale * precisions[1]
xp: uint256[N_COINS] = [xx[0] * precisions[0], xx[1] * price_scale_i / PRECISION]
if i == 0:
price_scale_i = PRECISION * precisions[0]
if update_D:
D0 = self.newton_D(A_gamma[0], A_gamma[1], xp)
else:
D0 = self.D
D: uint256 = D0
# Charge the fee on D, not on y, e.g. reducing invariant LESS than charging the user
fee: uint256 = self._fee(xp)
dD: uint256 = token_amount * D / token_supply
D -= (dD - (fee * dD / (2 * 10**10) + 1))
y: uint256 = self.newton_y(A_gamma[0], A_gamma[1], xp, D, i)
dy: uint256 = (xp[i] - y) * PRECISION / price_scale_i
xp[i] = y
# Price calc
p: uint256 = 0
if calc_price and dy > 10**5 and token_amount > 10**5:
# p_i = dD / D0 * sum'(p_k * x_k) / (dy - dD / D0 * y0)
S: uint256 = 0
precision: uint256 = precisions[0]
if i == 1:
S = xx[0] * precisions[0]
precision = precisions[1]
else:
S = xx[1] * precisions[1]
S = S * dD / D0
p = S * PRECISION / (dy * precision - dD * xx[i] * precision / D0)
if i == 0:
p = (10**18)**2 / p
return dy, p, D, xp
```
```shell
>>> CryptoSwap.calc_withdraw_one_coin(1000000000000000000, 0) # withdraw 1 LP token in coin[0]
43347133051647883
```
::::
## Oracles Methods
Oracle prices are updated whenever the `tweak_price` function is called. This occurs when any of the `_exchange()`, `add_liquidity()`, or `remove_liquidity_one_coin()` functions are called.
```vyper "Update Price Oracles"
@internal
def tweak_price(A_gamma: uint256[2],_xp: uint256[N_COINS], p_i: uint256, new_D: uint256):
price_oracle: uint256 = self._price_oracle
last_prices: uint256 = self.last_prices
price_scale: uint256 = self.price_scale
last_prices_timestamp: uint256 = self.last_prices_timestamp
p_new: uint256 = 0
if last_prices_timestamp < block.timestamp:
# MA update required
ma_half_time: uint256 = self.ma_half_time
alpha: uint256 = self.halfpow((block.timestamp - last_prices_timestamp) * 10**18 / ma_half_time)
price_oracle = (last_prices * (10**18 - alpha) + price_oracle * alpha) / 10**18
self._price_oracle = price_oracle
self.last_prices_timestamp = block.timestamp
D_unadjusted: uint256 = new_D # Withdrawal methods know new D already
if new_D == 0:
# We will need this a few times (35k gas)
D_unadjusted = self.newton_D(A_gamma[0], A_gamma[1], _xp)
if p_i > 0:
last_prices = p_i
else:
# calculate real prices
__xp: uint256[N_COINS] = _xp
dx_price: uint256 = __xp[0] / 10**6
__xp[0] += dx_price
last_prices = price_scale * dx_price / (_xp[1] - self.newton_y(A_gamma[0], A_gamma[1], __xp, D_unadjusted, 1))
self.last_prices = last_prices
total_supply: uint256 = CurveToken(self.token).totalSupply()
old_xcp_profit: uint256 = self.xcp_profit
old_virtual_price: uint256 = self.virtual_price
# Update profit numbers without price adjustment first
xp: uint256[N_COINS] = [D_unadjusted / N_COINS, D_unadjusted * PRECISION / (N_COINS * price_scale)]
xcp_profit: uint256 = 10**18
virtual_price: uint256 = 10**18
if old_virtual_price > 0:
xcp: uint256 = self.geometric_mean(xp, True)
virtual_price = 10**18 * xcp / total_supply
xcp_profit = old_xcp_profit * virtual_price / old_virtual_price
t: uint256 = self.future_A_gamma_time
if virtual_price < old_virtual_price and t == 0:
raise "Loss"
if t == 1:
self.future_A_gamma_time = 0
self.xcp_profit = xcp_profit
norm: uint256 = price_oracle * 10**18 / price_scale
if norm > 10**18:
norm -= 10**18
else:
norm = 10**18 - norm
adjustment_step: uint256 = max(self.adjustment_step, norm / 5)
needs_adjustment: bool = self.not_adjusted
# if not needs_adjustment and (virtual_price-10**18 > (xcp_profit-10**18)/2 + self.allowed_extra_profit):
# (re-arrange for gas efficiency)
if not needs_adjustment and (virtual_price * 2 - 10**18 > xcp_profit + 2*self.allowed_extra_profit) and (norm > adjustment_step) and (old_virtual_price > 0):
needs_adjustment = True
self.not_adjusted = True
if needs_adjustment:
if norm > adjustment_step and old_virtual_price > 0:
p_new = (price_scale * (norm - adjustment_step) + adjustment_step * price_oracle) / norm
# Calculate balances*prices
xp = [_xp[0], _xp[1] * p_new / price_scale]
# Calculate "extended constant product" invariant xCP and virtual price
D: uint256 = self.newton_D(A_gamma[0], A_gamma[1], xp)
xp = [D / N_COINS, D * PRECISION / (N_COINS * p_new)]
# We reuse old_virtual_price here but it's not old anymore
old_virtual_price = 10**18 * self.geometric_mean(xp, True) / total_supply
# Proceed if we've got enough profit
# if (old_virtual_price > 10**18) and (2 * (old_virtual_price - 10**18) > xcp_profit - 10**18):
if (old_virtual_price > 10**18) and (2 * old_virtual_price - 10**18 > xcp_profit):
self.price_scale = p_new
self.D = D
self.virtual_price = old_virtual_price
return
else:
self.not_adjusted = False
# Can instead do another flag variable if we want to save bytespace
self.D = D_unadjusted
self.virtual_price = virtual_price
self._claim_admin_fees()
return
# If we are here, the price_scale adjustment did not happen
# Still need to update the profit counter and D
self.D = D_unadjusted
self.virtual_price = virtual_price
# norm appeared < adjustment_step after
if needs_adjustment:
self.not_adjusted = False
self._claim_admin_fees()
```
### `lp_price`
::::description[`CryptoSwap.lp_price() -> uint256:`]
Getter for the approximate LP token price with regard to the token at index 0.
Returns: LP token price (`uint256`).
```vyper
@external
@view
def lp_price() -> uint256:
"""
Approximate LP token price
"""
return 2 * self.virtual_price * self.sqrt_int(self.internal_price_oracle()) / 10**18
@internal
@view
def internal_price_oracle() -> uint256:
price_oracle: uint256 = self._price_oracle
last_prices_timestamp: uint256 = self.last_prices_timestamp
if last_prices_timestamp < block.timestamp:
ma_half_time: uint256 = self.ma_half_time
last_prices: uint256 = self.last_prices
alpha: uint256 = self.halfpow((block.timestamp - last_prices_timestamp) * 10**18 / ma_half_time)
return (last_prices * (10**18 - alpha) + price_oracle * alpha) / 10**18
else:
return price_oracle
```
```shell
>>> CryptoSwap.lp_price()
41793722011818265
```
::::
### `price_oracle`
::::description[`CryptoSwap.price_oracle() -> uint256:`]
Getter for the oracle price of the coin at index `k` with regard to coin at index 0.
Returns: oracle price (`uint256`).
```vyper
@external
@view
def price_oracle() -> uint256:
return self.internal_price_oracle()
@internal
@view
def internal_price_oracle() -> uint256:
price_oracle: uint256 = self._price_oracle
last_prices_timestamp: uint256 = self.last_prices_timestamp
if last_prices_timestamp < block.timestamp:
ma_half_time: uint256 = self.ma_half_time
last_prices: uint256 = self.last_prices
alpha: uint256 = self.halfpow((block.timestamp - last_prices_timestamp) * 10**18 / ma_half_time)
return (last_prices * (10**18 - alpha) + price_oracle * alpha) / 10**18
else:
return price_oracle
```
```shell
>>> CryptoSwap.price_oracle()
409798289826499
```
::::
### `last_prices`
::::description[`CryptoSwap.last_prices() -> uint256: view`]
Getter for the last price of the coin at index `k` with regard to the coin at index 0. `last_price` stores the last price when calling the functions `_exchange()`, `add_liquidity()` or `remove_liquitiy_one_coin()`.
Returns: last price (`uint256`).
```vyper
last_prices: public(uint256)
```
```shell
>>> CryptoSwap.last_prices()
409119503867160
```
::::
### `last_prices_timestamp`
::::description[`CryptoSwap.last_prices_timestamp() -> uint256: view`]
Getter for the timestamp of the most recent update for `last_prices`.
Returns: timestamp (`uint256`).
```vyper
last_prices_timestamp: public(uint256)
```
```shell
>>> CryptoSwap.last_prices_timestamp()
1700314907
```
::::
### `price_scale`
::::description[`CryptoSwap.price_scale -> uint256: view`]
Getter for the price scale of the coin at index `k` with regard to the coin at index 0. Price scale determines the price band around which liquidity is
concentrated and is conditionally updated when calling the functions `_exchange()`, `add_liquidity()` or `remove_liquitiy_one_coin()`.
Returns: last price (`uint256`).
```vyper
price_scale: public(uint256) # Internal price scale
```
```shell
>>> CryptoSwap.price_scale()
410228896677145
```
::::
### `ma_half_time`
::::description[`CryptoSwap.ma_half_time() -> uint256: view`]
Getter for the moving-average (ma) half time in seconds.
Returns: ma half time (`uint256`).
```vyper
ma_half_time: public(uint256)
```
```shell
>>> CryptoSwap.ma_half_time()
600
```
::::
### `virtual_price`
::::description[`CryptoSwap.geometric_mean(unsorted_x: uint256[N_COINS], sort: bool) -> uint256:`]
Getter for the virtual price. This variable is cached, fast to read and mostly used internally.
Returns: virtual price (`uint256`).
```vyper
virtual_price: public(uint256) # Cached (fast to read) virtual price also used internally
```
```shell
>>> CryptoSwap.virtual_price()
1032276363484815360
```
::::
### `get_virtual_price`
::::description[`CryptoSwap.geometric_mean(unsorted_x: uint256[N_COINS], sort: bool) -> uint256:`]
Getter for the virtual price.
Returns: virtual price (`uint256`).
```vyper
@external
@view
def get_virtual_price() -> uint256:
return 10**18 * self.get_xcp(self.D) / CurveToken(self.token).totalSupply()
```
```shell
>>> CryptoSwap.get_virtual_price()
1032276363484815360
```
::::
## Fee Methods
Fees are charged based on the balance/imbalance of the pool. Fee is low when the pool is balanced and increases the more it is imbalanced.
### `fee`
::::description[`CryptoSwap.fee() -> uint256:`]
Getter for the fee charged by the pool at the current state.
Returns: fee (`uint256`).
```vyper
@external
@view
def fee() -> uint256:
return self._fee(self.xp())
@internal
@view
def _fee(xp: uint256[N_COINS]) -> uint256:
"""
f = fee_gamma / (fee_gamma + (1 - K))
where
K = prod(x) / (sum(x) / N)**N
(all normalized to 1e18)
"""
fee_gamma: uint256 = self.fee_gamma
f: uint256 = xp[0] + xp[1] # sum
f = fee_gamma * 10**18 / (
fee_gamma + 10**18 - (10**18 * N_COINS**N_COINS) * xp[0] / f * xp[1] / f
)
return (self.mid_fee * f + self.out_fee * (10**18 - f)) / 10**18
```
```shell
>>> CryptoSwap.fee()
26417626
```
::::
### `mid_fee`
::::description[`CryptoSwap.mid_fee() -> uint256: view`]
Getter for the current 'mid-fee'. This is the minimum fee and is charged when the pool is completely balanced.
Returns: mid fee (`uint256`).
```vyper
mid_fee: public(uint256)
```
```shell
>>> CryptoSwap.mid_fee()
26000000
```
::::
### `out_fee`
::::description[`CryptoSwap.out_fee() -> uint256: view`]
Getter for the 'out-fee'. This is the maximum fee and is charged when the pool is completely imbalanced.
Returns: out fee (`uint256`).
```vyper
out_fee: public(uint256)
```
```shell
>>> CryptoSwap.out_fee()
45000000
```
::::
### `fee_gamma`
::::description[`CryptoSwap.fee_gamma() -> uint256: view`]
Getter for the 'fee-gamma'. This parameter modifies the rate at which fees rise as imbalance intensifies. Smaller values result in rapid fee hikes with growing imbalances, while larger values lead to more gradual increments in fees as imbalance expands.
Returns: fee gamma (`uint256`).
```vyper
fee_gamma: public(uint256)
```
```shell
>>> CryptoSwap.fee_gamma()
230000000000000
```
::::
### `xcp_profit`
::::description[`CryptoSwap.xcp_profit() -> uint256: view`]
Getter for the current pool profits.
Returns: current profits (`uint256`).
```vyper
xcp_profit: public(uint256)
```
```shell
>>> CryptoSwap.xcp_profit()
1058938494058326335
```
::::
### `xcp_profit_a`
::::description[`CryptoSwap.xcp_profit_a() -> uint256:`]
Getter for the full profit at the last claim of admin fees.
Returns: profit at last claim (`uint256`).
```vyper
xcp_profit_a: public(uint256) # Full profit at last claim of admin fees
```
```shell
>>> CryptoSwap.xcp_profit_a()
1058927586013478083
```
::::
### `admin_fee`
::::description[`CryptoSwap.admin_fee() -> uint256:`]
Getter for the admin fee of the pool. This value is hardcoded to 50% (5000000000).
Returns: admin fee (`uint256`).
```vyper
admin_fee: public(uint256)
```
```shell
>>> CryptoSwap.admin_fee()
5000000000
```
::::
### `claim_admin_fees`
::::description[`CryptoSwap.admin_fee() -> uint256:`]
Function to claim admin fees from the pool and send them to the fee receiver. `fee_receiver` is set within the [Factory](https://etherscan.io/address/0xF18056Bbd320E96A48e3Fbf8bC061322531aac99).
Emits: `ClaimAdminFee`
```vyper
event ClaimAdminFee:
admin: indexed(address)
tokens: uint256
@external
@nonreentrant('lock')
def claim_admin_fees():
self._claim_admin_fees()
@internal
def _claim_admin_fees():
A_gamma: uint256[2] = self._A_gamma()
xcp_profit: uint256 = self.xcp_profit
xcp_profit_a: uint256 = self.xcp_profit_a
# Gulp here
for i in range(N_COINS):
coin: address = self.coins[i]
if coin == WETH20:
self.balances[i] = self.balance
else:
self.balances[i] = ERC20(coin).balanceOf(self)
vprice: uint256 = self.virtual_price
if xcp_profit > xcp_profit_a:
fees: uint256 = (xcp_profit - xcp_profit_a) * self.admin_fee / (2 * 10**10)
if fees > 0:
receiver: address = Factory(self.factory).fee_receiver()
if receiver != ZERO_ADDRESS:
frac: uint256 = vprice * 10**18 / (vprice - fees) - 10**18
claimed: uint256 = CurveToken(self.token).mint_relative(receiver, frac)
xcp_profit -= fees*2
self.xcp_profit = xcp_profit
log ClaimAdminFee(receiver, claimed)
total_supply: uint256 = CurveToken(self.token).totalSupply()
# Recalculate D b/c we gulped
D: uint256 = self.newton_D(A_gamma[0], A_gamma[1], self.xp())
self.D = D
self.virtual_price = 10**18 * self.get_xcp(D) / total_supply
if xcp_profit > xcp_profit_a:
self.xcp_profit_a = xcp_profit
```
```shell
>>> CryptoSwap.claim_admin_fees()
```
::::
## Price Scaling
Curve v2 pools adaptively adjust liquidity to optimize depth near prevailing market prices, thereby reducing slippage. This is achieved by maintaining a continuous EMA (exponential moving average) of the pool's recent exchange rates (termed "internal oracle"), and relocating liquidity around this EMA when it's economically sensible for LPs.
You can envision this mechanism as "resetting" the bonding curve to align the peak liquidity concentration (the curve's center) with the EMA. The price with the highest liquidity focus is termed the "price scale", while the ongoing EMA is labeled as the "price oracle."
The price scaling parameters can be adjusted by the admin of the pool, see [here](../pools/admin-controls.md).
### `allowed_extra_profit`
::::description[`CryptoSwap.allowed_extra_profit() -> uint256: view`]
Getter for the allowed extra profit.
Returns: extra profit allowed (`uint256`).
```vyper
allowed_extra_profit: public(uint256) # 2 * 10**12 - recommended value
```
```shell
>>> CryptoSwap.allowed_extra_profit()
2000000000000
```
::::
### `adjustment_step`
::::description[`CryptoSwap.adjustment_step() -> uint256: view`]
Getter for the minimum size of price scale adjustments.
Returns: adjustment step (`uint256`).
```vyper
adjustment_step: public(uint256)
```
```shell
>>> CryptoSwap.adjustment_step()
146000000000000
```
::::
## Bonding Curve Parameters
Similar to many AMMs, Curve v2 employs a bonding curve to determine asset prices based on the pool's availability of each asset. To centralize liquidity near the bonding curve's midpoint, Curve v2 utilizes an invariant that sits between the Stableswap (Curve v1) and the constant-product models (like Uniswap, Balancer, and others).
The bonding curve parameters can be adjusted by the admin of the pool, see [here](../pools/admin-controls.md).
### `A`
::::description[`CryptoSwap.A() -> uint256:`]
Getter for the current pool amplification value.
Returns: A (`uint256`).
```vyper
@view
@external
def A() -> uint256:
return self._A_gamma()[0]
@view
@internal
def _A_gamma() -> uint256[2]:
t1: uint256 = self.future_A_gamma_time
A_gamma_1: uint256 = self.future_A_gamma
gamma1: uint256 = bitwise_and(A_gamma_1, 2**128-1)
A1: uint256 = shift(A_gamma_1, -128)
if block.timestamp < t1:
# handle ramping up and down of A
A_gamma_0: uint256 = self.initial_A_gamma
t0: uint256 = self.initial_A_gamma_time
# Less readable but more compact way of writing and converting to uint256
# gamma0: uint256 = bitwise_and(A_gamma_0, 2**128-1)
# A0: uint256 = shift(A_gamma_0, -128)
# A1 = A0 + (A1 - A0) * (block.timestamp - t0) / (t1 - t0)
# gamma1 = gamma0 + (gamma1 - gamma0) * (block.timestamp - t0) / (t1 - t0)
t1 -= t0
t0 = block.timestamp - t0
t2: uint256 = t1 - t0
A1 = (shift(A_gamma_0, -128) * t2 + A1 * t0) / t1
gamma1 = (bitwise_and(A_gamma_0, 2**128-1) * t2 + gamma1 * t0) / t1
return [A1, gamma1]
```
```shell
>>> CryptoSwap.A()
400000
```
::::
### `gamma`
::::description[`CryptoSwap.gamma() -> uint256:`]
Getter for the current gamma value.
Returns: gamma (`uint256`).
```vyper
@view
@external
def gamma() -> uint256:
return self._A_gamma()[1]
@view
@internal
def _A_gamma() -> uint256[2]:
t1: uint256 = self.future_A_gamma_time
A_gamma_1: uint256 = self.future_A_gamma
gamma1: uint256 = bitwise_and(A_gamma_1, 2**128-1)
A1: uint256 = shift(A_gamma_1, -128)
if block.timestamp < t1:
# handle ramping up and down of A
A_gamma_0: uint256 = self.initial_A_gamma
t0: uint256 = self.initial_A_gamma_time
# Less readable but more compact way of writing and converting to uint256
# gamma0: uint256 = bitwise_and(A_gamma_0, 2**128-1)
# A0: uint256 = shift(A_gamma_0, -128)
# A1 = A0 + (A1 - A0) * (block.timestamp - t0) / (t1 - t0)
# gamma1 = gamma0 + (gamma1 - gamma0) * (block.timestamp - t0) / (t1 - t0)
t1 -= t0
t0 = block.timestamp - t0
t2: uint256 = t1 - t0
A1 = (shift(A_gamma_0, -128) * t2 + A1 * t0) / t1
gamma1 = (bitwise_and(A_gamma_0, 2**128-1) * t2 + gamma1 * t0) / t1
return [A1, gamma1]
```
```shell
>>> CryptoSwap.gamma()
145000000000000
```
::::
## Contract Info Methods
### `coins`
::::description[`CryptoSwap.coins(arg0: uint256) -> address: view`]
Getter for the coin at index `arg0`.
Returns: coin (`address`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `uint256` | index of coin |
```vyper
coins: public(address[N_COINS])
```
```shell
>>> CryptoSwap.coins(0)
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
```
::::
### `balances`
::::description[`CryptoSwap.balances(arg0: uint256) -> uint256: view`]
Getter for the pool balance of coin at index `arg0`.
Returns: coin balance (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `uint256` | index of coin |
```vyper
coins: public(address[N_COINS])
```
```shell
>>> CryptoSwap.balances(0)
669157518183204053847
```
::::
### `D`
::::description[`CryptoSwap.D() -> uint256: view`]
Getter for the D invariant.
Returns: D (`address`).
```vyper
D: public(uint256)
```
```shell
>>> CryptoSwap.D()
1386359478656960977136
```
::::
### `token`
::::description[`CryptoSwap.token() -> uint256: view`]
Getter for the LP token contract.
Returns: lp token (`address`).
```vyper
token: public(address)
```
```shell
>>> CryptoSwap.token()
'0xb34e1a3D07f9D180Bc2FDb9Fd90B8994423e33c1'
```
::::
### `factory`
::::description[`CryptoSwap.factory()`]
Getter for the factory contract.
Returns: factory (`address`).
```vyper
factory: public(address)
```
```shell
>>> CryptoSwap.factory()
'0xF18056Bbd320E96A48e3Fbf8bC061322531aac99'
```
::::
## Internal Math Functions
All these math functions are interally embedded into the contract. They can not be called externally.
### `geometric_mean`
::::description[`CryptoSwap.geometric_mean(unsorted_x: uint256[N_COINS], sort: bool) -> uint256:`]
Function to calculate the geometric mean of a list of numbers in 1e18 precision.
Returns: gemoetric mean (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `unsorted_x` | `uint256[N_COINS]` | array containing two values |
| `sort` | `bool` | whether to sort or not |
```vyper
N_COINS: constant(int128) = 2
@internal
@pure
def geometric_mean(unsorted_x: uint256[N_COINS], sort: bool) -> uint256:
"""
(x[0] * x[1] * ...) **(1/N)
"""
x: uint256[N_COINS] = unsorted_x
if sort and x[0] < x[1]:
x = [unsorted_x[1], unsorted_x[0]]
D: uint256 = x[0]
diff: uint256 = 0
for i in range(255):
D_prev: uint256 = D
# tmp: uint256 = 10**18
# for _x in x:
# tmp = tmp * _x / D
# D = D * ((N_COINS - 1) * 10**18 + tmp) / (N_COINS * 10**18)
# line below makes it for 2 coins
D = (D + x[0] * x[1] / D) / N_COINS
if D > D_prev:
diff = D - D_prev
else:
diff = D_prev - D
if diff <= 1 or diff * 10**18 < D:
return D
raise "Did not converge"
```
::::
### `newton_D`
::::description[`CryptoSwap.newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS]) -> uint256:`]
Function to find the D invariant using Newton method.
Returns: D invariant (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `AMN` | `uint256` | `ANN = A * N**N` |
| `gamma` | `uint256` | `AMM.gamma()` value |
| `x_unsorted` | `uint256[N_COINS]` | unsorted array of coin balances |
```vyper
N_COINS: constant(int128) = 2
@internal
@view
def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS]) -> uint256:
"""
Finding the invariant using Newton method.
ANN is higher by the factor A_MULTIPLIER
ANN is already A * N**N
Currently uses 60k gas
"""
# Safety checks
assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A
assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
# Initial value of invariant D is that for constant-product invariant
x: uint256[N_COINS] = x_unsorted
if x[0] < x[1]:
x = [x_unsorted[1], x_unsorted[0]]
assert x[0] > 10**9 - 1 and x[0] < 10**15 * 10**18 + 1 # dev: unsafe values x[0]
assert x[1] * 10**18 / x[0] > 10**14-1 # dev: unsafe values x[i] (input)
D: uint256 = N_COINS * self.geometric_mean(x, False)
S: uint256 = x[0] + x[1]
for i in range(255):
D_prev: uint256 = D
# K0: uint256 = 10**18
# for _x in x:
# K0 = K0 * _x * N_COINS / D
# collapsed for 2 coins
K0: uint256 = (10**18 * N_COINS**2) * x[0] / D * x[1] / D
_g1k0: uint256 = gamma + 10**18
if _g1k0 > K0:
_g1k0 = _g1k0 - K0 + 1
else:
_g1k0 = K0 - _g1k0 + 1
# D / (A * N**N) * _g1k0**2 / gamma**2
mul1: uint256 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN
# 2*N*K0 / _g1k0
mul2: uint256 = (2 * 10**18) * N_COINS * K0 / _g1k0
neg_fprime: uint256 = (S + S * mul2 / 10**18) + mul1 * N_COINS / K0 - mul2 * D / 10**18
# D -= f / fprime
D_plus: uint256 = D * (neg_fprime + S) / neg_fprime
D_minus: uint256 = D*D / neg_fprime
if 10**18 > K0:
D_minus += D * (mul1 / neg_fprime) / 10**18 * (10**18 - K0) / K0
else:
D_minus -= D * (mul1 / neg_fprime) / 10**18 * (K0 - 10**18) / K0
if D_plus > D_minus:
D = D_plus - D_minus
else:
D = (D_minus - D_plus) / 2
diff: uint256 = 0
if D > D_prev:
diff = D - D_prev
else:
diff = D_prev - D
if diff * 10**14 < max(10**16, D): # Could reduce precision for gas efficiency here
# Test that we are safe with the next newton_y
for _x in x:
frac: uint256 = _x * 10**18 / D
assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe values x[i]
return D
raise "Did not converge"
```
::::
### `newton_y`
::::description[`CryptoSwap.newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> uint256:`]
Function to calculate x[i] given balances `x` and invariant D.
Returns: y (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `AMN` | `uint256` | `ANN = A * N**N` |
| `gamma` | `uint256` | `AMM.gamma()` value |
| `x` | `uint256[N_COINS]` | array containing coin balances |
| `D` | `uint256` | D invariant |
| `i` | `uint256` | coin index to calculate x[i] for |
```vyper
N_COINS: constant(int128) = 2
@internal
@pure
def newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> uint256:
"""
Calculating x[i] given other balances x[0..N_COINS-1] and invariant D
ANN = A * N**N
"""
# Safety checks
assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A
assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert D > 10**17 - 1 and D < 10**15 * 10**18 + 1 # dev: unsafe values D
x_j: uint256 = x[1 - i]
y: uint256 = D**2 / (x_j * N_COINS**2)
K0_i: uint256 = (10**18 * N_COINS) * x_j / D
# S_i = x_j
# frac = x_j * 1e18 / D => frac = K0_i / N_COINS
assert (K0_i > 10**16*N_COINS - 1) and (K0_i < 10**20*N_COINS + 1) # dev: unsafe values x[i]
# x_sorted: uint256[N_COINS] = x
# x_sorted[i] = 0
# x_sorted = self.sort(x_sorted) # From high to low
# x[not i] instead of x_sorted since x_soted has only 1 element
convergence_limit: uint256 = max(max(x_j / 10**14, D / 10**14), 100)
for j in range(255):
y_prev: uint256 = y
K0: uint256 = K0_i * y * N_COINS / D
S: uint256 = x_j + y
_g1k0: uint256 = gamma + 10**18
if _g1k0 > K0:
_g1k0 = _g1k0 - K0 + 1
else:
_g1k0 = K0 - _g1k0 + 1
# D / (A * N**N) * _g1k0**2 / gamma**2
mul1: uint256 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN
# 2*K0 / _g1k0
mul2: uint256 = 10**18 + (2 * 10**18) * K0 / _g1k0
yfprime: uint256 = 10**18 * y + S * mul2 + mul1
_dyfprime: uint256 = D * mul2
if yfprime < _dyfprime:
y = y_prev / 2
continue
else:
yfprime -= _dyfprime
fprime: uint256 = yfprime / y
# y -= f / f_prime; y = (y * fprime - f) / fprime
# y = (yfprime + 10**18 * D - 10**18 * S) // fprime + mul1 // fprime * (10**18 - K0) // K0
y_minus: uint256 = mul1 / fprime
y_plus: uint256 = (yfprime + 10**18 * D) / fprime + y_minus * 10**18 / K0
y_minus += 10**18 * S / fprime
if y_plus < y_minus:
y = y_prev / 2
else:
y = y_plus - y_minus
diff: uint256 = 0
if y > y_prev:
diff = y - y_prev
else:
diff = y_prev - y
if diff < max(convergence_limit, y / 10**14):
frac: uint256 = y * 10**18 / D
assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y
return y
raise "Did not converge"
```
::::
### `halfpow`
::::description[`CryptoSwap.halfpow(power: uint256) -> uint256:`]
Function to calculate the halfpow.
Returns: halfpow (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `power` | `uint256` | value |
```vyper
@internal
@pure
def halfpow(power: uint256) -> uint256:
"""
1e18 * 0.5 **(power/1e18)
Inspired by: https://github.com/balancer-labs/balancer-core/blob/master/contracts/BNum.sol#L128
"""
intpow: uint256 = power / 10**18
otherpow: uint256 = power - intpow * 10**18
if intpow > 59:
return 0
result: uint256 = 10**18 / (2**intpow)
if otherpow == 0:
return result
term: uint256 = 10**18
x: uint256 = 5 * 10**17
S: uint256 = 10**18
neg: bool = False
for i in range(1, 256):
K: uint256 = i * 10**18
c: uint256 = K - 10**18
if otherpow > c:
c = otherpow - c
neg = not neg
else:
c -= otherpow
term = term * (c * x / 10**18) / K
if neg:
S -= term
else:
S += term
if term < EXP_PRECISION:
return result * S / 10**18
raise "Did not converge"
```
::::
---
## CryptoSwap Exchange: Overview
The *Automatic Market-Making with Dynamic Peg (CryptoSwap)* algorithm introduces a new approach for creating liquidity for assets which are not necessarily pegged to each other.
The core of this algorithm lies in its ability to **concentrate liquidity around a price point determined by an internal oracle**, adjusting this price in a way that balances potential losses and system profits.
Key features include the use of **transformed pegged invariants**, a method for quantifying profits and losses, and the **CurveCrypto invariant**, specifically designed for efficient execution on the EVM.
The algorithm also incorporates a **dynamic fee structure**that responds to changing market conditions. This approach seeks to enhance liquidity provision and optimize returns for liquidity providers.
:::pdf[Whitepaper]
For a detailed overview of the design, please read the official [whitepaper](/pdf/whitepapers/whitepaper_cryptoswap.pdf).
:::
---
## Implementations
*There have been several implementations of the CryptoSwap algorithm:*
:::deploy[Contract Source]
*Source code is available on GitHub:*
- genesis contracts: [https://github.com/curvefi/curve-crypto-contract](https://github.com/curvefi/curve-crypto-contract)
- twocrypto-ng: [https://github.com/curvefi/twocrypto-ng](https://github.com/curvefi/twocrypto-ng)
- tricrypto-ng: [https://github.com/curvefi/tricrypto-ng](https://github.com/curvefi/tricrypto-ng)
:::
| Type | Description |
| :----------------: | ---------------------------------------------------------------------- |
| **`CryptoSwap`**| Genesis two-coin volatile asset pool integration. |
| **`Tricrypto`**| Genesis three-coin volatile asset pool integration. |
| **`TwoCrypto-NG`**| Improved version of `CryptoSwap`, more [here](../twocrypto-ng/overview.md). |
| **`Tricrypto-NG`**| Improved version of `Tricrypto`, more [here](../tricrypto-ng/overview.md). |
---
## Parameters
Because different trading pairs can exhibit drastically different price dynamics, Curve v2 offers a variety of tunable parameters that can be used to optimize for different types of assets.
**The CryptoSwap market-making algorithm contains of three different classes of parameters:**- *Bonding Curve:* `A` and `gamma`
- *Price Scaling:* `ma_time`, `allowed_extra_profit` and `adjustment_step`
- *Fees:* `mid_fee`, `out_fee` and `fee_gamma`
:::tip[Explainer for Parameters]
An excellent deep-dive article on the parameters: [https://nagaking.substack.com/p/deep-dive-curve-v2-parameters](https://nagaking.substack.com/p/deep-dive-curve-v2-parameters).
:::
## Bonding Curve Parameters
Similar to many AMMs, Curve v2 employs a bonding curve to determine asset prices according to the pool's supply of each asset. To centralize liquidity around the midpoint of the bonding curve, Curve v2 adopts an invariant that falls between the StableSwap (Curve v1) approach and the constant-product method used by platforms like Uniswap and Balancer.
- **`A`**: regulates the concentration of liquidity at the core of the bonding curve
- **`gamma`**: regulates the overall breadth of the curve
## Price Scaling
Curve v2 pools automatically adjust liquidity to optimize depth close to the prevailing market rates, reducing slippage. This is achieved by tracking a continuous EMA (exponential moving average) of the pool's latest exchange rates (referred to as an "internal oracle") and reallocating liquidity around this EMA only when it's economically beneficial for LPs.
- **`ma_time`**: regulates the duration of the EMA price oracle
- **`allowed_extra_profit`**: excess profit required to allow price re-pegging
- **`adjustment_step`**: minimum size of price scale adjustments
## Fees
Fees are charged based on the balance/imbalance of the pool. Fee is low when the pool is balanced and increases the more it is imbalanced.
*There are three different kind of fees:*
- **`fee_mid`**: charged fee when pool is perfectly balanced (minimum possible fee).
- **`out_fee`**: charged fee when pools is completely imbalanced (maximum possible fee).
- **`fee_gamma`**: determines the speed at which the fee increases when the pool becomes imbalanced. A low value leads to a more rapid fee increase, while a high value causes the fee to rise more gradually.
---
## Limits
## Liquidity Pools:::warning
The transaction will revert if the following requirements are not met.
:::
### `deploy_pool`
The pool **deployment is permissionless**, but it must adhere to certain parameter limitations:
| Parameter | Limitation |
| -------------------- | ---------------------------------------------------- |
| `A` | A_min - 1 < A < A_max + 1 |
| `gamma` | gamma_min - 1 < gamma < gamma_max + 1 |
| `mid_fee` | fee_min - 1 < mid_fee < fee_max - 1 |
| `out_fee` | out_fee >= mid_fee AND out_fee < fee_max - 1 |
| `admin_fee` | < 10^18 + 1 |
| `allowed_extra_profit` | allowed_extra_profit < 10^16 + 1 |
| `fee_gamma` | 0 < fee_gamma < 10^18 + 1 |
| `adjustment_step` | 0 < adjustment_step < 10^18 + 1 |
| `ma_half_time` | 0 < ma_half_time < 604800 |
| `initial_price` | 10^6 < initial_price < 10^30 |
- No duplicate coins.
- Only two coins.
- Maximum of 18 decimals of a coin.
*With:*
| Parameters | Value |
| ----------------- | --------------------------------------- |
| n_coins | 2 |
| A_multiplier | 10000 |
| A_min | (n_coins^n_coins * A_multiplier) / 10 = 4000 |
| A_max | n_coins^n_coins * A_multiplier * 100000 = 4000000000 |
| gamma_min | 10^10 = 10000000000 |
| gamma_max | 2 * 10^16 = 20000000000000000 |
| fee_min | 5 * 10^5 = 500000 |
| fee_max | 10 * 10^9 = 10000000000 |
::::description[`Factory.deploy_pool(_name: String[32], _symbol: String[10], _coins: address[2], A: uint256, gamma: uint256, mid_fee: uint256, out_fee: uint256, allowed_extra_profit: uint256, fee_gamma: uint256, adjustment_step: uint256, admin_fee: uint256, ma_half_time: uint256, initial_price: uint256) -> address:`]
Function to deploy a cryptoswap pool form the `pool_implementations`. This function will also deploy the according LP token from the `token_implementation`.
Returns: Deployed pool (`address`).
Emits: `CryptoPoolDeployed`
| Input | Type | Description |
| ---------------------- | ------------- | ----------- |
| `_name` | `String[32]` | Name of the new plain pool |
| `_symbol` | `String[10]` | Symbol for the new metapool’s LP token. This value will be concatenated with the factory symbol. |
| `_coins` | `address[2]` | List of addresses of the coins being used in the pool |
| `A` | `uint256` | Amplification coefficient |
| `gamma` | `uint256` | Gamma |
| `mid_fee` | `uint256` | Mid fee |
| `out_fee` | `uint256` | Out fee |
| `allowed_extra_profit` | `uint256` | Allowed extra profit |
| `fee_gamma` | `uint256` | Fee Gamma |
| `adjustment_step` | `uint256` | Adjustment step |
| `admin_fee` | `uint256` | Admin fee |
| `ma_half_time` | `uint256` | Moving-Average half time |
| `initial_price` | `uint256` | Initial price |
```vyper
event CryptoPoolDeployed:
token: address
coins: address[2]
A: uint256
gamma: uint256
mid_fee: uint256
out_fee: uint256
allowed_extra_profit: uint256
fee_gamma: uint256
adjustment_step: uint256
admin_fee: uint256
ma_half_time: uint256
initial_price: uint256
deployer: address
N_COINS: constant(int128) = 2
A_MULTIPLIER: constant(uint256) = 10000
# Limits
MAX_ADMIN_FEE: constant(uint256) = 10 * 10 **9
MIN_FEE: constant(uint256) = 5 * 10 **5 # 0.5 bps
MAX_FEE: constant(uint256) = 10 * 10 **9
MIN_GAMMA: constant(uint256) = 10 **10
MAX_GAMMA: constant(uint256) = 2 * 10 **16
MIN_A: constant(uint256) = N_COINS **N_COINS * A_MULTIPLIER / 10
MAX_A: constant(uint256) = N_COINS **N_COINS * A_MULTIPLIER * 100000
@external
def deploy_pool(
_name: String[32],
_symbol: String[10],
_coins: address[2],
A: uint256,
gamma: uint256,
mid_fee: uint256,
out_fee: uint256,
allowed_extra_profit: uint256,
fee_gamma: uint256,
adjustment_step: uint256,
admin_fee: uint256,
ma_half_time: uint256,
initial_price: uint256
) -> address:
"""
@notice Deploy a new pool
@param _name Name of the new plain pool
@param _symbol Symbol for the new plain pool - will be concatenated with factory symbol
Other parameters need some description
@return Address of the deployed pool
"""
# Validate parameters
assert A > MIN_A-1
assert A < MAX_A+1
assert gamma > MIN_GAMMA-1
assert gamma < MAX_GAMMA+1
assert mid_fee > MIN_FEE-1
assert mid_fee < MAX_FEE-1
assert out_fee >= mid_fee
assert out_fee < MAX_FEE-1
assert admin_fee < 10**18+1
assert allowed_extra_profit < 10**16+1
assert fee_gamma < 10**18+1
assert fee_gamma > 0
assert adjustment_step < 10**18+1
assert adjustment_step > 0
assert ma_half_time < 7 * 86400
assert ma_half_time > 0
assert initial_price > 10**6
assert initial_price < 10**30
assert _coins[0] != _coins[1], "Duplicate coins"
decimals: uint256[2] = empty(uint256[2])
for i in range(2):
d: uint256 = ERC20(_coins[i]).decimals()
assert d < 19, "Max 18 decimals for coins"
decimals[i] = d
precisions: uint256 = (18 - decimals[0]) + shift(18 - decimals[1], 8)
name: String[64] = concat("Curve.fi Factory Crypto Pool: ", _name)
symbol: String[32] = concat(_symbol, "-f")
token: address = create_forwarder_to(self.token_implementation)
pool: address = create_forwarder_to(self.pool_implementation)
Token(token).initialize(name, symbol, pool)
CryptoPool(pool).initialize(
A, gamma, mid_fee, out_fee, allowed_extra_profit, fee_gamma,
adjustment_step, admin_fee, ma_half_time, initial_price,
token, _coins, precisions)
length: uint256 = self.pool_count
self.pool_list[length] = pool
self.pool_count = length + 1
self.pool_data[pool].token = token
self.pool_data[pool].decimals = shift(decimals[0], 8) + decimals[1]
self.pool_data[pool].coins = _coins
key: uint256 = bitwise_xor(convert(_coins[0], uint256), convert(_coins[1], uint256))
length = self.market_counts[key]
self.markets[key][length] = pool
self.market_counts[key] = length + 1
log CryptoPoolDeployed(
token, _coins,
A, gamma, mid_fee, out_fee, allowed_extra_profit, fee_gamma,
adjustment_step, admin_fee, ma_half_time, initial_price,
msg.sender)
return pool
```
```shell
>>> CryptoFactory.deploy_pool(
_name: crv/weth crypto pool,
_symbol: crv/eth,
_coins: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "0xD533a949740bb3306d119CC777fa900bA034cd52",
A: 20000000,
gamma: 10000000000000000,
mid_fee: 3000000,
out_fee: 45000000,
allowed_extra_profit: 10000000000,
fee_gamma: 300000000000000000,
adjustment_step: 5500000000000,
admin_fee: 5000000000,
ma_half_time: 600,
initial_price: todo,
)
'returns address of the deployed pool'
```
::::
## Liquidity Gauge:::info
Liquidity gauges can only be successfully deployed from the same contract from which the pool was deployed!
:::
### `deploy_gauge`
::::description[`deploy_gauge(_pool: address) -> address`]
Function to deploy a liquidity gauge for a factory pool. The deployed gauge is created from the `gauge_implementation`.
Emits: `LiquidityGaugeDeployed`
| Input | Type | Description |
| -----------| --------- | --------------------------------------------- |
| `_pool` | `address` | Factory pool address to deploy a gauge for |
```vyper
event LiquidityGaugeDeployed:
pool: address
token: address
gauge: address
@external
def deploy_gauge(_pool: address) -> address:
"""
@notice Deploy a liquidity gauge for a factory pool
@param _pool Factory pool address to deploy a gauge for
@return Address of the deployed gauge
"""
assert self.pool_data[_pool].coins[0] != ZERO_ADDRESS, "Unknown pool"
assert self.pool_data[_pool].liquidity_gauge == ZERO_ADDRESS, "Gauge already deployed"
gauge: address = create_forwarder_to(self.gauge_implementation)
token: address = self.pool_data[_pool].token
LiquidityGauge(gauge).initialize(token)
self.pool_data[_pool].liquidity_gauge = gauge
log LiquidityGaugeDeployed(_pool, token, gauge)
return gauge
```
```shell
Factory.deploy_gauge('0x...')
'returns address of the deployed gauge'
```
::::
---
## Implementations
**The CryptoSwap Factory makes use of the `create_forwarder_to` function to deploy its contracts from the implementations.**:::warning
**Implementation contracts are upgradable.**They can either be replaced, or additional implementation contracts can be added. Therefore, please always make sure to check the most recent ones.
:::
It utilizes three different implementations:
- **`pool_implementation`**, containing a contract that is used to deploy the pools.
- **`token_implementation`**, containing a contract that is used to deploy LP tokens.
- **`gauge_implementation`**, containing a blueprint contract that is used when deploying gauges for pools.
## Query Implementations
### `pool_implementations`
::::description[`Factory.pool_implementations() -> address: view`]
Getter for the pool implementation contract.
Returns: pool implementation (`address`).
```vyper
pool_implementation: public(address)
```
```shell
>>> Factory.pool_implementations()
'0xa85461AFc2DEEC01bDA23b5cd267d51F765fba10'
```
::::
### `token_implementation`
::::description[`Factory.token_implementation() -> address: view`]
Getter for the token implementation contract.
Returns: token implementation (`address`).
```vyper
token_implementation: public(address)
```
```shell
>>> Factory.token_implementation()
'0xc08550A4cc5333f40e593eCc4C4724808085D304'
```
::::
### `gauge_implementation`
::::description[`Factory.gauge_implementation() -> address: view`]
Getter for the gauge implementation contract.
Returns: gauge implementation (`address`).
```vyper
gauge_implementation: public(address)
```
```shell
>>> Factory.gauge_implementation()
'0xdc892358d55d5Ae1Ec47a531130D62151EBA36E5'
```
::::
## Set New Implementation
*New implementations can be set via these admin-only functions:*
### `set_pool_implementation`
::::description[`Factory.set_pool_implementation(_pool_implementation: address):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new pool implementation contract.
Emits event: `UpdatePoolImplementation`
| Input | Type | Description |
| --------------------- | --------- | ------------------------- |
| `_pool_implementation`| `address` | New pool implementation |
```vyper
event UpdatePoolImplementation:
_old_pool_implementation: address
_new_pool_implementation: address
pool_implementation: public(address)
@external
def set_pool_implementation(_pool_implementation: address):
"""
@notice Set pool implementation
@dev Set to ZERO_ADDRESS to prevent deployment of new pools
@param _pool_implementation Address of the new pool implementation
"""
assert msg.sender == self.admin # dev: admin only
log UpdatePoolImplementation(self.pool_implementation, _pool_implementation)
self.pool_implementation = _pool_implementation
```
```shell
>>> Factory.set_pool_implementation("todo")
'todo'
```
::::
### `set_token_implementation`
::::description[`Factory.set_token_implementation(_token_implementation: address):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new token implementation contract.
Emits event: `UpdateTokenImplementation`
| Input | Type | Description |
| ---------------------- | --------- | ------------------------- |
| `_token_implementation`| `address` | New token implementation |
```vyper
event UpdateTokenImplementation:
_old_token_implementation: address
_new_token_implementation: address
token_implementation: public(address)
@external
def set_token_implementation(_token_implementation: address):
"""
@notice Set token implementation
@dev Set to ZERO_ADDRESS to prevent deployment of new pools
@param _token_implementation Address of the new token implementation
"""
assert msg.sender == self.admin # dev: admin only
log UpdateTokenImplementation(self.token_implementation, _token_implementation)
self.token_implementation = _token_implementation
```
```shell
>>> Factory.set_token_implementation("todo")
'todo'
```
::::
### `set_gauge_implementation`
::::description[`Factory.set_fee_receiver(_fee_receiver: address):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new gauge implementation contract.
Emits event: `UpdateGaugeImplementation`
| Input | Type | Description |
| ----------------------- | --------- | ------------------------ |
| `_gauge_implementation` | `address` | New gauge implementation |
```vyper
event UpdateGaugeImplementation:
_old_gauge_implementation: address
_new_gauge_implementation: address
gauge_implementation: public(address)
@external
def set_gauge_implementation(_gauge_implementation: address):
"""
@notice Set gauge implementation
@dev Set to ZERO_ADDRESS to prevent deployment of new gauges
@param _gauge_implementation Address of the new token implementation
"""
assert msg.sender == self.admin # dev: admin-only function
log UpdateGaugeImplementation(self.gauge_implementation, _gauge_implementation)
self.gauge_implementation = _gauge_implementation
```
```shell
>>> Factory.set_gauge_implementation("todo")
'todo'
```
::::
---
## Overview(Cryptoswap)
The [CryptoSwap](../../../stableswap-ng/overview.md) Factory allows the permissionless deployment of two-coin volatile asset pools, LP tokens, and liquidity gauges. **Liquidity pool and LP token share the same contract.**:::deploy[Contract Source & Deployment]
**CryptoSwap Factory**contract is deployed to the Ethereum mainnet at: [0xf18056bbd320e96a48e3fbf8bc061322531aac99](https://etherscan.io/address/0xf18056bbd320e96a48e3fbf8bc061322531aac99#code).
A list of all deployed contracts can be found [here](../../../../deployments.md).
:::
## Implementations
More on [implementations](./implementations.md).
---
## Deployer Api(Stableswap)
## Liquidity Pools:::warning
Transaction will fail if the requirements are not met.
:::
### `deploy_plain_pool`
The pool **deployment is permissionless**, but it must adhere to certain parameter limitations:
| Parameter | Limitation |
| --------- | ---------- |
| `_fee` | 4000000 (0.04%) ≤ `_fee` ≤ 100000000 (1%) |
- Valid **`_implementation_idx`**(cannot be **`ZERO_ADDRESS`**).
- Minimum of 2 coins and maximum of 4 coins.
- Cannot pair with a coin which is included in a basepool.
- If paired against plain ETH (0xE...EeE), ETH must be the first coin of the pool (**`_coins[0] = plain ETH`**).
- Maximum of 18 decimals for the coins.
- No duplicate coins.
::::description[`Factory.deploy_plain_pool(_name: String[32], _symbol: String[10], _coins: address[4], _A: uint256, _fee: uint256, _asset_type: uint256 = 0, _implementation_idx: uint256 = 0) → address: nonpayable`]
Function to deploy a plain pool.
Returns: Deployed pool (`address`).
Emits: `PlainPoolDeployed`
| Input | Type | Description |
| -------------------- | ------------- | ----------- |
| `_name` | `String[32]` | Name of the new plain pool |
| `_symbol` | `String[10]` | Symbol for the new pool’s LP token. This value will be concatenated with the factory symbol |
| `_coins` | `address[4]` | List of addresses of the coins being used in the pool |
| `_A` | `uint256` | Amplification coefficient |
| `_fee` | `uint256` | Trade fee, given as an integer with `1e10` precision |
| `_asset_type` | `uint256` | Asset type of the pool as an integer. 0 = `USD`, 1 = `ETH`, 2 = `BTC`, 3 = Other |
| `_implementation_idx`| `uint256` | Index of the implementation to use. All possible implementations for a pool of N_COINS can be publicly accessed via `plain_implementations(N_COINS)` |
```vyper
event PlainPoolDeployed:
coins: address[MAX_PLAIN_COINS]
A: uint256
fee: uint256
deployer: address
MAX_PLAIN_COINS: constant(int128) = 4 # max coins in a plain pool
@external
def deploy_plain_pool(
_name: String[32],
_symbol: String[10],
_coins: address[MAX_PLAIN_COINS],
_A: uint256,
_fee: uint256,
_asset_type: uint256 = 0,
_implementation_idx: uint256 = 0,
) -> address:
"""
@notice Deploy a new plain pool
@param _name Name of the new plain pool
@param _symbol Symbol for the new plain pool - will be
concatenated with factory symbol
@param _coins List of addresses of the coins being used in the pool.
@param _A Amplification co-efficient - a lower value here means
less tolerance for imbalance within the pool's assets.
Suggested values include:
* Uncollateralized algorithmic stablecoins: 5-10
* Non-redeemable, collateralized assets: 100
* Redeemable assets: 200-400
@param _fee Trade fee, given as an integer with 1e10 precision. The
minimum fee is 0.04% (4000000), the maximum is 1% (100000000).
50% of the fee is distributed to veCRV holders.
@param _asset_type Asset type for pool, as an integer
0 = USD, 1 = ETH, 2 = BTC, 3 = Other
@param _implementation_idx Index of the implementation to use. All possible
implementations for a pool of N_COINS can be publicly accessed
via `plain_implementations(N_COINS)`
@return Address of the deployed pool
"""
# fee must be between 0.04% and 1%
assert _fee >= 4000000 and _fee <= 100000000, "Invalid fee"
n_coins: uint256 = MAX_PLAIN_COINS
rate_multipliers: uint256[MAX_PLAIN_COINS] = empty(uint256[MAX_PLAIN_COINS])
decimals: uint256[MAX_PLAIN_COINS] = empty(uint256[MAX_PLAIN_COINS])
for i in range(MAX_PLAIN_COINS):
coin: address = _coins[i]
if coin == ZERO_ADDRESS:
assert i > 1, "Insufficient coins"
n_coins = i
break
assert self.base_pool_assets[coin] == False, "Invalid asset, deploy a metapool"
if _coins[i] == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE:
assert i == 0, "ETH must be first coin"
decimals[0] = 18
else:
decimals[i] = ERC20(coin).decimals()
assert decimals[i] < 19, "Max 18 decimals for coins"
rate_multipliers[i] = 10 **(36 - decimals[i])
for x in range(i, i+MAX_PLAIN_COINS):
if x+1 == MAX_PLAIN_COINS:
break
if _coins[x+1] == ZERO_ADDRESS:
break
assert coin != _coins[x+1], "Duplicate coins"
implementation: address = self.plain_implementations[n_coins][_implementation_idx]
assert implementation != ZERO_ADDRESS, "Invalid implementation index"
pool: address = create_forwarder_to(implementation)
CurvePlainPool(pool).initialize(_name, _symbol, _coins, rate_multipliers, _A, _fee)
length: uint256 = self.pool_count
self.pool_list[length] = pool
self.pool_count = length + 1
self.pool_data[pool].decimals = decimals
self.pool_data[pool].n_coins = n_coins
self.pool_data[pool].base_pool = ZERO_ADDRESS
self.pool_data[pool].implementation = implementation
if _asset_type != 0:
self.pool_data[pool].asset_type = _asset_type
for i in range(MAX_PLAIN_COINS):
coin: address = _coins[i]
if coin == ZERO_ADDRESS:
break
self.pool_data[pool].coins[i] = coin
raw_call(
coin,
concat(
method_id("approve(address,uint256)"),
convert(pool, bytes32),
convert(MAX_UINT256, bytes32)
)
)
for j in range(MAX_PLAIN_COINS):
if i < j:
swappable_coin: address = _coins[j]
key: uint256 = bitwise_xor(convert(coin, uint256), convert(swappable_coin, uint256))
length = self.market_counts[key]
self.markets[key][length] = pool
self.market_counts[key] = length + 1
log PlainPoolDeployed(_coins, _A, _fee, msg.sender)
return pool
```
```shell
>>> Factory.deploy_plain_pool(
_name: "alUSD-crvUSD",
_symbol: "alcrvUSD",
_coins: ['0xbc6da0fe9ad5f3b0d58160288917aa56653660e9', '0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'],
_A: 200,
_fee: 4000000,
_asset_type: 0,
_implementation_idx: 0,
)
'returns address of deployed pool'
```
::::
### `deploy_metapool`
Limitations when deploying meta pools:
| Parameter | Limitation |
| --------- | ---------- |
| `_fee` | 4000000 (0.04%) ≤ `_fee` ≤ 100000000 (0.1%) |
- Valid **`_implementation_idx`**(cannot be **`ZERO_ADDRESS`**).
- Maximum of 18 decimals for the coins.
::::description[`Factory.deploy_metapool(_base_pool: address, _name: String[32], _symbol: String[10], _coin: address, _A: uint256, _fee: uint256, _implementation_idx: uint256 = 0) -> address:`]
Function to deploy a metapool.
Returns: Deployed metapool (`address`).
Emits: `MetaPoolDeployed`
| Input | Type | Description |
| -------------------- | ------------- | ----------- |
| `_base_pool` | `address` | Address of the base pool to pair the token with |
| `_name` | `String[32]` | Name of the new metapool |
| `_symbol` | `String[10]` | Symbol for the new metapool’s LP token. This value will be concatenated with the base pool symbol. |
| `_coin` | `address` | Address of the coin being used in the metapool |
| `_A` | `uint256` | Amplification coefficient |
| `_fee` | `uint256` | Trade fee, given as an integer with `1e10` precision |
| `_implementation_idx`| `uint256` | Index of the implementation to use. All possible implementations for a pool of N_COINS can be publicly accessed via `plain_implementations(N_COINS)` |
```vyper
event MetaPoolDeployed:
coin: address
base_pool: address
A: uint256
fee: uint256
deployer: address
@external
def deploy_metapool(
_base_pool: address,
_name: String[32],
_symbol: String[10],
_coin: address,
_A: uint256,
_fee: uint256,
_implementation_idx: uint256 = 0,
) -> address:
"""
@notice Deploy a new metapool
@param _base_pool Address of the base pool to use
within the metapool
@param _name Name of the new metapool
@param _symbol Symbol for the new metapool - will be
concatenated with the base pool symbol
@param _coin Address of the coin being used in the metapool
@param _A Amplification co-efficient - a higher value here means
less tolerance for imbalance within the pool's assets.
Suggested values include:
* Uncollateralized algorithmic stablecoins: 5-10
* Non-redeemable, collateralized assets: 100
* Redeemable assets: 200-400
@param _fee Trade fee, given as an integer with 1e10 precision. The
minimum fee is 0.04% (4000000), the maximum is 1% (100000000).
50% of the fee is distributed to veCRV holders.
@param _implementation_idx Index of the implementation to use. All possible
implementations for a BASE_POOL can be publicly accessed
via `metapool_implementations(BASE_POOL)`
@return Address of the deployed pool
"""
# fee must be between 0.04% and 1%
assert _fee >= 4000000 and _fee <= 100000000, "Invalid fee"
implementation: address = self.base_pool_data[_base_pool].implementations[_implementation_idx]
assert implementation != ZERO_ADDRESS, "Invalid implementation index"
# things break if a token has >18 decimals
decimals: uint256 = ERC20(_coin).decimals()
assert decimals < 19, "Max 18 decimals for coins"
pool: address = create_forwarder_to(implementation)
CurvePool(pool).initialize(_name, _symbol, _coin, 10 **(36 - decimals), _A, _fee)
ERC20(_coin).approve(pool, MAX_UINT256)
# add pool to pool_list
length: uint256 = self.pool_count
self.pool_list[length] = pool
self.pool_count = length + 1
base_lp_token: address = self.base_pool_data[_base_pool].lp_token
self.pool_data[pool].decimals = [decimals, 0, 0, 0]
self.pool_data[pool].n_coins = 2
self.pool_data[pool].base_pool = _base_pool
self.pool_data[pool].coins[0] = _coin
self.pool_data[pool].coins[1] = self.base_pool_data[_base_pool].lp_token
self.pool_data[pool].implementation = implementation
is_finished: bool = False
for i in range(MAX_COINS):
swappable_coin: address = self.base_pool_data[_base_pool].coins[i]
if swappable_coin == ZERO_ADDRESS:
is_finished = True
swappable_coin = base_lp_token
key: uint256 = bitwise_xor(convert(_coin, uint256), convert(swappable_coin, uint256))
length = self.market_counts[key]
self.markets[key][length] = pool
self.market_counts[key] = length + 1
if is_finished:
break
log MetaPoolDeployed(_coin, _base_pool, _A, _fee, msg.sender)
return pool
```
```shell
>>> Factory.deploy_metapool(
_base_pool: '0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7',
_name: "crvusd/3CRV",
_symbol: "crvUSD3CRV",
_coin: '0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E',
_A: 200,
_fee: 4000000,
_implementation_idx: uint256 = 0,
)
'returns address of the deployed pool'
```
::::
## Liquidity Gauge:::info
Liquidity gauges can only be successfully deployed from the same contract from which the pool was deployed!
:::
### `deploy_gauge`
::::description[`Factory.deploy_gauge(_pool: address) -> address:`]
Function to deploy a liquidity gauge for a factory pool.
Returns: Deployed gauge (`address`).
Emits: `LiquidityGaugeDeployed`
| Input | Type | Description |
| -------- | --------- | -------------------------------------------- |
| `_pool` | `address` | Factory pool address to deploy a gauge for |
```vyper
event LiquidityGaugeDeployed:
pool: address
gauge: address
@external
def deploy_gauge(_pool: address) -> address:
"""
@notice Deploy a liquidity gauge for a factory pool
@param _pool Factory pool address to deploy a gauge for
@return Address of the deployed gauge
"""
assert self.pool_data[_pool].coins[0] != ZERO_ADDRESS, "Unknown pool"
assert self.pool_data[_pool].liquidity_gauge == ZERO_ADDRESS, "Gauge already deployed"
implementation: address = self.gauge_implementation
assert implementation != ZERO_ADDRESS, "Gauge implementation not set"
gauge: address = create_forwarder_to(implementation)
LiquidityGauge(gauge).initialize(_pool)
self.pool_data[_pool].liquidity_gauge = gauge
log LiquidityGaugeDeployed(_pool, gauge)
return gauge
```
```shell
>>> Factory.deploy_gauge("pool address")
'deployed gauge address'
```
::::
---
## number of coins -> implementation addresses
**The Stableswap Factory utilizes the `create_forwarder_to` function to deploy its contracts from the implementations.**:::warning
**Implementation contracts are upgradable.**They can either be replaced, or additional implementation contracts can be added. Therefore, please always make sure to check the most recent ones.
:::
It utilizes three different implementations:
- **`metapool_implementations`**, containing multiple contracts that are used to deploy metapools.
- **`plain_implementations`**, containing multiple contracts that are used to deploy plain pools.
- **`gauge_implementation`**, containing a contract which is used when deploying liquidity gauges for pools.
## Query Implementations
### `metapool_implementations`
::::description[`Factory.metapool_implementations(_base_pool: address) -> address[10]:`]
Getter for a list of implementation contracts for metapools targetting `_base_pool`.
Returns: metapool implementation contracts (`address[10]`).
| Input | Type | Description |
| ------------ | --------- | ----------- |
| `_base_pool` | `address` | Base pool |
```vyper
base_pool_data: HashMap[address, BasePoolArray]
@view
@external
def metapool_implementations(_base_pool: address) -> address[10]:
"""
@notice Get a list of implementation contracts for metapools targetting the given base pool
@dev A base pool is the pool for the LP token contained within the metapool
@param _base_pool Address of the base pool
@return List of implementation contract addresses
"""
return self.base_pool_data[_base_pool].implementations
```
```shell
>>> Factory.metapool_implementation('0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7')
'[[0x213be373FDff327658139C7df330817DAD2d5bBE]
[0x55Aa9BF126bCABF0bDC17Fa9E39Ec9239e1ce7A9]
[0x0000000000000000000000000000000000000000]
[0x0000000000000000000000000000000000000000]
[0x0000000000000000000000000000000000000000]
[0x0000000000000000000000000000000000000000]
[0x0000000000000000000000000000000000000000]
[0x0000000000000000000000000000000000000000]
[0x0000000000000000000000000000000000000000]
[0x0000000000000000000000000000000000000000]]'
```
::::
### `plain_implementations`
::::description[`Factory.plain_implementations(arg0: uint256, arg1: uint256) -> address: view`]
Getter for the plain implementations index `arg1` for a plain pool with a number of `arg0` coins.
Returns: Plain implementation (`address`).
| Input | Type | Description |
| -------- | --------- | -------------------------- |
| `arg0` | `uint256` | Number of coins in pool |
| `arg1` | `uint256` | Index of implementation |
```vyper
# number of coins -> implementation addresses
# for "plain pools" (as opposed to metapools), implementation contracts
# are organized according to the number of coins in the pool
plain_implementations: public(HashMap[uint256, address[10]])
```
```shell
>>> Factory.plain_implementations(2, 0)
'0x6523Ac15EC152Cb70a334230F6c5d62C5Bd963f1'
```
::::
### `gauge_implementation`
::::description[`Factory.gauge_implementations() -> address: view`]
Getter for the gauge implementation of the Factory.
Returns: gauge implementation (`address`).
```vyper
gauge_implementation: public(address)
```
```shell
>>> Factory.gauge_implementation()
'0x5aE854b098727a9f1603A1E21c50D52DC834D846'
```
::::
### `get_implementation_address`
::::description[`Factory.get_implementation_address(_pool: address) -> address:`]
Getter for the address of the implementation contract used for a factory pool.
Returns: Implementation (`address`).
| Input | Type | Description |
| -------- | --------- | ------------------------ |
| `_pool` | `address` | Factory pool address |
```vyper
@view
@external
def get_implementation_address(_pool: address) -> address:
"""
@notice Get the address of the implementation contract used for a factory pool
@param _pool Pool address
@return Implementation contract address
"""
return self.pool_data[_pool].implementation
```
```shell
>>> Factory.gauge_implementation()
'0x5aE854b098727a9f1603A1E21c50D52DC834D846'
```
::::
## Set New Implementation
*New implementations can be set via these admin-only functions:*
### `set_metapool_implementation`
::::description[`Factory.set_metapool_implementations(_base_pool: address, _implementations: address[10]):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set new metapool implementations.
| Input | Type | Description |
| ------------------- | --------------- | ------------------------------------ |
| `_base_pool` | `address` | Base pool to add implementations for |
| `_implementations` | `address[10]` | New metapool implementations |
```vyper
base_pool_data: HashMap[address, BasePoolArray]
@external
def set_metapool_implementations(
_base_pool: address,
_implementations: address[10],
):
"""
@notice Set implementation contracts for a metapool
@dev Only callable by admin
@param _base_pool Pool address to add
@param _implementations Implementation address to use when deploying metapools
"""
assert msg.sender == self.admin # dev: admin-only function
assert self.base_pool_data[_base_pool].coins[0] != ZERO_ADDRESS # dev: base pool does not exist
for i in range(10):
new_imp: address = _implementations[i]
current_imp: address = self.base_pool_data[_base_pool].implementations[i]
if new_imp == current_imp:
if new_imp == ZERO_ADDRESS:
break
else:
self.base_pool_data[_base_pool].implementations[i] = new_imp
```
```shell
>>> Factory.set_metapool_implementation("todo")
'todo'
```
::::
### `set_plain_implementation`
::::description[`Factory.set_plain_implementations(_n_coins: uint256, _implementations: address[10]):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set new plain pool implementations.
| Input | Type | Description |
| ---------------------- | ---------- | --------------------------------------------- |
| `_n_coins` | `uint256` | Number of coins in pool to set implementations for |
| `_pool_implementation` | `address` | New plain pool implementations |
```vyper
# number of coins -> implementation addresses
# for "plain pools" (as opposed to metapools), implementation contracts
# are organized according to the number of coins in the pool
plain_implementations: public(HashMap[uint256, address[10]])
@external
def set_plain_implementations(
_n_coins: uint256,
_implementations: address[10],
):
assert msg.sender == self.admin # dev: admin-only function
for i in range(10):
new_imp: address = _implementations[i]
current_imp: address = self.plain_implementations[_n_coins][i]
if new_imp == current_imp:
if new_imp == ZERO_ADDRESS:
break
else:
self.plain_implementations[_n_coins][i] = new_imp
```
```shell
>>> Factory.set_plain_implementation("todo")
'todo'
```
::::
### `set_gauge_implementation`
::::description[`Factory.set_gauge_implementation(_gauge_implementation: address):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new gauge implementation contract.
| Input | Type | Description |
| ----------------------- | --------- | -------------------------- |
| `_gauge_implementation` | `address` | New gauge implementation |
```vyper
gauge_implementation: public(address)
@external
def set_gauge_implementation(_gauge_implementation: address):
assert msg.sender == self.admin # dev: admin-only function
self.gauge_implementation = _gauge_implementation
```
```shell
>>> Factory.set_gauge_implementation("todo")
'todo'
```
::::
---
## Overview(Stableswap)
The Metapool Factory, which was the first Curve Factory, allows the permissionless deployment of regular [stableswap](../../stableswap/overview.md) plain and metapools, LP tokens, and liquidity gauges. **Liquidity pool and LP token DO NOT share the same contract.**:::deploy[Contract Source & Deployment]
The **Metapool Factory**contract is deployed to the Ethereum mainnet at: [0xb9fc157394af804a3578134a6585c0dc9cc990d4](https://etherscan.io/address/0xb9fc157394af804a3578134a6585c0dc9cc990d4#code).
A list of all deployed contracts can be found [here](../../../../deployments.md).
:::
## Implementations
More on [implementations](./implementations.md).
---
## Overview(Cross-asset-swaps)
Curve integrates with Synthetix to allow large scale swaps between different asset classes with minimal
slippage. Utilizing Synthetix’ zero-slippage synth conversions and Curve’s deep liquidity and low fees,
we can perform fully on-chain cross asset swaps at scale with a 0.38% fee and minimal slippage.
Cross asset swaps are performed using the `SynthSwap` contract, deployed to the mainnet at
[0x58A3c68e2D3aAf316239c003779F71aCb870Ee47](https://etherscan.io/address/0x58A3c68e2D3aAf316239c003779F71aCb870Ee47).
Source code and information on the technical implementation are available on
[Github](https://github.com/curvefi/curve-cross-asset-swaps).
## How `SynthSwap` works
As an example, suppose we have asset `A` and wish to exchange it for asset `D`. For this swap to be possible,
`A` and `D` must meet the following requirements:
- must be of different asset classes (e.g. USD, EUR, BTC, ETH),
- must be exchangeable for a Synthetic asset within one of Curve’s pools (e.g. sUSD, sBTC).
The swap can be visualized as `A -> B -> C | C -> D`:
1. The initial asset `A` is exchanged on Curve for `B`, a synth of the same asset class.
2. `B` is converted to `C`, a synth of the same asset class as `D`.
3. A [settlement period](https://docs.synthetix.io/integrations/settlement/) passes to account for sudden price
movements between `B` and `C`.
4. Once the [settlement period](https://docs.synthetix.io/integrations/settlement/) has passed, `C` is exchanged on
Curve for the desired asset `D`.
For a more detailed reasoning behind the settlement period logic, refer to
[Synthetix SIP-37](https://sips.synthetix.io/sips/sip-37/).
## Settler NFT
Swaps cannot occur atomically due to the Synthetix settlement period. Each unsettled swap is represented by an ERC721
non-fungible token.
Each NFT has a unique token ID. Token IDs are never re-used. The NFT is minted upon initiating the swap and burned
when the swap is completed.
The NFT, and associated right to claim, is fully transferable. It is not possible to transfer the rights to a partial
claim. The approved operator for an NFT also has the right to complete the swap with the underlying asset.
Token IDs are not sequential. This contract does not support the enumerable ERC721 extension. This decision is based
on gas efficiency.
## Front-running Considerations
The benefits from these swaps are most apparent when the exchange amount is greater than $1m USD equivalent. As such,
the initiation of a swap gives a strong indicator other market participants that a 2nd post-settlement swap will be
coming. We attempt to minimize the risks from this in several ways:
`C -> D` is not declared on-chain when performing the swap from `A -> C`.
It is possible to perform a partial swap from `C -> D`, and to swap into multiple final assets. The NFT persists until
it has no remaining underlying balance of `C`.
There is no fixed time frame for the second swap. A user can perform it immediately or wait until market conditions
are more favorable.
It is possible to withdraw `C` without performing a second swap.
It is possible to perform additional `A -> B -> C` swaps to increase the balance of an already existing NFT.
The range of available actions and time frames make it significantly more difficult to predict the outcome of a swap
and trade against it.
---
## synth -> curve pool where it can be traded
This section discusses the different methods in the Curve
[SynthSwap](https://etherscan.io/address/0x58A3c68e2D3aAf316239c003779F71aCb870Ee47) contract.
## Adding and Finding Swappable Assets
In general, any asset that is within a Curve pool also containing a Synth may be used in a cross asset swap.
### `SynthSwap.add_synth`
::::description[`def add_synth(_synth: address, _pool: address)`]
Add a new swappable synth. This method is callable by anyone, however `_pool` must exist within the Curve
pool registry and `_synth` must be a valid synth that is swappable within the pool.
| Input | Type | Description |
| ----------- | -------| ----|
| `_synth` | `address` | Address of the synth |
| `_pool` | `address` | Address of Curve pool containing the synth |
Emits: NewSynth
```vyper
@external
def add_synth(_synth: address, _pool: address):
"""
@notice Add a new swappable synth
@dev Callable by anyone, however `_pool` must exist within the Curve
pool registry and `_synth` must be a valid synth that is swappable
within the pool
@param _synth Address of the synth to add
@param _pool Address of the Curve pool where `_synth` is swappable
"""
assert self.synth_pools[_synth] == ZERO_ADDRESS # dev: already added
# this will revert if `_synth` is not actually a synth
self.currency_keys[_synth] = Synth(_synth).currencyKey()
registry: address = AddressProvider(ADDRESS_PROVIDER).get_registry()
pool_coins: address[8] = Registry(registry).get_coins(_pool)
has_synth: bool = False
for coin in pool_coins:
if coin == ZERO_ADDRESS:
assert has_synth # dev: synth not in pool
break
if coin == _synth:
self.synth_pools[_synth] = _pool
has_synth = True
self.swappable_synth[coin] = _synth
log NewSynth(_synth, _pool)
```
```shell
>>> todo:
```
::::
### `SynthSwap.synth_pools`
::::description[`SynthSwap.synth_pools(_synth: address) → address: view`]
Get the address of the Curve pool used to swap a synthetic asset. If this function returns `ZERO_ADDRESS`,
the given synth cannot be used within cross-asset swaps.
| Input | Type | Description |
| ----------- | -------| ----|
| `_synth` | `address` | Address of the synth |
```vyper
# synth -> curve pool where it can be traded
synth_pools: public(HashMap[address, address])
...
```
```shell
>>> todo:
```
::::
### `SynthSwap.swappable_synth`
::::description[`SynthSwap.swappable_synth(_token: address) → address: view`]
Get the address of the synthetic asset that `_token` may be directly swapped for. If this function returns
`ZERO_ADDRESS`, `_token` cannot be used within a cross-asset swap.
| Input | Type | Description |
| ----------- | -------| ----|
| `_token` | `address` | Address of the synth |
```vyper
# synth -> curve pool where it can be traded
synth_pools: public(HashMap[address, address])
...
```
```shell
>>> todo:
```
::::
## Estimate Swap Amounts
### `SynthSwap.get_swap_into_synth_amount`
::::description[`SynthSwap.get_swap_into_synth_amount(_from: address, _synth: address, _amount: uint256) → uint256: view`]
Returns the expected amount of `_synth` received in the swap.
| Input | Type | Description |
| ----------- | -------| ----|
| `_from` | `address` | Address of the initial asset being exchanged |
| `_synth` | `address` | Address of the synth being swapped into |
| `_amount` | `uint256` | Amount of _from to swap |
```vyper
@view
@internal
def _get_swap_into(_from: address, _synth: address, _amount: uint256) -> uint256:
registry: address = AddressProvider(ADDRESS_PROVIDER).get_registry()
intermediate_synth: address = self.swappable_synth[_from]
pool: address = self.synth_pools[intermediate_synth]
synth_amount: uint256 = _amount
if _from != intermediate_synth:
i: int128 = 0
j: int128 = 0
i, j = Registry(registry).get_coin_indices(pool, _from, intermediate_synth)
synth_amount = Curve(pool).get_dy(i, j, _amount)
return self.exchanger.getAmountsForExchange(
synth_amount,
self.currency_keys[intermediate_synth],
self.currency_keys[_synth],
)[0]
@view
@external
def get_swap_into_synth_amount(_from: address, _synth: address, _amount: uint256) -> uint256:
"""
@notice Return the amount received when performing a cross-asset swap
@dev Used to calculate `_expected` when calling `swap_into_synth`. Be sure to
reduce the value slightly to account for market movement prior to the
transaction confirmation.
@param _from Address of the initial asset being exchanged
@param _synth Address of the synth being swapped into
@param _amount Amount of `_from` to swap
@return uint256 Expected amount of `_synth` received
"""
return self._get_swap_into(_from, _synth, _amount)
```
```shell
>>> synth_swap = Contract('0x58A3c68e2D3aAf316239c003779F71aCb870Ee47')
>>> dai = Contract('0x6b175474e89094c44da98b954eedeac495271d0f')
>>> sbtc = Contract('0xfe18be6b3bd88a2d2a7f928d00292e7a9963cfc6')
>>> synthswap.get_swap_into_synth_amount(dai, sbtc, 100000 * 1e18)
2720559215249173192
```
:::note
This method is used to calculate `_expected` when calling `swap_into_synth`. You should reduce the value
slightly to account for market movement prior to the transaction confirming.
:::
::::
### `SynthSwap.get_swap_from_synth_amount`
::::description[`SynthSwap.get_swap_from_synth_amount(_synth: address, _to: address, _amount: uint256) → uint256: view`]
Returns the expected amount of `_to` received in the swap.
| Input | Type | Description |
| ----------- | -------| ----|
| `_synth` | `address` | Address of the synth being swapped out of |
| `_to` | `address` | Address of the asset to swap into |
| `_amount` | `uint256` | Amount of `_synth` to swap |
```vyper
@view
@internal
def _get_swap_from(_synth: address, _to: address, _amount: uint256) -> uint256:
registry: address = AddressProvider(ADDRESS_PROVIDER).get_registry()
pool: address = self.synth_pools[_synth]
i: int128 = 0
j: int128 = 0
i, j = Registry(registry).get_coin_indices(pool, _synth, _to)
return Curve(pool).get_dy(i, j, _amount)
@view
@external
def get_swap_from_synth_amount(_synth: address, _to: address, _amount: uint256) -> uint256:
"""
@notice Return the amount received when swapping out of a settled synth
@dev Used to calculate `_expected` when calling `swap_from_synth`. Be sure to
reduce the value slightly to account for market movement prior to the
transaction confirmation.
@param _synth Address of the synth being swapped out of
@param _to Address of the asset to swap into
@param _amount Amount of `_synth` being exchanged
@return uint256 Expected amount of `_to` received
"""
return self._get_swap_from(_synth, _to, _amount)
```
```shell
>>> synth_swap = Contract('0x58A3c68e2D3aAf316239c003779F71aCb870Ee47')
>>> sbtc = Contract('0xfe18be6b3bd88a2d2a7f928d00292e7a9963cfc6')
>>> wbtc = Contract('0x2260fac5e5542a773aa44fbcfedf7c193bc2c599')
>>> synthswap.get_swap_from_synth_amount(sbtc, wbtc, 2720559215249173192)
273663013
```
::::
### `SynthSwap.get_estimated_swap_amount`
::::description[`SynthSwap.get_estimated_swap_amount(_from: address, _to: address, _amount: uint256) → uint256: view`]
Estimate the final amount of `_to` received when swapping between `_from` and `_to`.
| Input | Type | Description |
| ----------- | -------| ----|
| `_from` | `address` | Address of the initial asset being exchanged |
| `_to` | `address` | Address of the asset to swap into |
| `_amount` | `uint256` | Amount of `_from` to swap |
```vyper
@view
@internal
def _get_swap_into(_from: address, _synth: address, _amount: uint256) -> uint256:
registry: address = AddressProvider(ADDRESS_PROVIDER).get_registry()
intermediate_synth: address = self.swappable_synth[_from]
pool: address = self.synth_pools[intermediate_synth]
synth_amount: uint256 = _amount
if _from != intermediate_synth:
i: int128 = 0
j: int128 = 0
i, j = Registry(registry).get_coin_indices(pool, _from, intermediate_synth)
synth_amount = Curve(pool).get_dy(i, j, _amount)
return self.exchanger.getAmountsForExchange(
synth_amount,
self.currency_keys[intermediate_synth],
self.currency_keys[_synth],
)[0]
...
@view
@internal
def _get_swap_from(_synth: address, _to: address, _amount: uint256) -> uint256:
registry: address = AddressProvider(ADDRESS_PROVIDER).get_registry()
pool: address = self.synth_pools[_synth]
i: int128 = 0
j: int128 = 0
i, j = Registry(registry).get_coin_indices(pool, _synth, _to)
return Curve(pool).get_dy(i, j, _amount)
...
@view
@external
def get_estimated_swap_amount(_from: address, _to: address, _amount: uint256) -> uint256:
"""
@notice Estimate the final amount received when swapping between `_from` and `_to`
@dev Actual received amount may be different if synth rates change during settlement
@param _from Address of the initial asset being exchanged
@param _to Address of the asset to swap into
@param _amount Amount of `_from` being exchanged
@return uint256 Estimated amount of `_to` received
"""
synth: address = self.swappable_synth[_to]
synth_amount: uint256 = self._get_swap_into(_from, synth, _amount)
return self._get_swap_from(synth, _to, synth_amount)
```
```shell
>>> synth_swap = Contract('0x58A3c68e2D3aAf316239c003779F71aCb870Ee47')
>>> dai = Contract('0x6b175474e89094c44da98b954eedeac495271d0f')
>>> wbtc = Contract('0x2260fac5e5542a773aa44fbcfedf7c193bc2c599')
>>> synthswap.get_estimated_swap_amount(dai, wbtc, 100000 * 1e18)
273663013
```
:::note
This method is for estimating the received amount from a complete swap over two transactions. If `_to` is a
Synth, you should use `get_swap_into_synth_amount` instead.
:::
:::note
As swaps take a settlement period into account, the actual received amount may be different due to rate changes
during the settlement period.
:::
::::
## Initiate a Swap
### `SynthSwap.swap_into_synth`
::::description[`SynthSwap.swap_into_synth(_from: address, _synth: address, _amount: uint256, _expected: uint256, _receiver: address = msg.sender, _existing_token_id: uint256 = 0) → uint256: payable`]
Perform a cross-asset swap between `_from` and `_synth`. Returns the `uint256` token ID of the NFT representing
the unsettled swap. The token ID is also available from the emitted `TokenUpdate` event.
| Input | Type | Description |
| ----------- | -------| ----|
| `_from` | `address` | Address of the initial asset being exchanged. For Ether swaps, use `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`. |
| `_synth` | `address` | Address of the synth to swap into |
| `_amount` | `uint256` | Amount of `_from` to swap. If you are swapping from Ether, you must also send exactly this much Ether with the transaction. If you are swapping any other asset, you must have given approval to the swap contract to transfer at least this amount. |
| `_expected` | `uint256` | Minimum amount of `_synth` to receive |
| `_receiver` | `address` | Address of the recipient of `_synth`. Defaults to the `msg.sender`. |
| `_existing_token_id` | `uint256` | Token ID to deposit `_synth` into. If not given, a new NFT is minted for the generated synth. When set as non-zero, the token ID must be owned by the caller and must already represent the same synth as is being swapped into. |
Emits: NewSettler
Transfer
TokenUpdate
```vyper
@payable
@external
def swap_into_synth(
_from: address,
_synth: address,
_amount: uint256,
_expected: uint256,
_receiver: address = msg.sender,
_existing_token_id: uint256 = 0,
) -> uint256:
"""
@notice Perform a cross-asset swap between `_from` and `_synth`
@dev Synth swaps require a settlement time to complete and so the newly
generated synth cannot immediately be transferred onward. Calling
this function mints an NFT which represents ownership of the generated
synth. Once the settlement time has passed, the owner may claim the
synth by calling to `swap_from_synth` or `withdraw`.
@param _from Address of the initial asset being exchanged
@param _synth Address of the synth being swapped into
@param _amount Amount of `_from` to swap
@param _expected Minimum amount of `_synth` to receive
@param _receiver Address of the recipient of `_synth`, if not given
defaults to `msg.sender`
@param _existing_token_id Token ID to deposit `_synth` into. If left as 0, a new NFT
is minted for the generated synth. If non-zero, the token ID
must be owned by `msg.sender` and must represent the same
synth as is being swapped into.
@return uint256 NFT token ID
"""
settler: address = ZERO_ADDRESS
token_id: uint256 = 0
if _existing_token_id == 0:
# if no token ID is given we are initiating a new swap
count: uint256 = self.id_count
if count == 0:
# if there are no availale settler contracts we must deploy a new one
settler = create_forwarder_to(self.settler_implementation)
Settler(settler).initialize()
token_id = convert(settler, uint256)
log NewSettler(settler)
else:
count -= 1
token_id = self.available_token_ids[count]
settler = convert(token_id % (2**160), address)
self.id_count = count
else:
# if a token ID is given we are adding to the balance of an existing swap
# so must check to make sure this is a permitted action
settler = convert(_existing_token_id % (2**160), address)
token_id = _existing_token_id
owner: address = self.id_to_owner[_existing_token_id]
if msg.sender != owner:
assert owner != ZERO_ADDRESS, "Unknown Token ID"
assert (
self.owner_to_operators[owner][msg.sender] or
msg.sender == self.id_to_approval[_existing_token_id]
), "Caller is not owner or operator"
assert owner == _receiver, "Receiver is not owner"
assert Settler(settler).synth() == _synth, "Incorrect synth for Token ID"
registry_swap: address = AddressProvider(ADDRESS_PROVIDER).get_address(2)
intermediate_synth: address = self.swappable_synth[_from]
synth_amount: uint256 = 0
if intermediate_synth == _from:
# if `_from` is already a synth, no initial curve exchange is required
assert ERC20(_from).transferFrom(msg.sender, settler, _amount)
synth_amount = _amount
else:
if _from != 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE:
# Vyper equivalent of SafeERC20Transfer, handles most ERC20 return values
response: Bytes[32] = raw_call(
_from,
concat(
method_id("transferFrom(address,address,uint256)"),
convert(msg.sender, bytes32),
convert(self, bytes32),
convert(_amount, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
if not self.is_approved[_from][registry_swap]:
response = raw_call(
_from,
concat(
method_id("approve(address,uint256)"),
convert(registry_swap, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
self.is_approved[_from][registry_swap] = True
# use Curve to exchange for initial synth, which is sent to the settler
synth_amount = RegistrySwap(registry_swap).exchange(
self.synth_pools[intermediate_synth],
_from,
intermediate_synth,
_amount,
0,
settler,
value=msg.value
)
# use Synthetix to convert initial synth into the target synth
initial_balance: uint256 = ERC20(_synth).balanceOf(settler)
Settler(settler).convert_synth(
_synth,
synth_amount,
self.currency_keys[intermediate_synth],
self.currency_keys[_synth]
)
final_balance: uint256 = ERC20(_synth).balanceOf(settler)
assert final_balance - initial_balance >= _expected, "Rekt by slippage"
# if this is a new swap, mint an NFT to represent the unsettled conversion
if _existing_token_id == 0:
self.id_to_owner[token_id] = _receiver
self.owner_to_token_count[_receiver] += 1
log Transfer(ZERO_ADDRESS, _receiver, token_id)
log TokenUpdate(token_id, _receiver, _synth, final_balance)
return token_id
```
```shell
>>> alice = accounts[0]
>>> synth_swap = Contract('0x58A3c68e2D3aAf316239c003779F71aCb870Ee47')
>>> dai = Contract('0x6b175474e89094c44da98b954eedeac495271d0f')
>>> sbtc = Contract('0xfe18be6b3bd88a2d2a7f928d00292e7a9963cfc6')
>>> expected = synth_swap.get_swap_into_synth_amount(dai, sbtc, dai.balanceOf(alice)) * 0.99
>>> tx = synth_swap.swap_into_synth(dai, sbtc, expected, {'from': alice})
Transaction sent: 0x83b311af19be08b8ec6241c3e834ccdf3b22586971de82a76a641e43bdf2b3ee
Gas price: 20 gwei Gas limit: 1200000 Nonce: 5
>>> tx.events['TokenUpdate']['token_id']
2423994707895209386239865227163451060473904619065
```
:::note
Synth swaps require a settlement time to complete and so the newly generated synth cannot immediately be
transferred onward. Calling this function mints an NFT representing ownership of the unsettled synth.
:::
::::
## Get Info about an Unsettled Swap
### `SynthSwap.token_info`
::::description[`SynthSwap.token_info(_token_id: uint256) → address, address, uint256, uint256: view`]
Get information about the underlying synth represented by an NFT.
Returns:
- the `address` of the owner of the NFT
- the `address` of the underlying synth
- the balance (`uint256`) of the underlying synth
- the current maximum number of seconds until the synth may be settled (`uint256`)
| Input | Type | Description |
| ----------- | -------| ----|
| `_token_id` | `uint256` | NFT token ID to query info about. Reverts if the token ID does not exist. |
```vyper
@view
@external
def token_info(_token_id: uint256) -> TokenInfo:
"""
@notice Get information about the synth represented by an NFT
@param _token_id NFT token ID to query info about
@return NFT owner
Address of synth within the NFT
Balance of the synth
Max settlement time in seconds
"""
info: TokenInfo = empty(TokenInfo)
info.owner = self.id_to_owner[_token_id]
assert info.owner != ZERO_ADDRESS
settler: address = convert(_token_id % (2**160), address)
info.synth = Settler(settler).synth()
info.underlying_balance = ERC20(info.synth).balanceOf(settler)
if not self.is_settled[_token_id]:
currency_key: bytes32 = self.currency_keys[info.synth]
reclaim: uint256 = 0
rebate: uint256 = 0
reclaim, rebate = self.exchanger.settlementOwing(settler, currency_key)
info.underlying_balance = info.underlying_balance - reclaim + rebate
info.time_to_settle = self.exchanger.maxSecsLeftInWaitingPeriod(settler, currency_key)
return info
```
```shell
>>> synth_swap = Contract('0x58A3c68e2D3aAf316239c003779F71aCb870Ee47')
>>> synthswap.token_info(2423994707895209386239865227163451060473904619065).dict()
{
'owner': "0xEF422dBBF46120dE627fFb913C9AFaD44c735618",
'synth': "0x57Ab1ec28D129707052df4dF418D58a2D46d5f51",
'time_to_settle': 0,
'underlying_balance': 1155647333395694644849
}
```
::::
## Complete a Swap
### `SynthSwap.swap_from_synth`
::::description[`SynthSwap.swap_from_synth(_token_id: uint256, _to: address, _amount: uint256, _expected: uint256, _receiver: address = msg.sender) → uint256: nonpayable`]
Swap the underlying synth represented by an NFT into another asset. Callable by the owner or operator of
`_token_id` after the synth settlement period has passed. If `_amount` is equal to the total remaining balance of
the synth represented by the NFT, the NFT is burned.
Returns the remaining balance of the underlying synth within the active NFT.
| Input | Type | Description |
| ----------- | -------| ----|
| `_token_id` | `uint256` | The identifier for an NFT |
| `_to` | `address` | Address of the asset to swap into |
| `_amount` | `uint256` | Amount of the underlying synth to swap |
| `_expected` | `uint256` | Minimum amount of `_to` to receive |
| `_receiver` | `address` | Address to send the final received asset to. Defaults to `msg.sender`. |
Emits: Transfer
TokenUpdate
```vyper
@external
def swap_from_synth(
_token_id: uint256,
_to: address,
_amount: uint256,
_expected: uint256,
_receiver: address = msg.sender,
) -> uint256:
"""
@notice Swap the synth represented by an NFT into another asset.
@dev Callable by the owner or operator of `_token_id` after the synth settlement
period has passed. If `_amount` is equal to the entire balance within
the NFT, the NFT is burned.
@param _token_id The identifier for an NFT
@param _to Address of the asset to swap into
@param _amount Amount of the synth to swap
@param _expected Minimum amount of `_to` to receive
@param _receiver Address of the recipient of the synth,
if not given defaults to `msg.sender`
@return uint256 Synth balance remaining in `_token_id`
"""
owner: address = self.id_to_owner[_token_id]
if msg.sender != self.id_to_owner[_token_id]:
assert owner != ZERO_ADDRESS, "Unknown Token ID"
assert (
self.owner_to_operators[owner][msg.sender] or
msg.sender == self.id_to_approval[_token_id]
), "Caller is not owner or operator"
settler: address = convert(_token_id % (2**160), address)
synth: address = self.swappable_synth[_to]
pool: address = self.synth_pools[synth]
# ensure the synth is settled prior to swapping
if not self.is_settled[_token_id]:
currency_key: bytes32 = self.currency_keys[synth]
self.exchanger.settle(settler, currency_key)
self.is_settled[_token_id] = True
# use Curve to exchange the synth for another asset which is sent to the receiver
remaining: uint256 = Settler(settler).exchange(_to, pool, _amount, _expected, _receiver)
# if the balance of the synth within the NFT is now zero, burn the NFT
if remaining == 0:
self.id_to_owner[_token_id] = ZERO_ADDRESS
self.id_to_approval[_token_id] = ZERO_ADDRESS
self.is_settled[_token_id] = False
self.owner_to_token_count[msg.sender] -= 1
count: uint256 = self.id_count
# add 2**160 to increment the nonce for next time this settler is used
self.available_token_ids[count] = _token_id + 2**160
self.id_count = count + 1
owner = ZERO_ADDRESS
synth = ZERO_ADDRESS
log Transfer(msg.sender, ZERO_ADDRESS, _token_id)
log TokenUpdate(_token_id, owner, synth, remaining)
return remaining
```
```shell
>>> wbtc = Contract('0x2260fac5e5542a773aa44fbcfedf7c193bc2c599')
>>> amount = synth_swap.token_info(token_id)['underlying_balance']
>>> expected = swynth_swap.get_swap_from_synth_amount(sbtc, wbtc, amount) * 0.99
>>> synth_swap.swap_from_synth(token_id, wbtc, amount, expected, {'from': alice})
Transaction sent: 0x83b311af19be08b8ec6241c3e834ccdf3b22586971de82a76a641e43bdf2b3ee
Gas price: 20 gwei Gas limit: 800000 Nonce: 6
```
::::
### `SynthSwap.withdraw`
::::description[`StableSwap.withdraw(_token_id: uint256, _amount: uint256, _receiver: address = msg.sender) → uint256: nonpayable`]
Withdraw the underlying synth represented by an NFT. Callable by the owner or operator of `_token_id`
after the synth settlement period has passed. If `_amount` is equal to the total remaining balance of the
synth represented by the NFT, the NFT is burned.
Returns the remaining balance of the underlying synth within the active NFT.
| Input | Type | Description |
| ----------- | -------| ----|
| `_token_id` | `uint256` | The identifier for an NFT |
| `_amount` | `uint256` | Amount of the underlying synth to swap |
| `_receiver` | `address` | Address of the recipient of the withdrawn synth. Defaults to the `msg.sender`. |
Emits: Transfer
TokenUpdate
```vyper
@external
def withdraw(_token_id: uint256, _amount: uint256, _receiver: address = msg.sender) -> uint256:
"""
@notice Withdraw the synth represented by an NFT.
@dev Callable by the owner or operator of `_token_id` after the synth settlement
period has passed. If `_amount` is equal to the entire balance within
the NFT, the NFT is burned.
@param _token_id The identifier for an NFT
@param _amount Amount of the synth to withdraw
@param _receiver Address of the recipient of the synth,
if not given defaults to `msg.sender`
@return uint256 Synth balance remaining in `_token_id`
"""
owner: address = self.id_to_owner[_token_id]
if msg.sender != self.id_to_owner[_token_id]:
assert owner != ZERO_ADDRESS, "Unknown Token ID"
assert (
self.owner_to_operators[owner][msg.sender] or
msg.sender == self.id_to_approval[_token_id]
), "Caller is not owner or operator"
settler: address = convert(_token_id % (2**160), address)
synth: address = Settler(settler).synth()
# ensure the synth is settled prior to withdrawal
if not self.is_settled[_token_id]:
currency_key: bytes32 = self.currency_keys[synth]
self.exchanger.settle(settler, currency_key)
self.is_settled[_token_id] = True
remaining: uint256 = Settler(settler).withdraw(_receiver, _amount)
# if the balance of the synth within the NFT is now zero, burn the NFT
if remaining == 0:
self.id_to_owner[_token_id] = ZERO_ADDRESS
self.id_to_approval[_token_id] = ZERO_ADDRESS
self.is_settled[_token_id] = False
self.owner_to_token_count[msg.sender] -= 1
count: uint256 = self.id_count
# add 2**160 to increment the nonce for next time this settler is used
self.available_token_ids[count] = _token_id + 2**160
self.id_count = count + 1
owner = ZERO_ADDRESS
synth = ZERO_ADDRESS
log Transfer(msg.sender, ZERO_ADDRESS, _token_id)
log TokenUpdate(_token_id, owner, synth, remaining)
return remaining
```
```shell
>>> amount = synth_swap.token_info(token_id)['underlying_balance']
>>> synth_swap.withdraw(token_id, amount, {'from': alice})
Transaction sent: 0x83b311af19be08b8ec6241c3e834ccdf3b22586971de82a76a641e43bdf2b3ee
Gas price: 20 gwei Gas limit: 800000 Nonce: 6
```
::::
### `SynthSwap.settle`
::::description[`StableSwap.settle(_token_id: uint256) → bool: nonpayable`]
Settle the synth represented in an NFT. Note that settlement is performed when swapping or withdrawing,
there is no requirement to call this function separately. Returns `True`.
| Input | Type | Description |
| ----------- | -------| ----|
| `_token_id` | `uint256` | The identifier for an NFT |
```vyper
@external
def settle(_token_id: uint256) -> bool:
"""
@notice Settle the synth represented in an NFT.
@dev Settlement is performed when swapping or withdrawing, there
is no requirement to call this function separately.
@param _token_id The identifier for an NFT
@return bool Success
"""
if not self.is_settled[_token_id]:
assert self.id_to_owner[_token_id] != ZERO_ADDRESS, "Unknown Token ID"
settler: address = convert(_token_id % (2**160), address)
synth: address = Settler(settler).synth()
currency_key: bytes32 = self.currency_keys[synth]
self.exchanger.settle(settler, currency_key) # dev: settlement failed
self.is_settled[_token_id] = True
return True
```
::::
---
## Deposit Zap (Old)
While Curve lending pools support swaps in both the wrapped _and_ underlying coins, not all lending pools allow
liquidity providers to deposit or withdraw the underlying coin.
For example, the `Compound` Pool allows swaps between `cDai` and `cUSDC` (wrapped coins), as well as swaps between
`DAI` and `USDC` (underlying coins). However, liquidity providers are not able to deposit `DAI` or `USDC` to the
pool directly. The main reason for why this is not supported by all Curve lending pools lies in the
[size limit of contracts](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-170.md). Lending pools may differ in
complexity and can end up being very close to the
contract byte code size limit. In order to overcome this restriction, liquidity can be added and
removed to and from a lending pool in the underlying coins via a different contract, called a
deposit zap, tailored to lending pools.
For an overview of the Curve lending pool implementation, please refer to the Lending Pool section.
The template source code for a lending pool deposit zap may be viewed on
[GitHub](https://github.com/curvefi/curve-contract/blob/master/contracts/pool-templates/y/DepositTemplateY.vy).
:::note
Lending pool deposit zaps may differ in their API. Older pools do not implement the
[newer API template](https://github.com/curvefi/curve-contract/blob/master/contracts/pool-templates/y/DepositTemplateY.vy).
:::
# Deposit Zap (Old)
Older Curve lending pool deposit zaps do not implement the
[template API](https://github.com/curvefi/curve-contract/blob/master/contracts/pool-templates/y/DepositTemplateY.vy).
The deposit zaps which employ an older API are:
- `DepositBUSD`: [BUSD pool deposit zap](https://etherscan.io/address/0xb6c057591e073249f2d9d88ba59a46cfc9b59edb#code)
- `DepositCompound`: [Compound pool deposit zap](https://etherscan.io/address/0xeb21209ae4c2c9ff2a86aca31e123764a3b6bc06#code)
- `DepositPAX`: [PAX pool deposit zap](https://etherscan.io/address/0xa50ccc70b6a011cffddf45057e39679379187287#code)
- `DepositUSDT`: [USDT pool deposit zap](https://etherscan.io/address/0xac795d2c97e60df6a99ff1c814727302fd747a80#code)
- `DepositY`: [Y pool deposit zap](https://etherscan.io/address/0xbbc81d23ea2c3ec7e56d39296f0cbb648873a5d3#code)
While not a lending pool, note that the following contract also implements the newer deposit zap API:
- `DepositSUSD`: [SUSD pool deposit zap](https://etherscan.io/address/0xfcba3e75865d2d561be8d220616520c171f12851#code)
:::note
Getters generated for public arrays changed between Vyper `0.1.x` and `0.2.x` to accept `uint256`
instead of `int128` in order to handle the lookups. Older deposit zap contracts (v1) use
vyper `0.1.x...`, while newer zaps (v2) use vyper `0.2.x...`.
:::
The following Brownie console interaction examples are using the
[Compound Pool Deposit Zap](https://etherscan.io/address/0xeb21209ae4c2c9ff2a86aca31e123764a3b6bc06).
## Get Deposit Zap Information
### `DepositZap.curve`
::::description[`DepositZap.curve() → address: view`]
Getter for the pool associated with this deposit contract.
```vyper hl_lines="3 13"
coins: public(address[N_COINS])
underlying_coins: public(address[N_COINS])
curve: public(address)
token: public(address)
...
@public
def __init__(_coins: address[N_COINS], _underlying_coins: address[N_COINS],
_curve: address, _token: address):
self.coins = _coins
self.underlying_coins = _underlying_coins
self.curve = _curve
self.token = _token
```
```shell
>>> zap.curve()
'0xA2B47E3D5c44877cca798226B7B8118F9BFb7A56'
```
::::
### `DepositZap.underlying_coins`
::::description[`DepositZap.underlying_coins(i: int128) → address: view`]
Getter for the array of underlying coins within the associated pool.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | Index of the underlying coin for which to get the address|
```vyper hl_lines="6 16"
N_COINS: constant(int128) = 4
...
coins: public(address[N_COINS])
underlying_coins: public(address[N_COINS])
curve: public(address)
token: public(address)
...
@public
def __init__(_coins: address[N_COINS], _underlying_coins: address[N_COINS],
_curve: address, _token: address):
self.coins = _coins
self.underlying_coins = _underlying_coins
self.curve = _curve
self.token = _token
```
```shell
>>> zap.underlying_coins(0)
'0x6B175474E89094C44Da98b954EedeAC495271d0F'
>>> zap.underlying_coins(1)
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
```
::::
### `DepositZap.coins`
::::description[`DepositZap.coins(i: int128) → address: view`]
Getter for the array of wrapped coins within the associated pool.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | Index of the coin for which to get the address |
```vyper hl_lines="5 15"
N_COINS: constant(int128) = 4
...
coins: public(address[N_COINS])
underlying_coins: public(address[N_COINS])
curve: public(address)
token: public(address)
...
@public
def __init__(_coins: address[N_COINS], _underlying_coins: address[N_COINS],
_curve: address, _token: address):
self.coins = _coins
self.underlying_coins = _underlying_coins
self.curve = _curve
self.token = _token
```
```shell
>>> zap.coins(0)
'0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643'
>>> zap.coins(1)
'0x39AA39c021dfbaE8faC545936693aC917d5E7563'
```
::::
### `DepositZap.token`
::::description[`DepositZap.token() → address: view`]
Getter for the LP token of the associated pool.
```vyper hl_lines="4 14"
coins: public(address[N_COINS])
underlying_coins: public(address[N_COINS])
curve: public(address)
token: public(address)
...
@public
def __init__(_coins: address[N_COINS], _underlying_coins: address[N_COINS],
_curve: address, _token: address):
self.coins = _coins
self.underlying_coins = _underlying_coins
self.curve = _curve
self.token = _token
```
```shell
>>> zap.coins(0)
'0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643'
>>> zap.coins(1)
'0x39AA39c021dfbaE8faC545936693aC917d5E7563'
```
::::
## Add/Remove Liquidity
### `DepositZap.add_liquidity`
::::description[`DepositZap.add_liquidity(uamounts: uint256[N_COINS], min_mint_amount: uint256)`]
Wrap underlying coins and deposit them in the pool.
| Input | Type | Description |
| ----------- | -------| ----|
| `uamounts` | `uint256[N_COINS]` | List of amounts of underlying coins to deposit |
| `min_mint_amount` | `uint256` | Minimum amount of LP token to mint from the deposit |
Emits: AddLiquidity
Transfer
```vyper
USE_LENDING: constant(bool[N_COINS]) = [True, True]
...
@public
@nonreentrant('lock')
def add_liquidity(uamounts: uint256[N_COINS], min_mint_amount: uint256):
use_lending: bool[N_COINS] = USE_LENDING
tethered: bool[N_COINS] = TETHERED
amounts: uint256[N_COINS] = ZEROS
for i in range(N_COINS):
uamount: uint256 = uamounts[i]
if uamount > 0:
# Transfer the underlying coin from owner
if tethered[i]:
USDT(self.underlying_coins[i]).transferFrom(
msg.sender, self, uamount)
else:
assert_modifiable(ERC20(self.underlying_coins[i])\
.transferFrom(msg.sender, self, uamount))
# Mint if needed
if use_lending[i]:
ERC20(self.underlying_coins[i]).approve(self.coins[i], uamount)
ok: uint256 = cERC20(self.coins[i]).mint(uamount)
if ok > 0:
raise "Could not mint coin"
amounts[i] = cERC20(self.coins[i]).balanceOf(self)
ERC20(self.coins[i]).approve(self.curve, amounts[i])
else:
amounts[i] = uamount
ERC20(self.underlying_coins[i]).approve(self.curve, uamount)
Curve(self.curve).add_liquidity(amounts, min_mint_amount)
tokens: uint256 = ERC20(self.token).balanceOf(self)
assert_modifiable(ERC20(self.token).transfer(msg.sender, tokens))
```
```vyper
@public
@nonreentrant('lock')
def add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256):
# Amounts is amounts of c-tokens
assert not self.is_killed
tethered: bool[N_COINS] = TETHERED
use_lending: bool[N_COINS] = USE_LENDING
fees: uint256[N_COINS] = ZEROS
_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
_admin_fee: uint256 = self.admin_fee
token_supply: uint256 = self.token.totalSupply()
rates: uint256[N_COINS] = self._current_rates()
# Initial invariant
D0: uint256 = 0
old_balances: uint256[N_COINS] = self.balances
if token_supply > 0:
D0 = self.get_D_mem(rates, old_balances)
new_balances: uint256[N_COINS] = old_balances
for i in range(N_COINS):
if token_supply == 0:
assert amounts[i] > 0
# balances store amounts of c-tokens
new_balances[i] = old_balances[i] + amounts[i]
# Invariant after change
D1: uint256 = self.get_D_mem(rates, new_balances)
assert D1 > D0
# We need to recalculate the invariant accounting for fees
# to calculate fair user's share
D2: uint256 = D1
if token_supply > 0:
# Only account for fees if we are not the first to deposit
for i in range(N_COINS):
ideal_balance: uint256 = D1 * old_balances[i] / D0
difference: uint256 = 0
if ideal_balance > new_balances[i]:
difference = ideal_balance - new_balances[i]
else:
difference = new_balances[i] - ideal_balance
fees[i] = _fee * difference / FEE_DENOMINATOR
self.balances[i] = new_balances[i] - (fees[i] * _admin_fee / FEE_DENOMINATOR)
new_balances[i] -= fees[i]
D2 = self.get_D_mem(rates, new_balances)
else:
self.balances = new_balances
# Calculate, how much pool tokens to mint
mint_amount: uint256 = 0
if token_supply == 0:
mint_amount = D1 # Take the dust if there was any
else:
mint_amount = token_supply * (D2 - D0) / D0
assert mint_amount >= min_mint_amount, "Slippage screwed you"
# Take coins from the sender
for i in range(N_COINS):
if tethered[i] and not use_lending[i]:
USDT(self.coins[i]).transferFrom(msg.sender, self, amounts[i])
else:
assert_modifiable(
cERC20(self.coins[i]).transferFrom(msg.sender, self, amounts[i]))
# Mint pool tokens
self.token.mint(msg.sender, mint_amount)
log.AddLiquidity(msg.sender, amounts, fees, D1, token_supply + mint_amount)
```
```vyper
@public
def mint(_to: address, _value: uint256):
"""
@dev Mint an amount of the token and assigns it to an account.
This encapsulates the modification of balances such that the
proper events are emitted.
@param _to The account that will receive the created tokens.
@param _value The amount that will be created.
"""
assert msg.sender == self.minter
assert _to != ZERO_ADDRESS
self.total_supply += _value
self.balanceOf[_to] += _value
log.Transfer(ZERO_ADDRESS, _to, _value)
```
```shell
>>> todo:
```
::::
### `DepositZap.remove_liquidity`
::::description[`DepositZap.remove_liquidity(_amount: uint256, min_uamounts: uint256[N_COINS])`]
Withdraw and unwrap coins from the pool.
| Input | Type | Description |
| ----------- | -------| ----|
| `_amount` | `uint256` | Quantity of LP tokens to burn in the withdrawal |
| `min_uamounts` | `uint256[N_COINS]` | Minimum amounts of underlying coins to receive |
Emits: Transfer
RemoveLiquidity
```vyper
@private
def _send_all(_addr: address, min_uamounts: uint256[N_COINS], one: int128):
use_lending: bool[N_COINS] = USE_LENDING
tethered: bool[N_COINS] = TETHERED
for i in range(N_COINS):
if (one < 0) or (i == one):
if use_lending[i]:
_coin: address = self.coins[i]
_balance: uint256 = cERC20(_coin).balanceOf(self)
if _balance == 0: # Do nothing if there are 0 coins
continue
ok: uint256 = cERC20(_coin).redeem(_balance)
if ok > 0:
raise "Could not redeem coin"
_ucoin: address = self.underlying_coins[i]
_uamount: uint256 = ERC20(_ucoin).balanceOf(self)
assert _uamount >= min_uamounts[i], "Not enough coins withdrawn"
# Send only if we have something to send
if _uamount >= 0:
if tethered[i]:
USDT(_ucoin).transfer(_addr, _uamount)
else:
assert_modifiable(ERC20(_ucoin).transfer(_addr, _uamount))
@public
@nonreentrant('lock')
def remove_liquidity(_amount: uint256, min_uamounts: uint256[N_COINS]):
zeros: uint256[N_COINS] = ZEROS
assert_modifiable(ERC20(self.token).transferFrom(msg.sender, self, _amount))
Curve(self.curve).remove_liquidity(_amount, zeros)
self._send_all(msg.sender, min_uamounts, -1)
```
```vyper
@public
@nonreentrant('lock')
def remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS]):
total_supply: uint256 = self.token.totalSupply()
amounts: uint256[N_COINS] = ZEROS
fees: uint256[N_COINS] = ZEROS
tethered: bool[N_COINS] = TETHERED
use_lending: bool[N_COINS] = USE_LENDING
for i in range(N_COINS):
value: uint256 = self.balances[i] * _amount / total_supply
assert value >= min_amounts[i], "Withdrawal resulted in fewer coins than expected"
self.balances[i] -= value
amounts[i] = value
if tethered[i] and not use_lending[i]:
USDT(self.coins[i]).transfer(msg.sender, value)
else:
assert_modifiable(cERC20(self.coins[i]).transfer(
msg.sender, value))
self.token.burnFrom(msg.sender, _amount) # Will raise if not enough
log.RemoveLiquidity(msg.sender, amounts, fees, total_supply - _amount)
```
```vyper
@private
def _burn(_to: address, _value: uint256):
"""
@dev Internal function that burns an amount of the token of a given
account.
@param _to The account whose tokens will be burned.
@param _value The amount that will be burned.
"""
assert _to != ZERO_ADDRESS
self.total_supply -= _value
self.balanceOf[_to] -= _value
log.Transfer(_to, ZERO_ADDRESS, _value)
...
@public
def burnFrom(_to: address, _value: uint256):
"""
@dev Burn an amount of the token from a given account.
@param _to The account whose tokens will be burned.
@param _value The amount that will be burned.
"""
assert msg.sender == self.minter, "Only minter is allowed to burn"
self._burn(_to, _value)
```
```shell
>>> todo:
```
::::
### `DepositZap.remove_liquidity_imbalance`
::::description[`DepositZap.remove_liquidity_imbalance(uamounts: uint256[N_COINS], max_burn_amount: uint256)`]
Withdraw and unwrap coins from the pool in an imbalanced amount.
| Input | Type | Description |
| ----------- | -------| ----|
| `uamounts` | `uint256[N_COINS]` | List of amounts of underlying coins to withdraw |
| `max_burn_amount` | `uint256` | Maximum amount of LP token to burn in the withdrawal |
Emits: Transfer
RemoveLiquidityImbalance
```vyper
@private
def _send_all(_addr: address, min_uamounts: uint256[N_COINS], one: int128):
use_lending: bool[N_COINS] = USE_LENDING
tethered: bool[N_COINS] = TETHERED
for i in range(N_COINS):
if (one < 0) or (i == one):
if use_lending[i]:
_coin: address = self.coins[i]
_balance: uint256 = cERC20(_coin).balanceOf(self)
if _balance == 0: # Do nothing if there are 0 coins
continue
ok: uint256 = cERC20(_coin).redeem(_balance)
if ok > 0:
raise "Could not redeem coin"
_ucoin: address = self.underlying_coins[i]
_uamount: uint256 = ERC20(_ucoin).balanceOf(self)
assert _uamount >= min_uamounts[i], "Not enough coins withdrawn"
# Send only if we have something to send
if _uamount >= 0:
if tethered[i]:
USDT(_ucoin).transfer(_addr, _uamount)
else:
assert_modifiable(ERC20(_ucoin).transfer(_addr, _uamount))
@public
@nonreentrant('lock')
def remove_liquidity_imbalance(uamounts: uint256[N_COINS], max_burn_amount: uint256):
"""
Get max_burn_amount in, remove requested liquidity and transfer back what is left
"""
use_lending: bool[N_COINS] = USE_LENDING
tethered: bool[N_COINS] = TETHERED
_token: address = self.token
amounts: uint256[N_COINS] = uamounts
for i in range(N_COINS):
if use_lending[i] and amounts[i] > 0:
rate: uint256 = cERC20(self.coins[i]).exchangeRateCurrent()
amounts[i] = amounts[i] * LENDING_PRECISION / rate
# if not use_lending - all good already
# Transfrer max tokens in
_tokens: uint256 = ERC20(_token).balanceOf(msg.sender)
if _tokens > max_burn_amount:
_tokens = max_burn_amount
assert_modifiable(ERC20(_token).transferFrom(msg.sender, self, _tokens))
Curve(self.curve).remove_liquidity_imbalance(amounts, max_burn_amount)
# Transfer unused tokens back
_tokens = ERC20(_token).balanceOf(self)
assert_modifiable(ERC20(_token).transfer(msg.sender, _tokens))
# Unwrap and transfer all the coins we've got
self._send_all(msg.sender, ZEROS, -1)
```
```vyper
@public
@nonreentrant('lock')
def remove_liquidity_imbalance(amounts: uint256[N_COINS], max_burn_amount: uint256):
assert not self.is_killed
tethered: bool[N_COINS] = TETHERED
use_lending: bool[N_COINS] = USE_LENDING
token_supply: uint256 = self.token.totalSupply()
assert token_supply > 0
_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
_admin_fee: uint256 = self.admin_fee
rates: uint256[N_COINS] = self._current_rates()
old_balances: uint256[N_COINS] = self.balances
new_balances: uint256[N_COINS] = old_balances
D0: uint256 = self.get_D_mem(rates, old_balances)
for i in range(N_COINS):
new_balances[i] -= amounts[i]
D1: uint256 = self.get_D_mem(rates, new_balances)
fees: uint256[N_COINS] = ZEROS
for i in range(N_COINS):
ideal_balance: uint256 = D1 * old_balances[i] / D0
difference: uint256 = 0
if ideal_balance > new_balances[i]:
difference = ideal_balance - new_balances[i]
else:
difference = new_balances[i] - ideal_balance
fees[i] = _fee * difference / FEE_DENOMINATOR
self.balances[i] = new_balances[i] - (fees[i] * _admin_fee / FEE_DENOMINATOR)
new_balances[i] -= fees[i]
D2: uint256 = self.get_D_mem(rates, new_balances)
token_amount: uint256 = (D0 - D2) * token_supply / D0
assert token_amount > 0
assert token_amount <= max_burn_amount, "Slippage screwed you"
for i in range(N_COINS):
if tethered[i] and not use_lending[i]:
USDT(self.coins[i]).transfer(msg.sender, amounts[i])
else:
assert_modifiable(cERC20(self.coins[i]).transfer(msg.sender, amounts[i]))
self.token.burnFrom(msg.sender, token_amount) # Will raise if not enough
log.RemoveLiquidityImbalance(msg.sender, amounts, fees, D1, token_supply - token_amount)
```
```vyper
@private
def _burn(_to: address, _value: uint256):
"""
@dev Internal function that burns an amount of the token of a given
account.
@param _to The account whose tokens will be burned.
@param _value The amount that will be burned.
"""
assert _to != ZERO_ADDRESS
self.total_supply -= _value
self.balanceOf[_to] -= _value
log.Transfer(_to, ZERO_ADDRESS, _value)
...
@public
def burnFrom(_to: address, _value: uint256):
"""
@dev Burn an amount of the token from a given account.
@param _to The account whose tokens will be burned.
@param _value The amount that will be burned.
"""
assert msg.sender == self.minter, "Only minter is allowed to burn"
self._burn(_to, _value)
```
```shell
>>> todo:
```
::::
### `DepositZap.remove_liquidity_one_coin`
::::description[`DepositZap.remove_liquidity_one_coin(_token_amount: uint256, i: int128, min_uamount: uint256, donate_dust: bool = False)`]
Withdraw and unwrap a single coin from the pool.
| Input | Type | Description |
| ----------- | -------| ----|
| `_token_amount` | `uint256` | Amount of LP tokens to burn in the withdrawal |
| `i` | `int128` | Index value of the coin to withdraw |
| `min_uamount` | `uint256` | Minimum amount of underlying coin to receive |
| `donate_dust` | `bool` | Donates collected dust liquidity to `msg.sender` |
Emits: Transfer
RemoveLiquidityImbalance
```vyper
@public
@nonreentrant('lock')
def remove_liquidity_one_coin(_token_amount: uint256, i: int128, min_uamount: uint256, donate_dust: bool = False):
"""
Remove _amount of liquidity all in a form of coin i
"""
use_lending: bool[N_COINS] = USE_LENDING
rates: uint256[N_COINS] = ZEROS
_token: address = self.token
for j in range(N_COINS):
if use_lending[j]:
rates[j] = cERC20(self.coins[j]).exchangeRateCurrent()
else:
rates[j] = LENDING_PRECISION
dy: uint256 = self._calc_withdraw_one_coin(_token_amount, i, rates)
assert dy >= min_uamount, "Not enough coins removed"
assert_modifiable(
ERC20(self.token).transferFrom(msg.sender, self, _token_amount))
amounts: uint256[N_COINS] = ZEROS
amounts[i] = dy * LENDING_PRECISION / rates[i]
token_amount_before: uint256 = ERC20(_token).balanceOf(self)
Curve(self.curve).remove_liquidity_imbalance(amounts, _token_amount)
# Unwrap and transfer all the coins we've got
self._send_all(msg.sender, ZEROS, i)
if not donate_dust:
# Transfer unused tokens back
token_amount_after: uint256 = ERC20(_token).balanceOf(self)
if token_amount_after > token_amount_before:
assert_modifiable(ERC20(_token).transfer(
msg.sender, token_amount_after - token_amount_before)
)
```
```vyper
@public
@nonreentrant('lock')
def remove_liquidity_imbalance(amounts: uint256[N_COINS], max_burn_amount: uint256):
assert not self.is_killed
tethered: bool[N_COINS] = TETHERED
use_lending: bool[N_COINS] = USE_LENDING
token_supply: uint256 = self.token.totalSupply()
assert token_supply > 0
_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
_admin_fee: uint256 = self.admin_fee
rates: uint256[N_COINS] = self._current_rates()
old_balances: uint256[N_COINS] = self.balances
new_balances: uint256[N_COINS] = old_balances
D0: uint256 = self.get_D_mem(rates, old_balances)
for i in range(N_COINS):
new_balances[i] -= amounts[i]
D1: uint256 = self.get_D_mem(rates, new_balances)
fees: uint256[N_COINS] = ZEROS
for i in range(N_COINS):
ideal_balance: uint256 = D1 * old_balances[i] / D0
difference: uint256 = 0
if ideal_balance > new_balances[i]:
difference = ideal_balance - new_balances[i]
else:
difference = new_balances[i] - ideal_balance
fees[i] = _fee * difference / FEE_DENOMINATOR
self.balances[i] = new_balances[i] - (fees[i] * _admin_fee / FEE_DENOMINATOR)
new_balances[i] -= fees[i]
D2: uint256 = self.get_D_mem(rates, new_balances)
token_amount: uint256 = (D0 - D2) * token_supply / D0
assert token_amount > 0
assert token_amount <= max_burn_amount, "Slippage screwed you"
for i in range(N_COINS):
if tethered[i] and not use_lending[i]:
USDT(self.coins[i]).transfer(msg.sender, amounts[i])
else:
assert_modifiable(cERC20(self.coins[i]).transfer(msg.sender, amounts[i]))
self.token.burnFrom(msg.sender, token_amount) # Will raise if not enough
log.RemoveLiquidityImbalance(msg.sender, amounts, fees, D1, token_supply - token_amount)
```
```vyper
@private
def _burn(_to: address, _value: uint256):
"""
@dev Internal function that burns an amount of the token of a given
account.
@param _to The account whose tokens will be burned.
@param _value The amount that will be burned.
"""
assert _to != ZERO_ADDRESS
self.total_supply -= _value
self.balanceOf[_to] -= _value
log.Transfer(_to, ZERO_ADDRESS, _value)
...
@public
def burnFrom(_to: address, _value: uint256):
"""
@dev Burn an amount of the token from a given account.
@param _to The account whose tokens will be burned.
@param _value The amount that will be burned.
"""
assert msg.sender == self.minter, "Only minter is allowed to burn"
self._burn(_to, _value)
```
:::note
The underlying pool method called when the older DepositZap contract's `remove_liquidity_one_coin` is called
emits RemoveLiquidityImbalance whereas the newer
contract emits RemoveLiquidityOne. This is because
the older contracts do not have the `remove_liquidity_one_coin`, and instead use `remove_liquidity_imbalance`.
:::
```shell
>>> todo:
```
::::
### `DepositZap.calc_withdraw_one_coin`
::::description[`DepositZap.calc_withdraw_one_coin(_token_amount: uint256, i: int128) → uint256`]
Calculate the amount received when withdrawing a single underlying coin.
| Input | Type | Description |
| ----------- | -------| ----|
| `_token_amount` | `uint256` | Amount of LP tokens to burn in the withdrawal |
| `i` | `int128` | Index value of the coin to withdraw |
```vyper
@private
@constant
def _calc_withdraw_one_coin(_token_amount: uint256, i: int128, rates: uint256[N_COINS]) -> uint256:
# First, need to calculate
# * Get current D
# * Solve Eqn against y_i for D - _token_amount
use_lending: bool[N_COINS] = USE_LENDING
# tethered: bool[N_COINS] = TETHERED
crv: address = self.curve
A: uint256 = Curve(crv).A()
fee: uint256 = Curve(crv).fee() * N_COINS / (4 * (N_COINS - 1))
fee += fee * FEE_IMPRECISION / FEE_DENOMINATOR # Overcharge to account for imprecision
precisions: uint256[N_COINS] = PRECISION_MUL
total_supply: uint256 = ERC20(self.token).totalSupply()
xp: uint256[N_COINS] = PRECISION_MUL
S: uint256 = 0
for j in range(N_COINS):
xp[j] *= Curve(crv).balances(j)
if use_lending[j]:
# Use stored rate b/c we have imprecision anyway
xp[j] = xp[j] * rates[j] / LENDING_PRECISION
S += xp[j]
# if not use_lending - all good already
D0: uint256 = self.get_D(A, xp)
D1: uint256 = D0 - _token_amount * D0 / total_supply
xp_reduced: uint256[N_COINS] = xp
# xp = xp - fee * | xp * D1 / D0 - (xp - S * dD / D0 * (0, ... 1, ..0))|
for j in range(N_COINS):
dx_expected: uint256 = 0
b_ideal: uint256 = xp[j] * D1 / D0
b_expected: uint256 = xp[j]
if j == i:
b_expected -= S * (D0 - D1) / D0
if b_ideal >= b_expected:
dx_expected = (b_ideal - b_expected)
else:
dx_expected = (b_expected - b_ideal)
xp_reduced[j] -= fee * dx_expected / FEE_DENOMINATOR
dy: uint256 = xp_reduced[i] - self.get_y(A, i, xp_reduced, D1)
dy = dy / precisions[i]
return dy
@public
@constant
def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256:
rates: uint256[N_COINS] = ZEROS
use_lending: bool[N_COINS] = USE_LENDING
for j in range(N_COINS):
if use_lending[j]:
rates[j] = cERC20(self.coins[j]).exchangeRateStored()
else:
rates[j] = 10 **18
return self._calc_withdraw_one_coin(_token_amount, i, rates)
```
```shell
>>> todo:
```
::::
### `DepositZap.withdraw_donated_dust`
::::description[`DepositZap.withdraw_donated_dust()`]
Donates any LP tokens of the associated pool held by this contract to the contract owner.
```vyper
@public
@nonreentrant('lock')
def withdraw_donated_dust():
owner: address = Curve(self.curve).owner()
assert msg.sender == owner
_token: address = self.token
assert_modifiable(
ERC20(_token).transfer(owner, ERC20(_token).balanceOf(self)))
```
```shell
>>> todo:
```
::::
## Deposit Zap (New)
Compared to the older deposit zaps, the newer zaps mainly optimize for gas efficiency. The API is only modified
in part, specifically with regards to `return` values and variable naming.
## Get Deposit Zap Information
### `DepositZap.curve`
::::description[`DepositZap.curve() → address: view`]
Getter for the pool associated with this deposit contract.
```vyper hl_lines="5 14 47"
@external
def __init__(
_coins: address[N_COINS],
_underlying_coins: address[N_COINS],
_curve: address,
_token: address
):
"""
@notice Contract constructor
@dev Where a token does not use wrapping, use the same address
for `_coins` and `_underlying_coins`
@param _coins List of wrapped coin addresses
@param _underlying_coins List of underlying coin addresses
@param _curve Pool address
@param _token Pool LP token address
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
assert _underlying_coins[i] != ZERO_ADDRESS
# approve underlying and wrapped coins for infinite transfers
_response: Bytes[32] = raw_call(
_underlying_coins[i],
concat(
method_id("approve(address,uint256)"),
convert(_coins[i], bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
_response = raw_call(
_coins[i],
concat(
method_id("approve(address,uint256)"),
convert(_curve, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
self.coins = _coins
self.underlying_coins = _underlying_coins
self.curve = _curve
self.lp_token = _token
```
```shell
>>> zap.curve()
'0xA2B47E3D5c44877cca798226B7B8118F9BFb7A56'
```
::::
### `DepositZap.underlying_coins`
::::description[`DepositZap.underlying_coins(i: int128) → address: view`]
Getter for the array of underlying coins within the associated pool.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | Index of the underlying coin for which to get the address|
```vyper hl_lines="1 8 17 23 27 50"
underlying_coins: public(address[N_COINS])
...
@external
def __init__(
_coins: address[N_COINS],
_underlying_coins: address[N_COINS],
_curve: address,
_token: address
):
"""
@notice Contract constructor
@dev Where a token does not use wrapping, use the same address
for `_coins` and `_underlying_coins`
@param _coins List of wrapped coin addresses
@param _underlying_coins List of underlying coin addresses
@param _curve Pool address
@param _token Pool LP token address
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
assert _underlying_coins[i] != ZERO_ADDRESS
# approve underlying and wrapped coins for infinite transfers
_response: Bytes[32] = raw_call(
_underlying_coins[i],
concat(
method_id("approve(address,uint256)"),
convert(_coins[i], bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
_response = raw_call(
_coins[i],
concat(
method_id("approve(address,uint256)"),
convert(_curve, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
self.coins = _coins
self.underlying_coins = _underlying_coins
self.curve = _curve
self.lp_token = _token
```
```shell
>>> zap.underlying_coins(0)
'0x6B175474E89094C44Da98b954EedeAC495271d0F'
>>> zap.underlying_coins(1)
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
```
::::
### `DepositZap.coins`
::::description[`DepositZap.coins(i: int128) → address: view`]
Getter for the array of wrapped coins within the associated pool.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | Index of the coin for which to get the address |
```vyper hl_lines="1 7 16 22 30 38 49"
coins: public(address[N_COINS])
...
@external
def __init__(
_coins: address[N_COINS],
_underlying_coins: address[N_COINS],
_curve: address,
_token: address
):
"""
@notice Contract constructor
@dev Where a token does not use wrapping, use the same address
for `_coins` and `_underlying_coins`
@param _coins List of wrapped coin addresses
@param _underlying_coins List of underlying coin addresses
@param _curve Pool address
@param _token Pool LP token address
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
assert _underlying_coins[i] != ZERO_ADDRESS
# approve underlying and wrapped coins for infinite transfers
_response: Bytes[32] = raw_call(
_underlying_coins[i],
concat(
method_id("approve(address,uint256)"),
convert(_coins[i], bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
_response = raw_call(
_coins[i],
concat(
method_id("approve(address,uint256)"),
convert(_curve, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
self.coins = _coins
self.underlying_coins = _underlying_coins
self.curve = _curve
self.lp_token = _token
```
```shell
>>> zap.coins(0)
'0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643'
>>> zap.coins(1)
'0x39AA39c021dfbaE8faC545936693aC917d5E7563'
```
::::
### `DepositZap.token`
::::description[`DepositZap.token() → address: view`]
Getter for the LP token of the associated pool.
```vyper hl_lines="1 10 19 52"
lp_token: public(address)
...
@external
def __init__(
_coins: address[N_COINS],
_underlying_coins: address[N_COINS],
_curve: address,
_token: address
):
"""
@notice Contract constructor
@dev Where a token does not use wrapping, use the same address
for `_coins` and `_underlying_coins`
@param _coins List of wrapped coin addresses
@param _underlying_coins List of underlying coin addresses
@param _curve Pool address
@param _token Pool LP token address
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
assert _underlying_coins[i] != ZERO_ADDRESS
# approve underlying and wrapped coins for infinite transfers
_response: Bytes[32] = raw_call(
_underlying_coins[i],
concat(
method_id("approve(address,uint256)"),
convert(_coins[i], bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
_response = raw_call(
_coins[i],
concat(
method_id("approve(address,uint256)"),
convert(_curve, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
self.coins = _coins
self.underlying_coins = _underlying_coins
self.curve = _curve
self.lp_token = _token
```
```shell
>>> zap.coins(0)
'0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643'
>>> zap.coins(1)
'0x39AA39c021dfbaE8faC545936693aC917d5E7563'
```
::::
## Add/Remove Liquidity
### `DepositZap.add_liquidity`
::::description[`DepositZap.add_liquidity(_underlying_amounts: uint256[N_COINS], _min_mint_amount: uint256) -> uint256`]
Wrap underlying coins and deposit them in the pool. Returns the amount of LP token received in exchange for the
deposited amounts.
| Input | Type | Description |
| ----------- | -------| ----|
| `_underlying_amounts` | `uint256[N_COINS]` | List of amounts of underlying coins to deposit |
| `_min_mint_amount` | `uint256` | Minimum amount of LP token to mint from the deposit |
Emits: Transfer
AddLiquidity
```vyper
@public
@nonreentrant('lock')
def add_liquidity(uamounts: uint256[N_COINS], min_mint_amount: uint256):
tethered: bool[N_COINS] = TETHERED
amounts: uint256[N_COINS] = ZEROS
for i in range(N_COINS):
uamount: uint256 = uamounts[i]
if uamount > 0:
# Transfer the underlying coin from owner
if tethered[i]:
USDT(self.underlying_coins[i]).transferFrom(
msg.sender, self, uamount)
else:
assert_modifiable(ERC20(self.underlying_coins[i])\
.transferFrom(msg.sender, self, uamount))
# Mint if needed
ERC20(self.underlying_coins[i]).approve(self.coins[i], uamount)
yERC20(self.coins[i]).deposit(uamount)
amounts[i] = yERC20(self.coins[i]).balanceOf(self)
ERC20(self.coins[i]).approve(self.curve, amounts[i])
Curve(self.curve).add_liquidity(amounts, min_mint_amount)
tokens: uint256 = ERC20(self.token).balanceOf(self)
assert_modifiable(ERC20(self.token).transfer(msg.sender, tokens))
```
```vyper
@external
@nonreentrant('lock')
def add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256) -> uint256:
"""
@notice Deposit coins into the pool
@param amounts List of amounts of coins to deposit
@param min_mint_amount Minimum amount of LP tokens to mint from the deposit
@return Amount of LP tokens received by depositing
"""
assert not self.is_killed # dev: is killed
amp: uint256 = self._A()
_lp_token: address = self.lp_token
token_supply: uint256 = ERC20(_lp_token).totalSupply()
# Initial invariant
D0: uint256 = 0
old_balances: uint256[N_COINS] = self.balances
if token_supply > 0:
D0 = self.get_D_mem(old_balances, amp)
new_balances: uint256[N_COINS] = old_balances
for i in range(N_COINS):
if token_supply == 0:
assert amounts[i] > 0 # dev: initial deposit requires all coins
# balances store amounts of c-tokens
new_balances[i] = old_balances[i] + amounts[i]
# Invariant after change
D1: uint256 = self.get_D_mem(new_balances, amp)
assert D1 > D0
# We need to recalculate the invariant accounting for fees
# to calculate fair user's share
D2: uint256 = D1
fees: uint256[N_COINS] = empty(uint256[N_COINS])
if token_supply > 0:
# Only account for fees if we are not the first to deposit
_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
_admin_fee: uint256 = self.admin_fee
for i in range(N_COINS):
ideal_balance: uint256 = D1 * old_balances[i] / D0
difference: uint256 = 0
if ideal_balance > new_balances[i]:
difference = ideal_balance - new_balances[i]
else:
difference = new_balances[i] - ideal_balance
fees[i] = _fee * difference / FEE_DENOMINATOR
self.balances[i] = new_balances[i] - (fees[i] * _admin_fee / FEE_DENOMINATOR)
new_balances[i] -= fees[i]
D2 = self.get_D_mem(new_balances, amp)
else:
self.balances = new_balances
# Calculate, how much pool tokens to mint
mint_amount: uint256 = 0
if token_supply == 0:
mint_amount = D1 # Take the dust if there was any
else:
mint_amount = token_supply * (D2 - D0) / D0
assert mint_amount >= min_mint_amount, "Slippage screwed you"
# Take coins from the sender
for i in range(N_COINS):
if amounts[i] > 0:
# "safeTransferFrom" which works for ERC20s which return bool or not
_response: Bytes[32] = raw_call(
self.coins[i],
concat(
method_id("transferFrom(address,address,uint256)"),
convert(msg.sender, bytes32),
convert(self, bytes32),
convert(amounts[i], bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool)
# Mint pool tokens
CurveToken(_lp_token).mint(msg.sender, mint_amount)
log AddLiquidity(msg.sender, amounts, fees, D1, token_supply + mint_amount)
return mint_amount
```
```shell
>>> todo:
```
::::
### `DepositZap.remove_liquidity`
::::description[`DepositZap.remove_liquidity(_amount: uint256, _min_underlying_amounts: uint256[N_COINS]) -> uint256[N_COINS]`]
Withdraw and unwrap coins from the pool. Returns list of amounts of underlying coins that were withdrawn.
| Input | Type | Description |
| ----------- | -------| ----|
| `_amount` | `uint256` | Quantity of LP tokens to burn in the withdrawal |
| `_min_underlying_amounts` | `uint256[N_COINS]` | Minimum amounts of underlying coins to receive |
Emits: Transfer
RemoveLiquidity
```vyper
@internal
def _unwrap_and_transfer(_addr: address, _min_amounts: uint256[N_COINS]) -> uint256[N_COINS]:
# unwrap coins and transfer them to the sender
use_lending: bool[N_COINS] = USE_LENDING
_amounts: uint256[N_COINS] = empty(uint256[N_COINS])
for i in range(N_COINS):
if use_lending[i]:
_coin: address = self.coins[i]
_balance: uint256 = ERC20(_coin).balanceOf(self)
if _balance == 0: # Do nothing if there are 0 coins
continue
yERC20(_coin).withdraw(_balance)
_ucoin: address = self.underlying_coins[i]
_uamount: uint256 = ERC20(_ucoin).balanceOf(self)
assert _uamount >= _min_amounts[i], "Not enough coins withdrawn"
# Send only if we have something to send
if _uamount != 0:
_response: Bytes[32] = raw_call(
_ucoin,
concat(
method_id("transfer(address,uint256)"),
convert(_addr, bytes32),
convert(_uamount, bytes32)
),
max_outsize=32
)
if len(_response) > 0:
assert convert(_response, bool)
_amounts[i] = _uamount
return _amounts
@external
@nonreentrant('lock')
def remove_liquidity(
_amount: uint256,
_min_underlying_amounts: uint256[N_COINS]
) -> uint256[N_COINS]:
"""
@notice Withdraw and unwrap coins from the pool
@dev Withdrawal amounts are based on current deposit ratios
@param _amount Quantity of LP tokens to burn in the withdrawal
@param _min_underlying_amounts Minimum amounts of underlying coins to receive
@return List of amounts of underlying coins that were withdrawn
"""
assert ERC20(self.lp_token).transferFrom(msg.sender, self, _amount)
Curve(self.curve).remove_liquidity(_amount, empty(uint256[N_COINS]))
return self._unwrap_and_transfer(msg.sender, _min_underlying_amounts)
```
```vyper
@external
@nonreentrant('lock')
def remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS]) -> uint256[N_COINS]:
"""
@notice Withdraw coins from the pool
@dev Withdrawal amounts are based on current deposit ratios
@param _amount Quantity of LP tokens to burn in the withdrawal
@param min_amounts Minimum amounts of underlying coins to receive
@return List of amounts of coins that were withdrawn
"""
_lp_token: address = self.lp_token
total_supply: uint256 = ERC20(_lp_token).totalSupply()
amounts: uint256[N_COINS] = empty(uint256[N_COINS])
fees: uint256[N_COINS] = empty(uint256[N_COINS]) # Fees are unused but we've got them historically in event
for i in range(N_COINS):
value: uint256 = self.balances[i] * _amount / total_supply
assert value >= min_amounts[i], "Withdrawal resulted in fewer coins than expected"
self.balances[i] -= value
amounts[i] = value
_response: Bytes[32] = raw_call(
self.coins[i],
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(value, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool)
CurveToken(_lp_token).burnFrom(msg.sender, _amount) # dev: insufficient funds
log RemoveLiquidity(msg.sender, amounts, fees, total_supply - _amount)
return amounts
```
```shell
>>> todo:
```
::::
### `DepositZap.remove_liquidity_imbalance`
::::description[`DepositZap.remove_liquidity_imbalance(_underlying_amounts: uint256[N_COINS], _max_burn_amount: uint256)`]
Withdraw and unwrap coins from the pool in an imbalanced amount.
Amounts in `_underlying_amounts` correspond to withdrawn amounts before any fees charge for unwrapping
Returns list of amounts of underlying coins that were withdrawn.
| Input | Type | Description |
| ----------- | -------| ----|
| `_underlying_amounts` | `uint256[N_COINS]` | List of amounts of underlying coins to withdraw |
| `_max_burn_amount` | `uint256` | Maximum amount of LP token to burn in the withdrawal |
Emits: Transfer
RemoveLiquidityImbalance
```vyper
@internal
def _unwrap_and_transfer(_addr: address, _min_amounts: uint256[N_COINS]) -> uint256[N_COINS]:
# unwrap coins and transfer them to the sender
use_lending: bool[N_COINS] = USE_LENDING
_amounts: uint256[N_COINS] = empty(uint256[N_COINS])
for i in range(N_COINS):
if use_lending[i]:
_coin: address = self.coins[i]
_balance: uint256 = ERC20(_coin).balanceOf(self)
if _balance == 0: # Do nothing if there are 0 coins
continue
yERC20(_coin).withdraw(_balance)
_ucoin: address = self.underlying_coins[i]
_uamount: uint256 = ERC20(_ucoin).balanceOf(self)
assert _uamount >= _min_amounts[i], "Not enough coins withdrawn"
# Send only if we have something to send
if _uamount != 0:
_response: Bytes[32] = raw_call(
_ucoin,
concat(
method_id("transfer(address,uint256)"),
convert(_addr, bytes32),
convert(_uamount, bytes32)
),
max_outsize=32
)
if len(_response) > 0:
assert convert(_response, bool)
_amounts[i] = _uamount
return _amounts
@external
@nonreentrant('lock')
def remove_liquidity_imbalance(
_underlying_amounts: uint256[N_COINS],
_max_burn_amount: uint256
) -> uint256[N_COINS]:
"""
@notice Withdraw and unwrap coins from the pool in an imbalanced amount
@dev Amounts in `_underlying_amounts` correspond to withdrawn amounts
before any fees charge for unwrapping.
@param _underlying_amounts List of amounts of underlying coins to withdraw
@param _max_burn_amount Maximum amount of LP token to burn in the withdrawal
@return List of amounts of underlying coins that were withdrawn
"""
use_lending: bool[N_COINS] = USE_LENDING
lp_token: address = self.lp_token
amounts: uint256[N_COINS] = _underlying_amounts
for i in range(N_COINS):
_amount: uint256 = amounts[i]
if use_lending[i] and _amount > 0:
rate: uint256 = yERC20(self.coins[i]).getPricePerFullShare()
amounts[i] = _amount * LENDING_PRECISION / rate
# if not use_lending - all good already
# Transfer max tokens in
_lp_amount: uint256 = ERC20(lp_token).balanceOf(msg.sender)
if _lp_amount > _max_burn_amount:
_lp_amount = _max_burn_amount
assert ERC20(lp_token).transferFrom(msg.sender, self, _lp_amount)
Curve(self.curve).remove_liquidity_imbalance(amounts, _max_burn_amount)
# Transfer unused LP tokens back
_lp_amount = ERC20(lp_token).balanceOf(self)
if _lp_amount != 0:
assert ERC20(lp_token).transfer(msg.sender, _lp_amount)
# Unwrap and transfer all the coins we've got
return self._unwrap_and_transfer(msg.sender, empty(uint256[N_COINS]))
```
```vyper
@external
@nonreentrant('lock')
def remove_liquidity_imbalance(amounts: uint256[N_COINS], max_burn_amount: uint256) -> uint256:
"""
@notice Withdraw coins from the pool in an imbalanced amount
@param amounts List of amounts of underlying coins to withdraw
@param max_burn_amount Maximum amount of LP token to burn in the withdrawal
@return Actual amount of the LP token burned in the withdrawal
"""
assert not self.is_killed # dev: is killed
amp: uint256 = self._A()
old_balances: uint256[N_COINS] = self.balances
new_balances: uint256[N_COINS] = old_balances
D0: uint256 = self.get_D_mem(old_balances, amp)
for i in range(N_COINS):
new_balances[i] -= amounts[i]
D1: uint256 = self.get_D_mem(new_balances, amp)
_lp_token: address = self.lp_token
token_supply: uint256 = ERC20(_lp_token).totalSupply()
assert token_supply != 0 # dev: zero total supply
_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
_admin_fee: uint256 = self.admin_fee
fees: uint256[N_COINS] = empty(uint256[N_COINS])
for i in range(N_COINS):
ideal_balance: uint256 = D1 * old_balances[i] / D0
difference: uint256 = 0
if ideal_balance > new_balances[i]:
difference = ideal_balance - new_balances[i]
else:
difference = new_balances[i] - ideal_balance
fees[i] = _fee * difference / FEE_DENOMINATOR
self.balances[i] = new_balances[i] - (fees[i] * _admin_fee / FEE_DENOMINATOR)
new_balances[i] -= fees[i]
D2: uint256 = self.get_D_mem(new_balances, amp)
token_amount: uint256 = (D0 - D2) * token_supply / D0
assert token_amount != 0 # dev: zero tokens burned
token_amount += 1 # In case of rounding errors - make it unfavorable for the "attacker"
assert token_amount <= max_burn_amount, "Slippage screwed you"
CurveToken(_lp_token).burnFrom(msg.sender, token_amount) # dev: insufficient funds
for i in range(N_COINS):
if amounts[i] != 0:
_response: Bytes[32] = raw_call(
self.coins[i],
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(amounts[i], bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool)
log RemoveLiquidityImbalance(msg.sender, amounts, fees, D1, token_supply - token_amount)
return token_amount
```
```shell
>>> todo:
```
::::
### `DepositZap.remove_liquidity_one_coin`
::::description[`DepositZap.remove_liquidity_one_coin(_token_amount: uint256, i: int128, min_uamount: uint256, donate_dust: bool = False)`]
Withdraw and unwrap a single coin from the pool. Returns amount of underlying coin received.
| Input | Type | Description |
| ----------- | -------| ----|
| `_amount` | `uint256` | Amount of LP tokens to burn in the withdrawal |
| `i` | `int128` | Index value of the coin to withdraw |
| `_min_underlying_amount` | `uint256` | Minimum amount of underlying coin to receive |
Emits: Transfer
RemoveLiquidityOne
```vyper
@external
@nonreentrant('lock')
def remove_liquidity_one_coin(
_amount: uint256,
i: int128,
_min_underlying_amount: uint256
) -> uint256:
"""
@notice Withdraw and unwrap a single coin from the pool
@param _amount Amount of LP tokens to burn in the withdrawal
@param i Index value of the coin to withdraw
@param _min_underlying_amount Minimum amount of underlying coin to receive
@return Amount of underlying coin received
"""
assert ERC20(self.lp_token).transferFrom(msg.sender, self, _amount)
Curve(self.curve).remove_liquidity_one_coin(_amount, i, 0)
use_lending: bool[N_COINS] = USE_LENDING
if use_lending[i]:
coin: address = self.coins[i]
_balance: uint256 = ERC20(coin).balanceOf(self)
yERC20(coin).withdraw(_balance)
coin: address = self.underlying_coins[i]
_balance: uint256 = ERC20(coin).balanceOf(self)
assert _balance >= _min_underlying_amount, "Not enough coins removed"
_response: Bytes[32] = raw_call(
coin,
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(_balance, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
return _balance
```
```vyper
@external
@nonreentrant('lock')
def remove_liquidity_one_coin(_token_amount: uint256, i: int128, _min_amount: uint256) -> uint256:
"""
@notice Withdraw a single coin from the pool
@param _token_amount Amount of LP tokens to burn in the withdrawal
@param i Index value of the coin to withdraw
@param _min_amount Minimum amount of coin to receive
@return Amount of coin received
"""
assert not self.is_killed # dev: is killed
dy: uint256 = 0
dy_fee: uint256 = 0
total_supply: uint256 = 0
dy, dy_fee, total_supply = self._calc_withdraw_one_coin(_token_amount, i)
assert dy >= _min_amount, "Not enough coins removed"
self.balances[i] -= (dy + dy_fee * self.admin_fee / FEE_DENOMINATOR)
CurveToken(self.lp_token).burnFrom(msg.sender, _token_amount) # dev: insufficient funds
_response: Bytes[32] = raw_call(
self.coins[i],
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(dy, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool)
log RemoveLiquidityOne(msg.sender, _token_amount, dy, total_supply - _token_amount)
return dy
```
```shell
>>> todo:
```
::::
---
## Metapool Deposits
While Curve metapools support swaps between base pool coins, the base pool LP token and metapool coins,
they do not allow liquidity providers to deposit and/or withdraw base pool coins.
For example, the `GUSD` metapool is a pool consisting of `GUSD` and `3CRV` (the LP token of the `3Pool`) and allows
for swaps between `GUSD`, `DAI`, `USDC`, `USDT` and `3CRV`. However, liquidity providers are not able to deposit
`DAI`, `USDC` or `USDT` to the pool directly. The main reason why this is not possible lies in the maximum byte
code size of contracts. Metapools are complex and can therefore end up being very close to the contract
byte code size limit. In order to overcome this restriction, liquidity can be added and removed to and
from a metapool in the base pool’s coins through a metapool deposit zap.
The template source code for a metapool deposit “zap” may be viewed on
[GitHub](https://github.com/curvefi/curve-contract/blob/master/contracts/pool-templates/meta/DepositTemplateMeta.vy).
:::note
Metapool deposit zaps contain the following private and hardcoded constants:
- `N_COINS`: Number of coins in the metapool (excluding base pool coins)
- `BASE_N_COINS`: Number of coins in the base pool
- `N_ALL_COINS`: All coins in the metapool, excluding the base pool LP token (`N_COINS + BASE_N_COINS - 1`)
:::
## Get Deposit Zap Information
### `DepositZap.pool`
::::description[`DepositZap.pool() → address: view`]
Getter for the metapool associated with this deposit contract.
```vyper hl_lines="1 12"
pool: public(address)
...
@external
def __init__(_pool: address, _token: address):
"""
@notice Contract constructor
@param _pool Metapool address
@param _token Pool LP token address
"""
self.pool = _pool
...
```
```shell
>>> todo:
```
::::
### `DepositZap.base_pool`
::::description[`DepositZap.base_pool() → address: view`]
Getter for the base pool of the metapool associated with this deposit contract.
```vyper hl_lines="1 15"
base_pool: public(address)
...
@external
def __init__(_pool: address, _token: address):
"""
@notice Contract constructor
@param _pool Metapool address
@param _token Pool LP token address
"""
self.pool = _pool
self.token = _token
base_pool: address = CurveMeta(_pool).base_pool()
self.base_pool = base_pool
...
```
```shell
>>> todo:
```
::::
### `DepositZap.base_coins`
::::description[`DepositZap.base_coins(i: uint256) → address: view`]
Getter for the array of the coins of the metapool’s base pool.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | Index of the coin for which to get the address |
```vyper hl_lines="1 35"
base_coins: public(address[BASE_N_COINS])
...
@external
def __init__(_pool: address, _token: address):
"""
@notice Contract constructor
@param _pool Metapool address
@param _token Pool LP token address
"""
self.pool = _pool
self.token = _token
base_pool: address = CurveMeta(_pool).base_pool()
self.base_pool = base_pool
for i in range(N_COINS):
coin: address = CurveMeta(_pool).coins(i)
self.coins[i] = coin
# approve coins for infinite transfers
_response: Bytes[32] = raw_call(
coin,
concat(
method_id("approve(address,uint256)"),
convert(_pool, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
for i in range(BASE_N_COINS):
coin: address = CurveBase(base_pool).coins(i)
self.base_coins[i] = coin
# approve underlying coins for infinite transfers
_response: Bytes[32] = raw_call(
coin,
concat(
method_id("approve(address,uint256)"),
convert(base_pool, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
...
```
```shell
>>> todo:
```
::::
### `DepositZap.token`
::::description[`DepositZap.token() → address: view`]
Getter for the LP token of the associated metapool.
```vyper hl_lines="1 13"
token: public(address)
...
@external
def __init__(_pool: address, _token: address):
"""
@notice Contract constructor
@param _pool Metapool address
@param _token Pool LP token address
"""
self.pool = _pool
self.token = _token
...
```
```shell
>>> todo:
```
::::
## Add/Remove Liquidity
:::note
For methods taking the index argument `i`, a number in the range from `0` to `N_ALL_COINS - 1` is valid.
This refers to all coins apart from the base pool LP token.
:::
### `DepositZap.add_liquidity`
::::description[`DepositZap.add_liquidity(_amounts: uint256[N_ALL_COINS], _min_mint_amount: uint256) → uint256`]
Wrap underlying coins and deposit them in the pool. Returns the amount of LP token received in exchange for
depositing.
| Input | Type | Description |
| ----------- | -------| ----|
| `_amounts` | `uint256[N_ALL_COINS]` | List of amounts of underlying coins to deposit |
| `_min_mint_amount` | `uint256` | Minimum amount of LP tokens to mint from the deposit |
Emits: AddLiquidity
Transfer
```vyper
@external
def add_liquidity(_amounts: uint256[N_ALL_COINS], _min_mint_amount: uint256) -> uint256:
"""
@notice Wrap underlying coins and deposit them in the pool
@param _amounts List of amounts of underlying coins to deposit
@param _min_mint_amount Minimum amount of LP tokens to mint from the deposit
@return Amount of LP tokens received by depositing
"""
meta_amounts: uint256[N_COINS] = empty(uint256[N_COINS])
base_amounts: uint256[BASE_N_COINS] = empty(uint256[BASE_N_COINS])
deposit_base: bool = False
# Transfer all coins in
for i in range(N_ALL_COINS):
amount: uint256 = _amounts[i]
if amount == 0:
continue
coin: address = ZERO_ADDRESS
if i < MAX_COIN:
coin = self.coins[i]
meta_amounts[i] = amount
else:
x: int128 = i - MAX_COIN
coin = self.base_coins[x]
base_amounts[x] = amount
deposit_base = True
# "safeTransferFrom" which works for ERC20s which return bool or not
_response: Bytes[32] = raw_call(
coin,
concat(
method_id("transferFrom(address,address,uint256)"),
convert(msg.sender, bytes32),
convert(self, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool) # dev: failed transfer
# end "safeTransferFrom"
# Handle potential Tether fees
if coin == FEE_ASSET:
amount = ERC20(FEE_ASSET).balanceOf(self)
if i < MAX_COIN:
meta_amounts[i] = amount
else:
base_amounts[i - MAX_COIN] = amount
# Deposit to the base pool
if deposit_base:
CurveBase(self.base_pool).add_liquidity(base_amounts, 0)
meta_amounts[MAX_COIN] = ERC20(self.coins[MAX_COIN]).balanceOf(self)
# Deposit to the meta pool
CurveMeta(self.pool).add_liquidity(meta_amounts, _min_mint_amount)
# Transfer meta token back
lp_token: address = self.token
lp_amount: uint256 = ERC20(lp_token).balanceOf(self)
assert ERC20(lp_token).transfer(msg.sender, lp_amount)
return lp_amount
```
```shell
```
::::
### `DepositZap.remove_liquidity`
::::description[`DepositZap.remove_liquidity(_amount: uint256, _min_amounts: uint256[N_ALL_COINS]) → uint256[N_ALL_COINS]`]
Withdraw and unwrap coins from the pool. Returns a list of amounts (`uint256[N_ALL_COINS]`) of underlying coins
that were withdrawn.
| Input | Type | Description |
| ----------- | -------| ----|
| `_amount` | `uint256` | Quantity of LP tokens to burn in the withdrawal |
| `_min_amounts` | `uint256[N_ALL_COINS]` | Minimum amounts of underlying coins to receive |
Emits: RemoveLiquidity
Transfer
```vyper
@external
def remove_liquidity(_amount: uint256, _min_amounts: uint256[N_ALL_COINS]) -> uint256[N_ALL_COINS]:
"""
@notice Withdraw and unwrap coins from the pool
@dev Withdrawal amounts are based on current deposit ratios
@param _amount Quantity of LP tokens to burn in the withdrawal
@param _min_amounts Minimum amounts of underlying coins to receive
@return List of amounts of underlying coins that were withdrawn
"""
_token: address = self.token
assert ERC20(_token).transferFrom(msg.sender, self, _amount)
min_amounts_meta: uint256[N_COINS] = empty(uint256[N_COINS])
min_amounts_base: uint256[BASE_N_COINS] = empty(uint256[BASE_N_COINS])
amounts: uint256[N_ALL_COINS] = empty(uint256[N_ALL_COINS])
# Withdraw from meta
for i in range(MAX_COIN):
min_amounts_meta[i] = _min_amounts[i]
CurveMeta(self.pool).remove_liquidity(_amount, min_amounts_meta)
# Withdraw from base
_base_amount: uint256 = ERC20(self.coins[MAX_COIN]).balanceOf(self)
for i in range(BASE_N_COINS):
min_amounts_base[i] = _min_amounts[MAX_COIN+i]
CurveBase(self.base_pool).remove_liquidity(_base_amount, min_amounts_base)
# Transfer all coins out
for i in range(N_ALL_COINS):
coin: address = ZERO_ADDRESS
if i < MAX_COIN:
coin = self.coins[i]
else:
coin = self.base_coins[i - MAX_COIN]
amounts[i] = ERC20(coin).balanceOf(self)
# "safeTransfer" which works for ERC20s which return bool or not
_response: Bytes[32] = raw_call(
coin,
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(amounts[i], bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool) # dev: failed transfer
# end "safeTransfer"
return amounts
```
```shell
>>> todo:
```
::::
### `DepositZap.remove_liquidity_one_coin`
::::description[`DepositZap.remove_liquidity_one_coin(_token_amount: uint256, i: int128, _min_amount: uint256) → uint256`]
Withdraw and unwrap a single coin from the metapool. Returns the amount of the underlying coin received.
| Input | Type | Description |
| ----------- | -------| ----|
| `_token_amount` | `uint256` | Amount of LP tokens to burn in the withdrawal |
| `i` | `int128` | Index value of the coin to withdraw |
| `_min_amount` | `uint256` | Minimum amount of underlying coin to receive |
Emits: RemoveLiquidityOne
Transfer
```vyper
@external
def remove_liquidity_one_coin(_token_amount: uint256, i: int128, _min_amount: uint256) -> uint256:
"""
@notice Withdraw and unwrap a single coin from the pool
@param _token_amount Amount of LP tokens to burn in the withdrawal
@param i Index value of the coin to withdraw
@param _min_amount Minimum amount of underlying coin to receive
@return Amount of underlying coin received
"""
assert ERC20(self.token).transferFrom(msg.sender, self, _token_amount)
coin: address = ZERO_ADDRESS
if i < MAX_COIN:
coin = self.coins[i]
# Withdraw a metapool coin
CurveMeta(self.pool).remove_liquidity_one_coin(_token_amount, i, _min_amount)
else:
coin = self.base_coins[i - MAX_COIN]
# Withdraw a base pool coin
CurveMeta(self.pool).remove_liquidity_one_coin(_token_amount, MAX_COIN, 0)
CurveBase(self.base_pool).remove_liquidity_one_coin(
ERC20(self.coins[MAX_COIN]).balanceOf(self), i-MAX_COIN, _min_amount
)
# Tranfer the coin out
coin_amount: uint256 = ERC20(coin).balanceOf(self)
# "safeTransfer" which works for ERC20s which return bool or not
_response: Bytes[32] = raw_call(
coin,
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(coin_amount, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool) # dev: failed transfer
# end "safeTransfer"
return coin_amount
```
```shell
>>> todo:
```
::::
### `DepositZap.remove_liquidity_imbalance`
::::description[`DepositZap.remove_liquidity_imbalance(_amounts: uint256[N_ALL_COINS], _max_burn_amount: uint256) → uint256`]
Withdraw coins from the pool in an imbalanced amount. Returns the actual amount of the LP token burned in the
withdrawal.
| Input | Type | Description |
| ----------- | -------| ----|
| `_amounts` | `uint256[N_ALL_COINS]` | List of amounts of underlying coins to withdraw |
| `_max_burn_amount` | `uint256` | Maximum amount of LP token to burn in the withdrawal |
Emits: RemoveLiquidityImbalance
Transfer
```vyper
@external
def remove_liquidity_imbalance(_amounts: uint256[N_ALL_COINS], _max_burn_amount: uint256) -> uint256:
"""
@notice Withdraw coins from the pool in an imbalanced amount
@param _amounts List of amounts of underlying coins to withdraw
@param _max_burn_amount Maximum amount of LP token to burn in the withdrawal
@return Actual amount of the LP token burned in the withdrawal
"""
base_pool: address = self.base_pool
meta_pool: address = self.pool
base_coins: address[BASE_N_COINS] = self.base_coins
meta_coins: address[N_COINS] = self.coins
lp_token: address = self.token
fee: uint256 = CurveBase(base_pool).fee() * BASE_N_COINS / (4 * (BASE_N_COINS - 1))
fee += fee * FEE_IMPRECISION / FEE_DENOMINATOR # Overcharge to account for imprecision
# Transfer the LP token in
assert ERC20(lp_token).transferFrom(msg.sender, self, _max_burn_amount)
withdraw_base: bool = False
amounts_base: uint256[BASE_N_COINS] = empty(uint256[BASE_N_COINS])
amounts_meta: uint256[N_COINS] = empty(uint256[N_COINS])
leftover_amounts: uint256[N_COINS] = empty(uint256[N_COINS])
# Prepare quantities
for i in range(MAX_COIN):
amounts_meta[i] = _amounts[i]
for i in range(BASE_N_COINS):
amount: uint256 = _amounts[MAX_COIN + i]
if amount != 0:
amounts_base[i] = amount
withdraw_base = True
if withdraw_base:
amounts_meta[MAX_COIN] = CurveBase(self.base_pool).calc_token_amount(amounts_base, False)
amounts_meta[MAX_COIN] += amounts_meta[MAX_COIN] * fee / FEE_DENOMINATOR + 1
# Remove liquidity and deposit leftovers back
CurveMeta(meta_pool).remove_liquidity_imbalance(amounts_meta, _max_burn_amount)
if withdraw_base:
CurveBase(base_pool).remove_liquidity_imbalance(amounts_base, amounts_meta[MAX_COIN])
leftover_amounts[MAX_COIN] = ERC20(meta_coins[MAX_COIN]).balanceOf(self)
if leftover_amounts[MAX_COIN] > 0:
CurveMeta(meta_pool).add_liquidity(leftover_amounts, 0)
# Transfer all coins out
for i in range(N_ALL_COINS):
coin: address = ZERO_ADDRESS
amount: uint256 = 0
if i < MAX_COIN:
coin = meta_coins[i]
amount = amounts_meta[i]
else:
coin = base_coins[i - MAX_COIN]
amount = amounts_base[i - MAX_COIN]
# "safeTransfer" which works for ERC20s which return bool or not
if amount > 0:
_response: Bytes[32] = raw_call(
coin,
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool) # dev: failed transfer
# end "safeTransfer"
# Transfer the leftover LP token out
leftover: uint256 = ERC20(lp_token).balanceOf(self)
if leftover > 0:
assert ERC20(lp_token).transfer(msg.sender, leftover)
return _max_burn_amount - leftover
```
```shell
>>> todo:
```
::::
### `DepositZap.calc_withdraw_one_coin`
::::description[`DepositZap.calc_withdraw_one_coin(_token_amount: uint256, i: int128) → uint256`]
Calculate the amount received when withdrawing and unwrapping a single coin. Returns the amount of coin `i` received.
| Input | Type | Description |
| ----------- | -------| ----|
| `_token_amount` | `uint256` | Amount of LP tokens to burn in the withdrawal |
| `i` | `int128` | Index value of the coin to withdraw (`i` should be in the range from `0` to `N_ALL_COINS - 1`, where the LP token of the base pool is removed). |
```vyper
@view
@external
def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256:
"""
@notice Calculate the amount received when withdrawing and unwrapping a single coin
@param _token_amount Amount of LP tokens to burn in the withdrawal
@param i Index value of the underlying coin to withdraw
@return Amount of coin received
"""
if i < MAX_COIN:
return CurveMeta(self.pool).calc_withdraw_one_coin(_token_amount, i)
else:
base_tokens: uint256 = CurveMeta(self.pool).calc_withdraw_one_coin(_token_amount, MAX_COIN)
return CurveBase(self.base_pool).calc_withdraw_one_coin(base_tokens, i-MAX_COIN)
```
```shell
>>> todo:
```
::::
### `DepositZap.calc_token_amount`
::::description[`DepositZap.calc_token_amount(_amounts: uint256[N_ALL_COINS], _is_deposit: bool) → uint256`]
Calculate addition or reduction in token supply from a deposit or withdrawal.
Returns the expected amount of LP tokens received.
| Input | Type | Description |
| ----------- | -------| ----|
| `_amounts` | `uint256[N_ALL_COINS]` | Amount of each underlying coin being deposited |
| `_is_deposit` | `bool` | Set `True` for deposits, `False` for withdrawals |
```vyper
@view
@external
def calc_token_amount(_amounts: uint256[N_ALL_COINS], _is_deposit: bool) -> uint256:
"""
@notice Calculate addition or reduction in token supply from a deposit or withdrawal
@dev This calculation accounts for slippage, but not fees.
Needed to prevent front-running, not for precise calculations!
@param _amounts Amount of each underlying coin being deposited
@param _is_deposit set True for deposits, False for withdrawals
@return Expected amount of LP tokens received
"""
meta_amounts: uint256[N_COINS] = empty(uint256[N_COINS])
base_amounts: uint256[BASE_N_COINS] = empty(uint256[BASE_N_COINS])
for i in range(MAX_COIN):
meta_amounts[i] = _amounts[i]
for i in range(BASE_N_COINS):
base_amounts[i] = _amounts[i + MAX_COIN]
base_tokens: uint256 = CurveBase(self.base_pool).calc_token_amount(base_amounts, _is_deposit)
meta_amounts[MAX_COIN] = base_tokens
return CurveMeta(self.pool).calc_token_amount(meta_amounts, _is_deposit)
```
```shell
>>> todo:
```
::::
---
## Overview(Deposit-contracts)
## Curve Stableswap Exchange: Deposit Contracts
Curve pools may rely on a different contract, called a _deposit zap_ for the addition and removal of underlying coins.
This is particularly useful for lending pools, which may only support the addition/removal of wrapped coins. Furthermore,
deposit zaps are also useful for metapools, which do not support the addition/removal of base pool coins.
---
## NOTE: By declaring `balanceOf` as public, vyper automatically generates a 'balanceOf()' getter
## Token Methods
### `CurveToken.name`
::::description[CurveToken.name() → string[64]: view]
Get token name.
```vyper hl_lines="1 8"
name: public(string[64])
...
@public
def __init__(_name: string[64], _symbol: string[32], _decimals: uint256, _supply: uint256):
init_supply: uint256 = _supply * 10 **_decimals
self.name = _name
self.symbol = _symbol
self.decimals = _decimals
self.balanceOf[msg.sender] = init_supply
self.total_supply = init_supply
self.minter = msg.sender
log.Transfer(ZERO_ADDRESS, msg.sender, init_supply)
```
```shell
>>> lp_token.symbol()
'Curve.fi yDAI/yUSDC/yUSDT/yBUSD'
```
::::
### `CurveToken.symbol`
::::description[CurveToken.symbol() → string[32]: view]
Get token symbol.
```vyper hl_lines="1 9"
symbol: public(string[32])
...
@public
def __init__(_name: string[64], _symbol: string[32], _decimals: uint256, _supply: uint256):
init_supply: uint256 = _supply * 10 **_decimals
self.name = _name
self.symbol = _symbol
self.decimals = _decimals
self.balanceOf[msg.sender] = init_supply
self.total_supply = init_supply
self.minter = msg.sender
log.Transfer(ZERO_ADDRESS, msg.sender, init_supply)
```
```shell
>>> lp_token.symbol()
'yDAI+yUSDC+yUSDT+yBUSD'
```
::::
### `CurveToken.decimals`
::::description[CurveToken.decimals() → uint256: view]
Get token precision (decimals).
```vyper hl_lines="1 10"
decimals: public(uint256)
...
@public
def __init__(_name: string[64], _symbol: string[32], _decimals: uint256, _supply: uint256):
init_supply: uint256 = _supply * 10 **_decimals
self.name = _name
self.symbol = _symbol
self.decimals = _decimals
self.balanceOf[msg.sender] = init_supply
self.total_supply = init_supply
self.minter = msg.sender
log.Transfer(ZERO_ADDRESS, msg.sender, init_supply)
```
```shell
>>> lp_token.decimals()
18
```
::::
### `CurveToken.balanceOf`
::::description[CurveToken.balanceOf(account: address) → uint256: view]
Get token balance for an account.
| Input | Type | Description |
| ----------- | -------| ----|
| `address` | `address` | Address to get the token balance for |
```vyper hl_lines="5 11 15"
# NOTE: By declaring `balanceOf` as public, vyper automatically generates a 'balanceOf()' getter
# method to allow access to account balances.
# The _KeyType will become a required parameter for the getter and it will return _ValueType.
# See: https://vyper.readthedocs.io/en/v0.1.0-beta.8/types.html?highlight=getter#mappings
balanceOf: public(map(address, uint256))
...
@public
def __init__(_name: string[64], _symbol: string[32], _decimals: uint256, _supply: uint256):
init_supply: uint256 = _supply * 10 **_decimals
self.name = _name
self.symbol = _symbol
self.decimals = _decimals
self.balanceOf[msg.sender] = init_supply
self.total_supply = init_supply
self.minter = msg.sender
log.Transfer(ZERO_ADDRESS, msg.sender, init_supply)
```
```shell
>>> lp_token.balanceOf("0x69fb7c45726cfe2badee8317005d3f94be838840")
72372801850459006740117197
```
::::
### `CurveToken.totalSupply`
::::description[CurveToken.totalSupply() → uint256: view]
Get total token supply.
```vyper
@public
@constant
def totalSupply() -> uint256:
"""
@dev Total number of tokens in existence.
"""
return self.total_supply
```
```shell
>>> lp_token.totalSupply()
73112516629065063732935484
```
::::
### `CurveToken.allowance`
::::description[CurveToken.allowance(_owner: address, _spender: address) → uint256: view]
This view method gets the allowance of an address (`_spender`) to spend on behalf of some other account `_owner`.
| Input | Type | Description |
| ----------- | -------| ----|
| `_spender` | `address` | Account that can spend up to the allowance |
| `_owner` | `address` | Account that is paying when ``_spender`` spends the allowance|
```vyper
@public
@constant
def allowance(_owner : address, _spender : address) -> uint256:
"""
@dev Function to check the amount of tokens that an owner allowed to a spender.
@param _owner The address which owns the funds.
@param _spender The address which will spend the funds.
@return An uint256 specifying the amount of tokens still available for the spender.
"""
return self.allowances[_owner][_spender]
```
::::
### `CurveToken.transfer`
::::description[CurveToken.transfer(_to: address, _value: uint256) → bool]
Transfer tokens to a specified address. `_from` address is implicitly `msg.sender`. Returns ``True`` if the
transfer succeeds.
| Input | Type | Description |
| ----------- | -------| ----|
| `_to` | `address` | Receiver of the tokens |
| `_value` | `uint256` | Amount of tokens to be transferred |
Emits: Transfer
```vyper
@public
def transfer(_to : address, _value : uint256) -> bool:
"""
@dev Transfer token for a specified address
@param _to The address to transfer to.
@param _value The amount to be transferred.
"""
# NOTE: vyper does not allow underflows
# so the following subtraction would revert on insufficient balance
self.balanceOf[msg.sender] -= _value
self.balanceOf[_to] += _value
log.Transfer(msg.sender, _to, _value)
return True
```
::::
### `CurveToken.transferFrom`
::::description[CurveToken.transferFrom(_from: address, _to: address, _value: uint256) → bool]
Transfer tokens from one address to another. `msg.sender` does the transfer on behalf of the `_from` address, and
requires sufficient spending allowance. Returns ``True`` if transfer succeeds.
| Input | Type | Description |
| ----------- | -------| ----|
| `_from` | `address` | Address which `msg.sender` want to send tokens from |
| `_to` | `address` | Address which `msg.sender` want to transfer to |
| `_value` | `uint256` | Amount of tokens to be transferred |
Emits: Transfer
```vyper
@public
def transferFrom(_from : address, _to : address, _value : uint256) -> bool:
"""
@dev Transfer tokens from one address to another.
Note that while this function emits a Transfer event, this is not required as per the specification,
and other compliant implementations may not emit the event.
@param _from address The address which you want to send tokens from
@param _to address The address which you want to transfer to
@param _value uint256 the amount of tokens to be transferred
"""
# NOTE: vyper does not allow underflows
# so the following subtraction would revert on insufficient balance
self.balanceOf[_from] -= _value
self.balanceOf[_to] += _value
if msg.sender != self.minter: # minter is allowed to transfer anything
# NOTE: vyper does not allow underflows
# so the following subtraction would revert on insufficient allowance
self.allowances[_from][msg.sender] -= _value
log.Transfer(_from, _to, _value)
return True
```
:::note
:::
While this function emits a Transfer event, this is not required as per the specification, and other compliant
implementations may not emit the event.
::::
### `CurveToken.approve`
::::description[CurveToken.approve(_spender: address, _value: uint256) → bool]
Approve the passed address to spend the specified amount of tokens on behalf of ``msg.sender``. Returns ``True`` on
successful approvals.
| Input | Type | Description |
| ----------- | -------| ----|
| `_spender` | `address` | Address which will spend the funds |
| `_value` | `uint256` | Amount of tokens to be spent |
Emits: Approval
```vyper
@public
def approve(_spender : address, _value : uint256) -> bool:
"""
@dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
Beware that changing an allowance with this method brings the risk that someone may use both the old
and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
@param _spender The address which will spend the funds.
@param _value The amount of tokens to be spent.
"""
assert _value == 0 or self.allowances[msg.sender][_spender] == 0
self.allowances[msg.sender][_spender] = _value
log.Approval(msg.sender, _spender, _value)
return True
```
:::warning
:::
Beware that changing an allowance with this method brings the risk that someone may use both the old and the new
allowance by unfortunate transaction ordering. One possible solution to mitigate this race condition is
to first reduce the spender’s allowance to 0 and set the desired value afterwards (see this
[GitHub issue](https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729)).
:::warning
:::
For Curve LP Tokens V1 and V2, **non-zero to non-zero approvals are prohibited**. Instead, after every non-zero
approval, the allowance for the spender must be reset to 0.
::::
## Minter Methods
The following methods are only callable by the ``minter`` (private attribute).
:::note
For Curve Token V1, the ``minter`` attribute is not ``public``.
:::
### `CurveToken.mint`
::::description[CurveToken.mint(_to: address, _value: uint256)]
This encapsulates the modification of balances such that the proper events are emitted.
| Input | Type | Description |
| ----------- | -------| ----|
| `_to` | `address` | Address that will receive the minted tokens |
| `_value` | `uint256` | Amount of tokens that will be minted |
Emits: Transfer
```vyper
@public
def mint(_to: address, _value: uint256):
"""
@dev Mint an amount of the token and assigns it to an account.
This encapsulates the modification of balances such that the
proper events are emitted.
@param _to The account that will receive the created tokens.
@param _value The amount that will be created.
"""
assert msg.sender == self.minter
assert _to != ZERO_ADDRESS
self.total_supply += _value
self.balanceOf[_to] += _value
log.Transfer(ZERO_ADDRESS, _to, _value)
```
::::
### `CurveToken.burn`
::::description[CurveToken.burn(_value: uint256)]
Burn an amount of the token of ``msg.sender``.
| Input | Type | Description |
| ----------- | -------| ----|
| `_value` | `uint256` | Token amount that will be burned |
Emits: Transfer
```vyper
@private
def _burn(_to: address, _value: uint256):
"""
@dev Internal function that burns an amount of the token of a given
account.
@param _to The account whose tokens will be burned.
@param _value The amount that will be burned.
"""
assert _to != ZERO_ADDRESS
self.total_supply -= _value
self.balanceOf[_to] -= _value
log.Transfer(_to, ZERO_ADDRESS, _value)
@public
def burn(_value: uint256):
"""
@dev Burn an amount of the token of msg.sender.
@param _value The amount that will be burned.
"""
assert msg.sender == self.minter, "Only minter is allowed to burn"
self._burn(msg.sender, _value)
```
::::
### `CurveToken.burnFrom`
::::description[CurveToken.burnFrom(_to: address, _value: uint256)]
Burn an amount of the token from a given account.
| Input | Type | Description |
| ----------- | -------| ----|
| `_to` | `address` | Account whose tokens will be burned|
| `_value` | `uint256` | Token amount that will be burned |
Emits: Transfer
```vyper
@private
def _burn(_to: address, _value: uint256):
"""
@dev Internal function that burns an amount of the token of a given
account.
@param _to The account whose tokens will be burned.
@param _value The amount that will be burned.
"""
assert _to != ZERO_ADDRESS
self.total_supply -= _value
self.balanceOf[_to] -= _value
log.Transfer(_to, ZERO_ADDRESS, _value)
@public
def burnFrom(_to: address, _value: uint256):
"""
@dev Burn an amount of the token from a given account.
@param _to The account whose tokens will be burned.
@param _value The amount that will be burned.
"""
assert msg.sender == self.minter, "Only minter is allowed to burn"
self._burn(_to, _value)
```
::::
### `CurveToken.set_minter`
::::description[CurveToken.set_minter(_minter: address)]
Set a new minter for the token.
| Input | Type | Description |
| ----------- | -------| ----|
| `_minter` | `address` | Address of the new minter |
```vyper
@public
def set_minter(_minter: address):
assert msg.sender == self.minter
self.minter = _minter
```
::::
---
## Curve Token V2
The implementation for a Curve Token V2 may be viewed on
[GitHub](https://github.com/curvefi/curve-contract/blob/master/contracts/tokens/CurveTokenV2.vy).
:::note
Compared to Curve Token v1, the following changes have been made to the API:
- `minter` attribute is public and therefore a `minter` getter has been generated
- `name` and `symbol` attributes can be set via `set_name`
- `mint` method returns `bool`
- `burnFrom` method returns `bool`
- `burn` method has been removed
:::
:::warning
For Curve LP Tokens V1 and V2, non-zero to non-zero approvals are prohibited. Instead, after every non-zero approval,
the allowance for the spender must be reset to `0`.
:::
### `CurveToken.minter`
::::description[`CurveToken.minter() → address: view`]
Getter for the address of the `minter` of the token.
```vyper hl_lines="1 11"
minter: public(address)
@external
def __init__(_name: String[64], _symbol: String[32], _decimals: uint256, _supply: uint256):
init_supply: uint256 = _supply * 10 **_decimals
self.name = _name
self.symbol = _symbol
self.decimals = _decimals
self.balanceOf[msg.sender] = init_supply
self.total_supply = init_supply
self.minter = msg.sender
log Transfer(ZERO_ADDRESS, msg.sender, init_supply)
```
```shell
>>> lp_token.minter()
""
```
::::
### `CurveToken.set_name`
::::description[`CurveToken.set_name(_name: String[64], _symbol: String[32])`]
Set the name and symbol of the token. This method can only be called by minter.
| Input | Type | Description |
| ----------- | -------| ----|
| `name` | `String[64]` | New name of token |
| `symbol` | `String[32]` | New symbol of token |
```vyper hl_lines="1 11"
@external
def set_name(_name: String[64], _symbol: String[32]):
assert Curve(self.minter).owner() == msg.sender
self.name = _name
self.symbol = _symbol
```
```shell
>>> lp_token.minter()
todo: ""
```
::::
### `CurveToken.mint`
::::description[`CurveToken.mint(_to: address, _value: uint256) → bool`]
Mint an amount of the token and assign it to an account. This encapsulates the modification of balances such that
the proper events are emitted. Returns `True` if not reverted.
| Input | Type | Description |
| ----------- | -------| ----|
| `_to` | `address` | Receiver of minted tokens |
| `_value` | `uint256` | Amount of tokens minted |
Emits: Transfer
```vyper
@external
def mint(_to: address, _value: uint256) -> bool:
"""
@dev Mint an amount of the token and assigns it to an account.
This encapsulates the modification of balances such that the
proper events are emitted.
@param _to The account that will receive the created tokens.
@param _value The amount that will be created.
"""
assert msg.sender == self.minter
assert _to != ZERO_ADDRESS
self.total_supply += _value
self.balanceOf[_to] += _value
log Transfer(ZERO_ADDRESS, _to, _value)
return True
```
```shell
>>> lp_token.mint()
todo: ""
```
::::
### `CurveToken.burnFrom`
::::description[`CurveToken.burnFrom(_to: address, _value: uint256) → bool`]
Burn an amount of the token from a given account. Returns `True` if not reverted.
| Input | Type | Description |
| ----------- | -------| ----|
| `_to` | `address` | Account whose tokens will be burned |
| `_value` | `uint256` | Amount that will be burned |
Emits: Transfer
```vyper
@external
def burnFrom(_to: address, _value: uint256) -> bool:
"""
@dev Burn an amount of the token from a given account.
@param _to The account whose tokens will be burned.
@param _value The amount that will be burned.
"""
assert msg.sender == self.minter
assert _to != ZERO_ADDRESS
self.total_supply -= _value
self.balanceOf[_to] -= _value
log Transfer(_to, ZERO_ADDRESS, _value)
return True
```
```shell
>>> lp_token.burnFrom()
todo: ""
```
::::
---
## Curve Token V3
The Curve Token V3 is more gas efficient than versions 1 and 2. The implementation for a Curve Token V3 may be viewed on
[GitHub](https://github.com/curvefi/curve-contract/blob/master/contracts/tokens/CurveTokenV3.vy).
:::note
Compared to the Curve Token V2 API, there have been the following changes:
`increaseAllowance` and `decreaseAllowance` methods added to mitigate race conditions.
:::
### `CurveToken.increaseAllowance`
::::description[`CurveToken.increaseAllowance(_spender: address, _added_value: uint256) → bool`]
Increase the allowance granted to `_spender` by the `msg.sender`.
This is alternative to `approve` that can be used as a mitigation for the potential race condition. Returns `True`
if success.
| Input | Type | Description |
| ----------- | -------| ----|
| `_spender` | `address` | Account whose tokens will be burned |
| `_value` | `uint256` | Amount that will be burned |
Emits: Approval
```vyper
@external
def approve(_spender : address, _value : uint256) -> bool:
"""
@notice Approve the passed address to transfer the specified amount of
tokens on behalf of msg.sender
@dev Beware that changing an allowance via this method brings the risk
that someone may use both the old and new allowance by unfortunate
transaction ordering. This may be mitigated with the use of
{increaseAllowance} and {decreaseAllowance}.
https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
@param _spender The address which will transfer the funds
@param _value The amount of tokens that may be transferred
@return bool success
"""
self.allowance[msg.sender][_spender] = _value
log Approval(msg.sender, _spender, _value)
return True
@external
def increaseAllowance(_spender: address, _added_value: uint256) -> bool:
"""
@notice Increase the allowance granted to `_spender` by the caller
@dev This is alternative to {approve} that can be used as a mitigation for
the potential race condition
@param _spender The address which will transfer the funds
@param _added_value The amount of to increase the allowance
@return bool success
"""
allowance: uint256 = self.allowance[msg.sender][_spender] + _added_value
self.allowance[msg.sender][_spender] = allowance
log Approval(msg.sender, _spender, allowance)
return True
```
```shell
>>> lp_token.increaseAllowance()
todo: ""
```
::::
### `CurveToken.decreaseAllowance`
::::description[`CurveToken.decreaseAllowance(_spender: address, _subtracted_value: uint256) → bool`]
Decrease the allowance granted to `_spender` by the `msg.sender`.
This is alternative to `approve` that can be used as a mitigation for the potential race condition. Returns `True`
if success.
| Input | Type | Description |
| ----------- | -------| ----|
| `_spender` | `address` | Account whose tokens will be burned |
| `_added_value` | `uint256` | Amount that will be burned |
Emits: Approval
```vyper
@external
def approve(_spender : address, _value : uint256) -> bool:
"""
@notice Approve the passed address to transfer the specified amount of
tokens on behalf of msg.sender
@dev Beware that changing an allowance via this method brings the risk
that someone may use both the old and new allowance by unfortunate
transaction ordering. This may be mitigated with the use of
{increaseAllowance} and {decreaseAllowance}.
https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
@param _spender The address which will transfer the funds
@param _value The amount of tokens that may be transferred
@return bool success
"""
self.allowance[msg.sender][_spender] = _value
log Approval(msg.sender, _spender, _value)
return True
@external
def decreaseAllowance(_spender: address, _subtracted_value: uint256) -> bool:
"""
@notice Decrease the allowance granted to `_spender` by the caller
@dev This is alternative to {approve} that can be used as a mitigation for
the potential race condition
@param _spender The address which will transfer the funds
@param _subtracted_value The amount of to decrease the allowance
@return bool success
"""
allowance: uint256 = self.allowance[msg.sender][_spender] - _subtracted_value
self.allowance[msg.sender][_spender] = allowance
log Approval(msg.sender, _spender, allowance)
return True
```
```shell
>>> lp_token.decreaseAllowance()
todo: ""
```
::::
---
## Overview(Lp-tokens)
In exchange for depositing coins into a Curve pool, liquidity providers receive pool LP (liquidity pool) tokens.
A Curve pool LP token is an ERC20 contract specific to the Curve pool. Hence, LP tokens are transferrable.
Holders of pool LP tokens may deposit and stake the token into a pool’s liquidity gauge in order to receive ``CRV`` token rewards.
Alternatively, if the LP token is supported by a metapool, the token may be deposited into the respective metapool
in exchange for the metapool’s LP token (see here).
Currently, the following versions of Curve Stableswap LP tokens exist:
- [CurveTokenV1](https://github.com/curvefi/curve-contract/blob/master/contracts/tokens/CurveTokenV1.vy): LP token targetting Vyper [^0.1.0-beta.16](https://vyper.readthedocs.io/en/stable/release-notes.html#v0-1-0-beta-16)
- [CurveTokenV2](https://github.com/curvefi/curve-contract/blob/master/contracts/tokens/CurveTokenV2.vy): LP token targetting Vyper [^0.2.0](https://vyper.readthedocs.io/en/stable/release-notes.html#v0-2-1)
- [CurveTokenV3](https://github.com/curvefi/curve-contract/blob/master/contracts/tokens/CurveTokenV3.vy): LP token targetting Vyper [^0.2.0](https://vyper.readthedocs.io/en/stable/release-notes.html#v0-2-1)
todo: add hyperlink to deployment addresses
The version of each pool’s LP token can be found in the Deployment Addresses.
:::note
For older Curve pools the ``token`` attribute is not always ``public`` and a getter has not been explicitly implemented.
:::
---
## Curve Stableswap Exchange: Overview
Curve achieves extremely efficient stablecoin trades by implementing the Stableswap invariant, which has significantly lower slippage for stablecoin trades than many other prominent invariants (e.g., constant-product). Note that in this context stablecoins refers to tokens that are stable representations of one another. This includes, for example, USD-pegged stablecoins (like DAI and USDC), but also ETH and sETH (synthetic ETH) or different versions of wrapped BTC. For a detailed overview of the Stableswap invariant design, please read the official [Stableswap whitepaper](https://curve.fi/files/stableswap-paper.pdf).
A Curve pool is essentially a smart contract that implements the Stableswap invariant and therefore contains the logic for exchanging stable tokens. However, while all Curve pools implement the Stableswap invariant, they may come in different pool flavors.
In its simplest form, a Curve pool is an implementation of the Stableswap invariant with 2 or more tokens, which can be referred to as a plain pool. Alternative and more complex pool flavors include pools with lending functionality, so-called lending pools, as well as metapools, which are pools that allow for the exchange of one or more tokens with the tokens of one or more underlying base pools.
Curve also integrates with Synthetix to offer cross-asset swaps.
All exchange functionality that Curve supports, as well as noteworthy implementation details, are explained in technical depth in this section.
---
## Admin Pool Settings
## Overview
The following are methods that may only be called by the pool admin (``owner``).
Additionally, some admin methods require a two-phase transaction process, whereby changes are committed in a first
transaction and after a forced delay applied via a second transaction. The minimum delay after which a committed
action can be applied is given by the constant pool attribute ``admin_actions_delay``, which is set to 3 days.
## Pool Ownership Methods
### `StableSwap.commit_transfer_ownership`
::::description[Stableswap.commit_transfer_ownership(_owner: address)]
Initiate an ownership transfer of pool to ``_owner``.
| Input | Type | Description |
| ----------- | -------| ----|
| `_owner` | `address` | Future owner of the pool contract |
Emits: CommitNewAdmin
```vyper
ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400
...
@external
def commit_transfer_ownership(_owner: address):
assert msg.sender == self.owner # dev: only owner
assert self.transfer_ownership_deadline == 0 # dev: active transfer
_deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY
self.transfer_ownership_deadline = _deadline
self.future_owner = _owner
log CommitNewAdmin(_deadline, _owner)
```
```shell
>>> pool.commit_transfer_ownership()
todo: console output
```
:::note
The ownership can not be transferred before ``transfer_ownership_deadline``, which is the timestamp of the
current block delayed by ``ADMIN_ACTIONS_DELAY``.
:::
::::
### `StableSwap.apply_transfer_ownership`
::::description[Stableswap.apply_transfer_ownership()]
Transfers ownership of the pool from current owner to the owner previously set via ``commit_transfer_ownership``.
Emits: NewAdmin
```vyper
@external
def apply_transfer_ownership():
assert msg.sender == self.owner # dev: only owner
assert block.timestamp >= self.transfer_ownership_deadline # dev: insufficient time
assert self.transfer_ownership_deadline != 0 # dev: no active transfer
self.transfer_ownership_deadline = 0
_owner: address = self.future_owner
self.owner = _owner
log NewAdmin(_owner)
```
```shell
>>> pool.apply_transfer_ownership()
todo: log output
```
:::warning
Pool ownership can only be transferred once.
:::
::::
### `StableSwap.revert_transfer_ownership()`
::::description[`StableSwap.revert_transfer_ownership()`]
Reverts any previously committed transfer of ownership. This method resets the
``transfer_ownership_deadline`` to ``0``.
```vyper
@external
def revert_transfer_ownership():
assert msg.sender == self.owner # dev: only owner
self.transfer_ownership_deadline = 0
```
```shell
>>> pool.revert_transfer_ownership()
todo: log output
```
::::
## Amplification Coefficient Admin Controls
The amplification coefficient ``A`` determines a pool’s tolerance for imbalance between the assets within it.
A higher value means that trades will incur slippage sooner as the assets within the pool become imbalanced.
:::note
Within the pools, ``A`` is in fact implemented as ``1 / A`` and therefore a higher value implies that the pool will
be more tolerant to slippage when imbalanced.
:::
The appropriate value for A is dependent upon the type of coin being used within the pool, and is subject to optimisation
and pool-parameter update based on the market history of the trading pair. It is possible to modify the amplification
coefficient for a pool after it has been deployed. However, it requires a vote within the Curve DAO and must reach a
15% quorum.
### `StableSwap.ramp_A`
::::description[`StableSwap.ramp_A(_future_A: uint256, _future_time: uint256)`]
Ramp ``A`` up or down by setting a new ``A`` to take effect at a future point in time.
| Input | Type | Description |
| ----------- | -------| ----|
| `_future_A` | `uint256` | New future value of ``A`` |
| `_future_time` | `uint256` | Timestamp at which new ``A`` should take effect |
Emits: RampA
```vyper
MIN_RAMP_TIME: constant(uint256) = 86400
MAX_A_CHANGE: constant(uint256) = 10
MAX_A: constant(uint256) = 10 **6
...
@external
def ramp_A(_future_A: uint256, _future_time: uint256):
assert msg.sender == self.owner # dev: only owner
assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME
assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time
_initial_A: uint256 = self._A()
_future_A_p: uint256 = _future_A * A_PRECISION
assert _future_A > 0 and _future_A < MAX_A
if _future_A_p < _initial_A:
assert _future_A_p * MAX_A_CHANGE >= _initial_A
else:
assert _future_A_p <= _initial_A * MAX_A_CHANGE
self.initial_A = _initial_A
self.future_A = _future_A_p
self.initial_A_time = block.timestamp
self.future_A_time = _future_time
log RampA(_initial_A, _future_A_p, block.timestamp, _future_time)
```
```shell
>>> pool.ramp_A()
todo: log output
```
::::
### `StableSwap.stop_ramp_A`
::::description[Stableswap.stop_ramp_A()]
Stop ramping ``A`` up or down and sets ``A`` to current ``A``.
Emits: StopRampA
```vyper
@external
def stop_ramp_A():
assert msg.sender == self.owner # dev: only owner
current_A: uint256 = self._A()
self.initial_A = current_A
self.future_A = current_A
self.initial_A_time = block.timestamp
self.future_A_time = block.timestamp
# now (block.timestamp < t1) is always False, so we return saved A
log StopRampA(current_A, block.timestamp)
```
```shell
>>> pool.stop_ramp_A()
todo: log output
```
::::
## Swap Fees Admin Controls
todo: hyperlink to fee collection and distribution
Curve pools charge fees on token swaps, where the fee may differ between pools. An admin fee is charged on the pool fee.
For an overview of how fees are distributed, please refer to Fee Collection and Distribution.
### `StableSwap.commit_new_fee`
::::description[`StableSwap.commit_new_fee(_new_fee: uint256, _new_admin_fee: uint256)`]
The method commits new fee params: these fees do not take immediate effect.
| Input | Type | Description |
| ----------- | -------| ----|
| `_new_fee` | `uint256` | New pool fee |
| `_new_admin_fee` | `uint256` | New admin fee (expressed as a percentage of the pool fee) |
Emits: CommitNewFee
```vyper
MAX_ADMIN_FEE: constant(uint256) = 10 * 10 **9
MAX_FEE: constant(uint256) = 5 * 10 **9
ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400
@external
def commit_new_fee(new_fee: uint256, new_admin_fee: uint256):
assert msg.sender == self.owner # dev: only owner
assert self.admin_actions_deadline == 0 # dev: active action
assert new_fee <= MAX_FEE # dev: fee exceeds maximum
assert new_admin_fee <= MAX_ADMIN_FEE # dev: admin fee exceeds maximum
_deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY
self.admin_actions_deadline = _deadline
self.future_fee = new_fee
self.future_admin_fee = new_admin_fee
log CommitNewFee(_deadline, new_fee, new_admin_fee)
```
```shell
>>> pool.commit_new_fee()
todo: log output
```
:::note
Both the pool ``fee`` and the ``admin_fee`` are capped by the constants ``MAX_FEE`` and ``MAX_ADMIN_FEE``,
respectively. By default ``MAX_FEE`` is set at 50% and ``MAX_ADMIN_FEE`` at 100% (which is charged on the
``MAX_FEE`` amount).
:::
::::
### `StableSwap.apply_new_fee`
::::description[Stableswap.apply_new_fee()]
Apply the previously committed new pool and admin fees for the pool.
Emits: NewFee
```vyper
@external
def apply_new_fee():
assert msg.sender == self.owner # dev: only owner
assert block.timestamp >= self.admin_actions_deadline # dev: insufficient time
assert self.admin_actions_deadline != 0 # dev: no active action
self.admin_actions_deadline = 0
_fee: uint256 = self.future_fee
_admin_fee: uint256 = self.future_admin_fee
self.fee = _fee
self.admin_fee = _admin_fee
log NewFee(_fee, _admin_fee)
```
```shell
>>> pool.commit_new_fee()
todo: log output
```
:::note
Unlike ownership transfers, pool and admin fees may be set more than once.
:::
::::
### `StableSwap.revert_new_parameters`
::::description[`StableSwap.revert_new_parameters()`]
Resets any previously committed new fees.
```vyper
@external
def revert_new_parameters():
assert msg.sender == self.owner # dev: only owner
self.admin_actions_deadline = 0
```
```shell
>>> pool.revert_new_parameters()
todo: log output
```
::::
### `StableSwap.admin_balances`
::::description[`StableSwap.admin_balances(i: uint256) → uint256`]
Get the admin balance for a single coin in the pool.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint256` | Index of the coin to get admin balance for |
```vyper
@view
@external
def admin_balances(i: uint256) -> uint256:
return ERC20(self.coins[i]).balanceOf(self) - self.balances[i]
```
```shell
>>> pool.admin_balances()
todo: log output
```
::::
### `StableSwap.withdraw_admin_fees`
::::description[`StableSwap.withdraw_admin_fees()`]
Withdraws and transfers admin fees of the pool to the pool owner.
```vyper
@external
def withdraw_admin_fees():
assert msg.sender == self.owner # dev: only owner
for i in range(N_COINS):
c: address = self.coins[i]
value: uint256 = ERC20(c).balanceOf(self) - self.balances[i]
if value > 0:
assert ERC20(c).transfer(msg.sender, value)
```
```shell
>>> pool.withdraw_admin_fees()
todo: log output
```
::::
### `StableSwap.donate_admin_fees`
::::description[`StableSwap.donate_admin_fees()`]
Donate all admin fees to the pool’s liquidity providers.
```vyper
@external
def donate_admin_fees():
assert msg.sender == self.owner # dev: only owner
for i in range(N_COINS):
self.balances[i] = ERC20(self.coins[i]).balanceOf(self)
```
```shell
>>> pool.donate_admin_fees()
todo: log output
```
:::note
Older Curve pools do not implement this method.
:::
::::
## Kill a Pool
### `StableSwap.kill_me`
::::description[`StableSwap.kill_me()`]
Pause a pool by setting the ``is_killed`` boolean flag to ``True``.
This disables the following pool functionality:
- add_liquidity
- exchange
- remove_liquidity_imbalance
- remove_liquidity_one_coin
It is only possible for existing LPs to remove liquidity via ``remove_liquidity`` from a paused pool.
```vyper hl_lines="10 26 39 53 61"
@external
@nonreentrant('lock')
def add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256) -> uint256:
"""
@notice Deposit coins into the pool
@param amounts List of amounts of coins to deposit
@param min_mint_amount Minimum amount of LP tokens to mint from the deposit
@return Amount of LP tokens received by depositing
"""
assert not self.is_killed # dev: is killed
...
@external
@nonreentrant('lock')
def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256:
"""
@notice Perform an exchange between two coins
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param dx Amount of `i` being exchanged
@param min_dy Minimum amount of `j` to receive
@return Actual amount of `j` received
"""
assert not self.is_killed # dev: is killed
...
@external
@nonreentrant('lock')
def remove_liquidity_imbalance(amounts: uint256[N_COINS], max_burn_amount: uint256) -> uint256:
"""
@notice Withdraw coins from the pool in an imbalanced amount
@param amounts List of amounts of underlying coins to withdraw
@param max_burn_amount Maximum amount of LP token to burn in the withdrawal
@return Actual amount of the LP token burned in the withdrawal
"""
assert not self.is_killed # dev: is killed
...
@external
@nonreentrant('lock')
def remove_liquidity_one_coin(_token_amount: uint256, i: int128, _min_amount: uint256) -> uint256:
"""
@notice Withdraw a single coin from the pool
@param _token_amount Amount of LP tokens to burn in the withdrawal
@param i Index value of the coin to withdraw
@param _min_amount Minimum amount of coin to receive
@return Amount of coin received
"""
assert not self.is_killed # dev: is killed
...
@external
def kill_me():
assert msg.sender == self.owner # dev: only owner
assert self.kill_deadline > block.timestamp # dev: deadline has passed
self.is_killed = True
```
```shell
todo: add example
```
:::note
Pools can only be killed within the first 30 days after deployment.
:::
::::
### `StableSwap.unkill_me`
::::description[Stableswap.unkill_me]
Unpause a pool that was previously paused, re-enabling exchanges.
```vyper
@external
def unkill_me():
assert msg.sender == self.owner # dev: only owner
self.is_killed = False
```
```shell
todo: add example
```
::::
---
## Lending Pools
## Overview
Curve pools may contain lending functionality, whereby the underlying tokens are lent out on other protocols
(e.g., Compound or Yearn). Hence, the main difference to a plain pool is that a lending pool does not hold
the underlying token itself, but a **wrapped**representation of it.
Currently, Curve supports the following lending pools:
``aave``: [Aave pool](https://www.curve.fi/aave), with lending on [Aave](https://www.aave.com)
``busd``: [BUSD](https://www.curve.fi/busd) pool, with lending on [yearn.finance](https://www.yearn.finance)
``compound``: [Compound](https://www.curve.fi/compound) pool, with lending on [Compound](https://compound.finance/)
``ib``: [Iron Bank pool](https://curve.fi/ib), with lending on [Cream](https://v1.yearn.finance/lending)
``pax``: [PAX](https://curve.fi/pax) pool, with lending on [yearn.finance](https://www.yearn.finance)
``usdt``: [USDT pool](https://curve.fi/usdt), with lending on [Compound](https://www.curve.fi/compound)
``y``: [Y pool](https://curve.fi/y), with lending on [yearn.finance](https://www.yearn.finance)
An example of a Curve lending pool is
[Compound Pool](https://github.com/curvefi/curve-contract/tree/master/contracts/pools/compound),
which contains the wrapped tokens ``cDAI`` and ``cUSDC``, while the underlying tokens ``DAI`` and ``USDC`` are lent out
on Compound. Liquidity providers of the Compound Pool therefore receive interest generated on Compound in addition to
fees from token swaps in the pool.
Implementation of lending pools may differ with respect to how wrapped tokens accrue interest. There are two main types
of wrapped tokens that are used by lending pools:
``cToken-style tokens``: These are tokens, such as interest-bearing cTokens on Compound (e.g., ``cDAI``) or on yTokens
on Yearn, where interest accrues as the rate of the token increases.
``aToken-style tokens``: These are tokens, such as aTokens on AAVE (e.g., ``aDAI``), where interest accrues as the
balance of the token increases.
The template source code for lending pools may be viewed on GitHub.
:::Note
Lending pools also implement the ABI from plain pools. Refer to the plan pools documentation for overlapping methods.
:::
## Pool Info Methods
### `underlying_coins`
::::description[`StableSwap.underlying_coins(i: uint256) → address: view`]
Getter for the array of underlying coins within the pool.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint128` | Index of coin to swap from |
```vyper hl_lines="1 9 19 25 26 27 29 30 31 32 33 34 35 36 37 38 39 40 43"
underlying_coins: public(address[N_COINS])
...
@external
def __init__(
_owner: address,
_coins: address[N_COINS],
_underlying_coins: address[N_COINS],
_pool_token: address,
_A: uint256,
_fee: uint256,
_admin_fee: uint256
):
"""
@notice Contract constructor
@param _owner Contract owner address
@param _coins Addresses of ERC20 contracts of wrapped coins
@param _underlying_coins Addresses of ERC20 contracts of underlying coins
@param _pool_token Address of the token representing LP share
@param _A Amplification coefficient multiplied by n * (n - 1)
@param _fee Fee to charge for exchanges
@param _admin_fee Admin fee
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
assert _underlying_coins[i] != ZERO_ADDRESS
# approve underlying coins for infinite transfers
_response: Bytes[32] = raw_call(
_underlying_coins[i],
concat(
method_id("approve(address,uint256)"),
convert(_coins[i], bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
self.coins = _coins
self.underlying_coins = _underlying_coins
self.initial_A = _A * A_PRECISION
self.future_A = _A * A_PRECISION
self.fee = _fee
self.admin_fee = _admin_fee
self.owner = _owner
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT
self.lp_token = _pool_token
```
```shell
>>> lending_pool.coins(0)
'0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643'
>>> lending_pool.coins(1)
'0x39AA39c021dfbaE8faC545936693aC917d5E7563'
```
::::
## Exchange Methods
Like plain pools, lending pools have the ``exchange`` method. However, in the case of lending pools, calling ``exchange``
performs a swap between two wrapped tokens in the pool.
For example, calling ``exchange`` on the Compound Pool, would result in a swap between the wrapped tokens ``cDAI`` and ``cUSDC``.
### `exchange_underlying`
::::description[`StableSwap.exchange_underlying(i: int128, j: int128, dx: uint256, min_dy: uint256) → uint256`]
Perform an exchange between two underlying tokens. Index values can be found via the ``underlying_coins`` public
getter method. Returns the actual amount of coin ``j`` received.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | Index value for the underlying coin to send |
| `j` | `int128` | Index value of the underlying coin to receive |
| `_dx` | `uint256` | Amount of ``i`` being exchanged |
| `_min_dy` | `uint256` | Minimum amount of ``j`` to receive |
Emits: TokenExchangeUnderlying
```vyper
@external
@nonreentrant('lock')
def exchange_underlying(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256:
"""
@notice Perform an exchange between two underlying coins
@dev Index values can be found via the `underlying_coins` public getter method
@param i Index value for the underlying coin to send
@param j Index valie of the underlying coin to recieve
@param dx Amount of `i` being exchanged
@param min_dy Minimum amount of `j` to receive
@return Actual amount of `j` received
"""
dy: uint256 = self._exchange(i, j, dx)
assert dy >= min_dy, "Exchange resulted in fewer coins than expected"
u_coin_i: address = self.underlying_coins[i]
lending_pool: address = self.aave_lending_pool
# transfer underlying coin from msg.sender to self
_response: Bytes[32] = raw_call(
u_coin_i,
concat(
method_id("transferFrom(address,address,uint256)"),
convert(msg.sender, bytes32),
convert(self, bytes32),
convert(dx, bytes32)
),
max_outsize=32
)
if len(_response) != 0:
assert convert(_response, bool)
# deposit to aave lending pool
raw_call(
lending_pool,
concat(
method_id("deposit(address,uint256,address,uint16)"),
convert(u_coin_i, bytes32),
convert(dx, bytes32),
convert(self, bytes32),
convert(self.aave_referral, bytes32),
)
)
# withdraw `j` underlying from lending pool and transfer to caller
LendingPool(lending_pool).withdraw(self.underlying_coins[j], dy, msg.sender)
log TokenExchangeUnderlying(msg.sender, i, dx, j, dy)
return dy
```
```shell
>>> lending_pool.exchange_underlying()
todo: console output
```
:::note
Older Curve lending pools may not implement the same signature for ``exchange_underlying``. For instance, Compound
pool does not return anything for ``exchange_underlying`` and therefore costs more in terms of gas.
:::
::::
## Add/Remove Liquidity Methods
The function signatures for adding and removing liquidity to a lending pool are mostly the same as for a plain pool.
However, for lending pools, liquidity is added and removed in the wrapped token, not the underlying.
In order to be able to add and remove liquidity in the underlying token (e.g., remove DAI from Compound Pool instead of
``cDAI``) there exists a ``Deposit.vy`` contract (e.g., ([DepositCompound.vy](https://github.com/curvefi/curve-contract/blob/master/contracts/pools/compound/DepositCompound.vy)).
:::warning
Older Curve lending pools (e.g., Compound Pool) do not implement all plain pool methods for adding and removing
liquidity. For instance, ``remove_liquidity_one_coin`` is not implemented by Compound Pool).
:::
Some newer pools (e.g., [IB](https://github.com/curvefi/curve-contract/blob/master/contracts/pools/ib/StableSwapIB.vy))
have a modified signature for ``add_liquidity`` and allow the caller to specify whether the deposited liquidity is in
the wrapped or underlying token.
### `add_liquidity`
::::description[`StableSwap.add_liquidity(_amounts: uint256[N_COINS], _min_mint_amount: uint256, _use_underlying: bool = False) → uint256`]
Perform an exchange between two underlying tokens. Index values can be found via the ``underlying_coins`` public
getter method. Returns amount of LP tokens received in exchange for the deposited tokens.
| Input | Type | Description |
| ----------- | -------| ----|
| `_amounts` | `uint256[N_COINS]` | List of amounts of coins to deposit |
| `_min_mint_amount` | `uint256` | Minimum amount of LP tokens to mint from the deposit |
| `_use_underlying` | `bool` | If ``True``, deposit underlying assets instead of wrapped assets |
Emits: AddLiquidity
```vyper
@external
@nonreentrant('lock')
def add_liquidity(_amounts: uint256[N_COINS], _min_mint_amount: uint256, _use_underlying: bool = False) -> uint256:
"""
@notice Deposit coins into the pool
@param _amounts List of amounts of coins to deposit
@param _min_mint_amount Minimum amount of LP tokens to mint from the deposit
@param _use_underlying If True, deposit underlying assets instead of aTokens
@return Amount of LP tokens received by depositing
"""
assert not self.is_killed # dev: is killed
# Initial invariant
amp: uint256 = self._A()
old_balances: uint256[N_COINS] = self._balances()
lp_token: address = self.lp_token
token_supply: uint256 = ERC20(lp_token).totalSupply()
D0: uint256 = 0
if token_supply != 0:
D0 = self.get_D_precisions(old_balances, amp)
new_balances: uint256[N_COINS] = old_balances
for i in range(N_COINS):
if token_supply == 0:
assert _amounts[i] != 0 # dev: initial deposit requires all coins
new_balances[i] += _amounts[i]
# Invariant after change
D1: uint256 = self.get_D_precisions(new_balances, amp)
assert D1 > D0
# We need to recalculate the invariant accounting for fees
# to calculate fair user's share
fees: uint256[N_COINS] = empty(uint256[N_COINS])
mint_amount: uint256 = 0
if token_supply != 0:
# Only account for fees if we are not the first to deposit
ys: uint256 = (D0 + D1) / N_COINS
_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
_feemul: uint256 = self.offpeg_fee_multiplier
_admin_fee: uint256 = self.admin_fee
difference: uint256 = 0
for i in range(N_COINS):
ideal_balance: uint256 = D1 * old_balances[i] / D0
new_balance: uint256 = new_balances[i]
if ideal_balance > new_balance:
difference = ideal_balance - new_balance
else:
difference = new_balance - ideal_balance
xs: uint256 = old_balances[i] + new_balance
fees[i] = self._dynamic_fee(xs, ys, _fee, _feemul) * difference / FEE_DENOMINATOR
if _admin_fee != 0:
self.admin_balances[i] += fees[i] * _admin_fee / FEE_DENOMINATOR
new_balances[i] = new_balance - fees[i]
D2: uint256 = self.get_D_precisions(new_balances, amp)
mint_amount = token_supply * (D2 - D0) / D0
else:
mint_amount = D1 # Take the dust if there was any
assert mint_amount >= _min_mint_amount, "Slippage screwed you"
# Take coins from the sender
if _use_underlying:
lending_pool: address = self.aave_lending_pool
aave_referral: bytes32 = convert(self.aave_referral, bytes32)
# Take coins from the sender
for i in range(N_COINS):
amount: uint256 = _amounts[i]
if amount != 0:
coin: address = self.underlying_coins[i]
# transfer underlying coin from msg.sender to self
_response: Bytes[32] = raw_call(
coin,
concat(
method_id("transferFrom(address,address,uint256)"),
convert(msg.sender, bytes32),
convert(self, bytes32),
convert(amount, bytes32)
),
max_outsize=32
)
if len(_response) != 0:
assert convert(_response, bool)
# deposit to aave lending pool
raw_call(
lending_pool,
concat(
method_id("deposit(address,uint256,address,uint16)"),
convert(coin, bytes32),
convert(amount, bytes32),
convert(self, bytes32),
aave_referral,
)
)
else:
for i in range(N_COINS):
amount: uint256 = _amounts[i]
if amount != 0:
assert ERC20(self.coins[i]).transferFrom(msg.sender, self, amount) # dev: failed transfer
# Mint pool tokens
CurveToken(lp_token).mint(msg.sender, mint_amount)
log AddLiquidity(msg.sender, _amounts, fees, D1, token_supply + mint_amount)
return mint_amount
```
```shell
>>> lending_pool.add_liquidity()
todo: console output
```
::::
---
## Token corresponding to the pool is always the last one
A metapool is a pool where a stablecoin is paired against the LP token from another pool, a so-called _base_ pool.
For example, a liquidity provider may deposit ``DAI`` into
[3Pool](https://etherscan.io/address/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7#code) and in exchange receive the
pool’s LP token ``3CRV``. The ``3CRV`` LP token may then be deposited into the
[GUSD metapool](https://etherscan.io/address/0x4f062658EaAF2C1ccf8C8e36D6824CDf41167956), which contains the
coins ``GUSD`` and ``3CRV``, in exchange for the metapool’s LP token gusd3CRV. The obtained LP token may then be staked
in the metapool’s liquidity gauge for ``CRV`` rewards.
Metapools provide an opportunity for the base pool liquidity providers to earn additional trading fees by depositing
their LP tokens into the metapool. Note that the ``CRV`` rewards received for staking LP tokens into the pool’s liquidity
gauge may differ for the base pool’s liquidity gauge and the metapool’s liquidity gauge. For details on liquidity
gauges and protocol rewards, please refer to Liquidity Gauges and Minting CRV.
:::note
Metapools also implement the ABI from plain pools. The template source code for metapools may be viewed on
[GitHub](https://github.com/curvefi/curve-contract/blob/master/contracts/pool-templates/meta/SwapTemplateMeta.vy).
:::
## Pool Info Methods
### `base_coins`
::::description[`StableSwap.base_coins(i: uint256) → address: view`]
Get the coins of the base pool. Returns `address` of the coin at index `i`.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint256` | Coin index |
```vyper hl_lines="2 6 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59"
# Token corresponding to the pool is always the last one
BASE_POOL_COINS: constant(int128) = 3
...
base_coins: public(address[BASE_POOL_COINS])
...
@external
def __init__(
_owner: address,
_coins: address[N_COINS],
_pool_token: address,
_base_pool: address,
_A: uint256,
_fee: uint256,
_admin_fee: uint256
):
"""
@notice Contract constructor
@param _owner Contract owner address
@param _coins Addresses of ERC20 conracts of coins
@param _pool_token Address of the token representing LP share
@param _base_pool Address of the base pool (which will have a virtual price)
@param _A Amplification coefficient multiplied by n * (n - 1)
@param _fee Fee to charge for exchanges
@param _admin_fee Admin fee
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
self.coins = _coins
self.initial_A = _A * A_PRECISION
self.future_A = _A * A_PRECISION
self.fee = _fee
self.admin_fee = _admin_fee
self.owner = _owner
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT
self.token = CurveToken(_pool_token)
self.base_pool = _base_pool
self.base_virtual_price = Curve(_base_pool).get_virtual_price()
self.base_cache_updated = block.timestamp
for i in range(BASE_POOL_COINS):
_base_coin: address = Curve(_base_pool).coins(convert(i, uint256))
self.base_coins[i] = _base_coin
# approve underlying coins for infinite transfers
_response: Bytes[32] = raw_call(
_base_coin,
concat(
method_id("approve(address,uint256)"),
convert(_base_pool, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
```
```shell
>>> metapool.base_coins(0)
'0x6B175474E89094C44Da98b954EedeAC495271d0F'
>>> metapool.base_coins(1)
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
>>> metapool.base_coins(2)
'0xdAC17F958D2ee523a2206206994597C13D831ec7'
```
::::
### `StableSwap.coins`
::::description[`StableSwap.coins(i: uint256) → address: view`]
Get the coins of the metapool. Returns `address` of coin at index `i`.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint256` | Coin index |
```vyper hl_lines="1 5 12 22 29 30 31"
N_COINS: constant(int128) = 2
...
coins: public(address[N_COINS])
...
@external
def __init__(
_owner: address,
_coins: address[N_COINS],
_pool_token: address,
_base_pool: address,
_A: uint256,
_fee: uint256,
_admin_fee: uint256
):
"""
@notice Contract constructor
@param _owner Contract owner address
@param _coins Addresses of ERC20 conracts of coins
@param _pool_token Address of the token representing LP share
@param _base_pool Address of the base pool (which will have a virtual price)
@param _A Amplification coefficient multiplied by n * (n - 1)
@param _fee Fee to charge for exchanges
@param _admin_fee Admin fee
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
self.coins = _coins
self.initial_A = _A * A_PRECISION
self.future_A = _A * A_PRECISION
self.fee = _fee
self.admin_fee = _admin_fee
self.owner = _owner
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT
self.token = CurveToken(_pool_token)
self.base_pool = _base_pool
self.base_virtual_price = Curve(_base_pool).get_virtual_price()
self.base_cache_updated = block.timestamp
for i in range(BASE_POOL_COINS):
_base_coin: address = Curve(_base_pool).coins(convert(i, uint256))
self.base_coins[i] = _base_coin
# approve underlying coins for infinite transfers
_response: Bytes[32] = raw_call(
_base_coin,
concat(
method_id("approve(address,uint256)"),
convert(_base_pool, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
```
```shell
>>> metapool.coins(0)
'0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd'
>>> metapool.coins(1)
'0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490'
```
In this console example, ``coins(0)`` is the metapool’s coin (``GUSD``) and ``coins(1)`` is the LP token of
the base pool (``3CRV``).
::::
### `StableSwap.base_pool`
::::description[`StableSwap.base_pool() → address: view`]
Get the address of the base pool. Returns `address` of the base pool implementation.
```vyper hl_lines="1 10 20 36 40"
base_pool: public(address)
...
@external
def __init__(
_owner: address,
_coins: address[N_COINS],
_pool_token: address,
_base_pool: address,
_A: uint256,
_fee: uint256,
_admin_fee: uint256
):
"""
@notice Contract constructor
@param _owner Contract owner address
@param _coins Addresses of ERC20 conracts of coins
@param _pool_token Address of the token representing LP share
@param _base_pool Address of the base pool (which will have a virtual price)
@param _A Amplification coefficient multiplied by n * (n - 1)
@param _fee Fee to charge for exchanges
@param _admin_fee Admin fee
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
self.coins = _coins
self.initial_A = _A * A_PRECISION
self.future_A = _A * A_PRECISION
self.fee = _fee
self.admin_fee = _admin_fee
self.owner = _owner
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT
self.token = CurveToken(_pool_token)
self.base_pool = _base_pool
self.base_virtual_price = Curve(_base_pool).get_virtual_price()
self.base_cache_updated = block.timestamp
for i in range(BASE_POOL_COINS):
_base_coin: address = Curve(_base_pool).coins(convert(i, uint256))
self.base_coins[i] = _base_coin
# approve underlying coins for infinite transfers
_response: Bytes[32] = raw_call(
_base_coin,
concat(
method_id("approve(address,uint256)"),
convert(_base_pool, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
```
```shell
>>> metapool.base_pool()
'0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7'
```
::::
### `StableSwap.base_virtual_price`
::::description[`StableSwap.base_virtual_price() → uint256: view`]
Get the current price of the base pool LP token relative to the underlying base pool assets.
```vyper hl_lines="1 37"
base_virtual_price: public(uint256)
...
@external
def __init__(
_owner: address,
_coins: address[N_COINS],
_pool_token: address,
_base_pool: address,
_A: uint256,
_fee: uint256,
_admin_fee: uint256
):
"""
@notice Contract constructor
@param _owner Contract owner address
@param _coins Addresses of ERC20 conracts of coins
@param _pool_token Address of the token representing LP share
@param _base_pool Address of the base pool (which will have a virtual price)
@param _A Amplification coefficient multiplied by n * (n - 1)
@param _fee Fee to charge for exchanges
@param _admin_fee Admin fee
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
self.coins = _coins
self.initial_A = _A * A_PRECISION
self.future_A = _A * A_PRECISION
self.fee = _fee
self.admin_fee = _admin_fee
self.owner = _owner
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT
self.token = CurveToken(_pool_token)
self.base_pool = _base_pool
self.base_virtual_price = Curve(_base_pool).get_virtual_price()
self.base_cache_updated = block.timestamp
for i in range(BASE_POOL_COINS):
_base_coin: address = Curve(_base_pool).coins(convert(i, uint256))
self.base_coins[i] = _base_coin
# approve underlying coins for infinite transfers
_response: Bytes[32] = raw_call(
_base_coin,
concat(
method_id("approve(address,uint256)"),
convert(_base_pool, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
```
```shell
>>> metapool.base_virtual_price()
1014750545929625438
```
:::note
The base pool’s virtual price is only fetched from the base pool if the cached price has expired. A
fetched based pool virtual price is cached for 10 minutes (``BASE_CACHE_EXPIRES: constant(int128) = 10 * 60``).
:::
::::
### `StableSwap.base_cache_update()`
::::description[`StableSwap.base_cache_update() → uint256: view`]
Get the timestamp at which the base pool virtual price was last cached.
```vyper hl_lines="1 5 42 64 67 75"
base_cache_updated: public(uint256)
...
BASE_CACHE_EXPIRES: constant(int128) = 10 * 60 # 10 min
...
@external
def __init__(
_owner: address,
_coins: address[N_COINS],
_pool_token: address,
_base_pool: address,
_A: uint256,
_fee: uint256,
_admin_fee: uint256
):
"""
@notice Contract constructor
@param _owner Contract owner address
@param _coins Addresses of ERC20 conracts of coins
@param _pool_token Address of the token representing LP share
@param _base_pool Address of the base pool (which will have a virtual price)
@param _A Amplification coefficient multiplied by n * (n - 1)
@param _fee Fee to charge for exchanges
@param _admin_fee Admin fee
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
self.coins = _coins
self.initial_A = _A * A_PRECISION
self.future_A = _A * A_PRECISION
self.fee = _fee
self.admin_fee = _admin_fee
self.owner = _owner
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT
self.token = CurveToken(_pool_token)
self.base_pool = _base_pool
self.base_virtual_price = Curve(_base_pool).get_virtual_price()
self.base_cache_updated = block.timestamp
for i in range(BASE_POOL_COINS):
_base_coin: address = Curve(_base_pool).coins(convert(i, uint256))
self.base_coins[i] = _base_coin
# approve underlying coins for infinite transfers
_response: Bytes[32] = raw_call(
_base_coin,
concat(
method_id("approve(address,uint256)"),
convert(_base_pool, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
...
@internal
def _vp_rate() -> uint256:
if block.timestamp > self.base_cache_updated + BASE_CACHE_EXPIRES:
vprice: uint256 = Curve(self.base_pool).get_virtual_price()
self.base_virtual_price = vprice
self.base_cache_updated = block.timestamp
return vprice
else:
return self.base_virtual_price
@internal
@view
def _vp_rate_ro() -> uint256:
if block.timestamp > self.base_cache_updated + BASE_CACHE_EXPIRES:
return Curve(self.base_pool).get_virtual_price()
else:
return self.base_virtual_price
```
```shell
>>> metapool.base_cache_updated()
1616583340
```
::::
## Exchange Methods
Similar to lending pools, on metapools exchanges can be made either between the coins the metapool actually holds
(another pool’s LP token and some other coin) or between the metapool’s underlying coins. In the context of a metapool,
**underlying**coins refers to the metapool’s coin and any of the base pool’s coins. The base pool’s LP token is **not**included as an underlying coin.
For example, the GUSD metapool would have the following:
Coins: ``GUSD``, ``3CRV`` (3Pool LP)
Underlying coins: ``GUSD``, ``DAI``, ``USDC``, ``USDT``
:::note
While metapools contain public getters for ``coins`` and ``base_coins``, there exists no getter for obtaining a list
of all underlying coins.
:::
### `StableSwap.exchange`
::::description[`StableSwap.exchange(i: int128, j: int128, _dx: uint256, _min_dy: uint256) → uint256`]
Perform an exchange between two (non-underlying) coins in the metapool. Index values can be found via the ``coins``
public getter method.
Returns: the actual amount of coin ``j`` received.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | Index value for the coin to send |
| `j` | `int128` | Index value of the coin to receive|
| `_dx` | `uint256` | Amount of ``i`` being exchanged |
| `_min_dy` | `uint256` | Minimum amount of ``j`` to receive |
Emits: TokenExchange
todo: explain how fee is calculated
```vyper
@external
@nonreentrant('lock')
def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256:
"""
@notice Perform an exchange between two coins
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param dx Amount of `i` being exchanged
@param min_dy Minimum amount of `j` to receive
@return Actual amount of `j` received
"""
assert not self.is_killed # dev: is killed
rates: uint256[N_COINS] = RATES
rates[MAX_COIN] = self._vp_rate()
old_balances: uint256[N_COINS] = self.balances
xp: uint256[N_COINS] = self._xp_mem(rates[MAX_COIN], old_balances)
x: uint256 = xp[i] + dx * rates[i] / PRECISION
y: uint256 = self.get_y(i, j, x, xp)
dy: uint256 = xp[j] - y - 1 # -1 just in case there were some rounding errors
dy_fee: uint256 = dy * self.fee / FEE_DENOMINATOR
# Convert all to real units
dy = (dy - dy_fee) * PRECISION / rates[j]
assert dy >= min_dy, "Too few coins in result"
dy_admin_fee: uint256 = dy_fee * self.admin_fee / FEE_DENOMINATOR
dy_admin_fee = dy_admin_fee * PRECISION / rates[j]
# Change balances exactly in same way as we change actual ERC20 coin amounts
self.balances[i] = old_balances[i] + dx
# When rounding errors happen, we undercharge admin fee in favor of LP
self.balances[j] = old_balances[j] - dy - dy_admin_fee
assert ERC20(self.coins[i]).transferFrom(msg.sender, self, dx)
assert ERC20(self.coins[j]).transfer(msg.sender, dy)
log TokenExchange(msg.sender, i, dx, j, dy)
return dy
```
```shell
>>> lending_pool.exchange()
todo: console output
```
::::
### `StableSwap.exchange_underlying`
::::description[Stableswap.exchange_underlying(i: int128, j: int128, _dx: uint256, _min_dy: uint256) → uint256]
Perform an exchange between two underlying tokens. Index values are the ``coins`` followed by the ``base_coins``,
where the base pool LP token is **not**included as a value.
Returns: the actual amount of coin ``j`` received.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | Index value for the coin to send |
| `j` | `int128` | Index value of the coin to receive|
| `_dx` | `uint256` | Amount of ``i`` being exchanged |
| `_min_dy` | `uint256` | Minimum amount of ``j`` to receive |
Emits: TokenExchangeUnderlying
```vyper
@external
@nonreentrant('lock')
def exchange_underlying(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256:
"""
@notice Perform an exchange between two underlying coins
@dev Index values can be found via the `underlying_coins` public getter method
@param i Index value for the underlying coin to send
@param j Index valie of the underlying coin to recieve
@param dx Amount of `i` being exchanged
@param min_dy Minimum amount of `j` to receive
@return Actual amount of `j` received
"""
assert not self.is_killed # dev: is killed
rates: uint256[N_COINS] = RATES
rates[MAX_COIN] = self._vp_rate()
_base_pool: address = self.base_pool
# Use base_i or base_j if they are >= 0
base_i: int128 = i - MAX_COIN
base_j: int128 = j - MAX_COIN
meta_i: int128 = MAX_COIN
meta_j: int128 = MAX_COIN
if base_i < 0:
meta_i = i
if base_j < 0:
meta_j = j
dy: uint256 = 0
# Addresses for input and output coins
input_coin: address = ZERO_ADDRESS
if base_i < 0:
input_coin = self.coins[i]
else:
input_coin = self.base_coins[base_i]
output_coin: address = ZERO_ADDRESS
if base_j < 0:
output_coin = self.coins[j]
else:
output_coin = self.base_coins[base_j]
# Handle potential Tether fees
dx_w_fee: uint256 = dx
if input_coin == FEE_ASSET:
dx_w_fee = ERC20(FEE_ASSET).balanceOf(self)
# "safeTransferFrom" which works for ERC20s which return bool or not
_response: Bytes[32] = raw_call(
input_coin,
concat(
method_id("transferFrom(address,address,uint256)"),
convert(msg.sender, bytes32),
convert(self, bytes32),
convert(dx, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool) # dev: failed transfer
# end "safeTransferFrom"
# Handle potential Tether fees
if input_coin == FEE_ASSET:
dx_w_fee = ERC20(FEE_ASSET).balanceOf(self) - dx_w_fee
if base_i < 0 or base_j < 0:
old_balances: uint256[N_COINS] = self.balances
xp: uint256[N_COINS] = self._xp_mem(rates[MAX_COIN], old_balances)
x: uint256 = 0
if base_i < 0:
x = xp[i] + dx_w_fee * rates[i] / PRECISION
else:
# i is from BasePool
# At first, get the amount of pool tokens
base_inputs: uint256[BASE_N_COINS] = empty(uint256[BASE_N_COINS])
base_inputs[base_i] = dx_w_fee
coin_i: address = self.coins[MAX_COIN]
# Deposit and measure delta
x = ERC20(coin_i).balanceOf(self)
Curve(_base_pool).add_liquidity(base_inputs, 0)
# Need to convert pool token to "virtual" units using rates
# dx is also different now
dx_w_fee = ERC20(coin_i).balanceOf(self) - x
x = dx_w_fee * rates[MAX_COIN] / PRECISION
# Adding number of pool tokens
x += xp[MAX_COIN]
y: uint256 = self.get_y(meta_i, meta_j, x, xp)
# Either a real coin or token
dy = xp[meta_j] - y - 1 # -1 just in case there were some rounding errors
dy_fee: uint256 = dy * self.fee / FEE_DENOMINATOR
# Convert all to real units
# Works for both pool coins and real coins
dy = (dy - dy_fee) * PRECISION / rates[meta_j]
dy_admin_fee: uint256 = dy_fee * self.admin_fee / FEE_DENOMINATOR
dy_admin_fee = dy_admin_fee * PRECISION / rates[meta_j]
# Change balances exactly in same way as we change actual ERC20 coin amounts
self.balances[meta_i] = old_balances[meta_i] + dx_w_fee
# When rounding errors happen, we undercharge admin fee in favor of LP
self.balances[meta_j] = old_balances[meta_j] - dy - dy_admin_fee
# Withdraw from the base pool if needed
if base_j >= 0:
out_amount: uint256 = ERC20(output_coin).balanceOf(self)
Curve(_base_pool).remove_liquidity_one_coin(dy, base_j, 0)
dy = ERC20(output_coin).balanceOf(self) - out_amount
assert dy >= min_dy, "Too few coins in result"
else:
# If both are from the base pool
dy = ERC20(output_coin).balanceOf(self)
Curve(_base_pool).exchange(base_i, base_j, dx_w_fee, min_dy)
dy = ERC20(output_coin).balanceOf(self) - dy
# "safeTransfer" which works for ERC20s which return bool or not
_response = raw_call(
output_coin,
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(dy, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool) # dev: failed transfer
# end "safeTransfer"
log TokenExchangeUnderlying(msg.sender, i, dx, j, dy)
return dy
```
```shell
>>> lending_pool.exchange_underlying()
todo: console output
```
::::
---
## Overview(Pools)
A Curve pool is a smart contract that implements the Stableswap invariant and thereby allows for the exchange of two or more tokens.
More broadly, Curve pools can be split into three categories:
1. `Plain Pools`: A pool where two or more stablecoins are paired against each other.
2. `Lending Pools`: A pool where two or more *wrapped* tokens (e.g. `cDAI`) are paired against one another, while the underlying is lent out on some other protocol.
3. `Metapools`: A pool where a stablecoin is paired against the LP token from another pool.
Source code for Curve pools may be viewed on [GitHub](https://github.com/curvefi/curve-contract/tree/master/contracts).
:::warning
The API for plain, lending and metapools applies to all pools that are implemented based on [pool templates](https://github.com/curvefi/curve-contract/tree/master/contracts/pool-templates). When interacting with older Curve pools, there may be differences in terms of visibility, gas efficiency and/or variable naming. Furthermore, note that older contracts use ``vyper 0.1.x...`` and that the getters generated for public arrays changed between ``0.1.x`` and ``0.2.x`` to accept ``uint256`` instead of ``int128`` in order to handle the lookups.
Please **do not**assume for a Curve pool to implement the API outlined in this section but verify this before interacting with a pool contract.
:::
For information on code style please refer to the official [style guide](https://vyper.readthedocs.io/en/stable/style-guide.html).
---
## Plain Pools
The simplest Curve pool is a plain pool, which is an implementation of the Stableswap invariant for two or more tokens.
The key characteristic of a plain pool is that the pool contract holds all deposited assets at **all**times.
An example of a Curve plain pool is [3Pool](https://github.com/curvefi/curve-contract/tree/master/contracts/pools/3pool),
which contains the tokens ``DAI``, ``USDC`` and ``USDT``.
:::note
The API of plain pools is also implemented by lending and metapools.
:::
The following Brownie console interaction examples are using
[EURS](https://etherscan.io/address/0x0Ce6a5fF5217e38315f87032CF90686C96627CAA) Pool. The template source code for plain
pools may be viewed on
[GitHub](https://github.com/curvefi/curve-contract/blob/master/contracts/pool-templates/base/SwapTemplateBase.vy).
## Pool Info Methods
### `coins`
::::description[`StableSwap.coins(i: uint256) → address: view`]
Getter for the array of swappable coins within the pool.
Returns: coin address (`address`) for coin index `i`.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint256` | Coin index |
```vyper hl_lines="1 8 17 23 24 25"
coins: public(address[N_COINS])
...
@external
def __init__(
_owner: address,
_coins: address[N_COINS],
_pool_token: address,
_A: uint256,
_fee: uint256,
_admin_fee: uint256
):
"""
@notice Contract constructor
@param _owner Contract owner address
@param _coins Addresses of ERC20 contracts of coins
@param _pool_token Address of the token representing LP share
@param _A Amplification coefficient multiplied by n * (n - 1)
@param _fee Fee to charge for exchanges
@param _admin_fee Admin fee
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
self.coins = _coins
self.initial_A = _A * A_PRECISION
self.future_A = _A * A_PRECISION
self.fee = _fee
self.admin_fee = _admin_fee
self.owner = _owner
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT
self.lp_token = _pool_token
```
```shell
>>> pool.coin(0)
'0xdB25f211AB05b1c97D595516F45794528a807ad8'
```
::::
### `balances`
::::description[`StableSwap.balances(i: uint256) → uint256: view`]
Getter for the pool balances array.
Returns: Balance of coin (`uint256`) at index `i`.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint256` | Coin index |
```shell
>>> pool.balances(0)
2918187395
```
::::
### `owner`
::::description[`StableSwap.owner() → address: view`]
Getter for the admin/owner of the pool contract.
Returns: `address` of the admin of the pool contract.
```vyper hl_lines="1 7 16 30"
owner: public(address)
...
@external
def __init__(
_owner: address,
_coins: address[N_COINS],
_pool_token: address,
_A: uint256,
_fee: uint256,
_admin_fee: uint256
):
"""
@notice Contract constructor
@param _owner Contract owner address
@param _coins Addresses of ERC20 contracts of coins
@param _pool_token Address of the token representing LP share
@param _A Amplification coefficient multiplied by n * (n - 1)
@param _fee Fee to charge for exchanges
@param _admin_fee Admin fee
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
self.coins = _coins
self.initial_A = _A * A_PRECISION
self.future_A = _A * A_PRECISION
self.fee = _fee
self.admin_fee = _admin_fee
self.owner = _owner
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT
self.lp_token = _pool_token
```
```shell
>>> pool.owner()
'0xeCb456EA5365865EbAb8a2661B0c503410e9B347'
```
::::
### `lp_token`
::::description[`StableSwap.lp_token() → address: view`]
Getter for the LP token of the pool.
Returns: `address` of the `lp_token`.
```shell
>>> pool.lp_token()
'0x194eBd173F6cDacE046C53eACcE9B953F28411d1'
```
:::note
In older Curve pools ``lp_token`` may not be ``public`` and thus not visible.
:::
::::
### `A (Amplification factor)`
::::description[`StableSwap.A() → uint256: view`]
Getter for the amplification coefficient of the pool.
```vyper
A_PRECISION: constant(uint256) = 100
...
@view
@external
def A() -> uint256:
return self._A() / A_PRECISION
```
```shell
>>> pool.A()
100
```
:::note
The amplification coefficient is scaled by ``A_PRECISION`` (``=100``)
:::
::::
### `A_precise`
::::description[`StableSwap.A_precise() → uint256: view`]
Getter for the unscaled amplification coefficient of the pool.
```vyper
@view
@external
def A_precise() -> uint256:
return self._A()
```
```shell
>>> pool.A()
10000
```
::::
### `get_virtual_price`
::::description[`StableSwap.get_virtual_price() → uint256: view`]
Current virtual price of the pool LP token relative to the underlying pool assets.
```vyper
@view
@external
def get_virtual_price() -> uint256:
"""
@notice The current virtual price of the pool LP token
@dev Useful for calculating profits
@return LP token virtual price normalized to 1e18
"""
D: uint256 = self.get_D(self._xp(), self._A())
# D is in the units similar to DAI (e.g. converted to precision 1e18)
# When balanced, D = n * x_u - total virtual value of the portfolio
token_supply: uint256 = ERC20(self.lp_token).totalSupply()
return D * PRECISION / token_supply
```
```shell
>>> pool.get_virtual_price()
1001692838188850782
```
:::note
- The method returns ``virtual_price`` as an integer with ``1e18`` precision.
- ``virtual_price`` returns a price relative to the underlying. You can get the absolute price
by multiplying it with the price of the underlying assets.
:::
::::
### `fee`
::::description[`StableSwap.fee() → uint256: view`]
The pool swap fee.
```vyper hl_lines="1 11 20 28"
fee: public(uint256) # fee * 1e10
...
@external
def __init__(
_owner: address,
_coins: address[N_COINS],
_pool_token: address,
_A: uint256,
_fee: uint256,
_admin_fee: uint256
):
"""
@notice Contract constructor
@param _owner Contract owner address
@param _coins Addresses of ERC20 conracts of coins
@param _pool_token Address of the token representing LP share
@param _A Amplification coefficient multiplied by n * (n - 1)
@param _fee Fee to charge for exchanges
@param _admin_fee Admin fee
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
self.coins = _coins
self.initial_A = _A * A_PRECISION
self.future_A = _A * A_PRECISION
self.fee = _fee
self.admin_fee = _admin_fee
self.owner = _owner
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT
self.lp_token = _pool_token
```
```shell
>>> pool.fee()
4000000
```
:::note
The method returns ``fee`` as an integer with ``1e10`` precision.
:::
::::
### `admin_fee`
::::description[`StableSwap.admin_fee() → uint256: view`]
The percentage of the swap fee that is taken as an admin fee.
```vyper hl_lines="1 12 21 29"
admin_fee: public(uint256) # admin_fee * 1e10
...
@external
def __init__(
_owner: address,
_coins: address[N_COINS],
_pool_token: address,
_A: uint256,
_fee: uint256,
_admin_fee: uint256
):
"""
@notice Contract constructor
@param _owner Contract owner address
@param _coins Addresses of ERC20 conracts of coins
@param _pool_token Address of the token representing LP share
@param _A Amplification coefficient multiplied by n * (n - 1)
@param _fee Fee to charge for exchanges
@param _admin_fee Admin fee
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
self.coins = _coins
self.initial_A = _A * A_PRECISION
self.future_A = _A * A_PRECISION
self.fee = _fee
self.admin_fee = _admin_fee
self.owner = _owner
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT
self.lp_token = _pool_token
```
```shell
>>> pool.admin_fee()
5000000000
```
:::note
- The method returns an integer with with ``1e10`` precision.
- Admin fee is set at 50% (``5000000000``) and is paid out to veCRV holders.
:::
::::
## Exchange Methods
### `get_dy`
::::description[`StableSwap.get_dy(i: int128, j: int128, _dx: uint256) → uint256: view`]
Get the amount of coin ``j`` one would receive for swapping ``dx`` of coin ``i``.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint128` | Index of coin to swap from |
| `j` | `uint128` | Index of coin to swap to |
| `dx` | `uint256` | Amount of coin `i` to swap |
```vyper
@view
@external
def get_dy(i: int128, j: int128, dx: uint256) -> uint256:
xp: uint256[N_COINS] = self._xp()
rates: uint256[N_COINS] = RATES
x: uint256 = xp[i] + (dx * rates[i] / PRECISION)
y: uint256 = self.get_y(i, j, x, xp)
dy: uint256 = (xp[j] - y - 1)
_fee: uint256 = self.fee * dy / FEE_DENOMINATOR
return (dy - _fee) * PRECISION / rates[j]
```
```shell
>>> pool.get_dy(0, 1, 100)
996307731416690125
```
:::note
Note: In this example, the ``EURS Pool`` coins decimals for ``coins(0)`` and ``coins(1)`` are
``2`` and ``18``, respectively.
:::
::::
### `exchange`
::::description[`StableSwap.exchange(i: int128, j: int128, dx: uint256, min_dy: uint256) → uint256`]
Perform an exchange between two coins.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint128` | Index of coin to swap from |
| `j` | `uint128` | Index of coin to swap to |
| `dx` | `uint256` | Amount of coin `i` to swap |
| `min_dy` | `uint256` | Minimum amount of ``j`` to receive |
Returns the actual amount of coin ``j`` received. Index values can be found via the
``coins`` public getter method.
Emits: TokenExchange
```vyper
@external
@nonreentrant('lock')
def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256:
"""
@notice Perform an exchange between two coins
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param dx Amount of `i` being exchanged
@param min_dy Minimum amount of `j` to receive
@return Actual amount of `j` received
"""
assert not self.is_killed # dev: is killed
old_balances: uint256[N_COINS] = self.balances
xp: uint256[N_COINS] = self._xp_mem(old_balances)
rates: uint256[N_COINS] = RATES
x: uint256 = xp[i] + dx * rates[i] / PRECISION
y: uint256 = self.get_y(i, j, x, xp)
dy: uint256 = xp[j] - y - 1 # -1 just in case there were some rounding errors
dy_fee: uint256 = dy * self.fee / FEE_DENOMINATOR
# Convert all to real units
dy = (dy - dy_fee) * PRECISION / rates[j]
assert dy >= min_dy, "Exchange resulted in fewer coins than expected"
dy_admin_fee: uint256 = dy_fee * self.admin_fee / FEE_DENOMINATOR
dy_admin_fee = dy_admin_fee * PRECISION / rates[j]
# Change balances exactly in same way as we change actual ERC20 coin amounts
self.balances[i] = old_balances[i] + dx
# When rounding errors happen, we undercharge admin fee in favor of LP
self.balances[j] = old_balances[j] - dy - dy_admin_fee
# "safeTransferFrom" which works for ERC20s which return bool or not
_response: Bytes[32] = raw_call(
self.coins[i],
concat(
method_id("transferFrom(address,address,uint256)"),
convert(msg.sender, bytes32),
convert(self, bytes32),
convert(dx, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool)
_response = raw_call(
self.coins[j],
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(dy, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool)
log TokenExchange(msg.sender, i, dx, j, dy)
return dy
```
```shell
>>> expected = pool.get_dy(0, 1, 10**2) * 0.99
>>> pool.exchange(0, 1, 10**2, expected, {"from": alice})
```
::::
## Add/Remove Liquidity Methods
### `calc_token_amount`
::::description[`StableSwap.calc_token_amount(_amounts: uint256[N_COINS], _: bool) → uint256: view`]
Calculate addition or reduction in token supply from a deposit or withdrawal. Returns the expected amount of LP
tokens received. This calculation accounts for slippage, but not fees.
`N_COINS`: Number of coins in the pool.
| Input | Type | Description |
| ----------- | -------| ----|
| `amounts` | `uint256[N_COINS]` | Amount of each coin being deposited |
| `is_deposit` | `bool` | Set True for deposits, False for withdrawals |
```vyper
@view
@external
def calc_token_amount(amounts: uint256[N_COINS], is_deposit: bool) -> uint256:
"""
@notice Calculate addition or reduction in token supply from a deposit or withdrawal
@dev This calculation accounts for slippage, but not fees.
Needed to prevent front-running, not for precise calculations!
@param amounts Amount of each coin being deposited
@param is_deposit set True for deposits, False for withdrawals
@return Expected amount of LP tokens received
"""
amp: uint256 = self._A()
_balances: uint256[N_COINS] = self.balances
D0: uint256 = self.get_D_mem(_balances, amp)
for i in range(N_COINS):
if is_deposit:
_balances[i] += amounts[i]
else:
_balances[i] -= amounts[i]
D1: uint256 = self.get_D_mem(_balances, amp)
token_amount: uint256 = ERC20(self.lp_token).totalSupply()
diff: uint256 = 0
if is_deposit:
diff = D1 - D0
else:
diff = D0 - D1
return diff * token_amount / D0
```
```shell
>>> pool.calc_token_amount([10**2, 10**18], True)
1996887509167925969
```
::::
### `add_liquidity`
::::description[`StableSwap.add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256) → uint256`]
Deposit coins into the pool. Returns the amount of LP tokens received in exchange for the deposited tokens.
| Input | Type | Description |
| ----------- | -------| ----|
| `amounts` | `uint256[N_COINS]` | Amount of each coin being deposited |
| `min_mint_amount` | `uint256` | Minimum amount of LP tokens to mint from the deposit |
Emits: AddLiquidity
```vyper
@external
@nonreentrant('lock')
def add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256) -> uint256:
"""
@notice Deposit coins into the pool
@param amounts List of amounts of coins to deposit
@param min_mint_amount Minimum amount of LP tokens to mint from the deposit
@return Amount of LP tokens received by depositing
"""
assert not self.is_killed # dev: is killed
amp: uint256 = self._A()
_lp_token: address = self.lp_token
token_supply: uint256 = ERC20(_lp_token).totalSupply()
# Initial invariant
D0: uint256 = 0
old_balances: uint256[N_COINS] = self.balances
if token_supply > 0:
D0 = self.get_D_mem(old_balances, amp)
new_balances: uint256[N_COINS] = old_balances
for i in range(N_COINS):
if token_supply == 0:
assert amounts[i] > 0 # dev: initial deposit requires all coins
# balances store amounts of c-tokens
new_balances[i] = old_balances[i] + amounts[i]
# Invariant after change
D1: uint256 = self.get_D_mem(new_balances, amp)
assert D1 > D0
# We need to recalculate the invariant accounting for fees
# to calculate fair user's share
D2: uint256 = D1
fees: uint256[N_COINS] = empty(uint256[N_COINS])
if token_supply > 0:
# Only account for fees if we are not the first to deposit
_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
_admin_fee: uint256 = self.admin_fee
for i in range(N_COINS):
ideal_balance: uint256 = D1 * old_balances[i] / D0
difference: uint256 = 0
if ideal_balance > new_balances[i]:
difference = ideal_balance - new_balances[i]
else:
difference = new_balances[i] - ideal_balance
fees[i] = _fee * difference / FEE_DENOMINATOR
self.balances[i] = new_balances[i] - (fees[i] * _admin_fee / FEE_DENOMINATOR)
new_balances[i] -= fees[i]
D2 = self.get_D_mem(new_balances, amp)
else:
self.balances = new_balances
# Calculate, how much pool tokens to mint
mint_amount: uint256 = 0
if token_supply == 0:
mint_amount = D1 # Take the dust if there was any
else:
mint_amount = token_supply * (D2 - D0) / D0
assert mint_amount >= min_mint_amount, "Slippage screwed you"
# Take coins from the sender
for i in range(N_COINS):
if amounts[i] > 0:
# "safeTransferFrom" which works for ERC20s which return bool or not
_response: Bytes[32] = raw_call(
self.coins[i],
concat(
method_id("transferFrom(address,address,uint256)"),
convert(msg.sender, bytes32),
convert(self, bytes32),
convert(amounts[i], bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool)
# Mint pool tokens
CurveToken(_lp_token).mint(msg.sender, mint_amount)
log AddLiquidity(msg.sender, amounts, fees, D1, token_supply + mint_amount)
return mint_amount
```
```shell
>>> todo: add_liquidity console output example
```
::::
### `remove_liquidity`
::::description[`StableSwap.remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS]) → uint256[N_COINS]`]
Withdraw coins from the pool. Returns a list of the amounts for each coin that was withdrawn.
| Input | Type | Description |
| ----------- | -------| ----|
| `_amount` | `uint256` | Quantity of LP tokens to burn in the withdrawal |
| `min_amounts` | `uint256[N_COINS]`` | Minimum amounts of underlying coins to receive |
Emits: RemoveLiquidity
```vyper
@external
@nonreentrant('lock')
def remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS]) -> uint256[N_COINS]:
"""
@notice Withdraw coins from the pool
@dev Withdrawal amounts are based on current deposit ratios
@param _amount Quantity of LP tokens to burn in the withdrawal
@param min_amounts Minimum amounts of underlying coins to receive
@return List of amounts of coins that were withdrawn
"""
_lp_token: address = self.lp_token
total_supply: uint256 = ERC20(_lp_token).totalSupply()
amounts: uint256[N_COINS] = empty(uint256[N_COINS])
fees: uint256[N_COINS] = empty(uint256[N_COINS]) # Fees are unused but we've got them historically in event
for i in range(N_COINS):
value: uint256 = self.balances[i] * _amount / total_supply
assert value >= min_amounts[i], "Withdrawal resulted in fewer coins than expected"
self.balances[i] -= value
amounts[i] = value
_response: Bytes[32] = raw_call(
self.coins[i],
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(value, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool)
CurveToken(_lp_token).burnFrom(msg.sender, _amount) # dev: insufficient funds
log RemoveLiquidity(msg.sender, amounts, fees, total_supply - _amount)
return amounts
```
```shell
>>> todo: remove_liquidity console output example
```
::::
### `remove_liquidity_imbalance`
::::description[`StableSwap.remove_liquidity_imbalance(amounts: uint256[N_COINS], max_burn_amount: uint256) → uint256`]
Withdraw coins from the pool in an imbalanced amount. Returns a list of the amounts for each coin that was withdrawn.
| Input | Type | Description |
| ----------- | -------| ----|
| `amounts` | `uint256[N_COINS]` | List of amounts of underlying coins to withdraw |
| `max_burn_amount` | `uint256` | Maximum amount of LP token to burn in the withdrawal |
Emits: RemoveLiquidityImbalance
```vyper
@external
@nonreentrant('lock')
def remove_liquidity_imbalance(amounts: uint256[N_COINS], max_burn_amount: uint256) -> uint256:
"""
@notice Withdraw coins from the pool in an imbalanced amount
@param amounts List of amounts of underlying coins to withdraw
@param max_burn_amount Maximum amount of LP token to burn in the withdrawal
@return Actual amount of the LP token burned in the withdrawal
"""
assert not self.is_killed # dev: is killed
amp: uint256 = self._A()
old_balances: uint256[N_COINS] = self.balances
new_balances: uint256[N_COINS] = old_balances
D0: uint256 = self.get_D_mem(old_balances, amp)
for i in range(N_COINS):
new_balances[i] -= amounts[i]
D1: uint256 = self.get_D_mem(new_balances, amp)
_lp_token: address = self.lp_token
token_supply: uint256 = ERC20(_lp_token).totalSupply()
assert token_supply != 0 # dev: zero total supply
_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
_admin_fee: uint256 = self.admin_fee
fees: uint256[N_COINS] = empty(uint256[N_COINS])
for i in range(N_COINS):
ideal_balance: uint256 = D1 * old_balances[i] / D0
difference: uint256 = 0
if ideal_balance > new_balances[i]:
difference = ideal_balance - new_balances[i]
else:
difference = new_balances[i] - ideal_balance
fees[i] = _fee * difference / FEE_DENOMINATOR
self.balances[i] = new_balances[i] - (fees[i] * _admin_fee / FEE_DENOMINATOR)
new_balances[i] -= fees[i]
D2: uint256 = self.get_D_mem(new_balances, amp)
token_amount: uint256 = (D0 - D2) * token_supply / D0
assert token_amount != 0 # dev: zero tokens burned
token_amount += 1 # In case of rounding errors - make it unfavorable for the "attacker"
assert token_amount <= max_burn_amount, "Slippage screwed you"
CurveToken(_lp_token).burnFrom(msg.sender, token_amount) # dev: insufficient funds
for i in range(N_COINS):
if amounts[i] != 0:
_response: Bytes[32] = raw_call(
self.coins[i],
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(amounts[i], bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool)
log RemoveLiquidityImbalance(msg.sender, amounts, fees, D1, token_supply - token_amount)
return token_amount
```
```shell
>>> todo: remove_liquidity_imbalance console output example
```
::::
### `calc_withdraw_one_coin`
::::description[`StableSwap.calc_withdraw_one_coin(_token_amount: uint256, i: int128) → uint256`]
Calculate the amount received when withdrawing a single coin.
| Input | Type | Description |
| ----------- | -------| ----|
| `_token_amount` | `uint256` | Amount of LP tokens to burn in the withdrawal |
| `i` | `int128` | Index value of the coin to withdraw |
```vyper
@view
@internal
def _calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> (uint256, uint256, uint256):
# First, need to calculate
# * Get current D
# * Solve Eqn against y_i for D - _token_amount
amp: uint256 = self._A()
xp: uint256[N_COINS] = self._xp()
D0: uint256 = self.get_D(xp, amp)
total_supply: uint256 = ERC20(self.lp_token).totalSupply()
D1: uint256 = D0 - _token_amount * D0 / total_supply
new_y: uint256 = self.get_y_D(amp, i, xp, D1)
xp_reduced: uint256[N_COINS] = xp
_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
for j in range(N_COINS):
dx_expected: uint256 = 0
if j == i:
dx_expected = xp[j] * D1 / D0 - new_y
else:
dx_expected = xp[j] - xp[j] * D1 / D0
xp_reduced[j] -= _fee * dx_expected / FEE_DENOMINATOR
dy: uint256 = xp_reduced[i] - self.get_y_D(amp, i, xp_reduced, D1)
precisions: uint256[N_COINS] = PRECISION_MUL
dy = (dy - 1) / precisions[i] # Withdraw less to account for rounding errors
dy_0: uint256 = (xp[i] - new_y) / precisions[i] # w/o fees
return dy, dy_0 - dy, total_supply
@view
@external
def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256:
"""
@notice Calculate the amount received when withdrawing a single coin
@param _token_amount Amount of LP tokens to burn in the withdrawal
@param i Index value of the coin to withdraw
@return Amount of coin received
"""
return self._calc_withdraw_one_coin(_token_amount, i)[0]
```
```shell
>>> todo: calculate_withdraw_one_coin console output example
```
::::
### `remove_liquidity_one_coin`
::::description[`StableSwap.remove_liquidity_one_coin(_token_amount: uint256, i: int128, _min_amount: uint256) → uint256`]
Withdraw a single coin from the pool. Returns the amount of coin ``i`` received.
| Input | Type | Description |
| ----------- | -------| ----|
| `_token_amount` | `uint256` | Amount of LP tokens to burn in the withdrawal |
| `i` | `int128` | Index value of the coin to withdraw |
| `_min_amount` | `uint256` | Minimum amount of coin to receive |
Emits: RemoveLiquidityOne
```vyper
@external
@nonreentrant('lock')
def remove_liquidity_one_coin(_token_amount: uint256, i: int128, _min_amount: uint256) -> uint256:
"""
@notice Withdraw a single coin from the pool
@param _token_amount Amount of LP tokens to burn in the withdrawal
@param i Index value of the coin to withdraw
@param _min_amount Minimum amount of coin to receive
@return Amount of coin received
"""
assert not self.is_killed # dev: is killed
dy: uint256 = 0
dy_fee: uint256 = 0
total_supply: uint256 = 0
dy, dy_fee, total_supply = self._calc_withdraw_one_coin(_token_amount, i)
assert dy >= _min_amount, "Not enough coins removed"
self.balances[i] -= (dy + dy_fee * self.admin_fee / FEE_DENOMINATOR)
CurveToken(self.lp_token).burnFrom(msg.sender, _token_amount) # dev: insufficient funds
_response: Bytes[32] = raw_call(
self.coins[i],
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(dy, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool)
log RemoveLiquidityOne(msg.sender, _token_amount, dy, total_supply - _token_amount)
return dy
```
```shell
>>> todo: remove_liquidity_one_coin console output example
```
::::
---
## Curve StableSwap Exchange: Overview(Legacy)
The StableSwap algorithm integrates features of both the constant sum and constant product formulas, adjusting between these models based on the balance of assets in the pool. For a detailed overview of the StableSwap invariant design, please read the official [whitepaper](/pdf/whitepapers/whitepaper_stableswap.pdf).
The Curve StableSwap exchange utilizes a specific algorithm known as the **StableSwap invariant to facilitate the trading of stablecoins and other stable assets**. This method is designed to offer lower slippage in stablecoin transactions compared to other common algorithms like the constant-product invariant. In this context, "stablecoins" refer to digital assets that aim to maintain a stable value relative to another asset, including fiat-pegged stablecoins (e.g., DAI, USDC), synthetic versions of cryptocurrencies (e.g., synthetic ETH), or various forms of wrapped BTC.
A Curve pool, at its core, is a smart contract that implements the StableSwap invariant, enabling the exchange of tokens. While all Curve pools are based on this invariant, they can differ in structure. The simplest form of a Curve pool involves two or more tokens, known as a **"plain pool"**. Additionally, Curve offers **"metapools"**, which are designed to **facilitate exchanges between a token and the underlying pools of the LP token it is paired against.**For technical implementations, the **StableSwap algorithm combines a constant sum invariant with a constant product invariant**, represented mathematically in the form of equations that account for the total amount of tokens in a pool at equilibrium. This model adjusts as the pool's balance shifts, moving towards a constant product model when significant imbalances occur.
---
## Implementations:::github[GitHub]
The source code for the Curve Finance StableSwap contracts is openly accessible on GitHub:
- Genesis Contracts: The initial set of smart contracts for Curve pools can be found at [Curve's contracts repository](https://github.com/curvefi/curve-contract/tree/master/contracts/pools).
- StableSwap-ng: An updated version of the StableSwap algorithm, known as StableSwap-ng (next generation), is available at [Curve's StableSwap-ng repository](https://github.com/curvefi/stableswap-ng).
:::
*There have been two major on-chain implementations of the stableswap invariant across various chains:*
### Original StableSwap
The original stableswap was the first on-chain implementation of the stableswap invariant.
### StableSwap NG
The Stableswap-NG AMM infrastructure marks a sophisticated evolution from its original implementation, delivering enhanced technical capabilities. This upgraded framework allows for the inclusion of up to eight tokens in standard pools and two in metapools. It extends support to a variety of token types, including rate-oracle tokens like wstETH, ERC4626 tokens such as sDAI, and rebasing tokens like stETH.
However, native tokens like ETH are not directly supported within this implementation. For transactions involving ETH, its wrapped version, wETH, must be utilized. This decision is rooted in ensuring higher security standards.
Additionally, pools now have built-in [moving-average oracles](../stableswap-ng/pools/oracles.md).
For an in-depth exploration of the new features introduced by Stableswap-NG, please refer to the [StableSwap-NG Overview](../stableswap-ng/overview.md).
---
## Curve Registry Exchange Contract
The `CurveRegistryExchange` contract serves as a router for Curve liquidity pools. Users can utilize this contract to find pools, query exchange rates, and execute swaps directly.
:::danger[Outdated: Does not consider all liquidity pools]
The `CurveRegistryExchange` contract was deployed in December 2022 at [`0x99a58482bd75cbab83b27ec03ca68ff489b5788f`](https://etherscan.io/address/0x99a58482bd75cbab83b27ec03ca68ff489b5788f#code) on the Ethereum Mainnet. This contract does not support all pools due to newer versions of exchange contracts.
A new and updated version is available here: [`CurveRouterNG`](./curve-router-ng.md).
:::
---
## Exchanging Tokens
:::warning
This contract only considers liquidity sources that have been added to it. These sources are primarily the liquidity pools registered in `factory_registry` and `crypto_registry`.
:::
*The contract offers three distinct functions to facilitate token exchanges:*
- [`exchange`](#exchange_multiple): Conducts a regular exchange using a specified pool.
- [`exchange_with_best_rate`](#exchange_with_best_rate): Executes a token exchange using the pool that offers the best rate.
- [`exchange_multiple`](#exchange_multiple): Allows up to four token exchanges in a single transaction.
Additionally, there are helper functions available to retrieve essential data, such as [`get_exchange_amount`](#get_exchange_amount), [`get_best_rate`](#get_best_rate), or [`get_exchange_multiple_amount`](#get_exchange_multiple_amount).
### `exchange`
::::description[`CurveRegistryExchange.exchange(_pool: address, _from: address, _to: address, _amount: uint256, _expected: uint256, _receiver: address = msg.sender) -> uint256`]
Function to perform a token exchange using a specific pool. Prior to calling this function, the caller must approve this contract to transfer `_amount` of coins from `_from`.
| Input | Type | Description |
|--------------|-----------|-----------------------------------------------------|
| `_pool` | `address` | Address of the pool to use for the token exchange. |
| `_from` | `address` | Address of the coin being sent. |
| `_to` | `address` | Address of the coin being received. |
| `_amount` | `uint256` | Amount of coins to exchange. |
| `_expected` | `uint256` | Minimum amount of coins to receive. |
| `_receiver` | `address` | Receiver of the tokens. Defaults to `msg.sender`. |
Returns: amount received (`uint256`)
Emits: `TokenExchange`
```vyper
event TokenExchange:
buyer: indexed(address)
receiver: indexed(address)
pool: indexed(address)
token_sold: address
token_bought: address
amount_sold: uint256
amount_bought: uint256
@payable
@external
@nonreentrant("lock")
def exchange(
_pool: address,
_from: address,
_to: address,
_amount: uint256,
_expected: uint256,
_receiver: address = msg.sender,
) -> uint256:
"""
@notice Perform an exchange using a specific pool
@dev Prior to calling this function, the caller must approve
this contract to transfer `_amount` coins from `_from`
Works for both regular and factory-deployed pools
@param _pool Address of the pool to use for the swap
@param _from Address of coin being sent
@param _to Address of coin being received
@param _amount Quantity of `_from` being sent
@param _expected Minimum quantity of `_from` received
in order for the transaction to succeed
@param _receiver Address to transfer the received tokens to
@return uint256 Amount received
"""
if _from == ETH_ADDRESS:
assert _amount == msg.value, "Incorrect ETH amount"
else:
assert msg.value == 0, "Incorrect ETH amount"
if Registry(self.crypto_registry).get_lp_token(_pool) != ZERO_ADDRESS:
return self._crypto_exchange(_pool, _from, _to, _amount, _expected, msg.sender, _receiver)
registry: address = self.registry
if Registry(registry).get_lp_token(_pool) == ZERO_ADDRESS:
registry = self.factory_registry
return self._exchange(registry, _pool, _from, _to, _amount, _expected, msg.sender, _receiver)
@internal
def _crypto_exchange(
_pool: address,
_from: address,
_to: address,
_amount: uint256,
_expected: uint256,
_sender: address,
_receiver: address,
) -> uint256:
assert not self.is_killed
initial: address = _from
target: address = _to
if _from == ETH_ADDRESS:
initial = WETH_ADDRESS
if _to == ETH_ADDRESS:
target = WETH_ADDRESS
eth_amount: uint256 = 0
received_amount: uint256 = 0
i: uint256 = 0
j: uint256 = 0
i, j = CryptoRegistry(self.crypto_registry).get_coin_indices(_pool, initial, target) # dev: no market
# perform / verify input transfer
if _from == ETH_ADDRESS:
eth_amount = _amount
else:
response: Bytes[32] = raw_call(
_from,
_abi_encode(
_sender,
self,
_amount,
method_id=method_id("transferFrom(address,address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
# approve input token
if not self.is_approved[_from][_pool]:
response: Bytes[32] = raw_call(
_from,
_abi_encode(
_pool,
MAX_UINT256,
method_id=method_id("approve(address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
self.is_approved[_from][_pool] = True
# perform coin exchange
if ETH_ADDRESS in [_from, _to]:
CryptoPoolETH(_pool).exchange(i, j, _amount, _expected, True, value=eth_amount)
else:
CryptoPool(_pool).exchange(i, j, _amount, _expected)
# perform output transfer
if _to == ETH_ADDRESS:
received_amount = self.balance
raw_call(_receiver, b"", value=self.balance)
else:
received_amount = ERC20(_to).balanceOf(self)
response: Bytes[32] = raw_call(
_to,
_abi_encode(
_receiver,
received_amount,
method_id=method_id("transfer(address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
log TokenExchange(_sender, _receiver, _pool, _from, _to, _amount, received_amount)
return received_amount
@internal
def _exchange(
_registry: address,
_pool: address,
_from: address,
_to: address,
_amount: uint256,
_expected: uint256,
_sender: address,
_receiver: address,
) -> uint256:
assert not self.is_killed
eth_amount: uint256 = 0
received_amount: uint256 = 0
i: int128 = 0
j: int128 = 0
is_underlying: bool = False
i, j, is_underlying = Registry(_registry).get_coin_indices(_pool, _from, _to) # dev: no market
if is_underlying and _registry == self.factory_registry:
if Registry(_registry).is_meta(_pool):
base_coins: address[2] = self.base_coins[_pool]
if base_coins[0] == empty(address) and base_coins[1] == empty(address):
base_coins = [CurvePool(_pool).coins(0), CurvePool(_pool).coins(1)]
self.base_coins[_pool] = base_coins
# we only need to use exchange underlying if the input or output is not in the base coins
is_underlying = _from not in base_coins or _to not in base_coins
else:
# not a metapool so no underlying exchange method
is_underlying = False
# perform / verify input transfer
if _from == ETH_ADDRESS:
eth_amount = _amount
else:
response: Bytes[32] = raw_call(
_from,
_abi_encode(
_sender,
self,
_amount,
method_id=method_id("transferFrom(address,address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
# approve input token
if _from != ETH_ADDRESS and not self.is_approved[_from][_pool]:
response: Bytes[32] = raw_call(
_from,
_abi_encode(
_pool,
MAX_UINT256,
method_id=method_id("approve(address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
self.is_approved[_from][_pool] = True
# perform coin exchange
if is_underlying:
CurvePool(_pool).exchange_underlying(i, j, _amount, _expected, value=eth_amount)
else:
CurvePool(_pool).exchange(i, j, _amount, _expected, value=eth_amount)
# perform output transfer
if _to == ETH_ADDRESS:
received_amount = self.balance
raw_call(_receiver, b"", value=self.balance)
else:
received_amount = ERC20(_to).balanceOf(self)
response: Bytes[32] = raw_call(
_to,
_abi_encode(
_receiver,
received_amount,
method_id=method_id("transfer(address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
log TokenExchange(_sender, _receiver, _pool, _from, _to, _amount, received_amount)
return received_amount
```
::::
### `exchange_with_best_rate`
::::description[`CurveRegistryExchange.exchange_with_best_rate(_from: address, _to: address, _amount: uint256, _expected: uint256, _receiver: address = msg.sender) -> uint256`]
:::warning
This method does not check rates in factory-deployed pools.
:::
Function to perform a token exchange using the pool that offers the best rate. Prior to calling this function, the caller must approve this contract to transfer `_amount` of coins from `_from`.
Returns: received amount of the output token (`uint256`).
Emits: `TokenExchange`
| Input | Type | Description |
|-------------|-----------|-----------------------------------------------------|
| `_from` | `address` | Address of the coin being sent. |
| `_to` | `address` | Address of the coin being received. |
| `_amount` | `uint256` | Amount of coins to exchange. |
| `_expected` | `uint256` | Minimum amount of coins to receive. |
| `_receiver` | `address` | Receiver of the tokens. Defaults to `msg.sender`. |
```vyper
event TokenExchange:
buyer: indexed(address)
receiver: indexed(address)
pool: indexed(address)
token_sold: address
token_bought: address
amount_sold: uint256
amount_bought: uint256
@payable
@external
@nonreentrant("lock")
def exchange_with_best_rate(
_from: address,
_to: address,
_amount: uint256,
_expected: uint256,
_receiver: address = msg.sender,
) -> uint256:
"""
@notice Perform an exchange using the pool that offers the best rate
@dev Prior to calling this function, the caller must approve
this contract to transfer `_amount` coins from `_from`
Does NOT check rates in factory-deployed pools
@param _from Address of coin being sent
@param _to Address of coin being received
@param _amount Quantity of `_from` being sent
@param _expected Minimum quantity of `_from` received
in order for the transaction to succeed
@param _receiver Address to transfer the received tokens to
@return uint256 Amount received
"""
if _from == ETH_ADDRESS:
assert _amount == msg.value, "Incorrect ETH amount"
else:
assert msg.value == 0, "Incorrect ETH amount"
registry: address = self.registry
best_pool: address = ZERO_ADDRESS
max_dy: uint256 = 0
for i in range(65536):
pool: address = Registry(registry).find_pool_for_coins(_from, _to, i)
if pool == ZERO_ADDRESS:
break
dy: uint256 = self._get_exchange_amount(registry, pool, _from, _to, _amount)
if dy > max_dy:
best_pool = pool
max_dy = dy
return self._exchange(registry, best_pool, _from, _to, _amount, _expected, msg.sender, _receiver)
@view
@internal
def _get_exchange_amount(
_registry: address,
_pool: address,
_from: address,
_to: address,
_amount: uint256
) -> uint256:
"""
@notice Get the current number of coins received in an exchange
@param _registry Registry address
@param _pool Pool address
@param _from Address of coin to be sent
@param _to Address of coin to be received
@param _amount Quantity of `_from` to be sent
@return Quantity of `_to` to be received
"""
i: int128 = 0
j: int128 = 0
is_underlying: bool = False
i, j, is_underlying = Registry(_registry).get_coin_indices(_pool, _from, _to) # dev: no market
if is_underlying and (_registry == self.registry or Registry(_registry).is_meta(_pool)):
return CurvePool(_pool).get_dy_underlying(i, j, _amount)
return CurvePool(_pool).get_dy(i, j, _amount)
@internal
def _exchange(
_registry: address,
_pool: address,
_from: address,
_to: address,
_amount: uint256,
_expected: uint256,
_sender: address,
_receiver: address,
) -> uint256:
assert not self.is_killed
eth_amount: uint256 = 0
received_amount: uint256 = 0
i: int128 = 0
j: int128 = 0
is_underlying: bool = False
i, j, is_underlying = Registry(_registry).get_coin_indices(_pool, _from, _to) # dev: no market
if is_underlying and _registry == self.factory_registry:
if Registry(_registry).is_meta(_pool):
base_coins: address[2] = self.base_coins[_pool]
if base_coins[0] == empty(address) and base_coins[1] == empty(address):
base_coins = [CurvePool(_pool).coins(0), CurvePool(_pool).coins(1)]
self.base_coins[_pool] = base_coins
# we only need to use exchange underlying if the input or output is not in the base coins
is_underlying = _from not in base_coins or _to not in base_coins
else:
# not a metapool so no underlying exchange method
is_underlying = False
# perform / verify input transfer
if _from == ETH_ADDRESS:
eth_amount = _amount
else:
response: Bytes[32] = raw_call(
_from,
_abi_encode(
_sender,
self,
_amount,
method_id=method_id("transferFrom(address,address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
# approve input token
if _from != ETH_ADDRESS and not self.is_approved[_from][_pool]:
response: Bytes[32] = raw_call(
_from,
_abi_encode(
_pool,
MAX_UINT256,
method_id=method_id("approve(address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
self.is_approved[_from][_pool] = True
# perform coin exchange
if is_underlying:
CurvePool(_pool).exchange_underlying(i, j, _amount, _expected, value=eth_amount)
else:
CurvePool(_pool).exchange(i, j, _amount, _expected, value=eth_amount)
# perform output transfer
if _to == ETH_ADDRESS:
received_amount = self.balance
raw_call(_receiver, b"", value=self.balance)
else:
received_amount = ERC20(_to).balanceOf(self)
response: Bytes[32] = raw_call(
_to,
_abi_encode(
_receiver,
received_amount,
method_id=method_id("transfer(address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
log TokenExchange(_sender, _receiver, _pool, _from, _to, _amount, received_amount)
return received_amount
```
::::
### `exchange_multiple`
::::description[`CurveRegistryExchange.exchange_multiple(_route: address[9], _swap_params: uint256[3][4], _amount: uint256, _expected: uint256, _pools: address[4]=[ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS], _receiver: address=msg.sender) -> uint256`]
Function to perform up to four token exchanges in a single transaction. Prior to calling this function, the caller must approve this contract to transfer `_amount` coins from `_from`.
| Swap Type | Description |
| :-------: | ----------- |
| `1` | Stableswap `exchange` |
| `2` | Stableswap `exchange_underlying` |
| `3` | Cryptoswap `exchange` |
| `4` | Cryptoswap `exchange_underlying` |
| `5` | Factory metapools with lending base pool: `exchange_underlying` |
| `6` | Factory crypto-meta pools underlying exchange (`exchange` method in zap) |
| `7 - 11` | For wrapped coin (underlying for lending pool) -> LP token "exchange" (actually `add_liquidity`) |
| `12 - 14` | For LP token -> wrapped coin (underlying for lending or fake pool) "exchange" (actually `remove_liquidity_one_coin`) |
| `15` | For WETH -> ETH "exchange" (actually deposit/withdraw) |
Returns: expected amount of the final output token (`uint256`).
Emits: `ExchangeMultiple`
| Input | Type | Description |
| -------------- | --------------- | ------------ |
| `_route` | `address[9]` | Array of the route consisting of [initial token, pool, token, pool, token, ...]. |
| `_swap_params` | `uint256[3][4]` | Multidimensional array of `[i, j, swap_type]` where `i` and `j` are the correct values for the n'th pool in `_route`. |
| `_amount` | `uint256` | Amount of initial tokens (`_route[0]`) to be exchanged. |
| `_expected` | `uint256` | Minimum amount of coins to receive. |
| `_pools` | `address[4]` | Array of pools for swaps via zap contracts. This parameter is only needed for Polygon meta-factories underlying swaps. |
| `_receiver` | `address` | Receiver of the tokens. Defaults to `msg.sender`. |
```vyper
event ExchangeMultiple:
buyer: indexed(address)
receiver: indexed(address)
route: address[9]
swap_params: uint256[3][4]
pools: address[4]
amount_sold: uint256
amount_bought: uint256
@external
@payable
def exchange_multiple(
_route: address[9],
_swap_params: uint256[3][4],
_amount: uint256,
_expected: uint256,
_pools: address[4]=[ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS],
_receiver: address=msg.sender
) -> uint256:
"""
@notice Perform up to four swaps in a single transaction
@dev Routing and swap params must be determined off-chain. This
functionality is designed for gas efficiency over ease-of-use.
@param _route Array of [initial token, pool, token, pool, token, ...]
The array is iterated until a pool address of 0x00, then the last
given token is transferred to `_receiver`
@param _swap_params Multidimensional array of [i, j, swap type] where i and j are the correct
values for the n'th pool in `_route`. The swap type should be
1 for a stableswap `exchange`,
2 for stableswap `exchange_underlying`,
3 for a cryptoswap `exchange`,
4 for a cryptoswap `exchange_underlying`,
5 for factory metapools with lending base pool `exchange_underlying`,
6 for factory crypto-meta pools underlying exchange (`exchange` method in zap),
7-11 for wrapped coin (underlying for lending or fake pool) -> LP token "exchange" (actually `add_liquidity`),
12-14 for LP token -> wrapped coin (underlying for lending pool) "exchange" (actually `remove_liquidity_one_coin`)
15 for WETH -> ETH "exchange" (actually deposit/withdraw)
@param _amount The amount of `_route[0]` token being sent.
@param _expected The minimum amount received after the final swap.
@param _pools Array of pools for swaps via zap contracts. This parameter is only needed for
Polygon meta-factories underlying swaps.
@param _receiver Address to transfer the final output token to.
@return Received amount of the final output token
"""
input_token: address = _route[0]
amount: uint256 = _amount
output_token: address = ZERO_ADDRESS
# validate / transfer initial token
if input_token == ETH_ADDRESS:
assert msg.value == amount
else:
assert msg.value == 0
response: Bytes[32] = raw_call(
input_token,
_abi_encode(
msg.sender,
self,
amount,
method_id=method_id("transferFrom(address,address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
for i in range(1,5):
# 4 rounds of iteration to perform up to 4 swaps
swap: address = _route[i*2-1]
pool: address = _pools[i-1] # Only for Polygon meta-factories underlying swap (swap_type == 4)
output_token = _route[i*2]
params: uint256[3] = _swap_params[i-1] # i, j, swap type
if not self.is_approved[input_token][swap]:
# approve the pool to transfer the input token
response: Bytes[32] = raw_call(
input_token,
_abi_encode(
swap,
MAX_UINT256,
method_id=method_id("approve(address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
self.is_approved[input_token][swap] = True
eth_amount: uint256 = 0
if input_token == ETH_ADDRESS:
eth_amount = amount
# perform the swap according to the swap type
if params[2] == 1:
CurvePool(swap).exchange(convert(params[0], int128), convert(params[1], int128), amount, 0, value=eth_amount)
elif params[2] == 2:
CurvePool(swap).exchange_underlying(convert(params[0], int128), convert(params[1], int128), amount, 0, value=eth_amount)
elif params[2] == 3:
if input_token == ETH_ADDRESS or output_token == ETH_ADDRESS:
CryptoPoolETH(swap).exchange(params[0], params[1], amount, 0, True, value=eth_amount)
else:
CryptoPool(swap).exchange(params[0], params[1], amount, 0)
elif params[2] == 4:
CryptoPool(swap).exchange_underlying(params[0], params[1], amount, 0, value=eth_amount)
elif params[2] == 5:
LendingBasePoolMetaZap(swap).exchange_underlying(pool, convert(params[0], int128), convert(params[1], int128), amount, 0)
elif params[2] == 6:
use_eth: bool = input_token == ETH_ADDRESS or output_token == ETH_ADDRESS
CryptoMetaZap(swap).exchange(pool, params[0], params[1], amount, 0, use_eth)
elif params[2] == 7:
_amounts: uint256[2] = [0, 0]
_amounts[params[0]] = amount
BasePool2Coins(swap).add_liquidity(_amounts, 0)
elif params[2] == 8:
_amounts: uint256[3] = [0, 0, 0]
_amounts[params[0]] = amount
BasePool3Coins(swap).add_liquidity(_amounts, 0)
elif params[2] == 9:
_amounts: uint256[3] = [0, 0, 0]
_amounts[params[0]] = amount
LendingBasePool3Coins(swap).add_liquidity(_amounts, 0, True) # example: aave on Polygon
elif params[2] == 10:
_amounts: uint256[4] = [0, 0, 0, 0]
_amounts[params[0]] = amount
BasePool4Coins(swap).add_liquidity(_amounts, 0)
elif params[2] == 11:
_amounts: uint256[5] = [0, 0, 0, 0, 0]
_amounts[params[0]] = amount
BasePool5Coins(swap).add_liquidity(_amounts, 0)
elif params[2] == 12:
# The number of coins doesn't matter here
BasePool3Coins(swap).remove_liquidity_one_coin(amount, convert(params[1], int128), 0)
elif params[2] == 13:
# The number of coins doesn't matter here
LendingBasePool3Coins(swap).remove_liquidity_one_coin(amount, convert(params[1], int128), 0, True) # example: aave on Polygon
elif params[2] == 14:
# The number of coins doesn't matter here
CryptoBasePool3Coins(swap).remove_liquidity_one_coin(amount, params[1], 0) # example: atricrypto3 on Polygon
elif params[2] == 15:
if input_token == ETH_ADDRESS:
wETH(swap).deposit(value=amount)
elif output_token == ETH_ADDRESS:
wETH(swap).withdraw(amount)
else:
raise "One of the coins must be ETH for swap type 15"
else:
raise "Bad swap type"
# update the amount received
if output_token == ETH_ADDRESS:
amount = self.balance
else:
amount = ERC20(output_token).balanceOf(self)
# sanity check, if the routing data is incorrect we will have a 0 balance and that is bad
assert amount != 0, "Received nothing"
# check if this was the last swap
if i == 4 or _route[i*2+1] == ZERO_ADDRESS:
break
# if there is another swap, the output token becomes the input for the next round
input_token = output_token
# validate the final amount received
assert amount >= _expected
# transfer the final token to the receiver
if output_token == ETH_ADDRESS:
raw_call(_receiver, b"", value=amount)
else:
response: Bytes[32] = raw_call(
output_token,
_abi_encode(
_receiver,
amount,
method_id=method_id("transfer(address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
log ExchangeMultiple(msg.sender, _receiver, _route, _swap_params, _pools, _amount, amount)
return amount
```
::::
### `get_exchange_amount`
::::description[`CurveRegistryExchange.get_exchange_amount(_pool: address, _from: address, _to: address, _amount: uint256) -> uint256`]
Getter for the current number of coins received in an exchange.
Returns: amount of tokens received (`uint256`).
| Input | Type | Description |
|-----------|-----------|----------------------------------|
| `_pool` | `address` | Pool address. |
| `_from` | `address` | Address of the coin being sent. |
| `_to` | `address` | Address of the coin being received. |
| `_amount` | `uint256` | Amount of coins to exchange. |
```vyper
@view
@external
def get_exchange_amount(_pool: address, _from: address, _to: address, _amount: uint256) -> uint256:
"""
@notice Get the current number of coins received in an exchange
@dev Works for both regular and factory-deployed pools
@param _pool Pool address
@param _from Address of coin to be sent
@param _to Address of coin to be received
@param _amount Quantity of `_from` to be sent
@return Quantity of `_to` to be received
"""
registry: address = self.crypto_registry
if Registry(registry).get_lp_token(_pool) != ZERO_ADDRESS:
initial: address = _from
target: address = _to
if _from == ETH_ADDRESS:
initial = WETH_ADDRESS
if _to == ETH_ADDRESS:
target = WETH_ADDRESS
return self._get_crypto_exchange_amount(registry, _pool, initial, target, _amount)
registry = self.registry
if Registry(registry).get_lp_token(_pool) == ZERO_ADDRESS:
registry = self.factory_registry
return self._get_exchange_amount(registry, _pool, _from, _to, _amount)
@view
@internal
def _get_exchange_amount(
_registry: address,
_pool: address,
_from: address,
_to: address,
_amount: uint256
) -> uint256:
"""
@notice Get the current number of coins received in an exchange
@param _registry Registry address
@param _pool Pool address
@param _from Address of coin to be sent
@param _to Address of coin to be received
@param _amount Quantity of `_from` to be sent
@return Quantity of `_to` to be received
"""
i: int128 = 0
j: int128 = 0
is_underlying: bool = False
i, j, is_underlying = Registry(_registry).get_coin_indices(_pool, _from, _to) # dev: no market
if is_underlying and (_registry == self.registry or Registry(_registry).is_meta(_pool)):
return CurvePool(_pool).get_dy_underlying(i, j, _amount)
return CurvePool(_pool).get_dy(i, j, _amount)
@view
@internal
def _get_crypto_exchange_amount(
_registry: address,
_pool: address,
_from: address,
_to: address,
_amount: uint256
) -> uint256:
"""
@notice Get the current number of coins received in an exchange
@param _registry Registry address
@param _pool Pool address
@param _from Address of coin to be sent
@param _to Address of coin to be received
@param _amount Quantity of `_from` to be sent
@return Quantity of `_to` to be received
"""
i: uint256 = 0
j: uint256 = 0
i, j = CryptoRegistry(_registry).get_coin_indices(_pool, _from, _to) # dev: no market
return CryptoPool(_pool).get_dy(i, j, _amount)
```
```shell
>>> CurveRegistryExchange.get_exchange_amount('0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7', '0x6b175474e89094c44da98b954eedeac495271d0f', '0xdac17f958d2ee523a2206206994597c13d831ec7', 1000000000000000000000000)
1000242275074 # swapping DAI for USDT using threepool
>>> CurveRegistryExchange.get_exchange_amount('0xd51a44d3fae010294c616388b506acda1bfaae46', '0xdac17f958d2ee523a2206206994597c13d831ec7', '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 10000000000)
3315937652359468906 # swapping USDT for ETH using tricrypto2
```
::::
### `get_best_rate`
::::description[`CurveRegistryExchange.get_best_rate(_from: address, _to: address, _amount: uint256, _exclude_pools: address[8] = EMPTY_POOL_LIST) -> (address, uint256)`]
:::info
This method checks for regular and factory pools.
:::
Function to find the pool offering the best rate for a given swap.
| Input | Type | Description |
| ---------------- | ------------ | ---------------------------------------------------- |
| `_from` | `address` | Address of the coin being sent. |
| `_to` | `address` | Address of the coin being received. |
| `_amount` | `uint256` | Amount of coins to send. |
| `_exclude_pools` | `address[8]` | List of up to 8 pools which should not be returned. |
Returns: pool address and amount received (`address`, `uint256`).
```vyper
@view
@external
def get_best_rate(
_from: address, _to: address, _amount: uint256, _exclude_pools: address[8] = EMPTY_POOL_LIST
) -> (address, uint256):
"""
@notice Find the pool offering the best rate for a given swap.
@dev Checks rates for regular and factory pools
@param _from Address of coin being sent
@param _to Address of coin being received
@param _amount Quantity of `_from` being sent
@param _exclude_pools A list of up to 8 addresses which shouldn't be returned
@return Pool address, amount received
"""
best_pool: address = ZERO_ADDRESS
max_dy: uint256 = 0
initial: address = _from
target: address = _to
if _from == ETH_ADDRESS:
initial = WETH_ADDRESS
if _to == ETH_ADDRESS:
target = WETH_ADDRESS
registry: address = self.crypto_registry
for i in range(65536):
pool: address = Registry(registry).find_pool_for_coins(initial, target, i)
if pool == ZERO_ADDRESS:
if i == 0:
# we only check for stableswap pools if we did not find any crypto pools
break
return best_pool, max_dy
elif pool in _exclude_pools:
continue
dy: uint256 = self._get_crypto_exchange_amount(registry, pool, initial, target, _amount)
if dy > max_dy:
best_pool = pool
max_dy = dy
registry = self.registry
for i in range(65536):
pool: address = Registry(registry).find_pool_for_coins(_from, _to, i)
if pool == ZERO_ADDRESS:
break
elif pool in _exclude_pools:
continue
dy: uint256 = self._get_exchange_amount(registry, pool, _from, _to, _amount)
if dy > max_dy:
best_pool = pool
max_dy = dy
registry = self.factory_registry
for i in range(65536):
pool: address = Registry(registry).find_pool_for_coins(_from, _to, i)
if pool == ZERO_ADDRESS:
break
elif pool in _exclude_pools:
continue
if ERC20(pool).totalSupply() == 0:
# ignore pools without TVL as the call to `get_dy` will revert
continue
dy: uint256 = self._get_exchange_amount(registry, pool, _from, _to, _amount)
if dy > max_dy:
best_pool = pool
max_dy = dy
return best_pool, max_dy
@view
@internal
def _get_exchange_amount(
_registry: address,
_pool: address,
_from: address,
_to: address,
_amount: uint256
) -> uint256:
"""
@notice Get the current number of coins received in an exchange
@param _registry Registry address
@param _pool Pool address
@param _from Address of coin to be sent
@param _to Address of coin to be received
@param _amount Quantity of `_from` to be sent
@return Quantity of `_to` to be received
"""
i: int128 = 0
j: int128 = 0
is_underlying: bool = False
i, j, is_underlying = Registry(_registry).get_coin_indices(_pool, _from, _to) # dev: no market
if is_underlying and (_registry == self.registry or Registry(_registry).is_meta(_pool)):
return CurvePool(_pool).get_dy_underlying(i, j, _amount)
return CurvePool(_pool).get_dy(i, j, _amount)
@view
@internal
def _get_crypto_exchange_amount(
_registry: address,
_pool: address,
_from: address,
_to: address,
_amount: uint256
) -> uint256:
"""
@notice Get the current number of coins received in an exchange
@param _registry Registry address
@param _pool Pool address
@param _from Address of coin to be sent
@param _to Address of coin to be received
@param _amount Quantity of `_from` to be sent
@return Quantity of `_to` to be received
"""
i: uint256 = 0
j: uint256 = 0
i, j = CryptoRegistry(_registry).get_coin_indices(_pool, _from, _to) # dev: no market
return CryptoPool(_pool).get_dy(i, j, _amount)
```
```shell
>>> CurveRegistryExchange.get_best_rate('0x6b175474e89094c44da98b954eedeac495271d0f', '0xdac17f958d2ee523a2206206994597c13d831ec7', 1000000000000000000000000)
'0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7', 1000242275724
```
::::
### `get_exchange_multiple_amount`
::::description[`CurveRegistryExchange.get_exchange_multiple_amount(_route: address[9], _swap_params: uint256[3][4], _amount: uint256, _pools: address[4]=[ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS]) -> uint256`]
Getter for the amount of output tokens when performing a swap with multiple steps. The function iterates over the route addresses and, once the `ZERO_ADDRESS` is reached, the last given token is transferred to `_receiver`.
| Swap Type | Description |
| :-------: | ----------- |
| `1` | Stableswap `exchange` |
| `2` | Stableswap `exchange_underlying` |
| `3` | Cryptoswap: `exchange` |
| `4` | Cryptoswap: `exchange_underlying` |
| `5` | Factory metapools with lending base pool: `exchange_underlying` |
| `6` | Factory crypto-meta pools underlying exchange (`exchange` method in zap) |
| `7 - 11` | For wrapped coin (underlying for lending pool) -> LP token "exchange" (actually `add_liquidity`) |
| `12 - 14` | For LP token -> wrapped coin (underlying for lending or fake pool) "exchange" (actually `remove_liquidity_one_coin`) |
| `15` | For WETH -> ETH "exchange" (actually deposit/withdraw) |
| Input | Type | Description |
| -------------- | --------------- | ------------ |
| `_route` | `address[9]` | Array of the route consisting of [initial token, pool, token, pool, token, ...]. |
| `_swap_params` | `uint256[3][4]` | Multidimensional array of `[i, j, swap_type]` where `i` and `j` are the correct values for the n'th pool in `_route`. |
| `_amount` | `uint256` | Amount of initial tokens (`_route[0]`) to exchange. |
| `_pools` | `address[4]` | Array of pools for swaps via zap contracts. This parameter is only needed for Polygon meta-factories underlying swaps. |
Returns: expected amount of the final output token (`uint256`).
```vyper
@view
@external
def get_exchange_multiple_amount(
_route: address[9],
_swap_params: uint256[3][4],
_amount: uint256,
_pools: address[4]=[ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS]
) -> uint256:
"""
@notice Get the current number the final output tokens received in an exchange
@dev Routing and swap params must be determined off-chain. This
functionality is designed for gas efficiency over ease-of-use.
@param _route Array of [initial token, pool, token, pool, token, ...]
The array is iterated until a pool address of 0x00, then the last
given token is transferred to `_receiver`
@param _swap_params Multidimensional array of [i, j, swap type] where i and j are the correct
values for the n'th pool in `_route`. The swap type should be
1 for a stableswap `exchange`,
2 for stableswap `exchange_underlying`,
3 for a cryptoswap `exchange`,
4 for a cryptoswap `exchange_underlying`,
5 for factory metapools with lending base pool `exchange_underlying`,
6 for factory crypto-meta pools underlying exchange (`exchange` method in zap),
7-11 for wrapped coin (underlying for lending pool) -> LP token "exchange" (actually `add_liquidity`),
12-14 for LP token -> wrapped coin (underlying for lending or fake pool) "exchange" (actually `remove_liquidity_one_coin`)
15 for WETH -> ETH "exchange" (actually deposit/withdraw)
@param _amount The amount of `_route[0]` token to be sent.
@param _pools Array of pools for swaps via zap contracts. This parameter is only needed for
Polygon meta-factories underlying swaps.
@return Expected amount of the final output token
"""
amount: uint256 = _amount
for i in range(1,5):
# 4 rounds of iteration to perform up to 4 swaps
swap: address = _route[i*2-1]
pool: address = _pools[i-1] # Only for Polygon meta-factories underlying swap (swap_type == 4)
params: uint256[3] = _swap_params[i-1] # i, j, swap type
# Calc output amount according to the swap type
if params[2] == 1:
amount = CurvePool(swap).get_dy(convert(params[0], int128), convert(params[1], int128), amount)
elif params[2] == 2:
amount = CurvePool(swap).get_dy_underlying(convert(params[0], int128), convert(params[1], int128), amount)
elif params[2] == 3:
amount = CryptoPool(swap).get_dy(params[0], params[1], amount)
elif params[2] == 4:
amount = CryptoPool(swap).get_dy_underlying(params[0], params[1], amount)
elif params[2] == 5:
amount = CurvePool(pool).get_dy_underlying(convert(params[0], int128), convert(params[1], int128), amount)
elif params[2] == 6:
amount = CryptoMetaZap(swap).get_dy(pool, params[0], params[1], amount)
elif params[2] == 7:
_amounts: uint256[2] = [0, 0]
_amounts[params[0]] = amount
amount = BasePool2Coins(swap).calc_token_amount(_amounts, True)
elif params[2] in [8, 9]:
_amounts: uint256[3] = [0, 0, 0]
_amounts[params[0]] = amount
amount = BasePool3Coins(swap).calc_token_amount(_amounts, True)
elif params[2] == 10:
_amounts: uint256[4] = [0, 0, 0, 0]
_amounts[params[0]] = amount
amount = BasePool4Coins(swap).calc_token_amount(_amounts, True)
elif params[2] == 11:
_amounts: uint256[5] = [0, 0, 0, 0, 0]
_amounts[params[0]] = amount
amount = BasePool5Coins(swap).calc_token_amount(_amounts, True)
elif params[2] in [12, 13]:
# The number of coins doesn't matter here
amount = BasePool3Coins(swap).calc_withdraw_one_coin(amount, convert(params[1], int128))
elif params[2] == 14:
# The number of coins doesn't matter here
amount = CryptoBasePool3Coins(swap).calc_withdraw_one_coin(amount, params[1])
elif params[2] == 15:
# ETH <--> WETH rate is 1:1
pass
else:
raise "Bad swap type"
# check if this was the last swap
if i == 4 or _route[i*2+1] == ZERO_ADDRESS:
break
return amount
```
::::
---
## Registry Contracts
:::warning[Outdated]
The contract retrieves pool data from various registries sourced from the [`AddressProvider`](https://etherscan.io/address/0x0000000022d53366457f9d5e68ec105046fc4383). Since its deployment, a [new `AddressProvider`](../../integration/address-provider.md) along with additional registries have been deployed.
:::
### `registry`
::::description[`CurveRegistryExchange.registry() -> address: view`]
Getter for the registry contract.
Returns: registry (`address`).
```vyper
registry: public(address)
@external
def __init__(_address_provider: address, _calculator: address, _weth: address):
"""
@notice Constructor function
"""
self.address_provider = AddressProvider(_address_provider)
self.registry = AddressProvider(_address_provider).get_registry()
self.factory_registry = AddressProvider(_address_provider).get_address(3)
self.crypto_registry = AddressProvider(_address_provider).get_address(5)
self.default_calculator = _calculator
WETH_ADDRESS = _weth
```
```shell
>>> CurveRegistryExchange.registry()
'0x90E00ACe148ca3b23Ac1bC8C240C2a7Dd9c2d7f5'
```
::::
### `factory_registry`
::::description[`CurveRegistryExchange.factory_registry() -> address: view`]
Getter for the factory regstiry contract.
Returns: factory registry (`address`).
```vyper
factory_registry: public(address)
@external
def __init__(_address_provider: address, _calculator: address, _weth: address):
"""
@notice Constructor function
"""
self.address_provider = AddressProvider(_address_provider)
self.registry = AddressProvider(_address_provider).get_registry()
self.factory_registry = AddressProvider(_address_provider).get_address(3)
self.crypto_registry = AddressProvider(_address_provider).get_address(5)
self.default_calculator = _calculator
WETH_ADDRESS = _weth
```
```shell
>>> CurveRegistryExchange.factory_registry()
'0xB9fC157394Af804a3578134A6585C0dc9cc990d4'
```
::::
### `crypto_registry`
::::description[`CurveRegistryExchange.crypto_registry() -> address: view`]
Getter for the crypto registry contract.
Returns: crypto registry (`address`).
```vyper
crypto_registry: public(address)
@external
def __init__(_address_provider: address, _calculator: address, _weth: address):
"""
@notice Constructor function
"""
self.address_provider = AddressProvider(_address_provider)
self.registry = AddressProvider(_address_provider).get_registry()
self.factory_registry = AddressProvider(_address_provider).get_address(3)
self.crypto_registry = AddressProvider(_address_provider).get_address(5)
self.default_calculator = _calculator
WETH_ADDRESS = _weth
```
```shell
>>> CurveRegistryExchange.crypto_registry()
'0x8F942C20D02bEfc377D41445793068908E2250D0'
```
::::
### `update_registry_address`
::::description[`CurveRegistryExchange.update_registry_address() -> bool`]
Function to update `registry`, `factory_registry` and `crypto_registry`. This function is callable by anyone and sets the variables to the current vaules in the `AddressProvider` contract.
Returns: True (`bool`).
```vyper
address_provider: AddressProvider
registry: public(address)
factory_registry: public(address)
crypto_registry: public(address)
@external
def update_registry_address() -> bool:
"""
@notice Update registry address
@dev The registry address is kept in storage to reduce gas costs.
If a new registry is deployed this function should be called
to update the local address from the address provider.
@return bool success
"""
address_provider: address = self.address_provider.address
self.registry = AddressProvider(address_provider).get_registry()
self.factory_registry = AddressProvider(address_provider).get_address(3)
self.crypto_registry = AddressProvider(address_provider).get_address(5)
return True
```
::::
---
## Calculator Contract
The contract is designed to set a calculator contract that can perform various tasks. However, this has not been configured.
### `default_calculator`
::::description[`CurveRegistryExchange.default_calculator() -> address: view`]
Getter for the default calculator. The default calculator can be set by the `admin` of the `AddressProvider` contract using the `set_default_calculator` function.
Returns: calculator contract (`address`).
```vyper
default_calculator: public(address)
@external
def __init__(_address_provider: address, _calculator: address, _weth: address):
"""
@notice Constructor function
"""
self.address_provider = AddressProvider(_address_provider)
self.registry = AddressProvider(_address_provider).get_registry()
self.factory_registry = AddressProvider(_address_provider).get_address(3)
self.crypto_registry = AddressProvider(_address_provider).get_address(5)
self.default_calculator = _calculator
WETH_ADDRESS = _weth
```
```shell
>>> CurveRegistryExchange.default_calculator()
'0x0000000000000000000000000000000000000000'
```
::::
### `get_calculator`
::::description[`CurveRegistryExchange.get_calculator(_pool: address) -> address: view`]
Getter for the calculator contract of `_pool`. The calculator of pool can be set by the `admin` of the `AddressProvider` contract using the `set_calculator` function.
Returns: calculator contract (`address`).
| Input | Type | Description |
| ------- | --------- | ------------ |
| `_pool` | `address` | Liquidity pool address. |
```vyper
@view
@external
def get_calculator(_pool: address) -> address:
"""
@notice Set calculator contract
@dev Used to calculate `get_dy` for a pool
@param _pool Pool address
@return `CurveCalc` address
"""
calculator: address = self.pool_calculator[_pool]
if calculator == ZERO_ADDRESS:
return self.default_calculator
else:
return calculator
```
```shell
>>> CurveRegistryExchange.get_calculator('0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7')
'0x0000000000000000000000000000000000000000'
```
::::
### `set_calculator`
::::description[`CurveRegistryExchange.set_calculator(_pool: address, _calculator: address) -> bool`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the `AddressProvider` contract.
:::
Function to set a calculator contract for a pool.
| Input | Type | Description |
| ------- | --------- | ------------ |
| `_pool` | `address` | Liquidity pool to set the calculator for. |
| `_calculator` | `address` | Calculator contract. |
Returns: True (`bool`).
```vyper
pool_calculator: HashMap[address, address]
@external
def set_calculator(_pool: address, _calculator: address) -> bool:
"""
@notice Set calculator contract
@dev Used to calculate `get_dy` for a pool
@param _pool Pool address
@param _calculator `CurveCalc` address
@return bool success
"""
assert msg.sender == self.address_provider.admin() # dev: admin-only function
self.pool_calculator[_pool] = _calculator
return True
```
::::
### `set_default_calculator`
::::description[`CurveRegistryExchange.set_default_calculator(_calculator: address) -> bool`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the `AddressProvider` contract.
:::
Function to set the default calculator contract.
Returns: True (`bool`).
| Input | Type | Description |
| ------- | --------- | ------------ |
| `_calculator` | `address` | Calculator address. |
```vyper
default_calculator: public(address)
@external
def set_default_calculator(_calculator: address) -> bool:
"""
@notice Set default calculator contract
@dev Used to calculate `get_dy` for a pool
@param _calculator `CurveCalc` address
@return bool success
"""
assert msg.sender == self.address_provider.admin() # dev: admin-only function
self.default_calculator = _calculator
return True
```
::::
---
## Killing the Router
The `admin` of the `AddressProvider` contract has the ability to set the `is_killed` status of the `CurveRegistryExchange` contract via the `set_killed` function. Setting this status to `true` disables all exchanges in the contract. The status can be reversed, or "unkilled," to allow token exchanges to resume.
### `is_killed`
::::description[`CurveRegistryExchange.is_killed() -> boool: view`]
:::warning
If the `is_killed` status is set to `true`, the contract will not allow any token exchanges and will revert when trying to exchange tokens.
:::
Getter for the `is_killed` status of the contract. The status can be set by the `admin` of the `AddressProvider` contract via the `set_killed` function.
Returns: true or false (`bool`).
```python
is_killed: public(bool)
```
```shell
>>> CurveRegistryExchange.is_killed()
'false'
```
::::
### `set_killed`
::::description[`CurveRegistryExchange.set_killed(_is_killed: bool) -> bool`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the `AddressProvider` contract.
:::
Function to set the `is_killed` status for the contract.
| Input | Type | Description |
| ------------ | ------ | ------------ |
| `_is_killed` | `bool` | `true` or `false`. |
Returns: True (`bool`).
```vyper
@external
def set_killed(_is_killed: bool) -> bool:
"""
@notice Kill or unkill the contract
@param _is_killed Killed status of the contract
@return bool success
"""
assert msg.sender == self.address_provider.admin() # dev: admin-only function
self.is_killed = _is_killed
return True
```
::::
---
## Transfering Funds
In the event that the contract holds an ERC20 or ETH balance, these tokens can be claimed by the `admin` of the `AddressProvider` contract. Although this should not occur at all, a possible scenario in which this could happen is when users mistakenly send their tokens directly to the contract address.
### `claim_balance`
::::description[`CurveRegistryExchange.claim_balance(_token: address) -> bool`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the `AddressProvider` contract.
:::
Function to transfer an ERC20 or ETH balance held by this contract. When calling this function, the entire balance is transfered to the `admin` of the `AddressProvider`. This method can be used when tokens are mistakenly sent to the contract. Other than that, the contract does not hold any user assets.
Returns: True (`bool`).
| Input | Type | Description |
| ------- | --------- | ------------ |
| `_token` | `address` | Token to transfer. |
```vyper
@external
def claim_balance(_token: address) -> bool:
"""
@notice Transfer an ERC20 or ETH balance held by this contract
@dev The entire balance is transferred to the owner
@param _token Token address
@return bool success
"""
assert msg.sender == self.address_provider.admin() # dev: admin-only function
if _token == ETH_ADDRESS:
raw_call(msg.sender, b"", value=self.balance)
else:
amount: uint256 = ERC20(_token).balanceOf(self)
response: Bytes[32] = raw_call(
_token,
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
return True
```
::::
---
## Curve Router NG
The `CurveRouterNG` is used to perform token exchanges. It can **swap up to five tokens in a single transaction**.
Additionally, the contract provides **functions to calculate input and output amounts**with [`get_dy`](#get_dy) and [`get_dx`](#get_dx).
:::deploy[Contract Source & Deployment]
The `CurveRouterNG` contract has been deployed on most chains where Curve liquidity pools exist. For a full list of all deployments, see [here](../../deployments.md).
Source code for the contract can be found on [ GitHub](https://github.com/curvefi/curve-router-ng/tree/master/contracts).
:::
The contract utilizes **interfaces for all relevant Curve pools**, such as Stableswap, CryptoSwap, LLAMMA, and others, to execute swaps.
```vyper
interface StablePool:
def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256): payable
def exchange_underlying(i: int128, j: int128, dx: uint256, min_dy: uint256): payable
def get_dy(i: int128, j: int128, amount: uint256) -> uint256: view
def get_dy_underlying(i: int128, j: int128, amount: uint256) -> uint256: view
def coins(i: uint256) -> address: view
def calc_withdraw_one_coin(token_amount: uint256, i: int128) -> uint256: view
def remove_liquidity_one_coin(token_amount: uint256, i: int128, min_amount: uint256): nonpayable
interface CryptoPool:
def exchange(i: uint256, j: uint256, dx: uint256, min_dy: uint256): payable
def exchange_underlying(i: uint256, j: uint256, dx: uint256, min_dy: uint256): payable
def get_dy(i: uint256, j: uint256, amount: uint256) -> uint256: view
def get_dy_underlying(i: uint256, j: uint256, amount: uint256) -> uint256: view
def calc_withdraw_one_coin(token_amount: uint256, i: uint256) -> uint256: view
def remove_liquidity_one_coin(token_amount: uint256, i: uint256, min_amount: uint256): nonpayable
interface CryptoPoolETH:
def exchange(i: uint256, j: uint256, dx: uint256, min_dy: uint256, use_eth: bool): payable
interface LendingBasePoolMetaZap:
def exchange_underlying(pool: address, i: int128, j: int128, dx: uint256, min_dy: uint256): nonpayable
interface CryptoMetaZap:
def get_dy(pool: address, i: uint256, j: uint256, dx: uint256) -> uint256: view
def exchange(pool: address, i: uint256, j: uint256, dx: uint256, min_dy: uint256, use_eth: bool): payable
interface StablePool2Coins:
def add_liquidity(amounts: uint256[2], min_mint_amount: uint256): payable
def calc_token_amount(amounts: uint256[2], is_deposit: bool) -> uint256: view
interface CryptoPool2Coins:
def calc_token_amount(amounts: uint256[2]) -> uint256: view
interface StablePool3Coins:
def add_liquidity(amounts: uint256[3], min_mint_amount: uint256): payable
def calc_token_amount(amounts: uint256[3], is_deposit: bool) -> uint256: view
interface CryptoPool3Coins:
def calc_token_amount(amounts: uint256[3]) -> uint256: view
interface StablePool4Coins:
def add_liquidity(amounts: uint256[4], min_mint_amount: uint256): payable
def calc_token_amount(amounts: uint256[4], is_deposit: bool) -> uint256: view
interface CryptoPool4Coins:
def calc_token_amount(amounts: uint256[4]) -> uint256: view
interface StablePool5Coins:
def add_liquidity(amounts: uint256[5], min_mint_amount: uint256): payable
def calc_token_amount(amounts: uint256[5], is_deposit: bool) -> uint256: view
interface CryptoPool5Coins:
def calc_token_amount(amounts: uint256[5]) -> uint256: view
interface LendingStablePool3Coins:
def add_liquidity(amounts: uint256[3], min_mint_amount: uint256, use_underlying: bool): payable
def remove_liquidity_one_coin(token_amount: uint256, i: int128, min_amount: uint256, use_underlying: bool) -> uint256: nonpayable
interface Llamma:
def get_dx(i: uint256, j: uint256, out_amount: uint256) -> uint256: view
interface WETH:
def deposit(): payable
def withdraw(_amount: uint256): nonpayable
interface stETH:
def submit(_refferer: address): payable
interface frxETHMinter:
def submit(): payable
interface wstETH:
def getWstETHByStETH(_stETHAmount: uint256) -> uint256: view
def getStETHByWstETH(_wstETHAmount: uint256) -> uint256: view
def wrap(_stETHAmount: uint256) -> uint256: nonpayable
def unwrap(_wstETHAmount: uint256) -> uint256: nonpayable
interface sfrxETH:
def convertToShares(assets: uint256) -> uint256: view
def convertToAssets(shares: uint256) -> uint256: view
def deposit(assets: uint256, receiver: address) -> uint256: nonpayable
def redeem(shares: uint256, receiver: address, owner: address) -> uint256: nonpayable
interface wBETH:
def deposit(referral: address): payable
def exchangeRate() -> uint256: view
# SNX
interface SnxCoin:
def currencyKey() -> bytes32: nonpayable
interface Synthetix:
def exchangeAtomically(sourceCurrencyKey: bytes32, sourceAmount: uint256, destinationCurrencyKey: bytes32, trackingCode: bytes32, minAmount: uint256) -> uint256: nonpayable
interface SynthetixExchanger:
def getAmountsForAtomicExchange(sourceAmount: uint256, sourceCurrencyKey: bytes32, destinationCurrencyKey: bytes32) -> AtomicAmountAndFee: view
interface SynthetixAddressResolver:
def getAddress(name: bytes32) -> address: view
# Calc zaps
interface StableCalc:
def calc_token_amount(pool: address, token: address, amounts: uint256[10], n_coins: uint256, deposit: bool, use_underlying: bool) -> uint256: view
def get_dx(pool: address, i: int128, j: int128, dy: uint256, n_coins: uint256) -> uint256: view
def get_dx_underlying(pool: address, i: int128, j: int128, dy: uint256, n_coins: uint256) -> uint256: view
def get_dx_meta(pool: address, i: int128, j: int128, dy: uint256, n_coins: uint256, base_pool: address) -> uint256: view
def get_dx_meta_underlying(pool: address, i: int128, j: int128, dy: uint256, n_coins: uint256, base_pool: address, base_token: address) -> uint256: view
interface CryptoCalc:
def get_dx(pool: address, i: uint256, j: uint256, dy: uint256, n_coins: uint256) -> uint256: view
def get_dx_meta_underlying(pool: address, i: uint256, j: uint256, dy: uint256, n_coins: uint256, base_pool: address, base_token: address) -> uint256: view
struct AtomicAmountAndFee:
amountReceived: uint256
fee: uint256
exchangeFeeRate: uint256
```
---
## Route and Swap Parameters
The two most curcial input values when using the `CurveRouter` are `_route`, which determines the route of the exchange and `_swap_params`, which includes swap parameters such as input and output token, swap type, pool type and number of coins in the pool.
### `_route`
The route input is an array of up to 11 addresses. When calling the function, the array must always include 11 addresses. Unused spots in the array need to be filled with `ZERO_ADDRESS`. The route consists of tokens and pools or zaps. The first address is always the input token, the last one always the output token. The addresses inbetween compose the route the user wants to trade.
```py
[
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', # initial token: wETH
'0x7f86bf177dd4f3494b841a37e810a34dd56c829b', # pool1: TricryptoUSDC (USDC, wBTC, wETH)
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', # token in between: USDC
'0x4dece678ceceb27446b35c672dc7d61f30bad69e', # pool2: crvusd/USDC
'0xf939e0a03fb07f59a73314e73794be0e57ac1b4e', # output token: crvUSD
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000',
]
```
*The example demonstrates a swap of [wETH](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) for [crvUSD](https://etherscan.io/token/0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E). The process involves two stages. First, wETH is swapped for [USDC](https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48) using the [tricryptoUSDC](https://etherscan.io/address/0x7f86bf177dd4f3494b841a37e810a34dd56c829b) pool. Subsequently, USDC is exchanged for crvUSD using the [crvusd/USDC](https://etherscan.io/address/0x4dece678ceceb27446b35c672dc7d61f30bad69e) pool.*
---
### `_swap_params`
Swap parameters are defined in a multidimensional array which provides essential information about which tokens to swap using a specified pool.
The array structure includes the following elements: **`[i, j, swap_type, pool_type, n_coins]`**. The first array of this multidimensional setup corresponds to the initial token exchange in the swap sequence.
**`i`**: Index of the input token, fetched from `pool.coins('token address')`.
**`j`**: Index of the output token, fetched from `pool.coins('token address')`.
| `swap_type` | Description |
| :---: | ----------- |
| 1 | Standard token `exchange` |
| 2 | Underlying token `exchange_underlying` |
| 3 | Underlying exchange via zap for factory stable metapools and crypto-meta pools (`exchange_underlying` for stable, `exchange` in zap for crypto) |
| 4 | Coin to LP token "exchange" (effectively `add_liquidity`) |
| 5 | Lending pool underlying coin to LP token "exchange" (effectively `add_liquidity`) |
| 6 | LP token to coin "exchange" (effectively `remove_liquidity_one_coin`) |
| 7 | LP token to lending or fake pool underlying coin "exchange" (effectively `remove_liquidity_one_coin`) |
| 8 | Specialized swaps like ETH <-> WETH, ETH -> stETH or frxETH, and cross-liquidity between staked tokens (e.g., stETH <-> wstETH, frxETH <-> sfrxETH) |
| 9 | SNX-related swaps (e.g., sUSD, sEUR, sETH, sBTC) |
| `pool_type` | Description |
| :---------: | ----------- |
| 1 | Stable pools using the Stableswap algorithm |
| 2 | Two-coin Crypto pools using the Cryptoswap algorithm |
| 3 | Tricrypto pools with three coins using the Cryptoswap algorithm |
| 4 | Llamma pools, typically used in crvUSD and lending markets |
**`n_coins`**: Number of coins contained within the pool.
```py
[
[2, 0, 1, 3, 3], # first swap: wETH -> USDC using tricryptoUSDC pool
[0, 1, 1, 1, 2], # second swap: USDC -> crvUSD using crvusd/USDC pool
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
]
```
*Let's take a closer look at the first array, which represents the swap parameters for the first exchange (wETH -> USDC). `i = 2` because the coin index value of wETH in the tricryptoUSDC pool is 2. This can be obtained by calling `tricryptoUSDC.coins(n)`. Similarly, `j = 0` because USDC has the coin index value of 0. The swap type is a regular exchange, represented by `1`. The `pool_type` is 3, as it is a tricrypto pool (a cryptoswap algorithm consisting of three coins: USDC, wBTC, and wETH). The last value in the array represents the number of coins in the pool, which is 3.*
*The values of the second array should be set according to the crvusd/USDC pool.*
---
## Exchanging Tokens
The router has a single `exchange` function, which allows up to 5 swaps in a single transaction.
Routing and swap parameters need to be determined off-chain. The exchange functionality of the router is designed for gas efficiency over ease-of-use. An accompanying JavaScript library can be found on [ GitHub](https://github.com/curvefi/curve-router-js), which is used in the Curve UI to determine route and swap parameters.
### `exchange`
::::description[`Router.exchange(_route: address[11], _swap_params: uint256[5][5], _amount: uint256, _expected: uint256, _pools: address[5] = empty(address[5]), _receiver: address = msg.sender) -> uint256:`]
Function to perform a token exchange with up to 5 swaps in a single transaction.
| Input | Type | Description |
| -------------- | --------------- | ----------- |
| `_route` | `address[11]` | Route data: [see here](#_route) |
| `_swap_params` | `uint256[5][5]` | Swap parameters: [see here](#_swap_params) |
| `_amount` | `uint256` | The amount of the input token (`_route[0]`) to be sent. |
| `_expected` | `uint256` | The minimum amount received after the final swap. |
| `_pools` | `address[5]` | Array of pools for swaps via zap contracts. This parameter is only needed for `swap_type = 3`. |
| `receiver` | `address` | Address to transfer the final output token to. Defaults to `msg.sender`. |
Returns: received amount of the final output token (`uint256`).
```vyper
event Exchange:
sender: indexed(address)
receiver: indexed(address)
route: address[11]
swap_params: uint256[5][5]
pools: address[5]
in_amount: uint256
out_amount: uint256
ETH_ADDRESS: constant(address) = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
STETH_ADDRESS: constant(address) = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84
WSTETH_ADDRESS: constant(address) = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0
FRXETH_ADDRESS: constant(address) = 0x5E8422345238F34275888049021821E8E08CAa1f
SFRXETH_ADDRESS: constant(address) = 0xac3E018457B222d93114458476f3E3416Abbe38F
WBETH_ADDRESS: constant(address) = 0xa2E3356610840701BDf5611a53974510Ae27E2e1
WETH_ADDRESS: immutable(address)
# SNX
# https://github.com/Synthetixio/synthetix-docs/blob/master/content/addresses.md
SNX_ADDRESS_RESOLVER: constant(address) = 0x823bE81bbF96BEc0e25CA13170F5AaCb5B79ba83
SNX_TRACKING_CODE: constant(bytes32) = 0x4355525645000000000000000000000000000000000000000000000000000000 # CURVE
SNX_EXCHANGER_NAME: constant(bytes32) = 0x45786368616E6765720000000000000000000000000000000000000000000000 # Exchanger
snx_currency_keys: HashMap[address, bytes32]
@external
@payable
@nonreentrant('lock')
def exchange(
_route: address[11],
_swap_params: uint256[5][5],
_amount: uint256,
_expected: uint256,
_pools: address[5]=empty(address[5]),
_receiver: address=msg.sender
) -> uint256:
"""
@notice Performs up to 5 swaps in a single transaction.
@dev Routing and swap params must be determined off-chain. This
functionality is designed for gas efficiency over ease-of-use.
@param _route Array of [initial token, pool or zap, token, pool or zap, token, ...]
The array is iterated until a pool address of 0x00, then the last
given token is transferred to `_receiver`
@param _swap_params Multidimensional array of [i, j, swap type, pool_type, n_coins] where
i is the index of input token
j is the index of output token
The swap_type should be:
1. for `exchange`,
2. for `exchange_underlying`,
3. for underlying exchange via zap: factory stable metapools with lending base pool `exchange_underlying`
and factory crypto-meta pools underlying exchange (`exchange` method in zap)
4. for coin -> LP token "exchange" (actually `add_liquidity`),
5. for lending pool underlying coin -> LP token "exchange" (actually `add_liquidity`),
6. for LP token -> coin "exchange" (actually `remove_liquidity_one_coin`)
7. for LP token -> lending or fake pool underlying coin "exchange" (actually `remove_liquidity_one_coin`)
8. for ETH <-> WETH, ETH -> stETH or ETH -> frxETH, stETH <-> wstETH, frxETH <-> sfrxETH, ETH -> wBETH
9. for SNX swaps (sUSD, sEUR, sETH, sBTC)
pool_type: 1 - stable, 2 - crypto, 3 - tricrypto, 4 - llamma
n_coins is the number of coins in pool
@param _amount The amount of input token (`_route[0]`) to be sent.
@param _expected The minimum amount received after the final swap.
@param _pools Array of pools for swaps via zap contracts. This parameter is only needed for swap_type = 3.
@param _receiver Address to transfer the final output token to.
@return Received amount of the final output token.
"""
input_token: address = _route[0]
output_token: address = empty(address)
amount: uint256 = _amount
# validate / transfer initial token
if input_token == ETH_ADDRESS:
assert msg.value == amount
else:
assert msg.value == 0
assert ERC20(input_token).transferFrom(msg.sender, self, amount, default_return_value=True)
for i in range(1, 6):
# 5 rounds of iteration to perform up to 5 swaps
swap: address = _route[i*2-1]
pool: address = _pools[i-1] # Only for Polygon meta-factories underlying swap (swap_type == 6)
output_token = _route[i*2]
params: uint256[5] = _swap_params[i-1] # i, j, swap_type, pool_type, n_coins
if not self.is_approved[input_token][swap]:
assert ERC20(input_token).approve(swap, max_value(uint256), default_return_value=True, skip_contract_check=True)
self.is_approved[input_token][swap] = True
eth_amount: uint256 = 0
if input_token == ETH_ADDRESS:
eth_amount = amount
# perform the swap according to the swap type
if params[2] == 1:
if params[3] == 1: # stable
StablePool(swap).exchange(convert(params[0], int128), convert(params[1], int128), amount, 0, value=eth_amount)
else: # crypto, tricrypto or llamma
if input_token == ETH_ADDRESS or output_token == ETH_ADDRESS:
CryptoPoolETH(swap).exchange(params[0], params[1], amount, 0, True, value=eth_amount)
else:
CryptoPool(swap).exchange(params[0], params[1], amount, 0)
elif params[2] == 2:
if params[3] == 1: # stable
StablePool(swap).exchange_underlying(convert(params[0], int128), convert(params[1], int128), amount, 0, value=eth_amount)
else: # crypto or tricrypto
CryptoPool(swap).exchange_underlying(params[0], params[1], amount, 0, value=eth_amount)
elif params[2] == 3: # SWAP IS ZAP HERE !!!
if params[3] == 1: # stable
LendingBasePoolMetaZap(swap).exchange_underlying(pool, convert(params[0], int128), convert(params[1], int128), amount, 0)
else: # crypto or tricrypto
use_eth: bool = input_token == ETH_ADDRESS or output_token == ETH_ADDRESS
CryptoMetaZap(swap).exchange(pool, params[0], params[1], amount, 0, use_eth, value=eth_amount)
elif params[2] == 4:
if params[4] == 2:
amounts: uint256[2] = [0, 0]
amounts[params[0]] = amount
StablePool2Coins(swap).add_liquidity(amounts, 0, value=eth_amount)
elif params[4] == 3:
amounts: uint256[3] = [0, 0, 0]
amounts[params[0]] = amount
StablePool3Coins(swap).add_liquidity(amounts, 0, value=eth_amount)
elif params[4] == 4:
amounts: uint256[4] = [0, 0, 0, 0]
amounts[params[0]] = amount
StablePool4Coins(swap).add_liquidity(amounts, 0, value=eth_amount)
elif params[4] == 5:
amounts: uint256[5] = [0, 0, 0, 0, 0]
amounts[params[0]] = amount
StablePool5Coins(swap).add_liquidity(amounts, 0, value=eth_amount)
elif params[2] == 5:
amounts: uint256[3] = [0, 0, 0]
amounts[params[0]] = amount
LendingStablePool3Coins(swap).add_liquidity(amounts, 0, True, value=eth_amount) # example: aave on Polygon
elif params[2] == 6:
if params[3] == 1: # stable
StablePool(swap).remove_liquidity_one_coin(amount, convert(params[1], int128), 0)
else: # crypto or tricrypto
CryptoPool(swap).remove_liquidity_one_coin(amount, params[1], 0) # example: atricrypto3 on Polygon
elif params[2] == 7:
LendingStablePool3Coins(swap).remove_liquidity_one_coin(amount, convert(params[1], int128), 0, True) # example: aave on Polygon
elif params[2] == 8:
if input_token == ETH_ADDRESS and output_token == WETH_ADDRESS:
WETH(swap).deposit(value=amount)
elif input_token == WETH_ADDRESS and output_token == ETH_ADDRESS:
WETH(swap).withdraw(amount)
elif input_token == ETH_ADDRESS and output_token == STETH_ADDRESS:
stETH(swap).submit(0x0000000000000000000000000000000000000000, value=amount)
elif input_token == ETH_ADDRESS and output_token == FRXETH_ADDRESS:
frxETHMinter(swap).submit(value=amount)
elif input_token == STETH_ADDRESS and output_token == WSTETH_ADDRESS:
wstETH(swap).wrap(amount)
elif input_token == WSTETH_ADDRESS and output_token == STETH_ADDRESS:
wstETH(swap).unwrap(amount)
elif input_token == FRXETH_ADDRESS and output_token == SFRXETH_ADDRESS:
sfrxETH(swap).deposit(amount, self)
elif input_token == SFRXETH_ADDRESS and output_token == FRXETH_ADDRESS:
sfrxETH(swap).redeem(amount, self, self)
elif input_token == ETH_ADDRESS and output_token == WBETH_ADDRESS:
wBETH(swap).deposit(0xeCb456EA5365865EbAb8a2661B0c503410e9B347, value=amount)
else:
raise "Swap type 8 is only for ETH <-> WETH, ETH -> stETH or ETH -> frxETH, stETH <-> wstETH, frxETH <-> sfrxETH, ETH -> wBETH"
elif params[2] == 9:
Synthetix(swap).exchangeAtomically(self.snx_currency_keys[input_token], amount, self.snx_currency_keys[output_token], SNX_TRACKING_CODE, 0)
else:
raise "Bad swap type"
# update the amount received
if output_token == ETH_ADDRESS:
amount = self.balance
else:
amount = ERC20(output_token).balanceOf(self)
# sanity check, if the routing data is incorrect we will have a 0 balance and that is bad
assert amount != 0, "Received nothing"
# check if this was the last swap
if i == 5 or _route[i*2+1] == empty(address):
break
# if there is another swap, the output token becomes the input for the next round
input_token = output_token
amount -= 1 # Change non-zero -> non-zero costs less gas than zero -> non-zero
assert amount >= _expected, "Slippage"
# transfer the final token to the receiver
if output_token == ETH_ADDRESS:
raw_call(_receiver, b"", value=amount)
else:
assert ERC20(output_token).transfer(_receiver, amount, default_return_value=True)
log Exchange(msg.sender, _receiver, _route, _swap_params, _pools, _amount, amount)
return amount
```
```shell
>>> Router.get_dy([
'0x34635280737b5BFe6c7DC2FC3065D60d66e78185' # cvxPRISMA
'0x3b21C2868B6028CfB38Ff86127eF22E68d16d53B' # cvxPRISMA/PRISMA pool
'0xdA47862a83dac0c112BA89c6abC2159b95afd71C' # PRISMA
'0x322135Dd9cBAE8Afa84727d9aE1434b5B3EBA44B' # PRISMA/ETH pool
'0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' # ETH
'0x0000000000000000000000000000000000000000'
'0x0000000000000000000000000000000000000000'
'0x0000000000000000000000000000000000000000'
'0x0000000000000000000000000000000000000000'
'0x0000000000000000000000000000000000000000'
'0x0000000000000000000000000000000000000000'],
[[1, 0, 1, 1, 2], # first swap: cvxPRISMA <> PRISMA
[1, 0, 1, 2, 2], # second swap: PRISMA <> ETH
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]],
2697000000000000000000, # _amount
0, # _expected
[0x3b21C2868B6028CfB38Ff86127eF22E68d16d53B, # cvxPRISMA/PRISMA pool
0x322135Dd9cBAE8Afa84727d9aE1434b5B3EBA44B, # PRISMA/ETH pool
0x0000000000000000000000000000000000000000,
0x0000000000000000000000000000000000000000,
0x0000000000000000000000000000000000000000])
393335776549796040 # final output
```
::::
---
## Helper Functions
There are two function to estimate input and output token amounts:
- `get_dy` to estimate the amount of output tokens when exchanging a certain amount of input token
- `get_dx` to estimate the amount of input tokens when exchanging for a certain amount of output tokens
### `get_dy`
::::description[`Router.get_dy(_route: address[11], _swap_params: uint256[5][5], _amount: uint256, _pools: address[5] = empty(address[5])) -> uint256:`]
:::notebook[Jupyter Notebook]
An easy-to-follow Jupyter Notebook with some examples on how `get_dy` can be used can be found here: [https://try.vyperlang.org/hub/user-redirect/lab/tree/shared/mo-anon/integratooors/curve-router/get_dy.ipynb](https://try.vyperlang.org/hub/user-redirect/lab/tree/shared/mo-anon/integratooors/curve-router/get_dy.ipynb)
:::
Function to calculate the amount of final output tokens received when performing an exchange.
| Input | Type | Description |
| -------------- | ---------------- | ----------- |
| `_route` | `address[11]` | Route data: [see here](#_route) |
| `_swap_params` | `uint256[5][5]` | Swap parameters: [see here](#_swap_params) |
| `_amount` | `uint256` | The amount of input token (`_route[0]`) to be sent. |
| `_pools` | `address[5]` | Array of pools for swaps via zap contracts. This parameter defaults to an empty array and is only needed for `swap_type = 3`. |
Returns: expected amount of final output token (`uint256`).
```vyper
event Exchange:
sender: indexed(address)
receiver: indexed(address)
route: address[11]
swap_params: uint256[5][5]
pools: address[5]
in_amount: uint256
out_amount: uint256
ETH_ADDRESS: constant(address) = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
STETH_ADDRESS: constant(address) = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84
WSTETH_ADDRESS: constant(address) = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0
FRXETH_ADDRESS: constant(address) = 0x5E8422345238F34275888049021821E8E08CAa1f
SFRXETH_ADDRESS: constant(address) = 0xac3E018457B222d93114458476f3E3416Abbe38F
WBETH_ADDRESS: constant(address) = 0xa2E3356610840701BDf5611a53974510Ae27E2e1
WETH_ADDRESS: immutable(address)
# SNX
# https://github.com/Synthetixio/synthetix-docs/blob/master/content/addresses.md
SNX_ADDRESS_RESOLVER: constant(address) = 0x823bE81bbF96BEc0e25CA13170F5AaCb5B79ba83
SNX_TRACKING_CODE: constant(bytes32) = 0x4355525645000000000000000000000000000000000000000000000000000000 # CURVE
SNX_EXCHANGER_NAME: constant(bytes32) = 0x45786368616E6765720000000000000000000000000000000000000000000000 # Exchanger
snx_currency_keys: HashMap[address, bytes32]
@view
@external
def get_dy(
_route: address[11],
_swap_params: uint256[5][5],
_amount: uint256,
_pools: address[5]=empty(address[5])
) -> uint256:
"""
@notice Get amount of the final output token received in an exchange
@dev Routing and swap params must be determined off-chain. This
functionality is designed for gas efficiency over ease-of-use.
@param _route Array of [initial token, pool or zap, token, pool or zap, token, ...]
The array is iterated until a pool address of 0x00, then the last
given token is transferred to `_receiver`
@param _swap_params Multidimensional array of [i, j, swap type, pool_type, n_coins] where
i is the index of input token
j is the index of output token
The swap_type should be:
1. for `exchange`,
2. for `exchange_underlying`,
3. for underlying exchange via zap: factory stable metapools with lending base pool `exchange_underlying`
and factory crypto-meta pools underlying exchange (`exchange` method in zap)
4. for coin -> LP token "exchange" (actually `add_liquidity`),
5. for lending pool underlying coin -> LP token "exchange" (actually `add_liquidity`),
6. for LP token -> coin "exchange" (actually `remove_liquidity_one_coin`)
7. for LP token -> lending or fake pool underlying coin "exchange" (actually `remove_liquidity_one_coin`)
8. for ETH <-> WETH, ETH -> stETH or ETH -> frxETH, stETH <-> wstETH, frxETH <-> sfrxETH, ETH -> wBETH
9. for SNX swaps (sUSD, sEUR, sETH, sBTC)
pool_type: 1 - stable, 2 - crypto, 3 - tricrypto, 4 - llamma
n_coins is the number of coins in pool
@param _amount The amount of input token (`_route[0]`) to be sent.
@param _pools Array of pools for swaps via zap contracts. This parameter is needed only for swap_type = 3.
@return Expected amount of the final output token.
"""
input_token: address = _route[0]
output_token: address = empty(address)
amount: uint256 = _amount
for i in range(1, 6):
# 5 rounds of iteration to perform up to 5 swaps
swap: address = _route[i*2-1]
pool: address = _pools[i-1] # Only for Polygon meta-factories underlying swap (swap_type == 4)
output_token = _route[i * 2]
params: uint256[5] = _swap_params[i-1] # i, j, swap_type, pool_type, n_coins
# Calc output amount according to the swap type
if params[2] == 1:
if params[3] == 1: # stable
amount = StablePool(swap).get_dy(convert(params[0], int128), convert(params[1], int128), amount)
else: # crypto or llamma
amount = CryptoPool(swap).get_dy(params[0], params[1], amount)
elif params[2] == 2:
if params[3] == 1: # stable
amount = StablePool(swap).get_dy_underlying(convert(params[0], int128), convert(params[1], int128), amount)
else: # crypto
amount = CryptoPool(swap).get_dy_underlying(params[0], params[1], amount)
elif params[2] == 3: # SWAP IS ZAP HERE !!!
if params[3] == 1: # stable
amount = StablePool(pool).get_dy_underlying(convert(params[0], int128), convert(params[1], int128), amount)
else: # crypto
amount = CryptoMetaZap(swap).get_dy(pool, params[0], params[1], amount)
elif params[2] in [4, 5]:
if params[3] == 1: # stable
amounts: uint256[10] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
amounts[params[0]] = amount
amount = STABLE_CALC.calc_token_amount(swap, output_token, amounts, params[4], True, True)
else:
# Tricrypto pools have stablepool interface for calc_token_amount
if params[4] == 2:
amounts: uint256[2] = [0, 0]
amounts[params[0]] = amount
if params[3] == 2: # crypto
amount = CryptoPool2Coins(swap).calc_token_amount(amounts)
else: # tricrypto
amount = StablePool2Coins(swap).calc_token_amount(amounts, True)
elif params[4] == 3:
amounts: uint256[3] = [0, 0, 0]
amounts[params[0]] = amount
if params[3] == 2: # crypto
amount = CryptoPool3Coins(swap).calc_token_amount(amounts)
else: # tricrypto
amount = StablePool3Coins(swap).calc_token_amount(amounts, True)
elif params[4] == 4:
amounts: uint256[4] = [0, 0, 0, 0]
amounts[params[0]] = amount
if params[3] == 2: # crypto
amount = CryptoPool4Coins(swap).calc_token_amount(amounts)
else: # tricrypto
amount = StablePool4Coins(swap).calc_token_amount(amounts, True)
elif params[4] == 5:
amounts: uint256[5] = [0, 0, 0, 0, 0]
amounts[params[0]] = amount
if params[3] == 2: # crypto
amount = CryptoPool5Coins(swap).calc_token_amount(amounts)
else: # tricrypto
amount = StablePool5Coins(swap).calc_token_amount(amounts, True)
elif params[2] in [6, 7]:
if params[3] == 1: # stable
amount = StablePool(swap).calc_withdraw_one_coin(amount, convert(params[1], int128))
else: # crypto
amount = CryptoPool(swap).calc_withdraw_one_coin(amount, params[1])
elif params[2] == 8:
if input_token == WETH_ADDRESS or output_token == WETH_ADDRESS or \
(input_token == ETH_ADDRESS and output_token == STETH_ADDRESS) or \
(input_token == ETH_ADDRESS and output_token == FRXETH_ADDRESS):
# ETH <--> WETH rate is 1:1
# ETH ---> stETH rate is 1:1
# ETH ---> frxETH rate is 1:1
pass
elif input_token == WSTETH_ADDRESS:
amount = wstETH(swap).getStETHByWstETH(amount)
elif output_token == WSTETH_ADDRESS:
amount = wstETH(swap).getWstETHByStETH(amount)
elif input_token == SFRXETH_ADDRESS:
amount = sfrxETH(swap).convertToAssets(amount)
elif output_token == SFRXETH_ADDRESS:
amount = sfrxETH(swap).convertToShares(amount)
elif output_token == WBETH_ADDRESS:
amount = amount * 10**18 / wBETH(swap).exchangeRate()
else:
raise "Swap type 8 is only for ETH <-> WETH, ETH -> stETH or ETH -> frxETH, stETH <-> wstETH, frxETH <-> sfrxETH, ETH -> wBETH"
elif params[2] == 9:
snx_exchanger: address = SynthetixAddressResolver(SNX_ADDRESS_RESOLVER).getAddress(SNX_EXCHANGER_NAME)
atomic_amount_and_fee: AtomicAmountAndFee = SynthetixExchanger(snx_exchanger).getAmountsForAtomicExchange(
amount, self.snx_currency_keys[input_token], self.snx_currency_keys[output_token]
)
amount = atomic_amount_and_fee.amountReceived
else:
raise "Bad swap type"
# check if this was the last swap
if i == 5 or _route[i*2+1] == empty(address):
break
# if there is another swap, the output token becomes the input for the next round
input_token = output_token
return amount - 1
```
```shell
>>> Router.get_dy(
['0x34635280737b5BFe6c7DC2FC3065D60d66e78185', # crxPRISMA
'0x3b21C2868B6028CfB38Ff86127eF22E68d16d53B', # cvxPRISMA/PRISMA pool
'0xdA47862a83dac0c112BA89c6abC2159b95afd71C', # PRISMA
'0x322135Dd9cBAE8Afa84727d9aE1434b5B3EBA44B', # PRISMA/ETH pool
'0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', # ETH
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000'],
[[1, 0, 1, 1, 2], # first swap: cvxPRISMA <> PRISMA
[1, 0, 1, 2, 2], # second swap: PRISMA <> ETH
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]],
100000000000000000000, # _amount
['0x3b21C2868B6028CfB38Ff86127eF22E68d16d53B', # cvxPRISMA/PRISMA pool
'0x322135Dd9cBAE8Afa84727d9aE1434b5B3EBA44B', # PRISMA/ETH pool
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000'])
18597416260226417 # expected output
```
::::
### `get_dx`
::::description[`Router.get_dx(_route: address[11], _swap_params: uint256[5][5], _out_amount: uint256, _pools: address[5], _base_pools: address[5]=empty(address[5]), _base_tokens: address[5] = empty(address[5])) -> uint256:`]
:::notebook[Jupyter Notebook]
An easy-to-follow Jupyter Notebook with some examples on how `get_dx` can be used can be found here: [https://try.vyperlang.org/hub/user-redirect/lab/tree/shared/mo-anon/integratooors/curve-router/get_dx.ipynb](https://try.vyperlang.org/hub/user-redirect/lab/tree/shared/mo-anon/integratooors/curve-router/get_dx.ipynb).
:::
Function to calculate the amount of input tokens required to receive the desired amount of output tokens.
| Input | Type | Description |
| -------------- | --------------- | ----------- |
| `_route` | `address[11]` | Route data: [see here](#_route) |
| `_swap_params` | `uint256[5][5]` | Swap parameters: [see here](#_swap_params) |
| `_out_amount` | `uint256` | The desired amount of output coin to receive. |
| `_pools` | `address[5]` | Array of pools. |
| `_base_pools` | `address[5]` | Array of base pools (for meta pools). Defaults to an empty array. |
| `_base_tokens` | `address[5]` | Array of base LP tokens (for meta pools). Should be a zap address for double meta pools. Defaults to an empty array. |
Returns: required amount of input token (`uint256`).
```vyper
event Exchange:
sender: indexed(address)
receiver: indexed(address)
route: address[11]
swap_params: uint256[5][5]
pools: address[5]
in_amount: uint256
out_amount: uint256
ETH_ADDRESS: constant(address) = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
STETH_ADDRESS: constant(address) = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84
WSTETH_ADDRESS: constant(address) = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0
FRXETH_ADDRESS: constant(address) = 0x5E8422345238F34275888049021821E8E08CAa1f
SFRXETH_ADDRESS: constant(address) = 0xac3E018457B222d93114458476f3E3416Abbe38F
WBETH_ADDRESS: constant(address) = 0xa2E3356610840701BDf5611a53974510Ae27E2e1
WETH_ADDRESS: immutable(address)
# SNX
# https://github.com/Synthetixio/synthetix-docs/blob/master/content/addresses.md
SNX_ADDRESS_RESOLVER: constant(address) = 0x823bE81bbF96BEc0e25CA13170F5AaCb5B79ba83
SNX_TRACKING_CODE: constant(bytes32) = 0x4355525645000000000000000000000000000000000000000000000000000000 # CURVE
SNX_EXCHANGER_NAME: constant(bytes32) = 0x45786368616E6765720000000000000000000000000000000000000000000000 # Exchanger
snx_currency_keys: HashMap[address, bytes32]
@view
@external
def get_dx(
_route: address[11],
_swap_params: uint256[5][5],
_out_amount: uint256,
_pools: address[5],
_base_pools: address[5]=empty(address[5]),
_base_tokens: address[5]=empty(address[5]),
) -> uint256:
"""
@notice Calculate the input amount required to receive the desired output amount
@dev Routing and swap params must be determined off-chain. This
functionality is designed for gas efficiency over ease-of-use.
@param _route Array of [initial token, pool or zap, token, pool or zap, token, ...]
The array is iterated until a pool address of 0x00, then the last
given token is transferred to `_receiver`
@param _swap_params Multidimensional array of [i, j, swap type, pool_type, n_coins] where
i is the index of input token
j is the index of output token
The swap_type should be:
1. for `exchange`,
2. for `exchange_underlying`,
3. for underlying exchange via zap: factory stable metapools with lending base pool `exchange_underlying`
and factory crypto-meta pools underlying exchange (`exchange` method in zap)
4. for coin -> LP token "exchange" (actually `add_liquidity`),
5. for lending pool underlying coin -> LP token "exchange" (actually `add_liquidity`),
6. for LP token -> coin "exchange" (actually `remove_liquidity_one_coin`)
7. for LP token -> lending or fake pool underlying coin "exchange" (actually `remove_liquidity_one_coin`)
8. for ETH <-> WETH, ETH -> stETH or ETH -> frxETH, stETH <-> wstETH, frxETH <-> sfrxETH, ETH -> wBETH
9. for SNX swaps (sUSD, sEUR, sETH, sBTC)
pool_type: 1 - stable, 2 - crypto, 3 - tricrypto, 4 - llamma
n_coins is the number of coins in pool
@param _out_amount The desired amount of output coin to receive.
@param _pools Array of pools.
@param _base_pools Array of base pools (for meta pools).
@param _base_tokens Array of base lp tokens (for meta pools). Should be a zap address for double meta pools.
@return Required amount of input token to send.
"""
amount: uint256 = _out_amount
for _i in range(1, 6):
# 5 rounds of iteration to perform up to 5 swaps
i: uint256 = 6 - _i
swap: address = _route[i*2-1]
if swap == empty(address):
continue
input_token: address = _route[(i - 1) * 2]
output_token: address = _route[i * 2]
pool: address = _pools[i-1]
base_pool: address = _base_pools[i-1]
base_token: address = _base_tokens[i-1]
params: uint256[5] = _swap_params[i-1] # i, j, swap_type, pool_type, n_coins
n_coins: uint256 = params[4]
# Calc a required input amount according to the swap type
if params[2] == 1:
if params[3] == 1: # stable
if base_pool == empty(address): # non-meta
amount = STABLE_CALC.get_dx(pool, convert(params[0], int128), convert(params[1], int128), amount, n_coins)
else:
amount = STABLE_CALC.get_dx_meta(pool, convert(params[0], int128), convert(params[1], int128), amount, n_coins, base_pool)
elif params[3] in [2, 3]: # crypto or tricrypto
amount = CRYPTO_CALC.get_dx(pool, params[0], params[1], amount, n_coins)
else: # llamma
amount = Llamma(pool).get_dx(params[0], params[1], amount)
elif params[2] in [2, 3]:
if params[3] == 1: # stable
if base_pool == empty(address): # non-meta
amount = STABLE_CALC.get_dx_underlying(pool, convert(params[0], int128), convert(params[1], int128), amount, n_coins)
else:
amount = STABLE_CALC.get_dx_meta_underlying(pool, convert(params[0], int128), convert(params[1], int128), amount, n_coins, base_pool, base_token)
else: # crypto
amount = CRYPTO_CALC.get_dx_meta_underlying(pool, params[0], params[1], amount, n_coins, base_pool, base_token)
elif params[2] in [4, 5]:
# This is not correct. Should be something like calc_add_one_coin. But tests say that it's precise enough.
if params[3] == 1: # stable
amount = StablePool(swap).calc_withdraw_one_coin(amount, convert(params[0], int128))
else: # crypto
amount = CryptoPool(swap).calc_withdraw_one_coin(amount, params[0])
elif params[2] in [6, 7]:
if params[3] == 1: # stable
amounts: uint256[10] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
amounts[params[1]] = amount
amount = STABLE_CALC.calc_token_amount(swap, input_token, amounts, n_coins, False, True)
else:
# Tricrypto pools have stablepool interface for calc_token_amount
if n_coins == 2:
amounts: uint256[2] = [0, 0]
amounts[params[1]] = amount
if params[3] == 2: # crypto
amount = CryptoPool2Coins(swap).calc_token_amount(amounts) # This is not correct
else: # tricrypto
amount = StablePool2Coins(swap).calc_token_amount(amounts, False)
elif n_coins == 3:
amounts: uint256[3] = [0, 0, 0]
amounts[params[1]] = amount
if params[3] == 2: # crypto
amount = CryptoPool3Coins(swap).calc_token_amount(amounts) # This is not correct
else: # tricrypto
amount = StablePool3Coins(swap).calc_token_amount(amounts, False)
elif n_coins == 4:
amounts: uint256[4] = [0, 0, 0, 0]
amounts[params[1]] = amount
if params[3] == 2: # crypto
amount = CryptoPool4Coins(swap).calc_token_amount(amounts) # This is not correct
else: # tricrypto
amount = StablePool4Coins(swap).calc_token_amount(amounts, False)
elif n_coins == 5:
amounts: uint256[5] = [0, 0, 0, 0, 0]
amounts[params[1]] = amount
if params[3] == 2: # crypto
amount = CryptoPool5Coins(swap).calc_token_amount(amounts) # This is not correct
else: # tricrypto
amount = StablePool5Coins(swap).calc_token_amount(amounts, False)
elif params[2] == 8:
if input_token == WETH_ADDRESS or output_token == WETH_ADDRESS or \
(input_token == ETH_ADDRESS and output_token == STETH_ADDRESS) or \
(input_token == ETH_ADDRESS and output_token == FRXETH_ADDRESS):
# ETH <--> WETH rate is 1:1
# ETH ---> stETH rate is 1:1
# ETH ---> frxETH rate is 1:1
pass
elif input_token == WSTETH_ADDRESS:
amount = wstETH(swap).getWstETHByStETH(amount)
elif output_token == WSTETH_ADDRESS:
amount = wstETH(swap).getStETHByWstETH(amount)
elif input_token == SFRXETH_ADDRESS:
amount = sfrxETH(swap).convertToShares(amount)
elif output_token == SFRXETH_ADDRESS:
amount = sfrxETH(swap).convertToAssets(amount)
elif output_token == WBETH_ADDRESS:
amount = amount * wBETH(swap).exchangeRate() / 10**18
else:
raise "Swap type 8 is only for ETH <-> WETH, ETH -> stETH or ETH -> frxETH, stETH <-> wstETH, frxETH <-> sfrxETH, ETH -> wBETH"
elif params[2] == 9:
snx_exchanger: address = SynthetixAddressResolver(SNX_ADDRESS_RESOLVER).getAddress(SNX_EXCHANGER_NAME)
atomic_amount_and_fee: AtomicAmountAndFee = SynthetixExchanger(snx_exchanger).getAmountsForAtomicExchange(
10**18, self.snx_currency_keys[input_token], self.snx_currency_keys[output_token]
)
amount = amount * 10**18 / atomic_amount_and_fee.amountReceived
else:
raise "Bad swap type"
return amount
```
```shell
>>> Router.get_dx(
['0x34635280737b5BFe6c7DC2FC3065D60d66e78185', # crxPRISMA
'0x3b21C2868B6028CfB38Ff86127eF22E68d16d53B', # cvxPRISMA/PRISMA pool
'0xdA47862a83dac0c112BA89c6abC2159b95afd71C', # PRISMA
'0x322135Dd9cBAE8Afa84727d9aE1434b5B3EBA44B', # PRISMA/ETH pool
'0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', # ETH
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000'],
[[1, 0, 1, 1, 2], # first swap: cvxPRISMA <> PRISMA
[1, 0, 1, 2, 2], # second swap: PRISMA <> ETH
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]],
100000000000000000000, # _out_amount
['0x3b21C2868B6028CfB38Ff86127eF22E68d16d53B', # cvxPRISMA/PRISMA pool
'0x322135Dd9cBAE8Afa84727d9aE1434b5B3EBA44B', # PRISMA/ETH pool
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000'])
18597416260226417 # expected input
```
::::
---
## Plain and Meta-Pool Implementation with Custom Admin Controls
This implementation enables **arbitrary assignment of an `admin` and `admin_fee`**for a specific pool. The `Factory.admin()` is, and will always remain, one of the pool’s owners—ensuring full control via the DAO.
:::vyper[Contract Implementations]
The source code for the implementation can be found on GitHub in the [admin-implementation branch](https://github.com/curvefi/stableswap-ng/tree/admin-implementation). This implementation is initially used by CrossCurve on Sonic.
The implementations were added to the StableSwapFactory on Sonic via [Vote ID #1010](https://curve.fi/dao/ethereum/proposals/1010-OWNERSHIP/) and are deployed at `index = 710420`.
```py
>>> Factory.metapool_implementations(710420)
'0x8663426e8713922D81e44d73295759e74Afc230F'
>>> Factory.pool_implementations(710420)
'0xA7c2DD4356168153792EF05D27922064b3c71A26'
```
:::
The implementation introduces the ability to designate an additional admin with the following permissions:
- The admin has the authority to set the contract’s admin fee, which can range from 0% to 100%. Previously, this fee was hardcoded at 50%.
- Once an `admin` is set, their address replaces the `fee_receiver` and starts receiving all pool admin fees. If no admin is set, the `fee_receiver` remains the default recipient.
At contract initialization, the admin is set to `ZERO_ADDRESS`. This means assigning a new admin requires a DAO vote via the `set_admin` function. The `admin_fee` defaults to 50% but can be updated later.
---
### `admin`
::::description[`CurveStableSwap.admin() -> address: view`]
Getter for the admin of the contract. At contract initialization, the address of the admin is always set to `ZERO_ADDRESS`.
Returns: admin of the contract (`address`).
```vyper
@external
def __init__(
...
):
...
self.admin = empty(address)
...
admin: public(address)
```
This example returns the current admin of the contract.
```shell
>>> CurveStableSwap.admin()
'0x0000000000000000000000000000000000000000'
```
::::
### `set_admin`
::::description[`CurveStableSwap.set_admin(_new_admin: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the `Factory`.
:::
Function to set the admin of the contract. Setting the admin will revert if its not called by the admin of the `Factory`.
Emits: `SetAdmin` event.
| Input | Type | Description |
| ------------ | --------- | ------------------ |
| `_new_admin` | `address` | New admin address. |
```vyper
interface Factory:
def fee_receiver() -> address: view
event SetAdmin:
admin: address
admin: public(address)
@external
def set_admin(_new_admin: address):
assert msg.sender == factory.admin() # dev: only owner
self.admin = _new_admin
log SetAdmin(_new_admin)
@view
@internal
def _check_admins():
assert msg.sender == factory.admin() or msg.sender == self.admin # dev: only admin
```
This example sets the admin of the contract.
```shell
>>> CurveStableSwap.admin()
'0x0000000000000000000000000000000000000000'
>>> CurveStableSwap.set_admin('0x7a16ff8270133f063aab6c9977183d9e72835428')
>>> CurveStableSwap.admin()
'0x7a16ff8270133f063aab6c9977183d9e72835428'
```
::::
---
### `admin_fee`
::::description[`CurveStableSwap.admin_fee() -> uint256: view`]
Getter for the admin fee of the pool. At contract initialization, the admin fee is set to 50%.
Returns: admin fee (`uint256`).
```vyper
@external
def __init__(
...
):
...
self.admin_fee = 5000000000
...
admin_fee: public(uint256)
```
This example returns the current admin fee of the contract.
```shell
>>> CurveStableSwap.admin()
'0x0000000000000000000000000000000000000000'
```
::::
### `set_new_admin_fee`
::::description[`CurveStableSwap.set_new_admin_fee(_new_admin_fee: uint256):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the `Pool` or the `Factory`.
:::
Function to set the admin fee of the pool.
Emits: `SetAdmin` event.
| Input | Type | Description |
| ------------ | --------- | ------------------ |
| `_new_admin_fee` | `uint256` | New admin fee value. |
```vyper
interface Factory:
def fee_receiver() -> address: view
event ApplyNewAdminFee:
admin_fee: uint256
admin_fee: public(uint256)
FEE_DENOMINATOR: constant(uint256) = 10 **10
@external
def set_new_admin_fee(_new_admin_fee: uint256):
self._check_admins()
# FEE_DENOMINATOR = 1 = 100%
assert _new_admin_fee <= FEE_DENOMINATOR # dev: more than 100%
self.admin_fee = _new_admin_fee
log ApplyNewAdminFee(_new_admin_fee)
@view
@internal
def _check_admins():
assert msg.sender == factory.admin() or msg.sender == self.admin # dev: only admin
```
This example sets the admin of the contract.
```shell
>>> StableSwapNG.admin_fee()
5000000000
>>> StableSwapNG.set_new_admin_fee(0)
>>> StableSwapNG.admin_fee()
0
```
::::
---
## Stableswap-NG: Overview
:::deploy[Contract Source & Deployment]
Source code is available on [ GitHub](https://github.com/curvefi/stableswap-ng). The following documentation covers source code up until commit number [`5f582a6`](https://github.com/curvefi/stableswap-ng/commit/5f582a6b8f709d863825c5fbe026cd3b4fa2d840).
All stableswap-ng deployments can be found in the "Deployment Addresses" section. [↗](../../deployments.md)
:::
For an in-depth understanding of the Stableswap invariant design, please refer to the official [Stableswap whitepaper](/pdf/whitepapers/whitepaper_stableswap.pdf).
A Curve pool is essentially a smart contract that implements the Stableswap invariant, housing the logic for exchanging stable tokens. While all Curve pools share this core implementation, they come in various pool flavors.
In its simplest form, a Curve pool is an implementation of the Stableswap invariant involving two or more tokens, often referred to as a 'plain pool.' Alternatively, Curve offers more complex pool variants, including pools with rebasing tokens and metapools. Metapools facilitate the exchange of one or more tokens with those from one or more underlying tokens.
**New features:**
- price and D oracles
- dynamic fees
- [**`exchange_received`**](../stableswap-ng/pools/plainpool.md#exchange_received)
- [**`get_dx`**](../stableswap-ng/pools/plainpool.md#get_dx)
---
## Supported Assets
Stableswap-NG pools support the following asset types:
| Asset Type | Description |
| :---------: | ---------------------- |
| `0` | **Standard ERC20** token with no additional features |
| `1` | **Oracle** - token with rate oracle (e.g. wstETH) |
| `2` | **Rebasing** - token with rebase (e.g. stETH) |
| `3` | **ERC4626** - token with *`convertToAssets`* method (e.g. sDAI) |
*Consequently, supported tokens include:*
- ERC20 support for return True/revert, return True/False, return None
- ERC20 tokens can have arbitrary decimals (<=18)
- ERC20 tokens that rebase (either positive or fee on transfer)
- ERC20 tokens that have a rate oracle (e.g. wstETH, cbETH, sDAI, etc.) Oracle precision *must* be 10^18
- ERC4626 tokens with arbitrary precision (<=18) of Vault token and underlying asset
:::warning[Rebasing Tokens]
Pools including rebasing tokens work a bit differently compared to others.
The internal **`_balance()`** function - which is used to calculate the coin balances within the pool - makes sure that **LP's keep all rebases**.
:::
```vyper
@view
@internal
def _balances() -> DynArray[uint256, MAX_COINS]:
"""
@notice Calculates the pool's balances _excluding_ the admin's balances.
@dev If the pool contains rebasing tokens, this method ensures LPs keep all
rebases and admin only claims swap fees. This also means that, since
admin's balances are stored in an array and not inferred from read balances,
the fees in the rebasing token that the admin collects is immune to
slashing events.
"""
result: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances_i: uint256 = 0
for i in range(MAX_COINS_128):
if i == N_COINS_128:
break
if POOL_IS_REBASING_IMPLEMENTATION:
balances_i = ERC20(coins[i]).balanceOf(self) - self.admin_balances[i]
else:
balances_i = self.stored_balances[i] - self.admin_balances[i]
result.append(balances_i)
return result
```
---
## Dynamic Fees
Stableswap-NG introduces dynamic fees. The use of the **`offpeg_fee_multiplier`** allows the system to dynamically adjust fees based on the pool's state.
The internal **`_dynamic_fee()`** function calculates the fee **based on the balances and rates** of the tokens being exchanged. If the balances of the tokens being exchanged are highly imbalanced or significantly differ from its peg, the fee is adjusted using the **`offpeg_fee_multiplier`**.
:::bug
If the formulas below do not render, please make sure to refresh the site. A solution is being worked on.
:::
*Let's define some terms and variables for clarity:*
- Let $fee$ represent the fee, as retrieved by the method **`StableSwap.fee()`**
- Let $fee_m$ denote the off-peg fee multiplier, sourced from **`StableSwap.offpeg_fee_multiplier()`**
- **`FEE_DENOMINATOR`** is a constant with a value of $10^{10}$, representing the precision of the fee
- The terms $rate_{i}$ and $balance{i}$ refer to the specific rate and balance for coin $i$, respectively, and similarly, $rate_j$ and $balance_j$ for coin $j$
- $PRECISION_{i}$ and $PRECISION_{j}$ are the precision constants for the respective coins
*Given these, we define:*
$xp_{i} = \frac{{rate_{i} \times balance_{i}}}{{PRECISION_{i}}}$
$xp_{j} = \frac{{rate_{j} \times balance_{j}}}{{PRECISION_{j}}}$
$xp_{i}$ and $xp_{j}$ are the token balances of the pool adjusted for decimals and the pool's internal rates (stored in `stored_rates`).
*And we also have:*
$xps2 = (xp_{i} + xp_{j})^2$
**The dynamic fee is calculated by the following formula:**$$\text{dynamic fee} = \frac{{fee_{m} \times fee}}{\frac{(fee_{m} - 10^{10}) \times 4 \times xp_{i} \times xp_{j}}{xps2}+ 10^{10}}$$
```vyper
A_PRECISION: constant(uint256) = 100
MAX_COINS: constant(uint256) = 8
PRECISION: constant(uint256) = 10 **18
FEE_DENOMINATOR: constant(uint256) = 10 **10
@view
@external
def dynamic_fee(i: int128, j: int128, pool:address) -> uint256:
"""
@notice Return the fee for swapping between `i` and `j`
@param i Index value for the coin to send
@param j Index value of the coin to recieve
@return Swap fee expressed as an integer with 1e10 precision
"""
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
fee: uint256 = StableSwapNG(pool).fee()
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS)
return self._dynamic_fee(xp[i], xp[j], fee, fee_multiplier)
@view
@internal
def _dynamic_fee(xpi: uint256, xpj: uint256, _fee: uint256) -> uint256:
_offpeg_fee_multiplier: uint256 = self.offpeg_fee_multiplier
if _offpeg_fee_multiplier <= FEE_DENOMINATOR:
return _fee
xps2: uint256 = (xpi + xpj) **2
return (
(_offpeg_fee_multiplier * _fee) /
((_offpeg_fee_multiplier - FEE_DENOMINATOR) * 4 * xpi * xpj / xps2 + FEE_DENOMINATOR)
)
@view
@internal
def _get_rates_balances_xp(pool: address, N_COINS: uint256) -> (
DynArray[uint256, MAX_COINS],
DynArray[uint256, MAX_COINS],
DynArray[uint256, MAX_COINS],
):
rates: DynArray[uint256, MAX_COINS] = StableSwapNG(pool).stored_rates()
balances: DynArray[uint256, MAX_COINS] = StableSwapNG(pool).get_balances()
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
for idx in range(MAX_COINS):
if idx == N_COINS:
break
xp.append(rates[idx] * balances[idx] / PRECISION)
return rates, balances, xp
```
### Interactive Graph
The embedded graph has limited features, such as the inability to modify the axis. However, by clicking the *"edit graph on desmos"* button at the bottom right, one is redirected to the main Desmos site. There, a wider range of functionalities is available, allowing for further adjustments and detailed exploration of the graph.
---
## Oracles
The new generation (NG) of stableswap introduces two new pool-built-in oracles:
- **price oracle** (spot and moving-average price)
- moving average **D oracle**
More on oracles [here](./pools/oracles.md).
---
## `exchange_received`
This new function **allows the exchange of tokens without actually transferring tokens in**, as the exchange is based on the change of the coins balances within the pool.
Users of this method are dex aggregators, arbitrageurs, or other users who **do not wish to grant approvals to the contract**. They can instead send tokens directly to the contract and call **`exchange_received()`**.
:::abstract[Article]
Explore the `exchange_received` function's role in streamlining swaps without approvals, its efficiency benefits, and security considerations in a succinct article. Learn more about this innovative feature for cost-effective, secure trading through Curve pools: [How to Do Cheaper, Approval-Free Swaps](https://blog.curvemonitor.com/posts/exchange-received/).
:::
---
**The Stableswap-NG AMM infrastructure represents a technically enhanced iteration of the previous stableswap implementation. It comprises the following key components:**
Stableswap-NG has two main implementations: [**Plain Pools**](./pools/plainpool.md) and [**Metapools**](./pools/metapool.md).
The Pool Factory is used to **permissionlessly deploy new plain and metapools**, as well as **liquidity gauges**. It also acts as a registry for finding the deployed pools and querying information about them.
Contract which provides **mathematical utilities** for the AMM blueprint contracts.
Contract targeted at **integrators**. Contains **view-only external methods** for the AMMs.
A liquidity gauge blueprint implementation which deploys a liquidity gauge of a pool on Ethereum. Gauges on sidechains must be deployed via the `RootChainGaugeFactory`.
Exponential moving-average oracles for the `D` invariant and for the prices of coins within the AMM.
---
## [bytes4 method_id][bytes8 ][bytes20 oracle]
A metapool is a pool where a stablecoin is paired against the LP token from another pool, a so-called base pool.
:::deploy[Contract Source & Deployment]
Source code available on [Github](https://github.com/curvefi/stableswap-ng/blob/bff1522b30819b7b240af17ccfb72b0effbf6c47/contracts/main/CurveStableSwapMetaNG.vy).
:::
The deployment of metapools is permissionless and can be done via the [**`deploy_metapool`**](../../factory/stableswap-ng/deployer-api.md#deploy_metapool) function within the Stableswap-NG Factory.
:::warning[Examples]
The examples following each code block of the corresponding functions provide a basic illustration of input/output values. **When using the function in production, ensure not to set `_min_dy`, `_min_amount`, etc., to zero or other arbitrary numbers**. Otherwise, MEV bots may frontrun or sandwich your transaction, leading to a potential loss of funds.
The examples are based on the USDV<>3CRV metapool: [0xc273fd237F23cb13296D0Cc2897F0B7A61e83387](https://etherscan.io/address/0xc273fd237F23cb13296D0Cc2897F0B7A61e83387#code).
:::
:::info[**Oracle Methods Documentation**]
Comprehensive documentation for Oracle Methods is available on a dedicated page, accessible [here](./oracles.md).
:::
---
*The AMM metapool contract implementation utilizes two internal functions to transfer tokens/coins in and out of the pool and then accordingly update `stored_balances`.*
*These function slightly differ to the ones from plain pools, as there needs to be some additional logic because of the basepools:*
- **`_transfer_in()`**
| Input | Type | Description |
| ------------------------- | --------- | ------------------------------------------------------------------------------------------- |
| `coin_metapool_idx` | `int128` | Metapool index of the input coin. |
| `coin_basepool_idx` | `int128` | Basepool index of the input coin. |
| `dx` | `uint256` | Amount to transfer in. |
| `sender` | `address` | Address to transfer coins from. |
| `expect_optimistic_transfer` | `bool` | `True` if the contract expects an optimistic coin transfer (see [`exchange_received()`](#exchange_received)). |
| `is_base_pool_swap` | `bool` | If the exchange is a basepool swap (if `i and i > 0`); defaulted to `False`. |
```vyper
@internal
def _transfer_in(
coin_metapool_idx: int128,
coin_basepool_idx: int128,
dx: uint256,
sender: address,
expect_optimistic_transfer: bool,
is_base_pool_swap: bool = False,
) -> uint256:
"""
@notice Contains all logic to handle ERC20 token transfers.
@param coin_metapool_idx metapool index of input coin
@param coin_basepool_idx basepool index of input coin
@param dx amount of `_coin` to transfer into the pool.
@param sender address to transfer `_coin` from.
@param expect_optimistic_transfer True if contract expects an optimistic coin transfer
@param is_base_pool_swap Default is set to False.
@return amount of coins received
"""
_input_coin: ERC20 = ERC20(coins[coin_metapool_idx])
_stored_balance: uint256 = self.stored_balances[coin_metapool_idx]
_input_coin_is_in_base_pool: bool = False
# Check if _transfer_in is being called by _exchange_underlying:
if coin_basepool_idx >= 0 and coin_metapool_idx == 1:
_input_coin = ERC20(BASE_COINS[coin_basepool_idx])
_input_coin_is_in_base_pool = True
_dx: uint256 = _input_coin.balanceOf(self)
# ------------------------- Handle Transfers -----------------------------
if expect_optimistic_transfer:
if not _input_coin_is_in_base_pool:
_dx = _dx - _stored_balance
assert _dx >= dx # dev: pool did not receive tokens for swap
else:
assert dx > 0 # dev : do not transferFrom 0 tokens into the pool
assert _input_coin.transferFrom(
sender,
self,
dx,
default_return_value=True
)
_dx = _input_coin.balanceOf(self) - _dx
# ------------ Check if liquidity needs to be added somewhere ------------
if _input_coin_is_in_base_pool:
if is_base_pool_swap:
return _dx # <----- _exchange_underlying: all input goes to swap.
# So, we will not increment self.stored_balances for metapool_idx.
# Swap involves base <> meta pool interaction. Add incoming base pool
# token to the base pool, mint _dx base pool LP token (idx 1) and add
# that to self.stored_balances and return that instead.
_dx = self._meta_add_liquidity(_dx, coin_basepool_idx)
# ----------------------- Update Stored Balances -------------------------
self.stored_balances[coin_metapool_idx] += _dx
return _dx
@internal
def _meta_add_liquidity(dx: uint256, base_i: int128) -> uint256:
coin_i: address = coins[MAX_METAPOOL_COIN_INDEX]
x: uint256 = ERC20(coin_i).balanceOf(self)
if BASE_N_COINS == 2:
base_inputs: uint256[2] = empty(uint256[2])
base_inputs[base_i] = dx
StableSwap2(BASE_POOL).add_liquidity(base_inputs, 0)
if BASE_N_COINS == 3:
base_inputs: uint256[3] = empty(uint256[3])
base_inputs[base_i] = dx
StableSwap3(BASE_POOL).add_liquidity(base_inputs, 0)
return ERC20(coin_i).balanceOf(self) - x
```
- **`_transfer_out()`**
| Input | Type | Description |
| ---------- | --------- | ----------------------------------------- |
| `coin_idx` | `int128` | Index value of the token to transfer out. |
| `_amount` | `uint256` | Amount to transfer out. |
| `receiver` | `address` | Address to send the tokens to. |
```vyper
stored_balances: DynArray[uint256, MAX_COINS]
@internal
def _transfer_out(
_coin_idx: int128, _amount: uint256, receiver: address
):
"""
@notice Transfer a single token from the pool to receiver.
@param _coin_idx Index of the token to transfer out
@param _amount Amount of token to transfer out
@param receiver Address to send the tokens to
"""
coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self)
# ------------------------- Handle Transfers -----------------------------
assert ERC20(coins[_coin_idx]).transfer(
receiver, _amount, default_return_value=True
)
# ----------------------- Update Stored Balances -------------------------
self.stored_balances[_coin_idx] = coin_balance - _amount
```
:::info[Methods with underlying coins]
In metapools, `coin[0]` is always the metapool token, and `coin[1]` is always the basepool token. When working with the basepool underlying tokens such as `exchange_underlying` and others, the coin indices of the basepool are appended after `coin[0]`, which is the metapool token.
E.g., in the USDV<>3CRV pool: `coin[0]` = USDV, `coin[1]` = DAI, `coin[2]` = USDC, and `coin[3]` = USDT.
:::
---
## Exchange Methods
*Three functions for token exchanges:*
- The regular `exchange` function.
- A novel `exchange_received` function that executes a token exchange based on the internal balances of the pool.
- Additionally, the metapool implementation includes an `exchange_underlying` function, which allows tokens to be exchanged with the underlying tokens the asset is paired against. For example, swapping USDV for USDT in the USDV<>3CRV pool.
### `exchange`
::::description[`StableSwap.exchange(i: int128, j: int128, _dx: uint256, _min_dy: uint256, _receiver: address = msg.sender) -> uint256:`]
:::warning
This exchange swaps between the metapool token and the basepool LP token. coin[0] is always the metapool token and coin[1] is the basepool LP token. To exchange the metapool token against underlying coins, one needs to use the `exchange_underlying` function.
:::
Function to exchange `_dx` amount of coin `i` for coin `j` and receive a minimum amount of `_min_dy`.
Returns: amount of output coin received (`uint256`).
Emits: `TokenExchange`
| Input | Type | Description |
| ----------- | --------- | ------------------------------------------------------ |
| `i` | `int128` | Index value of the input coin. |
| `j` | `int128` | Index value of the output coin. |
| `_dx` | `uint256` | Amount of coin `i` being exchanged. |
| `_min_dy` | `uint256` | Minimum amount of coin `j` to receive. |
| `_receiver` | `address` | Receiver of the output tokens; defaults to msg.sender. |
```vyper
event TokenExchange:
buyer: indexed(address)
sold_id: int128
tokens_sold: uint256
bought_id: int128
tokens_bought: uint256
@external
@nonreentrant('lock')
def exchange(
i: int128,
j: int128,
_dx: uint256,
_min_dy: uint256,
_receiver: address = msg.sender,
) -> uint256:
"""
@notice Perform an exchange between two coins
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index value of the coin to recieve
@param _dx Amount of `i` being exchanged
@param _min_dy Minimum amount of `j` to receive
@return Actual amount of `j` received
"""
return self._exchange(
msg.sender,
i,
j,
_dx,
_min_dy,
_receiver,
False
)
@internal
def _exchange(
sender: address,
i: int128,
j: int128,
_dx: uint256,
_min_dy: uint256,
receiver: address,
expect_optimistic_transfer: bool
) -> uint256:
assert i != j # dev: coin index out of range
assert _dx > 0 # dev: do not exchange 0 coins
rates: DynArray[uint256, MAX_COINS] = self._stored_rates()
old_balances: DynArray[uint256, MAX_COINS] = self._balances()
xp: DynArray[uint256, MAX_COINS] = self._xp_mem(rates, old_balances)
# --------------------------- Do Transfer in -----------------------------
# `dx` is whatever the pool received after ERC20 transfer:
dx: uint256 = self._transfer_in(
i,
-1, # <----- we're not handling underlying coins here.
_dx,
sender,
expect_optimistic_transfer
)
# ------------------------------- Exchange -------------------------------
# xp[i] + dx * rates[i] / PRECISION
x: uint256 = xp[i] + unsafe_div(dx * rates[i], PRECISION)
dy: uint256 = self.__exchange(x, xp, rates, i, j)
assert dy >= _min_dy, "Exchange resulted in fewer coins than expected"
# --------------------------- Do Transfer out ----------------------------
self._transfer_out(j, dy, receiver)
# ------------------------------------------------------------------------
log TokenExchange(msg.sender, i, _dx, j, dy)
return dy
@internal
def __exchange(
x: uint256,
_xp: DynArray[uint256, MAX_COINS],
rates: DynArray[uint256, MAX_COINS],
i: int128,
j: int128,
) -> uint256:
amp: uint256 = self._A()
D: uint256 = math.get_D(_xp, amp, N_COINS)
y: uint256 = math.get_y(i, j, x, _xp, amp, D, N_COINS)
dy: uint256 = _xp[j] - y - 1 # -1 just in case there were some rounding errors
dy_fee: uint256 = dy * self._dynamic_fee((_xp[i] + x) / 2, (_xp[j] + y) / 2, self.fee) / FEE_DENOMINATOR
# Convert all to real units
dy = (dy - dy_fee) * PRECISION / rates[j]
self.admin_balances[j] += (
unsafe_div(dy_fee * admin_fee, FEE_DENOMINATOR) # dy_fee * admin_fee / FEE_DENOMINATOR
) * PRECISION / rates[j]
# Calculate and store state prices:
xp: DynArray[uint256, MAX_COINS] = _xp
xp[i] = x
xp[j] = y
# D is not changed because we did not apply a fee
self.upkeep_oracles(xp, amp, D)
return dy
```
```shell
>>> StableSwap.get_balances()
[4183467888075, 2556883713184291687567176]
>>> StableSwap.exchange(0, 1, 10**6, 0)
971169724887534588
>>> StableSwap.get_balances()
[4183468888075, 2556882741963895491313748]
```
::::
### `exchange_received`
::::description[`StableSwap.exchange_received(i: int128, j: int128, _dx: uint256, _min_dy: uint256, _receiver: address) -> uint256:`]
:::danger
`exchange_received` will revert if the pool contains a rebasing asset. A pool that contains a rebasing token should have an `asset_type` of 2. If this is not the case, the pool is using an incorrect implementation, and rebases can be stolen.
:::
Function to exchange `_dx` amount of coin `i` for coin `j`, receiving a minimum amount of `_min_dy`. This is done without actually transferring the coins into the pool within the same call. The exchange is based on the change in the balance of coin `i`, eliminating the need to grant approval to the contract. This function does only swap between the token paird against the basepool and the basepool tokens. The method can not be used to exchange for underlying coins of the basepool.
**A detailed article can be found here: https://blog.curvemonitor.com/posts/exchange-received/.**Returns: amount of output coin received (`uint256`).
Emits: `TokenExchange`
| Input | Type | Description |
| ------------ | ---------- | -------------------------------------------------- |
| `i` | `int128` | Index value of input coin. |
| `j` | `int128` | Index value of output coin. |
| `_dx` | `uint256` | Amount of coin `i` being exchanged. |
| `_min_dy` | `uint256` | Minimum amount of coin `j` to receive. |
| `_receiver` | `address` | Receiver of the output tokens; defaults to `msg.sender`. |
```vyper
event TokenExchange:
buyer: indexed(address)
sold_id: int128
tokens_sold: uint256
bought_id: int128
tokens_bought: uint256
@external
@nonreentrant('lock')
def exchange_received(
i: int128,
j: int128,
_dx: uint256,
_min_dy: uint256,
_receiver: address,
) -> uint256:
"""
@notice Perform an exchange between two coins without transferring token in
@dev The contract swaps tokens based on a change in balance of coin[i]. The
dx = ERC20(coin[i]).balanceOf(self) - self.stored_balances[i]. Users of
this method are dex aggregators, arbitrageurs, or other users who do not
wish to grant approvals to the contract: they would instead send tokens
directly to the contract and call `exchange_received`.
Note: This is disabled if pool contains rebasing tokens.
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param _dx Amount of `i` being exchanged
@param _min_dy Minimum amount of `j` to receive
@return Actual amount of `j` received
"""
assert not POOL_IS_REBASING_IMPLEMENTATION # dev: exchange_received not supported if pool contains rebasing tokens
return self._exchange(
msg.sender,
i,
j,
_dx,
_min_dy,
_receiver,
True, # <--------------------------------------- swap optimistically.
)
@internal
def _exchange(
sender: address,
i: int128,
j: int128,
_dx: uint256,
_min_dy: uint256,
receiver: address,
expect_optimistic_transfer: bool
) -> uint256:
assert i != j # dev: coin index out of range
assert _dx > 0 # dev: do not exchange 0 coins
rates: DynArray[uint256, MAX_COINS] = self._stored_rates()
old_balances: DynArray[uint256, MAX_COINS] = self._balances()
xp: DynArray[uint256, MAX_COINS] = self._xp_mem(rates, old_balances)
# --------------------------- Do Transfer in -----------------------------
# `dx` is whatever the pool received after ERC20 transfer:
dx: uint256 = self._transfer_in(
i,
-1, # <----- we're not handling underlying coins here.
_dx,
sender,
expect_optimistic_transfer
)
# ------------------------------- Exchange -------------------------------
# xp[i] + dx * rates[i] / PRECISION
x: uint256 = xp[i] + unsafe_div(dx * rates[i], PRECISION)
dy: uint256 = self.__exchange(x, xp, rates, i, j)
assert dy >= _min_dy, "Exchange resulted in fewer coins than expected"
# --------------------------- Do Transfer out ----------------------------
self._transfer_out(j, dy, receiver)
# ------------------------------------------------------------------------
log TokenExchange(msg.sender, i, _dx, j, dy)
return dy
@internal
def __exchange(
x: uint256,
_xp: DynArray[uint256, MAX_COINS],
rates: DynArray[uint256, MAX_COINS],
i: int128,
j: int128,
) -> uint256:
amp: uint256 = self._A()
D: uint256 = math.get_D(_xp, amp, N_COINS)
y: uint256 = math.get_y(i, j, x, _xp, amp, D, N_COINS)
dy: uint256 = _xp[j] - y - 1 # -1 just in case there were some rounding errors
dy_fee: uint256 = dy * self._dynamic_fee((_xp[i] + x) / 2, (_xp[j] + y) / 2, self.fee) / FEE_DENOMINATOR
# Convert all to real units
dy = (dy - dy_fee) * PRECISION / rates[j]
self.admin_balances[j] += (
unsafe_div(dy_fee * admin_fee, FEE_DENOMINATOR) # dy_fee * admin_fee / FEE_DENOMINATOR
) * PRECISION / rates[j]
# Calculate and store state prices:
xp: DynArray[uint256, MAX_COINS] = _xp
xp[i] = x
xp[j] = y
# D is not changed because we did not apply a fee
self.upkeep_oracles(xp, amp, D)
return dy
```
```shell
>>> USDV.transfer("0xc273fd237F23cb13296D0Cc2897F0B7A61e83387", 10**6)
>>> StableSwap.received(0, 1, 10**6, 0)
998545692103751082
```
:::note
More information on this method [here](../overview.md#exchange_received).
:::
::::
### `exchange_underlying`
::::description[`StableSwap.exchange_underlying(i: int128, j: int128, _dx: uint256, _min_dy: uint256, _receiver: address = msg.sender) -> uint256:`]
Function to exchange `_dx` amount of the underlying coin `i` for the underlying coin `j` and receive a minimum amount of `_min_dy`. Index values are the `coins` followed by the `base_coins`, where the base pool LP token is not included as a value.
Returns: amount of output coin received (`uint256`).
Emits: `TokenExchangeUnderlying`
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | Index value of underlying input coin. |
| `j` | `int128` | Index value of underlying output coin. |
| `_dx` | `uint256` | Amount of coin `i` being exchanged. |
| `_min_dy` | `uint256` | Minimum amount of coin `j` to receive. |
| `receiver` | `address` | Receiver of the output tokens; defaults to msg.sender. |
```vyper
event TokenExchangeUnderlying:
buyer: indexed(address)
sold_id: int128
tokens_sold: uint256
bought_id: int128
tokens_bought: uint256
@external
@nonreentrant('lock')
def exchange_underlying(
i: int128,
j: int128,
_dx: uint256,
_min_dy: uint256,
_receiver: address = msg.sender,
) -> uint256:
"""
@notice Perform an exchange between two underlying coins
@param i Index value for the underlying coin to send
@param j Index value of the underlying coin to receive
@param _dx Amount of `i` being exchanged
@param _min_dy Minimum amount of `j` to receive
@param _receiver Address that receives `j`
@return Actual amount of `j` received
"""
return self._exchange_underlying(
msg.sender,
i,
j,
_dx,
_min_dy,
_receiver,
False
)
@internal
def _exchange_underlying(
sender: address,
i: int128,
j: int128,
_dx: uint256,
_min_dy: uint256,
receiver: address,
expect_optimistic_transfer: bool = False
) -> uint256:
rates: DynArray[uint256, MAX_COINS] = self._stored_rates()
old_balances: DynArray[uint256, MAX_COINS] = self._balances()
xp: DynArray[uint256, MAX_COINS] = self._xp_mem(rates, old_balances)
dy: uint256 = 0
base_i: int128 = 0
base_j: int128 = 0
meta_i: int128 = 0
meta_j: int128 = 0
x: uint256 = 0
output_coin: address = empty(address)
# ------------------------ Determine coin indices ------------------------
# Get input coin indices:
if i > 0:
base_i = i - MAX_METAPOOL_COIN_INDEX
meta_i = 1
# Get output coin and indices:
if j == 0:
output_coin = coins[0]
else:
base_j = j - MAX_METAPOOL_COIN_INDEX
meta_j = 1
output_coin = BASE_COINS[base_j]
# --------------------------- Do Transfer in -----------------------------
# If incoming coin is supposed to go to the base pool, the _transfer_in
# method will add_liquidity in the base pool and return dx_w_fee LP tokens
dx_w_fee: uint256 = self._transfer_in(
meta_i,
base_i,
_dx,
sender,
expect_optimistic_transfer,
(i > 0 and j > 0), # <--- if True: do not add liquidity to base pool.
)
# ------------------------------- Exchange -------------------------------
if i == 0 or j == 0: # meta swap
if i == 0:
# xp[i] + dx_w_fee * rates[i] / PRECISION
x = xp[i] + unsafe_div(dx_w_fee * rates[i], PRECISION)
else:
# dx_w_fee is the number of base_pool LP tokens minted after
# base_pool.add_liquidity in self._transfer_in
# dx_w_fee * rates[MAX_METAPOOL_COIN_INDEX] / PRECISION
x = unsafe_div(dx_w_fee * rates[MAX_METAPOOL_COIN_INDEX], PRECISION)
x += xp[MAX_METAPOOL_COIN_INDEX]
dy = self.__exchange(x, xp, rates, meta_i, meta_j)
# Adjust stored balances of meta-level tokens:
self.stored_balances[meta_j] -= dy
# Withdraw from the base pool if needed
if j > 0:
out_amount: uint256 = ERC20(output_coin).balanceOf(self)
StableSwap(BASE_POOL).remove_liquidity_one_coin(dy, base_j, 0)
dy = ERC20(output_coin).balanceOf(self) - out_amount
assert dy >= _min_dy
else: # base pool swap (user should swap at base pool for better gas)
dy = ERC20(output_coin).balanceOf(self)
StableSwap(BASE_POOL).exchange(base_i, base_j, dx_w_fee, _min_dy)
dy = ERC20(output_coin).balanceOf(self) - dy
# --------------------------- Do Transfer out ----------------------------
assert ERC20(output_coin).transfer(receiver, dy, default_return_value=True)
# ------------------------------------------------------------------------
log TokenExchangeUnderlying(sender, i, _dx, j, dy)
return dy
@internal
def __exchange(
x: uint256,
_xp: DynArray[uint256, MAX_COINS],
rates: DynArray[uint256, MAX_COINS],
i: int128,
j: int128,
) -> uint256:
amp: uint256 = self._A()
D: uint256 = math.get_D(_xp, amp, N_COINS)
y: uint256 = math.get_y(i, j, x, _xp, amp, D, N_COINS)
dy: uint256 = _xp[j] - y - 1 # -1 just in case there were some rounding errors
dy_fee: uint256 = dy * self._dynamic_fee((_xp[i] + x) / 2, (_xp[j] + y) / 2, self.fee) / FEE_DENOMINATOR
# Convert all to real units
dy = (dy - dy_fee) * PRECISION / rates[j]
self.admin_balances[j] += (
unsafe_div(dy_fee * admin_fee, FEE_DENOMINATOR) # dy_fee * admin_fee / FEE_DENOMINATOR
) * PRECISION / rates[j]
# Calculate and store state prices:
xp: DynArray[uint256, MAX_COINS] = _xp
xp[i] = x
xp[j] = y
# D is not changed because we did not apply a fee
self.upkeep_oracles(xp, amp, D)
return dy
```
```shell
>>> StableSwap.exchange_underlying(0, 1, 10**6, 0)
998545692103751082
>>> StableSwap.exchange_underlying(0, 2, 10**6, 0)
998565
```
::::
### `get_dy`
::::description[`StableSwap.get_dy(i: int128, j: int128, dx: uint256) -> uint256`]
Function to calculate the predicted output amount `j` to receive at the pool's current state given an input of `dx` amount of coin `i`. This is just a simple getter method; the calculation logic is within the CurveStableSwapNGViews contract. See [here](../utility-contracts/views.md#get_dy).
Returns: predicted output amount of `j` (`uint256`).
| Input | Type | Description |
| ------ | -------- | ------------------------------------------ |
| `i` | `int128` | Index value of input coin. |
| `j` | `int128` | Index value of output coin. |
| `dx` | `uint256`| Amount of input coin being exchanged. |
```vyper
interface StableSwapViews:
def get_dx(i: int128, j: int128, dy: uint256, pool: address) -> uint256: view
def get_dy(i: int128, j: int128, dx: uint256, pool: address) -> uint256: view
def get_dx_underlying(i: int128, j: int128, dy: uint256, pool: address) -> uint256: view
def get_dy_underlying(i: int128, j: int128, dx: uint256, pool: address) -> uint256: view
def dynamic_fee(i: int128, j: int128, pool: address) -> uint256: view
def calc_token_amount(
_amounts: DynArray[uint256, MAX_COINS],
_is_deposit: bool,
_pool: address
) -> uint256: view
@view
@external
def get_dy(i: int128, j: int128, dx: uint256) -> uint256:
"""
@notice Calculate the current output dy given input dx
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param dx Amount of `i` being exchanged
@return Amount of `j` predicted
"""
return StableSwapViews(factory.views_implementation()).get_dy(i, j, dx, self)
```
```vyper
@view
@external
def get_dy(i: int128, j: int128, dx: uint256, pool: address) -> uint256:
"""
@notice Calculate the current output dy given input dx
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param dx Amount of `i` being exchanged
@return Amount of `j` predicted
"""
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS)
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION
D: uint256 = self.get_D(xp, amp, N_COINS)
x: uint256 = xp[i] + (dx * rates[i] / PRECISION)
y: uint256 = self.get_y(i, j, x, xp, amp, D, N_COINS)
dy: uint256 = xp[j] - y - 1
base_fee: uint256 = StableSwapNG(pool).fee()
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
fee: uint256 = self._dynamic_fee((xp[i] + x) / 2, (xp[j] + y) / 2, base_fee, fee_multiplier) * dy / FEE_DENOMINATOR
return (dy - fee) * PRECISION / rates[j]
```
```shell
>>> StableSwap.get_dy(0, 1, 10**6)
971173697952445825
```
::::
### `get_dy_underlying`
::::description[`StableSwap.get_dy_underlying(i: int128, j: int128, dx: uint256) -> uint256:`]
Function to calculate the predicted output amount `j` to receive given an input of `dx` amount of coin `i`, including underlying coins. Index values are the `coins` followed by the `base_coins`, where the base pool LP token is not included as a value.
Returns: predicted amount of `j` (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | Index value of input coin. |
| `j` | `int128` | Index value of output coin. |
| `dx` | `uint256` | Amount of input coin being exchanged. |
```vyper
interface StableSwapViews:
def get_dx(i: int128, j: int128, dy: uint256, pool: address) -> uint256: view
def get_dy(i: int128, j: int128, dx: uint256, pool: address) -> uint256: view
def get_dx_underlying(i: int128, j: int128, dy: uint256, pool: address) -> uint256: view
def get_dy_underlying(i: int128, j: int128, dx: uint256, pool: address) -> uint256: view
def dynamic_fee(i: int128, j: int128, pool: address) -> uint256: view
def calc_token_amount(
_amounts: DynArray[uint256, MAX_COINS],
_is_deposit: bool,
_pool: address
) -> uint256: view
@view
@external
def get_dy_underlying(i: int128, j: int128, dx: uint256) -> uint256:
return StableSwapViews(factory.views_implementation()).get_dy_underlying(i, j, dx, self)
```
```vyper
@view
@external
def get_dy_underlying(
i: int128,
j: int128,
dx: uint256,
pool: address,
) -> uint256:
"""
@notice Calculate the current output dy given input dx on underlying
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param dx Amount of `i` being exchanged
@return Amount of `j` predicted
"""
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
MAX_COIN: int128 = convert(N_COINS, int128) - 1
BASE_POOL: address = StableSwapNG(pool).BASE_POOL()
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS)
x: uint256 = 0
base_i: int128 = 0
base_j: int128 = 0
meta_i: int128 = 0
meta_j: int128 = 0
if i != 0:
base_i = i - MAX_COIN
meta_i = 1
if j != 0:
base_j = j - MAX_COIN
meta_j = 1
if i == 0:
x = xp[i] + dx * rates[0] / 10**18
else:
if j == 0:
# i is from BasePool
base_n_coins: uint256 = StableSwapNG(pool).BASE_N_COINS()
x = self._base_calc_token_amount(
dx, base_i, base_n_coins, BASE_POOL, True
) * rates[1] / PRECISION
# Adding number of pool tokens
x += xp[1]
else:
# If both are from the base pool
return StableSwapNG(BASE_POOL).get_dy(base_i, base_j, dx)
# This pool is involved only when in-pool assets are used
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION
D: uint256 = self.get_D(xp, amp, N_COINS)
y: uint256 = self.get_y(meta_i, meta_j, x, xp, amp, D, N_COINS)
dy: uint256 = xp[meta_j] - y - 1
# calculate output after subtracting dynamic fee
base_fee: uint256 = StableSwapNG(pool).fee()
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
dynamic_fee: uint256 = self._dynamic_fee((xp[meta_i] + x) / 2, (xp[meta_j] + y) / 2, base_fee, fee_multiplier)
dy = (dy - dynamic_fee * dy / FEE_DENOMINATOR)
# If output is going via the metapool
if j == 0:
dy = dy * 10**18 / rates[0]
else:
# j is from BasePool
# The fee is already accounted for
dy = StableSwapNG(BASE_POOL).calc_withdraw_one_coin(dy * PRECISION / rates[1], base_j)
return dy
```
```shell
>>> StableSwap.get_dy_underlying(0, 1, 10**6)
999713269803541066
>>> StableSwap.get_dy_underlying(0, 2, 10**6)
998565
>>> StableSwap.get_dy_underlying(0, 3, 10**6)
999171
>>> StableSwap.get_dy_underlying(3, 0, 10**6)
1000587
```
::::
### `get_dx`
::::description[`StableSwap.get_dx(i: int128, j: int128, dy: uint256) -> uint256:`]
Function to calculate the predicted input amount `i` to receive `dy` of coin `j` at the pool's current state. This is just a simple getter method; the calculation logic is within the CurveStableSwapNGViews contract. See [here](../utility-contracts/views.md#get_dx).
Returns: predicted input amount of `i` (`uint256`).
| Input | Type | Description |
| ------ | -------- | ------------------------------------------ |
| `i` | `int128` | Index value of input coin. |
| `j` | `int128` | Index value of output coin. |
| `dy` | `uint256`| Amount of output coin received. |
```vyper
interface StableSwapViews:
def get_dx(i: int128, j: int128, dy: uint256, pool: address) -> uint256: view
def get_dy(i: int128, j: int128, dx: uint256, pool: address) -> uint256: view
def dynamic_fee(i: int128, j: int128, pool: address) -> uint256: view
def calc_token_amount(
_amounts: DynArray[uint256, MAX_COINS],
_is_deposit: bool,
_pool: address
) -> uint256: view
@view
@external
def get_dx(i: int128, j: int128, dy: uint256) -> uint256:
"""
@notice Calculate the current input dx given output dy
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param dy Amount of `j` being received after exchange
@return Amount of `i` predicted
"""
return StableSwapViews(factory.views_implementation()).get_dx(i, j, dy, self)
```
```vyper
@view
@external
def get_dx(i: int128, j: int128, dy: uint256, pool: address) -> uint256:
"""
@notice Calculate the current input dx given output dy
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param dy Amount of `j` being received after exchange
@return Amount of `i` predicted
"""
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
return self._get_dx(i, j, dy, pool, False, N_COINS)
@view
@internal
def _get_dx(
i: int128,
j: int128,
dy: uint256,
pool: address,
static_fee: bool,
N_COINS: uint256
) -> uint256:
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS)
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION
D: uint256 = self.get_D(xp, amp, N_COINS)
base_fee: uint256 = StableSwapNG(pool).fee()
dy_with_fee: uint256 = dy * rates[j] / PRECISION + 1
fee: uint256 = base_fee
if not static_fee:
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
fee = self._dynamic_fee(xp[i], xp[j], base_fee, fee_multiplier)
y: uint256 = xp[j] - dy_with_fee * FEE_DENOMINATOR / (FEE_DENOMINATOR - fee)
x: uint256 = self.get_y(j, i, y, xp, amp, D, N_COINS)
return (x - xp[i]) * PRECISION / rates[i]
```
```shell
>>> StableSwap.get_dx(0, 1 10**6)
971173697952445825
```
::::
### `get_dx_underlying`
::::description[`StableSwap.get_dx_underlying(i: int128, j: int128, dy: uint256) -> uint256:`]
Function to calculate the predicted input amount `i` to receive `dy` of coin `j`, including underlying coins. Index values are the `coins` followed by the `base_coins`, where the base pool LP token is not included as a value.
Returns: predicted amount of `i` (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | Index value of input coin. |
| `j` | `int128` | Index value of output coin. |
| `dy` | `uint256` | Amount of output coin received. |
```vyper
interface StableSwapViews:
def get_dx(i: int128, j: int128, dy: uint256, pool: address) -> uint256: view
def get_dy(i: int128, j: int128, dx: uint256, pool: address) -> uint256: view
def get_dx_underlying(i: int128, j: int128, dy: uint256, pool: address) -> uint256: view
def get_dy_underlying(i: int128, j: int128, dx: uint256, pool: address) -> uint256: view
def dynamic_fee(i: int128, j: int128, pool: address) -> uint256: view
def calc_token_amount(
_amounts: DynArray[uint256, MAX_COINS],
_is_deposit: bool,
_pool: address
) -> uint256: view
@view
@external
def get_dx_underlying(i: int128, j: int128, dy: uint256) -> uint256:
return StableSwapViews(factory.views_implementation()).get_dx_underlying(i, j, dy, self)
```
```vyper
@view
@external
def get_dx_underlying(
i: int128,
j: int128,
dy: uint256,
pool: address,
) -> uint256:
BASE_POOL: address = StableSwapNG(pool).BASE_POOL()
BASE_N_COINS: uint256 = StableSwapNG(pool).BASE_N_COINS()
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
base_pool_has_static_fee: bool = self._has_static_fee(BASE_POOL)
# CASE 1: Swap does not involve Metapool at all. In this case, we kindly as the user
# to use the right pool for their swaps.
if min(i, j) > 0:
raise "Not a Metapool Swap. Use Base pool."
# CASE 2:
# 1. meta token_0 of (unknown amount) > base pool lp_token
# 2. base pool lp_token > calc_withdraw_one_coin gives dy amount of (j-1)th base coin
# So, need to do the following calculations:
# 1. calc_token_amounts on base pool for depositing liquidity on (j-1)th token > lp_tokens.
# 2. get_dx on metapool for i = 0, and j = 1 (base lp token) with amt calculated in (1).
if i == 0:
# Calculate LP tokens that are burnt to receive dy amount of base_j tokens.
lp_amount_burnt: uint256 = self._base_calc_token_amount(
dy, j - 1, BASE_N_COINS, BASE_POOL, False
)
return self._get_dx(0, 1, lp_amount_burnt, pool, False, N_COINS)
# CASE 3: Swap in token i-1 from base pool and swap out dy amount of token 0 (j) from metapool.
# 1. deposit i-1 token from base pool > receive base pool lp_token
# 2. swap base pool lp token > 0th token of the metapool
# So, need to do the following calculations:
# 1. get_dx on metapool with i = 0, j = 1 > gives how many base lp tokens are required for receiving
# dy amounts of i-1 tokens from the metapool
# 2. We have number of lp tokens: how many i-1 base pool coins are needed to mint that many tokens?
# We don't have a method where user inputs lp tokens and it gives number of coins of (i-1)th token
# is needed to mint that many base_lp_tokens. Instead, we will use calc_withdraw_one_coin. That's
# close enough.
lp_amount_required: uint256 = self._get_dx(1, 0, dy, pool, False, N_COINS)
return StableSwapNG(BASE_POOL).calc_withdraw_one_coin(lp_amount_required, i-1)
@view
@internal
def _get_dx(
i: int128,
j: int128,
dy: uint256,
pool: address,
static_fee: bool,
N_COINS: uint256
) -> uint256:
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS)
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION
D: uint256 = self.get_D(xp, amp, N_COINS)
base_fee: uint256 = StableSwapNG(pool).fee()
dy_with_fee: uint256 = dy * rates[j] / PRECISION + 1
fee: uint256 = base_fee
if not static_fee:
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
fee = self._dynamic_fee(xp[i], xp[j], base_fee, fee_multiplier)
y: uint256 = xp[j] - dy_with_fee * FEE_DENOMINATOR / (FEE_DENOMINATOR - fee)
x: uint256 = self.get_y(j, i, y, xp, amp, D, N_COINS)
return (x - xp[i]) * PRECISION / rates[i]
@view
@external
def calc_withdraw_one_coin(_burn_amount: uint256, i: int128, pool: address) -> uint256:
# First, need to calculate
# * Get current D
# * Solve Eqn against y_i for D - _token_amount
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS)
D0: uint256 = self.get_D(xp, amp, N_COINS)
total_supply: uint256 = StableSwapNG(pool).totalSupply()
D1: uint256 = D0 - _burn_amount * D0 / total_supply
new_y: uint256 = self.get_y_D(amp, i, xp, D1, N_COINS)
ys: uint256 = (D0 + D1) / (2 * N_COINS)
base_fee: uint256 = StableSwapNG(pool).fee() * N_COINS / (4 * (N_COINS - 1))
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
xp_reduced: DynArray[uint256, MAX_COINS] = xp
xp_j: uint256 = 0
xavg: uint256 = 0
dynamic_fee: uint256 = 0
for j in range(MAX_COINS):
if j == N_COINS:
break
dx_expected: uint256 = 0
xp_j = xp[j]
if convert(j, int128) == i:
dx_expected = xp_j * D1 / D0 - new_y
xavg = (xp[j] + new_y) / 2
else:
dx_expected = xp_j - xp_j * D1 / D0
xavg = xp[j]
dynamic_fee = self._dynamic_fee(xavg, ys, base_fee, fee_multiplier)
xp_reduced[j] = xp_j - dynamic_fee * dx_expected / FEE_DENOMINATOR
dy: uint256 = xp_reduced[i] - self.get_y_D(amp, i, xp_reduced, D1, N_COINS)
dy = (dy - 1) * PRECISION / rates[i] # Withdraw less to account for rounding errors
return dy
```
```shell
>>> StableSwap.get_dx_underlying(0, 1, 10**6)
998546308572137376
>>> StableSwap.get_dx_underlying(0, 3, 10**6)
999171
```
::::
---
## Adding and Removing Liquidity
There are no restrictions on how liquidity can be added or removed. Liquidity can be provided or removed in any proportion. However, there are fees associated with adding and removing liquidity that depend on the balances within the pool.
### `add_liquidity`
::::description[`StableSwap.add_liquidity(_amounts: DynArray[uint256, MAX_COINS], _min_mint_amount: uint256, _receiver: address = msg.sender) -> uint256:`]
Function to add liquidity into the pool and mint a minimum of `_min_mint_amount` of the corresponding LP tokens to `_receiver`. A value for the minimum amount is used to prevent being front-run by MEV bots.
Returns: amount of LP tokens received (`uint256`).
Emits: `Transfer` and `AddLiquidity`
| Input | Type | Description |
| ------------ | ------------------------------ | -------------------------------------------------- |
| `_amounts` | `DynArray[uint256, MAX_COINS]`| List of coin amounts to deposit. |
| `_min_amount`| `uint256` | Minimum amount of LP tokens to mint. |
| `_receiver` | `address` | Receiver of the LP tokens; defaults to `msg.sender`.|
```vyper
event Transfer:
sender: indexed(address)
receiver: indexed(address)
value: uint256
event AddLiquidity:
provider: indexed(address)
token_amounts: DynArray[uint256, MAX_COINS]
fees: DynArray[uint256, MAX_COINS]
invariant: uint256
token_supply: uint256
@external
@nonreentrant('lock')
def add_liquidity(
_amounts: DynArray[uint256, MAX_COINS],
_min_mint_amount: uint256,
_receiver: address = msg.sender
) -> uint256:
"""
@notice Deposit coins into the pool
@param _amounts List of amounts of coins to deposit
@param _min_mint_amount Minimum amount of LP tokens to mint from the deposit
@param _receiver Address that owns the minted LP tokens
@return Amount of LP tokens received by depositing
"""
amp: uint256 = self._A()
old_balances: DynArray[uint256, MAX_COINS] = self._balances()
rates: DynArray[uint256, MAX_COINS] = self._stored_rates()
# Initial invariant
D0: uint256 = self.get_D_mem(rates, old_balances, amp)
total_supply: uint256 = self.total_supply
new_balances: DynArray[uint256, MAX_COINS] = old_balances
# -------------------------- Do Transfers In -----------------------------
for i in range(N_COINS_128):
if _amounts[i] > 0:
new_balances[i] += self._transfer_in(
i,
-1, # <--- we're not handling underlying coins here
_amounts[i],
msg.sender,
False, # expect_optimistic_transfer
)
else:
assert total_supply != 0 # dev: initial deposit requires all coins
# ------------------------------------------------------------------------
# Invariant after change
D1: uint256 = self.get_D_mem(rates, new_balances, amp)
assert D1 > D0
# We need to recalculate the invariant accounting for fees
# to calculate fair user's share
fees: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
mint_amount: uint256 = 0
if total_supply > 0:
ideal_balance: uint256 = 0
difference: uint256 = 0
new_balance: uint256 = 0
ys: uint256 = (D0 + D1) / N_COINS
xs: uint256 = 0
_dynamic_fee_i: uint256 = 0
# Only account for fees if we are not the first to deposit
# base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
# unsafe math is safu here:
base_fee: uint256 = unsafe_div(unsafe_mul(self.fee, N_COINS), 4)
for i in range(N_COINS_128):
ideal_balance = D1 * old_balances[i] / D0
new_balance = new_balances[i]
# unsafe math is safu here:
if ideal_balance > new_balance:
difference = unsafe_sub(ideal_balance, new_balance)
else:
difference = unsafe_sub(new_balance, ideal_balance)
# fee[i] = _dynamic_fee(i, j) * difference / FEE_DENOMINATOR
xs = unsafe_div(rates[i] * (old_balances[i] + new_balance), PRECISION)
_dynamic_fee_i = self._dynamic_fee(xs, ys, base_fee)
fees.append(
unsafe_div(
_dynamic_fee_i * difference,
FEE_DENOMINATOR
)
)
# fees[i] * admin_fee / FEE_DENOMINATOR
self.admin_balances[i] += unsafe_div(fees[i] * admin_fee, FEE_DENOMINATOR)
new_balances[i] -= fees[i]
xp: DynArray[uint256, MAX_COINS] = self._xp_mem(rates, new_balances)
D1 = math.get_D(xp, amp, N_COINS) # <------ Reuse D1 for new D value.
mint_amount = total_supply * (D1 - D0) / D0
self.upkeep_oracles(xp, amp, D1)
else:
mint_amount = D1 # Take the dust if there was any
# (re)instantiate D oracle if totalSupply is zero.
self.last_D_packed = self.pack_2(D1, D1)
assert mint_amount >= _min_mint_amount, "Slippage screwed you"
# Mint pool tokens
total_supply += mint_amount
self.balanceOf[_receiver] += mint_amount
self.total_supply = total_supply
log Transfer(empty(address), _receiver, mint_amount)
log AddLiquidity(msg.sender, _amounts, fees, D1, total_supply)
return mint_amount
```
```shell
>>> StableSwap.get_balances()
[4183467888075, 2556883713184291687567176]
>>> StableSwap.add_liquidity([10**6, 0], 0)
9992841836391963157
>>> StableSwap.get_balances()
[4183477887978, 2556883713089250297597078]
```
::::
### `remove_liquidity`
::::description[`StableSwap.remove_liquidity(_burn_amount: uint256, _min_amounts: DynArray[uint256, MAX_COINS], _receiver: address = msg.sender, _claim_admin_fees: bool = True) -> DynArray[uint256, MAX_COINS]:`]
:::info
When removing liquidity in a balanced ratio, there is no need to update the price oracle, as this function does not alter the balance ratio within the pool. Calling this function only updates `D_oracle`.
The calculation of `D` does not use Newton methods, ensuring that `remove_liquidity` should always work, even if the pool gets borked.
:::
Function to remove `_min_amount` coins from the liquidity pool based on the pools current ratios by burning `_burn_amount` of LP tokens. Admin fees might be claimed after liquidity is removed.
Returns: amount of coins withdrawn (`DynArray[uint256, MAX_COINS]`).
Emits: `RemoveLiquidity`
| Input | Type | Description |
| ------------------ | ------------------------------ | -------------------------------------------------- |
| `_burn_amount` | `uint256` | Amount of LP tokens to be burned. |
| `_min_amounts` | `DynArray[uint256, MAX_COINS]` | Minimum amounts of coins to receive. |
| `_receiver` | `address` | Receiver of the coins; defaults to `msg.sender`. |
| `_claim_admin_fees`| `bool` | If admin fees should be claimed; defaults to `true`.|
```vyper
event RemoveLiquidity:
provider: indexed(address)
token_amounts: DynArray[uint256, MAX_COINS]
fees: DynArray[uint256, MAX_COINS]
token_supply: uint256
@external
@nonreentrant('lock')
def remove_liquidity(
_burn_amount: uint256,
_min_amounts: DynArray[uint256, MAX_COINS],
_receiver: address = msg.sender,
_claim_admin_fees: bool = True,
) -> DynArray[uint256, MAX_COINS]:
"""
@notice Withdraw coins from the pool
@dev Withdrawal amounts are based on current deposit ratios
@param _burn_amount Quantity of LP tokens to burn in the withdrawal
@param _min_amounts Minimum amounts of underlying coins to receive
@param _receiver Address that receives the withdrawn coins
@return List of amounts of coins that were withdrawn
"""
total_supply: uint256 = self.total_supply
assert _burn_amount > 0 # dev: invalid _burn_amount
amounts: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = self._balances()
value: uint256 = 0
for i in range(N_COINS_128):
value = unsafe_div(balances[i] * _burn_amount, total_supply)
assert value >= _min_amounts[i], "Withdrawal resulted in fewer coins than expected"
amounts.append(value)
self._transfer_out(i, value, _receiver)
self._burnFrom(msg.sender, _burn_amount) # dev: insufficient funds
# --------------------------- Upkeep D_oracle ----------------------------
ma_last_time_unpacked: uint256[2] = self.unpack_2(self.ma_last_time)
last_D_packed_current: uint256 = self.last_D_packed
old_D: uint256 = last_D_packed_current & (2**128 - 1)
self.last_D_packed = self.pack_2(
old_D - unsafe_div(old_D * _burn_amount, total_supply), # new_D = proportionally reduce D.
self._calc_moving_average(
last_D_packed_current,
self.D_ma_time,
ma_last_time_unpacked[1]
)
)
if ma_last_time_unpacked[1] < block.timestamp:
ma_last_time_unpacked[1] = block.timestamp
self.ma_last_time = self.pack_2(ma_last_time_unpacked[0], ma_last_time_unpacked[1])
# ------------------------------- Log event ------------------------------
log RemoveLiquidity(
msg.sender,
amounts,
empty(DynArray[uint256, MAX_COINS]),
total_supply - _burn_amount
)
# ------- Withdraw admin fees if _claim_admin_fees is set to True --------
if _claim_admin_fees:
self._withdraw_admin_fees()
return amounts
```
```shell
>>> StableSwap.get_balances()
[4183477887978, 2556883713089250297628878]
>>> StableSwap.remove_liquidity(10**18, [0, 0])
614190, 375384835097322069
>>> StableSwap.get_balances()
[4183477273788, 2556883337704415200306809]
```
::::
### `remove_liquidity_one_coin`
::::description[`StableSwap.remove_liquidity_one_coin(_burn_amount: uint256, i: int128, _min_received: uint256, _receiver: address = msg.sender) -> uint256:`]
Function to remove a minimum of `_min_received` of coin `i` by burning `_burn_amount` of LP tokens.
Returns: coins received (`uint256`).
Emits: `RemoveLiquidityOne`
| Input | Type | Description |
| --------------- | ---------- | -------------------------------------------------- |
| `_burn_amount` | `uint256` | Amount of LP tokens to burn/withdraw. |
| `i` | `int128` | Index value of the coin to withdraw. |
| `_min_received`| `uint256` | Minimum amount of coin to receive. |
| `_receiver` | `address` | Receiver of the coins; defaults to `msg.sender`. |
```vyper
event RemoveLiquidityOne:
provider: indexed(address)
token_id: int128
token_amount: uint256
coin_amount: uint256
token_supply: uint256
@external
@nonreentrant('lock')
def remove_liquidity_one_coin(
_burn_amount: uint256,
i: int128,
_min_received: uint256,
_receiver: address = msg.sender,
) -> uint256:
"""
@notice Withdraw a single coin from the pool
@param _burn_amount Amount of LP tokens to burn in the withdrawal
@param i Index value of the coin to withdraw
@param _min_received Minimum amount of coin to receive
@param _receiver Address that receives the withdrawn coins
@return Amount of coin received
"""
assert _burn_amount > 0 # dev: do not remove 0 LP tokens
dy: uint256 = 0
fee: uint256 = 0
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
amp: uint256 = empty(uint256)
D: uint256 = empty(uint256)
dy, fee, xp, amp, D = self._calc_withdraw_one_coin(_burn_amount, i)
assert dy >= _min_received, "Not enough coins removed"
# fee * admin_fee / FEE_DENOMINATOR
self.admin_balances[i] += unsafe_div(fee * admin_fee, FEE_DENOMINATOR)
self._burnFrom(msg.sender, _burn_amount)
log Transfer(msg.sender, empty(address), _burn_amount)
self._transfer_out(i, dy, _receiver)
log RemoveLiquidityOne(msg.sender, i, _burn_amount, dy, self.total_supply)
self.upkeep_oracles(xp, amp, D)
return dy
@view
@internal
def _calc_withdraw_one_coin(
_burn_amount: uint256,
i: int128
) -> (
uint256,
uint256,
DynArray[uint256, MAX_COINS],
uint256,
uint256
):
# First, need to:
# * Get current D
# * Solve Eqn against y_i for D - _token_amount
# get pool state
amp: uint256 = self._A()
rates: DynArray[uint256, MAX_COINS] = self._stored_rates()
xp: DynArray[uint256, MAX_COINS] = self._xp_mem(rates, self._balances())
D0: uint256 = math.get_D(xp, amp, N_COINS)
total_supply: uint256 = self.total_supply
D1: uint256 = D0 - _burn_amount * D0 / total_supply
new_y: uint256 = math.get_y_D(amp, i, xp, D1, N_COINS)
xp_reduced: DynArray[uint256, MAX_COINS] = xp
ys: uint256 = (D0 + D1) / (2 * N_COINS)
base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
dx_expected: uint256 = 0
xp_j: uint256 = 0
xavg: uint256 = 0
dynamic_fee: uint256 = 0
for j in range(N_COINS_128):
dx_expected = 0
xp_j = xp[j]
if j == i:
dx_expected = xp_j * D1 / D0 - new_y
xavg = (xp_j + new_y) / 2
else:
dx_expected = xp_j - xp_j * D1 / D0
xavg = xp_j
# xp_j - dynamic_fee * dx_expected / FEE_DENOMINATOR
dynamic_fee = self._dynamic_fee(xavg, ys, base_fee)
xp_reduced[j] = xp_j - unsafe_div(dynamic_fee * dx_expected, FEE_DENOMINATOR)
dy: uint256 = xp_reduced[i] - math.get_y_D(amp, i, xp_reduced, D1, N_COINS)
dy_0: uint256 = (xp[i] - new_y) * PRECISION / rates[i] # w/o fees
dy = (dy - 1) * PRECISION / rates[i] # Withdraw less to account for rounding errors
# calculate state price
xp[i] = new_y
return dy, dy_0 - dy, xp, amp, D1
```
```shell
>>> StableSwap.remove_liquidity_one_coin(10**18, 0, 0)
1000638
>>> StableSwap.remove_liquidity_one_coin(10**18, 1, 0)
971872609646322618
```
::::
### `remove_liquidity_imbalance`
::::description[`StableSwap.remove_liquidity_imbalance(_amounts: DynArray[uint256, MAX_COINS], _max_burn_amount: uint256, _receiver: address = msg.sender) -> uint256:`]
Function to burn a maximum of `_max_burn_amount` of LP tokens in order to receive `_amounts` of underlying tokens.
Returns: amount of LP tokens burned (`uint256`).
Emits: `RemoveLiquidityImbalance`
| Input | Type | Description |
| ------------------ | ------------------------------ | -------------------------------------------------- |
| `_amounts` | `DynArray[uint256, MAX_COINS]`| List of amounts of coins to withdraw. |
| `_max_burn_amount` | `uint256` | Maximum amount of LP tokens to burn. |
| `_receiver` | `address` | Receiver of the coins; defaults to `msg.sender`. |
```vyper
event RemoveLiquidityImbalance:
provider: indexed(address)
token_amounts: DynArray[uint256, MAX_COINS]
fees: DynArray[uint256, MAX_COINS]
invariant: uint256
token_supply: uint256
@external
@nonreentrant('lock')
def remove_liquidity_imbalance(
_amounts: DynArray[uint256, MAX_COINS],
_max_burn_amount: uint256,
_receiver: address = msg.sender
) -> uint256:
"""
@notice Withdraw coins from the pool in an imbalanced amount
@param _amounts List of amounts of underlying coins to withdraw
@param _max_burn_amount Maximum amount of LP token to burn in the withdrawal
@param _receiver Address that receives the withdrawn coins
@return Actual amount of the LP token burned in the withdrawal
"""
amp: uint256 = self._A()
rates: DynArray[uint256, MAX_COINS] = self._stored_rates()
old_balances: DynArray[uint256, MAX_COINS] = self._balances()
D0: uint256 = self.get_D_mem(rates, old_balances, amp)
new_balances: DynArray[uint256, MAX_COINS] = old_balances
for i in range(N_COINS_128):
if _amounts[i] != 0:
new_balances[i] -= _amounts[i]
self._transfer_out(i, _amounts[i], _receiver)
D1: uint256 = self.get_D_mem(rates, new_balances, amp)
base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
ys: uint256 = (D0 + D1) / N_COINS
fees: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
dynamic_fee: uint256 = 0
xs: uint256 = 0
ideal_balance: uint256 = 0
difference: uint256 = 0
new_balance: uint256 = 0
for i in range(N_COINS_128):
ideal_balance = D1 * old_balances[i] / D0
new_balance = new_balances[i]
if ideal_balance > new_balance:
difference = ideal_balance - new_balance
else:
difference = new_balance - ideal_balance
# base_fee * difference / FEE_DENOMINATOR
xs = unsafe_div(rates[i] * (old_balances[i] + new_balance), PRECISION)
dynamic_fee = self._dynamic_fee(xs, ys, base_fee)
fees.append(unsafe_div(dynamic_fee * difference, FEE_DENOMINATOR))
# fees[i] * admin_fee / FEE_DENOMINATOR
self.admin_balances[i] += unsafe_div(fees[i] * admin_fee, FEE_DENOMINATOR)
new_balances[i] -= fees[i]
D1 = self.get_D_mem(rates, new_balances, amp) # dev: reuse D1 for new D.
self.upkeep_oracles(self._xp_mem(rates, new_balances), amp, D1)
total_supply: uint256 = self.total_supply
burn_amount: uint256 = ((D0 - D1) * total_supply / D0) + 1
assert burn_amount > 1 # dev: zero tokens burned
assert burn_amount <= _max_burn_amount, "Slippage screwed you"
self._burnFrom(msg.sender, burn_amount)
log RemoveLiquidityImbalance(msg.sender, _amounts, fees, D1, total_supply)
return burn_amount
```
```shell
>>> StableSwap.remove_liquidity_imbalance([2000000, 10**18], 50**18)
3027537955031682593
```
:::note
This method removes liquidity in an imbalanced portion (2 USDV and one 3CRV) by burning a `burn_amount` of LP tokens (3027537955031682593).
:::
::::
### `calc_token_amount`
::::description[`StableSwap.calc_token_amount(_amounts: DynArray[uint256, MAX_COINS], _is_deposit: bool) -> uint256:`]
Function to calculate the addition or reduction of token supply from a deposit (add liquidity) or withdrawal (remove liquidity). This function does take fees into consideration.
Returns: amount of LP tokens (`uint256`).
| Input | Type | Description |
| -------------- | ------------------------------ | -------------------------------------------------- |
| `_amounts` | `DynArray[uint256, MAX_COINS]` | Amount of coins being deposited/withdrawn. |
| `_is_deposit` | `bool` | `true` = deposit, `false` = withdraw. |
```vyper
interface StableSwapViews:
def get_dx(i: int128, j: int128, dy: uint256, pool: address) -> uint256: view
def get_dy(i: int128, j: int128, dx: uint256, pool: address) -> uint256: view
def get_dx_underlying(i: int128, j: int128, dy: uint256, pool: address) -> uint256: view
def get_dy_underlying(i: int128, j: int128, dx: uint256, pool: address) -> uint256: view
def dynamic_fee(i: int128, j: int128, pool: address) -> uint256: view
def calc_token_amount(
_amounts: DynArray[uint256, MAX_COINS],
_is_deposit: bool,
_pool: address
) -> uint256: view
@view
@external
def calc_token_amount(
_amounts: DynArray[uint256, MAX_COINS],
_is_deposit: bool
) -> uint256:
"""
@notice Calculate addition or reduction in token supply from a deposit or withdrawal
@param _amounts Amount of each coin being deposited
@param _is_deposit set True for deposits, False for withdrawals
@return Expected amount of LP tokens received
"""
return StableSwapViews(factory.views_implementation()).calc_token_amount(_amounts, _is_deposit, self)
```
```vyper
@view
@external
def calc_token_amount(
_amounts: DynArray[uint256, MAX_COINS],
_is_deposit: bool,
pool: address
) -> uint256:
"""
@notice Calculate addition or reduction in token supply from a deposit or withdrawal
@param _amounts Amount of each coin being deposited
@param _is_deposit set True for deposits, False for withdrawals
@return Expected amount of LP tokens received
"""
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
old_balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, old_balances, xp = self._get_rates_balances_xp(pool, N_COINS)
# Initial invariant
D0: uint256 = self.get_D(xp, amp, N_COINS)
total_supply: uint256 = StableSwapNG(pool).totalSupply()
new_balances: DynArray[uint256, MAX_COINS] = old_balances
for i in range(MAX_COINS):
if i == N_COINS:
break
amount: uint256 = _amounts[i]
if _is_deposit:
new_balances[i] += amount
else:
new_balances[i] -= amount
# Invariant after change
for idx in range(MAX_COINS):
if idx == N_COINS:
break
xp[idx] = rates[idx] * new_balances[idx] / PRECISION
D1: uint256 = self.get_D(xp, amp, N_COINS)
# We need to recalculate the invariant accounting for fees
# to calculate fair user's share
D2: uint256 = D1
if total_supply > 0:
# Only account for fees if we are not the first to deposit
base_fee: uint256 = StableSwapNG(pool).fee() * N_COINS / (4 * (N_COINS - 1))
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
_dynamic_fee_i: uint256 = 0
xs: uint256 = 0
ys: uint256 = (D0 + D1) / N_COINS
for i in range(MAX_COINS):
if i == N_COINS:
break
ideal_balance: uint256 = D1 * old_balances[i] / D0
difference: uint256 = 0
new_balance: uint256 = new_balances[i]
if ideal_balance > new_balance:
difference = ideal_balance - new_balance
else:
difference = new_balance - ideal_balance
xs = old_balances[i] + new_balance
_dynamic_fee_i = self._dynamic_fee(xs, ys, base_fee, fee_multiplier)
new_balances[i] -= _dynamic_fee_i * difference / FEE_DENOMINATOR
for idx in range(MAX_COINS):
if idx == N_COINS:
break
xp[idx] = rates[idx] * new_balances[idx] / PRECISION
D2 = self.get_D(xp, amp, N_COINS)
else:
return D1 # Take the dust if there was any
diff: uint256 = 0
if _is_deposit:
diff = D2 - D0
else:
diff = D0 - D2
return diff * total_supply / D0
```
```shell
>>> StableSwap.calc_token_amount([10**6, 10**18], False)
2028274156743388789
```
::::
### `calc_withdraw_one_coin`
::::description[`StableSwap.calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256:`]
Function to calculate the amount of single token `i` withdrawn when burning `_burn_amount` LP tokens.
Returns: amount of tokens withdrawn (`uint256`).
| Input | Type | Description |
| --------------- | --------- | -------------------------------------------------- |
| `_burn_amount` | `uint256` | Amount of LP tokens to burn. |
| `i` | `int128` | Index value of the coin to withdraw. |
```vyper
@view
@external
def calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256:
"""
@notice Calculate the amount received when withdrawing a single coin
@param _burn_amount Amount of LP tokens to burn in the withdrawal
@param i Index value of the coin to withdraw
@return Amount of coin received
"""
return self._calc_withdraw_one_coin(_burn_amount, i)[0]
@view
@internal
def _calc_withdraw_one_coin(
_burn_amount: uint256,
i: int128
) -> (
uint256,
uint256,
DynArray[uint256, MAX_COINS],
uint256,
uint256
):
# First, need to:
# * Get current D
# * Solve Eqn against y_i for D - _token_amount
# get pool state
amp: uint256 = self._A()
rates: DynArray[uint256, MAX_COINS] = self._stored_rates()
xp: DynArray[uint256, MAX_COINS] = self._xp_mem(rates, self._balances())
D0: uint256 = math.get_D(xp, amp, N_COINS)
total_supply: uint256 = self.total_supply
D1: uint256 = D0 - _burn_amount * D0 / total_supply
new_y: uint256 = math.get_y_D(amp, i, xp, D1, N_COINS)
xp_reduced: DynArray[uint256, MAX_COINS] = xp
ys: uint256 = (D0 + D1) / (2 * N_COINS)
base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
dx_expected: uint256 = 0
xp_j: uint256 = 0
xavg: uint256 = 0
dynamic_fee: uint256 = 0
for j in range(N_COINS_128):
dx_expected = 0
xp_j = xp[j]
if j == i:
dx_expected = xp_j * D1 / D0 - new_y
xavg = (xp_j + new_y) / 2
else:
dx_expected = xp_j - xp_j * D1 / D0
xavg = xp_j
# xp_j - dynamic_fee * dx_expected / FEE_DENOMINATOR
dynamic_fee = self._dynamic_fee(xavg, ys, base_fee)
xp_reduced[j] = xp_j - unsafe_div(dynamic_fee * dx_expected, FEE_DENOMINATOR)
dy: uint256 = xp_reduced[i] - math.get_y_D(amp, i, xp_reduced, D1, N_COINS)
dy_0: uint256 = (xp[i] - new_y) * PRECISION / rates[i] # w/o fees
dy = (dy - 1) * PRECISION / rates[i] # Withdraw less to account for rounding errors
# calculate state price
xp[i] = new_y
return dy, dy_0 - dy, xp, amp, D1
```
```shell
>>> StableSwap.calc_withdraw_one_coin(10**18, 0)
1000638
>>> StableSwap.calc_withdraw_one_coin(10**18, 1)
971872610064262793
>>> StableSwap.get_balances()
[4183467888075, 2556883713184291687567176] # [coin[0], coin[1]]
```
::::
---
## Fee Methods
Stableswap-ng introduces a dynamic fee based on the imbalance of the coins within the pool and their pegs:
```vyper
offpeg_fee_multiplier: public(uint256) # * 1e10
@view
@internal
def _dynamic_fee(xpi: uint256, xpj: uint256, _fee: uint256) -> uint256:
_offpeg_fee_multiplier: uint256 = self.offpeg_fee_multiplier
if _offpeg_fee_multiplier <= FEE_DENOMINATOR:
return _fee
xps2: uint256 = (xpi + xpj) **2
return (
(_offpeg_fee_multiplier * _fee) /
((_offpeg_fee_multiplier - FEE_DENOMINATOR) * 4 * xpi * xpj / xps2 + FEE_DENOMINATOR)
)
```
More on dynamic fees [here](../overview.md#dynamic-fees).
### `fee`
::::description[`StableSwap.fee() -> uint256: view`]
Getter method for the fee of the pool. The fee is expressed as an integer with a 1e10 precision. This is the value set when initializing the contract and can be changed via [`set_new_fee`](#set_new_fee).
Returns: fee (`uint256`).
```vyper
fee: public(uint256) # fee * 1e10
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_math_implementation: address,
_base_pool: address,
_coins: DynArray[address, MAX_COINS],
_base_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
self.fee = _fee
...
```
```shell
>>> StableSwap.fee()
1000000
```
:::note
The method returns an integer with with 1e10 precision.
:::
::::
### `dynamic_fee`
::::description[`StableSwap.dynamic_fee(i: int128, j: int128) -> uint256:`]
Getter for the swap fee when exchanging between `i` and `j`. The swap fee is expressed as an integer with a 1e10 precision.
Returns: dynamic fee (`uint256`).
| Input | Type | Description |
| ------ | -------- | ------------------------------------------ |
| `i` | `int128` | Index value of input coin. |
| `j` | `int128` | Index value of output coin. |
```vyper
@view
@external
def dynamic_fee(i: int128, j: int128) -> uint256:
"""
@notice Return the fee for swapping between `i` and `j`
@param i Index value for the coin to send
@param j Index value of the coin to recieve
@return Swap fee expressed as an integer with 1e10 precision
"""
return StableSwapViews(factory.views_implementation()).dynamic_fee(i, j, self)
```
```vyper
@view
@external
def dynamic_fee(i: int128, j: int128, pool:address) -> uint256:
"""
@notice Return the fee for swapping between `i` and `j`
@param i Index value for the coin to send
@param j Index value of the coin to recieve
@return Swap fee expressed as an integer with 1e10 precision
"""
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
fee: uint256 = StableSwapNG(pool).fee()
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS)
return self._dynamic_fee(xp[i], xp[j], fee, fee_multiplier)
@view
@internal
def _dynamic_fee(xpi: uint256, xpj: uint256, _fee: uint256, _fee_multiplier: uint256) -> uint256:
if _fee_multiplier <= FEE_DENOMINATOR:
return _fee
xps2: uint256 = (xpi + xpj) **2
return (
(_fee_multiplier * _fee) /
((_fee_multiplier - FEE_DENOMINATOR) * 4 * xpi * xpj / xps2 + FEE_DENOMINATOR)
)
@view
@internal
def _get_rates_balances_xp(pool: address, N_COINS: uint256) -> (
DynArray[uint256, MAX_COINS],
DynArray[uint256, MAX_COINS],
DynArray[uint256, MAX_COINS],
):
rates: DynArray[uint256, MAX_COINS] = StableSwapNG(pool).stored_rates()
balances: DynArray[uint256, MAX_COINS] = StableSwapNG(pool).get_balances()
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
for idx in range(MAX_COINS):
if idx == N_COINS:
break
xp.append(rates[idx] * balances[idx] / PRECISION)
return rates, balances, xp
```
```shell
>>> StableSwap.dynamic_fee(0, 1)
1043403
```
:::note
The method returns an integer with with 1e10 precision.
:::
::::
### `admin_fee`
::::description[`StableSwap.admin_fee() -> uint256: view`]
Getter for the admin fee. It is a constant and is set to 50% (5000000000).
Returns: admin fee (`uint256`).
```vyper
admin_fee: public(constant(uint256)) = 5000000000
```
```shell
>>> StableSwap.admin_fee()
5000000000
```
:::note
The method returns an integer with with 1e10 precision.
:::
::::
### `offpeg_fee_multiplier`
::::description[`StableSwap.offpeg_fee_multiplier() -> uint256: view`]
Getter method for the off-peg fee multiplier. This value determines how much the fee increases when assets within the AMM depeg. This value can be changed via [`set_new_fee`](#set_new_fee).
Returns: offpeg fee multiplier (`uint256`)
```vyper
offpeg_fee_multiplier: public(uint256) # * 1e10
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_math_implementation: address,
_base_pool: address,
_coins: DynArray[address, MAX_COINS],
_base_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
self.offpeg_fee_multiplier = _offpeg_fee_multiplier
...
```
```shell
>>> StableSwap.offpeg_fee_multiplier()
50000000000
```
:::note
The method returns an integer with with 1e10 precision.
:::
::::
### `stored_rates`
::::description[`StableSwap.stored_rates() -> DynArray[uint256, MAX_COINS]:`view]
Getter for the rate multiplier of each coin.
Returns: stored rates (`DynArray[uint256, MAX_COINS]`).
:::info
If the coin has a rate oracle that has been properly initialized, this method queries that rate by static-calling an external contract.
:::
```vyper
rate_multipliers: immutable(DynArray[uint256, MAX_COINS])
# [bytes4 method_id][bytes8 ][bytes20 oracle]
oracles: DynArray[uint256, MAX_COINS]
@view
@external
def stored_rates() -> DynArray[uint256, MAX_COINS]:
return self._stored_rates()
@view
@internal
def _stored_rates() -> DynArray[uint256, MAX_COINS]:
"""
@notice Gets rate multipliers for each coin.
@dev If the coin has a rate oracle that has been properly initialised,
this method queries that rate by static-calling an external
contract.
"""
rates: DynArray[uint256, MAX_COINS] = [
rate_multipliers[0],
StableSwap(BASE_POOL).get_virtual_price()
]
oracles: DynArray[uint256, MAX_COINS] = self.oracles
if not self.oracles[0] == 0:
response: Bytes[32] = raw_call(
convert(oracles[0] % 2**160, address),
_abi_encode(oracles[0] & ORACLE_BIT_MASK),
max_outsize=32,
is_static_call=True,
)
# rates[0] * convert(response, uint256) / PRECISION
rates[0] = unsafe_div(rates[0] * convert(response, uint256), PRECISION)
return rates
```
```shell
>>> StableSwap.stored_rates()
[1000000000000000000000000000000, 1028532570390672122]
```
::::
### `admin_balances`
::::description[`StableSwap.admin_balances(arg0: uint256) -> uint256: view`]
Getter for the accumulated admin balance of the pool for a coin. These values essentially represent the claimable admin fee.
Returns: admin balances (`uint256`).
| Input | Type | Description |
| ------ | --------- | ------------------------------------------ |
| `arg0` | `uint256` | Index value of the coin. |
```vyper
admin_balances: public(DynArray[uint256, MAX_COINS])
```
```shell
>>> StableSwap.admin_balances(0)
73146476
>>> StableSwap.admin_balances(1)
129624596387098161946
```
::::
### `withdraw_admin_fees`
::::description[`StableSwap.withdraw_admin_fees():`]
Function to withdraw accumulated admin fees from the pool and send them to the `fee_receiver` set within the Factory.
```vyper
interface Factory:
def fee_receiver() -> address: view
def admin() -> address: view
def views_implementation() -> address: view
admin_balances: public(DynArray[uint256, MAX_COINS])
@external
def withdraw_admin_fees():
"""
@notice Claim admin fees. Callable by anyone.
"""
self._withdraw_admin_fees()
@internal
def _withdraw_admin_fees():
fee_receiver: address = factory.fee_receiver()
assert fee_receiver != empty(address) # dev: fee receiver not set
admin_balances: DynArray[uint256, MAX_COINS] = self.admin_balances
for i in range(N_COINS_128):
if admin_balances[i] > 0:
self._transfer_out(i, admin_balances[i], fee_receiver)
admin_balances[i] = 0
self.admin_balances = admin_balances
```
```shell
>>> StableSwap.withdraw_admin_fees()
```
::::
---
## Amplification Coefficient
The amplification coefficient **`A`**determines a pool’s tolerance for imbalance between the assets within it. A higher value means that trades will incur slippage sooner as the assets within the pool become imbalanced.
The appropriate value for A is dependent upon the type of coin being used within the pool, and is subject to optimisation. It is possible to modify the amplification coefficient for a pool via the **`ramp_A`**function. See [admin controls](#ramp_a).
When a ramping of A has been initialized, the process can be stopped by calling the function [**`stop_ramp_A()`**](#stop_ramp_a).
### `A`
::::description[`StableSwap.A() -> uint256: view`]
Getter for the amplification coefficient A.
Returns: A (`uint256`).
```vyper
A_PRECISION: constant(uint256) = 100
MAX_A: constant(uint256) = 10 **6
MAX_A_CHANGE: constant(uint256) = 10
initial_A: public(uint256)
future_A: public(uint256)
initial_A_time: public(uint256)
future_A_time: public(uint256)
@view
@external
def A() -> uint256:
return unsafe_div(self._A(), A_PRECISION)
@view
@internal
def _A() -> uint256:
"""
Handle ramping A up or down
"""
t1: uint256 = self.future_A_time
A1: uint256 = self.future_A
if block.timestamp < t1:
A0: uint256 = self.initial_A
t0: uint256 = self.initial_A_time
# Expressions in uint256 cannot have negative numbers, thus "if"
if A1 > A0:
return A0 + (A1 - A0) * (block.timestamp - t0) / (t1 - t0)
else:
return A0 - (A0 - A1) * (block.timestamp - t0) / (t1 - t0)
else: # when t1 == 0 or block.timestamp >= t1
return A1
```
```shell
>>> StableSwap.A()
500
```
:::note
The amplification coefficient is scaled by `A_precise`.
:::
::::
### `A_precise`
::::description[`StableSwap.A_precise() -> uint256: view`]
Getter for the precise A value, which is not divided by `A_precise` unlike `A`.
Returns: precise A (`uint256`).
```vyper
A_PRECISION: constant(uint256) = 100
MAX_A: constant(uint256) = 10 **6
MAX_A_CHANGE: constant(uint256) = 10
initial_A: public(uint256)
future_A: public(uint256)
initial_A_time: public(uint256)
future_A_time: public(uint256)
@view
@external
def A_precise() -> uint256:
return self._A()
```
```shell
>>> StableSwap.A_precise()
50000
```
::::
### `initial_A`
::::description[`StableSwap.initial_A() -> uint256: view`]
Getter for the initial A value. This is the A value when the ramping was initialized.
Returns: initial A (`uint256`).
```vyper
A_PRECISION: constant(uint256) = 100
MAX_A: constant(uint256) = 10 **6
MAX_A_CHANGE: constant(uint256) = 10
initial_A: public(uint256)
future_A: public(uint256)
initial_A_time: public(uint256)
future_A_time: public(uint256)
@external
def ramp_A(_future_A: uint256, _future_time: uint256):
assert msg.sender == factory.admin() # dev: only owner
assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME
assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time
_initial_A: uint256 = self._A()
_future_A_p: uint256 = _future_A * A_PRECISION
assert _future_A > 0 and _future_A < MAX_A
if _future_A_p < _initial_A:
assert _future_A_p * MAX_A_CHANGE >= _initial_A
else:
assert _future_A_p <= _initial_A * MAX_A_CHANGE
self.initial_A = _initial_A
self.future_A = _future_A_p
self.initial_A_time = block.timestamp
self.future_A_time = _future_time
log RampA(_initial_A, _future_A_p, block.timestamp, _future_time)
```
```shell
>>> StableSwap.initial_A()
500
```
::::
### `future_A`
::::description[`StableSwap.future_A() -> uint256: view`]
Getter for the future A value. This value is adjusted when ramping A.
Returns: future A (`uint256`).
```vyper
A_PRECISION: constant(uint256) = 100
MAX_A: constant(uint256) = 10 **6
MAX_A_CHANGE: constant(uint256) = 10
initial_A: public(uint256)
future_A: public(uint256)
initial_A_time: public(uint256)
future_A_time: public(uint256)
@external
def ramp_A(_future_A: uint256, _future_time: uint256):
assert msg.sender == factory.admin() # dev: only owner
assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME
assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time
_initial_A: uint256 = self._A()
_future_A_p: uint256 = _future_A * A_PRECISION
assert _future_A > 0 and _future_A < MAX_A
if _future_A_p < _initial_A:
assert _future_A_p * MAX_A_CHANGE >= _initial_A
else:
assert _future_A_p <= _initial_A * MAX_A_CHANGE
self.initial_A = _initial_A
self.future_A = _future_A_p
self.initial_A_time = block.timestamp
self.future_A_time = _future_time
log RampA(_initial_A, _future_A_p, block.timestamp, _future_time)
```
```shell
>>> StableSwap.future_A()
500
```
::::
### `initial_A_time`
::::description[`StableSwap.initial_A_time() -> uint256: view`]
Getter for the initial A time. This is the timestamp when ramping A was initialized.
Returns: initial A time (`uint256`).
```vyper
A_PRECISION: constant(uint256) = 100
MAX_A: constant(uint256) = 10 **6
MAX_A_CHANGE: constant(uint256) = 10
initial_A: public(uint256)
future_A: public(uint256)
initial_A_time: public(uint256)
future_A_time: public(uint256)
@external
def ramp_A(_future_A: uint256, _future_time: uint256):
assert msg.sender == factory.admin() # dev: only owner
assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME
assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time
_initial_A: uint256 = self._A()
_future_A_p: uint256 = _future_A * A_PRECISION
assert _future_A > 0 and _future_A < MAX_A
if _future_A_p < _initial_A:
assert _future_A_p * MAX_A_CHANGE >= _initial_A
else:
assert _future_A_p <= _initial_A * MAX_A_CHANGE
self.initial_A = _initial_A
self.future_A = _future_A_p
self.initial_A_time = block.timestamp
self.future_A_time = _future_time
log RampA(_initial_A, _future_A_p, block.timestamp, _future_time)
```
```shell
>>> StableSwap.initial_A_time()
0
```
::::
### `future_A_time`
::::description[`StableSwap.future_A_time() -> uint256: view`]
Getter for the future A time. This is the timestamp when ramping A should be finished.
Returns: future A time (`uint256`).
```vyper
A_PRECISION: constant(uint256) = 100
MAX_A: constant(uint256) = 10 **6
MAX_A_CHANGE: constant(uint256) = 10
initial_A: public(uint256)
future_A: public(uint256)
initial_A_time: public(uint256)
future_A_time: public(uint256)
@external
def ramp_A(_future_A: uint256, _future_time: uint256):
assert msg.sender == factory.admin() # dev: only owner
assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME
assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time
_initial_A: uint256 = self._A()
_future_A_p: uint256 = _future_A * A_PRECISION
assert _future_A > 0 and _future_A < MAX_A
if _future_A_p < _initial_A:
assert _future_A_p * MAX_A_CHANGE >= _initial_A
else:
assert _future_A_p <= _initial_A * MAX_A_CHANGE
self.initial_A = _initial_A
self.future_A = _future_A_p
self.initial_A_time = block.timestamp
self.future_A_time = _future_time
log RampA(_initial_A, _future_A_p, block.timestamp, _future_time)
```
```shell
>>> StableSwap.future_A_time()
0
```
::::
---
## Contract Info Methods
### `BASE_POOL`
::::description[`StableSwap.BASE_POOL() -> address: view`]
Getter for the base pool.
Returns: base pool (`address`).
```vyper
BASE_POOL: public(immutable(address))
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_math_implementation: address,
_base_pool: address,
_coins: DynArray[address, MAX_COINS],
_base_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
BASE_POOL = _base_pool
...
```
```shell
>>> StableSwap.BASE_POOL()
'0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7'
```
::::
### `BASE_N_COINS`
::::description[`StableSwap.BASE_N_COINS() -> uint256: view`]
Getter for the number of coins within the base pool.
Returns: number of coins (`uint256`).
```vyper
MAX_COINS: constant(uint256) = 8 # max coins is 8 in the factory
BASE_N_COINS: public(immutable(uint256))
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_math_implementation: address,
_base_pool: address,
_coins: DynArray[address, MAX_COINS],
_base_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
BASE_N_COINS = len(_base_coins)
...
```
```shell
>>> StableSwap.BASE_N_COINS()
3
```
::::
### `BASE_COINS`
::::description[`StableSwap.BASE_COINS(arg0: uint256) -> address: view`]
Getter for the coin at index value `arg0` within the base pool.
Returns: coin (`address`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `uint256` | Index value of the coin. |
```vyper
BASE_COINS: public(immutable(DynArray[address, MAX_COINS]))
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_math_implementation: address,
_base_pool: address,
_coins: DynArray[address, MAX_COINS],
_base_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
BASE_COINS = _base_coins
...
```
```shell
>>> StableSwap.BASE_COINS(0)
'0x6B175474E89094C44Da98b954EedeAC495271d0F'
>>> StableSwap.BASE_COINS(1)
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
>>> StableSwap.BASE_COINS(2)
'0xdAC17F958D2ee523a2206206994597C13D831ec7'
```
::::
### `coins`
::::description[`StableSwap.coins(arg0: uint256) -> addresss: view`]
Getter for the coin at index `arg0` within the metapool. coins[0] always return the coin paired against the basepool.
Returns: coin (`address`).
```vyper
coins: public(immutable(DynArray[address, MAX_COINS]))
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_math_implementation: address,
_base_pool: address,
_coins: DynArray[address, MAX_COINS],
_base_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
coins = _coins # <---------------- coins[1] is always base pool LP token.
...
```
```shell
>>> StableSwap.coins(0)
'0x0E573Ce2736Dd9637A0b21058352e1667925C7a8'
>>> StableSwap.coins(1)
'0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490'
```
:::note
The `coin[0]` is always the metapool token, and `coin[1]` is always the basepool token.
:::
::::
### `balances`
::::description[`StableSwap.balances(i: uint256) -> uint256: view`]
Getter for the current balance of coin `i` within the pool.
Returns: coin balance (`uint256`).
| Input | Type | Description |
| ------ | --------- | --------------------------- |
| `i` | `uint256` | Index value of the coin. |
```vyper
@view
@external
def balances(i: uint256) -> uint256:
"""
@notice Get the current balance of a coin within the
pool, less the accrued admin fees
@param i Index value for the coin to query balance of
@return Token balance
"""
return self._balances()[i]
@view
@internal
def _balances() -> DynArray[uint256, MAX_COINS]:
"""
@notice Calculates the pool's balances _excluding_ the admin's balances.
@dev If the pool contains rebasing tokens, this method ensures LPs keep all
rebases and admin only claims swap fees. This also means that, since
admin's balances are stored in an array and not inferred from read balances,
the fees in the rebasing token that the admin collects is immune to
slashing events.
"""
result: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances_i: uint256 = 0
for i in range(N_COINS_128):
if POOL_IS_REBASING_IMPLEMENTATION:
balances_i = ERC20(coins[i]).balanceOf(self) - self.admin_balances[i]
else:
balances_i = self.stored_balances[i] - self.admin_balances[i]
result.append(balances_i)
return result
```
```shell
>>> StableSwap.balances(0)
4183467888075
>>> StableSwap.balances(1)
2556883713184291687567176
```
::::
### `get_balances`
::::description[`StableSwap.get_balances() -> DynArray[uint256, MAX_COINS]: view`]
Getter for an array with all coin balances in the pool.
Returns: coin balances (`DynArray[uint256, MAX_COINS]`).
:::info
This getter method does not account for admin fees.
:::
```vyper
@view
@external
def get_balances() -> DynArray[uint256, MAX_COINS]:
return self._balances()
@view
@internal
def _balances() -> DynArray[uint256, MAX_COINS]:
"""
@notice Calculates the pool's balances _excluding_ the admin's balances.
@dev If the pool contains rebasing tokens, this method ensures LPs keep all
rebases and admin only claims swap fees. This also means that, since
admin's balances are stored in an array and not inferred from read balances,
the fees in the rebasing token that the admin collects is immune to
slashing events.
"""
result: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances_i: uint256 = 0
for i in range(N_COINS_128):
if POOL_IS_REBASING_IMPLEMENTATION:
balances_i = ERC20(coins[i]).balanceOf(self) - self.admin_balances[i]
else:
balances_i = self.stored_balances[i] - self.admin_balances[i]
result.append(balances_i)
return result
```
```shell
>>> StableSwap.get_balances()
[4183467888075, 2556883713184291687567176]
```
:::note
The returned values do not take admin fees into account.
:::
::::
### `N_COINS`
::::description[`StableSwap.N_COINS() -> uint256: view`]
Getter for the total number of coins in the pool.
Returns: number of coins (`uint256`).
:::info
A metapool always consists of two tokens - basepool token and the token paired against it.
:::
```vyper
N_COINS: public(constant(uint256)) = 2
```
```shell
>>> StableSwap.N_COINS()
2
```
::::
### `totalSupply`
::::description[`StableSwap.totalSupply() -> uint256: view`]
Getter for the total supply of the LP token.
Returns: total supply (`uint256`).
```vyper
total_supply: uint256
@view
@external
@nonreentrant('lock')
def totalSupply() -> uint256:
"""
@notice The total supply of pool LP tokens
@return self.total_supply, 18 decimals.
"""
return self.total_supply
```
```shell
>>> StableSwap.totalSupply()
6811356567627648910003460
```
::::
---
## LP Token
**Pool and LP tokens are the same smart contract.** The pool itself acts as an LP Token. When coins are deposited into a Curve pool, the depositor receives pool LP (liquidity provider) tokens in return. Each Curve pool has its unique ERC20 contract representing these LP tokens, making them transferable. Holding these LP tokens allows for their deposit and staking in the pool's liquidity gauge, earning CRV token rewards. Additionally, if a metapool supports the LP token, it can be deposited there to receive the metapool's distinct LP tokens.
### `transfer`
::::description[`StableSwap.transfer(_to : address, _value : uint256) -> bool:`]
Function to transfer `_value` tokens to `_to`.
Returns: true (`bool`).
Emits: `Transfer`
| Input | Type | Description |
| ----------- | -------| ----|
| `_to` | `address` | address to transfer token to |
| `_value` | `uint256` | amount of tokens to transfer |
```vyper
event Transfer:
sender: indexed(address)
receiver: indexed(address)
value: uint256
@external
def transfer(_to : address, _value : uint256) -> bool:
"""
@dev Transfer token for a specified address
@param _to The address to transfer to.
@param _value The amount to be transferred.
"""
self._transfer(msg.sender, _to, _value)
return True
@internal
def _transfer(_from: address, _to: address, _value: uint256):
# # NOTE: vyper does not allow underflows
# # so the following subtraction would revert on insufficient balance
self.balanceOf[_from] -= _value
self.balanceOf[_to] += _value
log Transfer(_from, _to, _value)
```
::::
### `transferFrom`
::::description[`StableSwap.transferFrom(_from : address, _to : address, _value : uint256) -> bool:`]
Function to transfer `_value` tokens from `_from` to `_to`.
Returns: true (`bool`).
Emits: `Transfer`
| Input | Type | Description |
| ----------- | -------| ----|
| `_from` | `address` | address to transfer token from |
| `_to` | `address` | address to transfer token to |
| `_value` | `uint256` | amount of tokens to transfer |
```vyper
event Transfer:
sender: indexed(address)
receiver: indexed(address)
value: uint256
@external
def transferFrom(_from : address, _to : address, _value : uint256) -> bool:
"""
@dev Transfer tokens from one address to another.
@param _from address The address which you want to send tokens from
@param _to address The address which you want to transfer to
@param _value uint256 the amount of tokens to be transferred
"""
self._transfer(_from, _to, _value)
_allowance: uint256 = self.allowance[_from][msg.sender]
if _allowance != max_value(uint256):
self.allowance[_from][msg.sender] = _allowance - _value
return True
@internal
def _transfer(_from: address, _to: address, _value: uint256):
# # NOTE: vyper does not allow underflows
# # so the following subtraction would revert on insufficient balance
self.balanceOf[_from] -= _value
self.balanceOf[_to] += _value
log Transfer(_from, _to, _value)
```
::::
### `allowance`
::::description[`StableSwap.allowance(arg0: address, arg1: address) -> uint256: view`]
Getter method to check the allowance of `arg0` for funds of `arg1`.
Returns: allowed amount (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `address` | Address of the spender |
| `arg1` | `address` | Address of the token owner |
```vyper
allowance: public(HashMap[address, HashMap[address, uint256]])
```
::::
### `approve`
::::description[`StableSwap.approve(_spender : address, _value : uint256) -> bool:`]
Function to approve `_spender` to transfer `_value` of tokens on behalf of `msg.sender`
Returns: true (`bool`).
Emits: `Approval`
| Input | Type | Description |
|-------------|-----------|---------------------------------|
| `_spender` | `address` | Address of the approved spender |
| `_value` | `uint256` | Amount of tokens to approve |
```vyper
event Approval:
owner: indexed(address)
spender: indexed(address)
value: uint256
@external
def approve(_spender : address, _value : uint256) -> bool:
"""
@notice Approve the passed address to transfer the specified amount of
tokens on behalf of msg.sender
@dev Beware that changing an allowance via this method brings the risk that
someone may use both the old and new allowance by unfortunate transaction
ordering: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
@param _spender The address which will transfer the funds
@param _value The amount of tokens that may be transferred
@return bool success
"""
self.allowance[msg.sender][_spender] = _value
log Approval(msg.sender, _spender, _value)
return True
```
::::
### `permit`
::::description[`StableSwap.permit(_owner: address, _spender: address, _value: uint256, _deadline: uint256, _v: uint8, _r: bytes32, _s: bytes32) -> bool:`]
Function to permit `spender` to spend up to `_value` amount of `_owner`'s tokens via a signature.
Returns: true (`bool`).
Emits: `Approval`
| Input | Type | Description |
| ----------- | -------| ----|
| `_owner` | `address` | Account which generated the signature and is granting an allowance |
| `_spender` | `address` | Account which will be granted an allowance |
| `_value` | `uint256` | Amount to approve |
| `_deadline` | `uint256` | Deadline by which signature must be submitted |
| `_v` | `uint8` | The last byte of the ECDSA signature |
| `_r` | `bytes32` | The first 32 bytes of the ECDSA signature |
| `_s` | `bytes32` | The second 32 bytes of the ECDSA signature |
```vyper
event Approval:
owner: indexed(address)
spender: indexed(address)
value: uint256
@external
def permit(
_owner: address,
_spender: address,
_value: uint256,
_deadline: uint256,
_v: uint8,
_r: bytes32,
_s: bytes32
) -> bool:
"""
@notice Approves spender by owner's signature to expend owner's tokens.
See https://eips.ethereum.org/EIPS/eip-2612.
@dev Inspired by https://github.com/yearn/yearn-vaults/blob/main/contracts/Vault.vy#L753-L793
@dev Supports smart contract wallets which implement ERC1271
https://eips.ethereum.org/EIPS/eip-1271
@param _owner The address which is a source of funds and has signed the Permit.
@param _spender The address which is allowed to spend the funds.
@param _value The amount of tokens to be spent.
@param _deadline The timestamp after which the Permit is no longer valid.
@param _v The bytes[64] of the valid secp256k1 signature of permit by owner
@param _r The bytes[0:32] of the valid secp256k1 signature of permit by owner
@param _s The bytes[32:64] of the valid secp256k1 signature of permit by owner
@return True, if transaction completes successfully
"""
assert _owner != empty(address)
assert block.timestamp <= _deadline
nonce: uint256 = self.nonces[_owner]
digest: bytes32 = keccak256(
concat(
b"\x19\x01",
self._domain_separator(),
keccak256(_abi_encode(EIP2612_TYPEHASH, _owner, _spender, _value, nonce, _deadline))
)
)
if _owner.is_contract:
sig: Bytes[65] = concat(_abi_encode(_r, _s), slice(convert(_v, bytes32), 31, 1))
# reentrancy not a concern since this is a staticcall
assert ERC1271(_owner).isValidSignature(digest, sig) == ERC1271_MAGIC_VAL
else:
assert ecrecover(digest, convert(_v, uint256), convert(_r, uint256), convert(_s, uint256)) == _owner
self.allowance[_owner][_spender] = _value
self.nonces[_owner] = nonce + 1
log Approval(_owner, _spender, _value)
return True
```
::::
### `name`
::::description[`StableSwap.name() -> String[64]: view`]
Getter for the name of the LP token.
Returns: name (`String[64]`).
```vyper
name: public(immutable(String[64]))
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
name = _name
...
```
```shell
>>> StableSwap.name()
'USDV-crvUSD'
```
::::
### `symbol`
::::description[`StableSwap.symbol() -> String[32]: view`]
Getter for the symbol of the LP token.
Returns: symbol (`String[32]`).
```vyper
symbol: public(immutable(String[32]))
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
symbol = _symbol
...
```
```shell
>>> StableSwap.symbol()
'USDVcrvUSD'
```
::::
### `decimals`
::::description[`StableSwap.decimals() -> uint8: view`]
Getter for the decimals of the LP token.
Returns: decimals (uint8).
```vyper
decimals: public(constant(uint8)) = 18
```
```shell
>>> StableSwap.decimals()
18
```
::::
### `version`
::::description[`StableSwap.version() -> String[8]: view`]
Getter for the version of the LP token.
Returns: version (`String[8]`).
```vyper
version: public(constant(String[8])) = "v7.0.0"
```
```shell
>>> StableSwap.version()
"v7.0.0"
```
::::
### `balanceOf`
::::description[`StableSwap.balanceOf(arg0: address) -> uint256: view`]
Getter for the LP token balance of `arg0`.
Returns: token balance (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `address` | address to check the balance of |
```vyper
balanceOf: public(HashMap[address, uint256])
```
```shell
>>> StableSwap.balanceOf("0x7a16fF8270133F063aAb6C9977183D9e72835428")
999808484451757093697730
```
::::
### `nonces`
::::description[`StableSwap.nonces(arg0: address) -> uint256: view`]
Getter for the nonce.
Returns: nonces (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `address` | address |
```vyper
nonces: public(HashMap[address, uint256])
```
::::
### `salt`
::::description[`StableSwap.salt() -> bytes32: view`]
Getter for the salt of the LP token.
Returns: salt (`bytes32`).
```vyper
salt: public(immutable(bytes32))
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
# EIP712 related params -----------------
NAME_HASH = keccak256(name)
salt = block.prevhash
CACHED_CHAIN_ID = chain.id
CACHED_DOMAIN_SEPARATOR = keccak256(
_abi_encode(
EIP712_TYPEHASH,
NAME_HASH,
VERSION_HASH,
chain.id,
self,
salt,
)
)
...
```
```shell
>>> StableSwap.salt()
HexBytes('0x814188b56f08130fe7b283343b64baa08f4d207229dc52776968b62b977c8f46')
```
::::
### `DOMAIN_SEPARATOR`
::::description[`StableSwap.DOMAIN_SEPARATOR() -> bytes32: view`]
Getter for the domain separator.
Returns: domain separator (`bytes32`).
```vyper
CACHED_DOMAIN_SEPARATOR: immutable(bytes32)
@view
@external
def DOMAIN_SEPARATOR() -> bytes32:
"""
@notice EIP712 domain separator.
@return bytes32 Domain Separator set for the current chain.
"""
return self._domain_separator()
@view
@internal
def _domain_separator() -> bytes32:
if chain.id != CACHED_CHAIN_ID:
return keccak256(
_abi_encode(
EIP712_TYPEHASH,
NAME_HASH,
VERSION_HASH,
chain.id,
self,
salt,
)
)
return CACHED_DOMAIN_SEPARATOR
```
```shell
>>> StableSwap.DOMAIN_SEPARATOR()
HexBytes('0xf60903716a331f2ad023b28477aceee88e5180cab4694c497f4f9cefac657989')
```
::::
---
## Admin Controls
The following methods are guarded and may only be called by the **`admin`** of the Stableswap-NG Factory.
### `ramp_A`
::::description[`StableSwap.ramp_A(_future_A: uint256, _future_time: uint256):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory.
:::
Function to ramp amplification coefficient A. Minimum ramp time is 86400 (24h).
*Limitations when ramping A:*
- `block.timestamp` >= `initial_A_time` + `MIN_RAMP_TIME`
- `_future_time` >= `block.timestamp` + `MIN_RAMP_TIME`
- `future_A` > 0
- `future_A` < `MAX_A (1000000)`
Emits: `RampA`
| Input | Type | Description |
| ----------- | -------| ----|
| `_future_A` | `uint256` | future A value |
| `_future_time` | `uint256` | timestamp until ramping should occur; needs to be at least 24h (`MIN_RAMP_TIME`) |
```vyper
A_PRECISION: constant(uint256) = 100
MAX_A: constant(uint256) = 10 **6
MAX_A_CHANGE: constant(uint256) = 10
MIN_RAMP_TIME: constant(uint256) = 86400
event RampA:
old_A: uint256
new_A: uint256
initial_time: uint256
future_time: uint256
@external
def ramp_A(_future_A: uint256, _future_time: uint256):
assert msg.sender == factory.admin() # dev: only owner
assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME
assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time
_initial_A: uint256 = self._A()
_future_A_p: uint256 = _future_A * A_PRECISION
assert _future_A > 0 and _future_A < MAX_A
if _future_A_p < _initial_A:
assert _future_A_p * MAX_A_CHANGE >= _initial_A
else:
assert _future_A_p <= _initial_A * MAX_A_CHANGE
self.initial_A = _initial_A
self.future_A = _future_A_p
self.initial_A_time = block.timestamp
self.future_A_time = _future_time
log RampA(_initial_A, _future_A_p, block.timestamp, _future_time)
```
::::
### `stop_ramp_A`
::::description[`StableSwap.stop_ramp_A():`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory.
:::
Function to immediately stop the ramping A. The current value during the ramping process will be finalized as `A`.
Emits: `StopRampA`
```vyper
event StopRampA:
A: uint256
t: uint256
@external
def stop_ramp_A():
assert msg.sender == factory.admin() # dev: only owner
current_A: uint256 = self._A()
self.initial_A = current_A
self.future_A = current_A
self.initial_A_time = block.timestamp
self.future_A_time = block.timestamp
# now (block.timestamp < t1) is always False, so we return saved A
log StopRampA(current_A, block.timestamp)
```
::::
### `set_new_fee`
::::description[`StableSwap.set_new_fee(_new_fee: uint256, _new_offpeg_fee_multiplier: uint256):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory.
:::
Function to set new values for `fee` and `offpeg_fee_multiplier`.
*Limitations when setting new parameters:*
- `_new_fee` <= `MAX_FEE` (5000000000)
- `_new_offpeg_fee_multiplier` * `_new_fee` <= `MAX_FEE` * `FEE_DENOMINATOR`
Emits: `ApplyNewFee`
| Input | Type | Description |
| ----------- | -------| ----|
| `_new_fee` | `uint256` | new fee |
| `_new_offpeg_fee_multiplier` | `uint256` | new off-peg fee multiplier |
```vyper
MAX_FEE: constant(uint256) = 5 * 10 **9
FEE_DENOMINATOR: constant(uint256) = 10 **10
event ApplyNewFee:
fee: uint256
offpeg_fee_multiplier: uint256
@external
def set_new_fee(_new_fee: uint256, _new_offpeg_fee_multiplier: uint256):
assert msg.sender == factory.admin()
# set new fee:
assert _new_fee <= MAX_FEE
self.fee = _new_fee
# set new offpeg_fee_multiplier:
assert _new_offpeg_fee_multiplier * _new_fee <= MAX_FEE * FEE_DENOMINATOR # dev: offpeg multiplier exceeds maximum
self.offpeg_fee_multiplier = _new_offpeg_fee_multiplier
log ApplyNewFee(_new_fee, _new_offpeg_fee_multiplier)
```
::::
### `set_ma_exp_time`
::::description[`StableSwap.set_ma_exp_time(_ma_exp_time: uint256, _D_ma_time: uint256):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory.
:::
Function to set the moving average window for `ma_exp_time` and `D_ma_time`.
*Limitations when setting new fee parameters:*
- `_ma_exp_time` and `_D_ma_time` > 0
| Input | Type | Description |
| ----------- | -------| ----|
| `_ma_exp_time` | `uint256` | new ma exp time |
| `_D_ma_time` | `uint256` | new D ma time |
```vyper
@external
def set_ma_exp_time(_ma_exp_time: uint256, _D_ma_time: uint256):
"""
@notice Set the moving average window of the price oracles.
@param _ma_exp_time Moving average window. It is time_in_seconds / ln(2)
"""
assert msg.sender == factory.admin() # dev: only owner
assert 0 not in [_ma_exp_time, _D_ma_time]
self.ma_exp_time = _ma_exp_time
self.D_ma_time = _D_ma_time
```
::::
---
## Stableswap-NG Oracles
:::danger[WARNING: Oracle Vulnerability]
A specific AMM implementation of [stableswap-ng](https://github.com/curvefi/stableswap-ng) has a bug that can cause the price oracle to change sharply if the tokens within the AMM **do not all have the same token decimal precision of 18 or if the tokens use external rates**. For example, the [`USDe<>USDC`](https://etherscan.io/address/0x02950460e2b9529d0e00284a5fa2d7bdf3fa4d72) pool has this issue, as USDe has a precision of 18 and USDC of 6.
A list of identified affected pools can be found in this [ Google Spreadsheet](https://docs.google.com/spreadsheets/d/130LPSQbAnMWTC1yVO23cqRSblrkYFfdHdRwYNpaaoYY/edit?usp=sharing).
**This bug only affects the use of the oracle and does not impact token exchanges or any liquidity actions at all. The AMM still functions as intended.** Pools deployed after **`Dec-12-2023 09:39:35 AM +UTC`** do not include the bug, as the fixed AMM implementation of the `StableSwapNG Factory` was [set to the updated version](https://etherscan.io/tx/0x5fc02a3f46e40a48ae4cecc07534bb3e0228b7a7a59b652801521f2af3a00b72).
*The source of the bug is in the AMM implementations [`code line 777`](https://github.com/curvefi/stableswap-ng/commit/4bb402ecb386979c113bee770ffbea9aebd5ae66#diff-5fb59b0d4563b84cdb3bb3740486847e798a1eca825b537ecbab95cb74d03847L777-L779) and was fixed in commit [`4bb402ecb386979c113bee770ffbea9aebd5ae66`](https://github.com/curvefi/stableswap-ng/commit/4bb402ecb386979c113bee770ffbea9aebd5ae66). The function did not take token precisions into account when updating the oracle in the `remove_liquidity_imbalance` function. The only change to fix the bug was made in a single line to ensure the `upkeep_oracle` calls the internal `_xp_mem` function before upkeeping the oracle:*
```vyper
### ----- old code (bugged) ----- ###
self.upkeep_oracles(new_balances, amp, D1)
### ----- new code (bug fixed) ----- ###
self.upkeep_oracles(self._xp_mem(rates, new_balances), amp, D1)
@pure
@internal
def _xp_mem(
_rates: DynArray[uint256, MAX_COINS],
_balances: DynArray[uint256, MAX_COINS]
) -> DynArray[uint256, MAX_COINS]:
result: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
for i in range(N_COINS_128, bound=MAX_COINS_128):
result.append(unsafe_div(_rates[i] * _balances[i], PRECISION))
return result
```
To **manually verify if a pool is using a correct (bug-free) implementation**, one can simply view the source code of the contract and check if `self.xp_mem(...)` is being called within `self.upkeep_oracles(...)` in the `remove_liquidity_imbalance` function.
:::
---
## Price and D Oracles
Stableswap-NG pools have the following oracles:
An exponential moving-average price oracle of an asset within the AMM with regard to the coin at index 0.
An exponential moving-average oracle of the D invariant.
:::example[Example: Price Oracle for crvusd/USDC]
The [`crvusd/USDC`](https://etherscan.io/address/0x4dece678ceceb27446b35c672dc7d61f30bad69e) pool consists of `crvUSD and USDC`.
Because `USDC` is the coin at index 0, `price_oracle()` returns the price of `crvUSD` with regard to `USDC`.
```vyper
>>> coins(0) = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' # USDC
>>> price_oracle() = 999043303185591283
0.99904330318 # price of crvUSD w.r.t USDC
```
*In order to get the reverse EMA (price of `USDC` with regard to `crvUSD`):*
$\frac{10^{36}}{\text{price\_oracle()}} = 1.0009576e^{18}$
:::
---
The AMM implementation utilizes two private variables, `last_prices_packed` and `last_D_packed`, to store the latest spot and EMA values. These values serve as the foundation for calculating the oracles.
:::danger[Oracle Manipulation Risk]
The spot price cannot be immediately used for the calculation of the moving average, as this would permit single-block oracle manipulation. Consequently, the `_calc_moving_average` method, which calculates the moving average of the oracle, uses `last_prices_packed` or `last_D_packed`. These variables retain prices from previous actions.
```vyper
@internal
@view
def _calc_moving_average(
packed_value: uint256,
averaging_window: uint256,
ma_last_time: uint256
) -> uint256:
last_spot_value: uint256 = packed_value & (2**128 - 1)
last_ema_value: uint256 = (packed_value >> 128)
if ma_last_time < block.timestamp: # calculate new_ema_value and return that.
alpha: uint256 = self.exp(
-convert(
unsafe_div(unsafe_mul(unsafe_sub(block.timestamp, ma_last_time), 10**18), averaging_window), int256
)
)
return unsafe_div(last_spot_value * (10**18 - alpha) + last_ema_value * alpha, 10**18)
return last_ema_value
```
:::
*The formula to calculate the exponential moving-average essentially comes down to:*
$$\alpha = e^{\text{power}}$$
$$\text{power} = \frac{(\text{block.timestamp} - \text{ma\_last\_time}) \times 10^{18}}{\text{ma\_time}}$$
$$\text{EMA} = \frac{\text{last\_spot\_value} \times (10^{18} - \alpha) + \text{last\_ema\_value} \times \alpha}{10^{18}}$$
*with:*
| Variable | Description |
| ------------------ | ---------------------------------------------------- |
| `block.timestamp` | Timestamp of the block. Since all transactions within a block share the same timestamp, the EMA oracles can only be updated once per block. |
| `last_prices_timestamp` | Last time the ma oracle was updated. Differentiates between D and price. |
| `ma_time` | Time window for the moving-average oracle; for the `price_oracle` it's `ma_exp_time`, and for the `D_oracle` it's `D_ma_time`. |
| `last_spot_value` | Last price within the AMM; for the `price_oracle` it's `last_price`, which is the first value of `last_prices_packed`. For calculating `D_oracle`, it's `last_D`, which is the first value in `last_D_packed`. |
| `last_ema_value` | Last EMA value; for calculating `price_oracle` it's `ma_price`, which is the second value packed in `last_prices_packed`. For calculating `D_oracle` it's `ma_D`, also the second value in `last_D_packed`. |
| `alpha` | Weighting multiplier that adjusts the impact of the latest spot value versus the previous EMA value in the new EMA calculation. |
| `exp` | Function that calculates the natural exponential function of a signed integer with a precision of 1e18. |
`price_oracle` calculation is based on the two values stored in `last_prices_packed`, `last_price` and `ema_price`. These values are conditionally updated:
Generally speaking, both values are simultaneously updated whenever `upkeep_oracles` is called. This happens at certain actions, see [here](#updating-oracles).
While `last_price` (spot price) is always updated at every relevant action, the `ema_price` is maximally updated once per block. There might be the case that there is more than one relevant action within the same block. Let's say there are two relevant actions within the block which would update both values:
If this is the case, `last_price` is updated at every action, so there will be two updated. `ema_price` on the other hand will only be updated once (at the first action) and will not change a second time. Reasoning behind this is to prevent single-block manipulation. The `ema_price` will just be updated at the next action outside of this block.
`D_oracle` calculation is based on the two values stored in `last_D_packed`, `last_D` and `ma_D`.
:::notebook[Jupyter Notebook]
For a practical **demonstration of how individual variables behave during the upkeep of the oracle**, a Jupyter notebook is available for reference. This notebook provides a plot showcasing the dynamics in the process.
It can be accessed here: https://try.vyperlang.org/hub/user-redirect/lab/tree/shared/mo-anon/stableswap-ng/oracles/ema_oracle.ipynb.
:::
---
### `price_oracle`
::::description[`StableSwap.price_oracle(i: uint256) -> uint256: view`]
Function to calculate the exponential moving average (EMA) price for the coin at index `i` with regard to the coin at index 0. The calculation is based on the last spot value (`last_price`), the last ma value (`ema_price`), the moving average time window (`ma_exp_time`), and on the difference between the current timestamp (`block.timestamp`) and the timestamp when the ma oracle was last updated (unpacks from the first value of `ma_last_time`).
`i = 0` will return the price oracle of `coin[1]`, `i = 1` the price oracle of `coin[2]`, and so on.
| Input | Type | Description |
| ------ | --------- | ---------------------------------- |
| `i` | `uint256` | Index value of the coin to calculate the EMA price for. i = 0 returns the price oracle for coin(1). |
Returns: EMA price of coin `i` (`uint256`).
```vyper
last_prices_packed: DynArray[uint256, MAX_COINS] # packing: last_price, ma_price
ma_exp_time: public(uint256)
ma_last_time: public(uint256) # packing: ma_last_time_p, ma_last_time_D
@external
@view
@nonreentrant('lock')
def price_oracle(i: uint256) -> uint256:
return self._calc_moving_average(
self.last_prices_packed[i],
self.ma_exp_time,
self.ma_last_time & (2**128 - 1)
)
@internal
@view
def _calc_moving_average(
packed_value: uint256,
averaging_window: uint256,
ma_last_time: uint256
) -> uint256:
last_spot_value: uint256 = packed_value & (2**128 - 1)
last_ema_value: uint256 = (packed_value >> 128)
if ma_last_time < block.timestamp: # calculate new_ema_value and return that.
alpha: uint256 = self.exp(
-convert(
(block.timestamp - ma_last_time) * 10**18 / averaging_window, int256
)
)
return (last_spot_value * (10**18 - alpha) + last_ema_value * alpha) / 10**18
return last_ema_value
```
```shell
>>> StableSwap.price_oracle(0)
1000187813326452556
```
::::
### `D_oracle`
::::description[`StableSwap.D_oracle() -> uint256: view`]
Function to calculate the exponential moving average (EMA) value for the `D` invariant, distinct from calculations for individual coins. This is based on the most recent "spot" value and EMA value of D, extracted from the private `last_D_packed` variable. It considers the moving average time window for D (`D_ma_time`), and calculates the difference between the current timestamp (`block.timestamp`) and the timestamp of the last update to the ma oracle of D, derived from the second value in `ma_last_time`.
Returns: EMA of D (`uint256`).
```vyper
last_D_packed: uint256 # packing: last_D, ma_D
D_ma_time: public(uint256)
ma_last_time: public(uint256) # packing: ma_last_time_p, ma_last_time_D
@external
@view
@nonreentrant('lock')
def D_oracle() -> uint256:
return self._calc_moving_average(
self.last_D_packed,
self.D_ma_time,
self.ma_last_time >> 128
)
@internal
@view
def _calc_moving_average(
packed_value: uint256,
averaging_window: uint256,
ma_last_time: uint256
) -> uint256:
last_spot_value: uint256 = packed_value & (2**128 - 1)
last_ema_value: uint256 = (packed_value >> 128)
if ma_last_time < block.timestamp: # calculate new_ema_value and return that.
alpha: uint256 = self.exp(
-convert(
(block.timestamp - ma_last_time) * 10**18 / averaging_window, int256
)
)
return (last_spot_value * (10**18 - alpha) + last_ema_value * alpha) / 10**18
return last_ema_value
```
```shell
>>> StableSwap.D_oracle()
2183776033162328612308290
```
::::
---
## Other Methods
### `last_price`
::::description[`StableSwap.last_price(i: uint256) -> uint256: view`]
:::warning[Revert]
This function reverts if `i >= MAX_COINS`.
:::
Getter method for the last stored price for the coin at index value `i`, stored in `last_prices_packed`. The spot price is retrieved from the lower 128 bits of the packed value in `last_prices_packed` and is updated whenever the internal `upkeep_oracles` method is called.
`i = 0` will return the last price of `coin[1]`, `i = 1` the last price of `coin[2]`, and so on.
| Input | Type | Description |
|-------|-----------|----------------------------------------------|
| `i` | `uint256` | Index value of the coin to get the last price for. |
Returns: last stored spot price of coin `i` (`uint256`).
```vyper
last_prices_packed: DynArray[uint256, MAX_COINS] # packing: last_price, ma_price
@view
@external
def last_price(i: uint256) -> uint256:
return self.last_prices_packed[i] & (2**128 - 1)
```
```shell
>>> StableSwap.last_price(0)
1000187811171795736
```
::::
### `ema_price`
::::description[`StableSwap.ema_price(i: uint256) -> uint256: view`]
:::warning[Revert]
This function will revert if `i >= MAX_COINS`.
:::
Getter method for the last stored exponential moving-average (EMA) price of the coin at index value `i`, retrieved from `last_prices_packed`. The EMA price is obtained by shifting the value in `last_prices_packed` to the right by 128 bits. This value is updated whenever the `upkeep_oracles()` function is internally called.
`i = 0` will return the last EMA price of `coin[1]`, `i = 1` of `coin[2]`, and so on.
| Input | Type | Description |
|-------|-----------|--------------------------------------------------|
| `i` | `uint256` | Index of the coin for which to retrieve the last EMA price. |
Returns: the last stored EMA price of coin `i` (`uint256`).
```vyper
last_prices_packed: DynArray[uint256, MAX_COINS] # packing: last_price, ma_price
@view
@external
def ema_price(i: uint256) -> uint256:
return (self.last_prices_packed[i] >> 128)
```
```shell
>>> StableSwap.ema_price(0)
1000187824576102231
```
::::
### `get_p`
::::description[`StableSwap.get_p(i: uint256) -> uint256: view`]
Function to calculate the current AMM spot price of coin `i` based on the coin balances in the pool, the amplification coefficient `A`, and the `D` invariant.
`i = 0` will return the price of `coin[1]`, `i = 1` the price of `coin[2]`, and so on.
| Input | Type | Description |
|-------|-----------|---------------------------------------------------|
| `i` | `uint256` | Index of the coin for which to calculate the current spot price. |
Returns: current spot price (`uint256`).
```vyper
@external
@view
def get_p(i: uint256) -> uint256:
"""
@notice Returns the AMM State price of token
@dev if i = 0, it will return the state price of coin[1].
@param i index of state price (0 for coin[1], 1 for coin[2], ...)
@return uint256 The state price quoted by the AMM for coin[i+1]
"""
amp: uint256 = self._A()
xp: DynArray[uint256, MAX_COINS] = self._xp_mem(
self._stored_rates(), self._balances()
)
D: uint256 = self.get_D(xp, amp)
return self._get_p(xp, amp, D)[i]
@internal
@pure
def _get_p(
xp: DynArray[uint256, MAX_COINS],
amp: uint256,
D: uint256,
) -> DynArray[uint256, MAX_COINS]:
# dx_0 / dx_1 only, however can have any number of coins in pool
ANN: uint256 = unsafe_mul(amp, N_COINS)
Dr: uint256 = unsafe_div(D, pow_mod256(N_COINS, N_COINS))
for i in range(MAX_COINS_128):
if i == N_COINS_128:
break
Dr = Dr * D / xp[i]
p: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp0_A: uint256 = ANN * xp[0] / A_PRECISION
for i in range(1, MAX_COINS):
if i == N_COINS:
break
p.append(10**18 * (xp0_A + Dr * xp[0] / xp[i]) / (xp0_A + Dr))
return p
```
```shell
>>> StableSwap.get_p(0)
1000187811171795736
```
::::
### `get_virtual_price`
::::description[`StableSwap.get_virtual_price() -> uint256: view`]
:::danger[Attack Vector]
This method may be vulnerable to donation-style attacks if the implementation contains rebasing tokens. For integrators, caution is advised.
:::
Getter for the current virtual price of the LP token, which represents a price relative to the underlying.
Returns: virtual price (`uint256`).
```vyper
@view
@external
@nonreentrant('lock')
def get_virtual_price() -> uint256:
"""
@notice The current virtual price of the pool LP token
@dev Useful for calculating profits.
The method may be vulnerable to donation-style attacks if implementation
contains rebasing tokens. For integrators, caution is advised.
@return LP token virtual price normalized to 1e18
"""
amp: uint256 = self._A()
xp: DynArray[uint256, MAX_COINS] = self._xp_mem(
self._stored_rates(), self._balances()
)
D: uint256 = self.get_D(xp, amp)
# D is in the units similar to DAI (e.g. converted to precision 1e18)
# When balanced, D = n * x_u - total virtual value of the portfolio
return D * PRECISION / self.total_supply
```
```shell
>>> StableSwap.get_virtual_price()
1000063971106330426
```
::::
### `ma_exp_time`
::::description[`StableSwap.ma_exp_time() -> uint256: view`]
Getter for the exponential moving-average time for the price oracle (`price_oracle`). This value can be adjusted via `set_ma_exp_time()`, as detailed in the [admin controls](./plainpool.md#set_ma_exp_time) section.
Returns: EMA time for the price oracle (`uint256`).
```vyper
ma_exp_time: public(uint256)
```
```shell
>>> StableSwap.ma_exp_time()
866
```
::::
### `D_ma_time`
::::description[`StableSwap.D_ma_time() -> uint256: view`]
Getter for the exponential moving-average time for the D oracle. This value can be adjusted via `set_ma_exp_time()`, as detailed in [admin controls](./plainpool.md#set_ma_exp_time).
Returns: EMA time for the D oracle (`uint256`).
```vyper
D_ma_time: public(uint256)
```
```shell
>>> StableSwap.D_ma_time()
62324
```
::::
### `ma_last_time`
::::description[`StableSwap.ma_last_time() -> uint256: view`]
:::warning[Distinction between price and D]
This variable contains two packed values because there needs to be a distinction between prices and the D invariant. The reasoning behind this is that the **moving-average price oracle is not updated if users remove liquidity in a balanced proportion (`remove_liquidity`), but the D oracle is.**
:::
Getter for the last time the exponential moving-average oracle of coin prices or the D invariant was updated. This variable contains two packed values: **ma_last_time_p**, which represents the timestamp of the last update for prices, and **ma_last_time_D**, which represents the last timestamp of the oracle update for the D invariant.
Returns: packed value (`uint256`).
```vyper
ma_last_time: public(uint256) # packing: ma_last_time_p, ma_last_time_D
```
```shell
>>> StableSwap.ma_last_time()
579359617954437487117250992339883299967854142015
```
:::note[Unpacking values]
The value needs to be unpacked, as it contains two values, **ma_last_time_p** and **ma_last_time_D**.
For example, 579359617954437487117250992339883299967854142015 is unpacked into two uint256 numbers. First, its lower 128 bits are isolated using a bitwise AND with 2**128 − 1, and then the value is shifted right by 128 bits to extract the upper 128 bits.
It returns: [1702584895, 1702584895], meaning both moving-average oracles were updated at the same time.
:::
:::
---
## Updating Oracles
The internal `upkeep_oracles` method is responsible for updating the price and D oracle.
:::info
Both EMA values, `ema_price` and `ma_D`, are updated maximally once per block. If there are two or more actions within the same block that would update the oracles, only the first action will update these values. The spot price (`last_price` or `last_D`) will always update.
The rationale behind this approach is that all transactions within a block share the same timestamp. Therefore, the condition `if ma_last_time < block.timestamp` can only be satisfied once per block (the first time it's called). If there are multiple actions that would trigger an oracle update, it will be updated in the next relevant action.
:::
```vyper
@internal
def upkeep_oracles(xp: DynArray[uint256, MAX_COINS], amp: uint256, D: uint256):
"""
@notice Upkeeps price and D oracles.
"""
ma_last_time_unpacked: uint256[2] = self.unpack_2(self.ma_last_time)
last_prices_packed_current: DynArray[uint256, MAX_COINS] = self.last_prices_packed
last_prices_packed_new: DynArray[uint256, MAX_COINS] = last_prices_packed_current
spot_price: DynArray[uint256, MAX_COINS] = self._get_p(xp, amp, D)
# -------------------------- Upkeep price oracle -------------------------
for i in range(MAX_COINS):
if i == N_COINS - 1:
break
if spot_price[i] != 0:
# Update packed prices -----------------
last_prices_packed_new[i] = self.pack_2(
min(spot_price[i], 2 * 10**18), # <----- Cap spot value by 2.
self._calc_moving_average(
last_prices_packed_current[i],
self.ma_exp_time,
ma_last_time_unpacked[0], # index 0 is ma_last_time for prices
)
)
self.last_prices_packed = last_prices_packed_new
# ---------------------------- Upkeep D oracle ---------------------------
last_D_packed_current: uint256 = self.last_D_packed
self.last_D_packed = self.pack_2(
D,
self._calc_moving_average(
last_D_packed_current,
self.D_ma_time,
ma_last_time_unpacked[1], # index 1 is ma_last_time for D
)
)
# Housekeeping: Update ma_last_time for p and D oracles ------------------
for i in range(2):
if ma_last_time_unpacked[i] < block.timestamp:
ma_last_time_unpacked[i] = block.timestamp
self.ma_last_time = self.pack_2(ma_last_time_unpacked[0], ma_last_time_unpacked[1])
```
---
### Price Oracles
The price oracle is updated when the `upkeep_oracles` method is called. This occurs in response to one of the following actions:
- Token exchange (`__exchange`)
- Liquidity addition (`add_liquidity`)
- Single-sided liquidity (`remove_liquidity_one_coin`)
- Liquidity removal in an imbalanced proportion (`remove_liquidity_imbalance`)
*When price oracles are upkept, the code calculates both the spot price and the moving-average price. These values are then packed and stored together in `last_prices_packed`.*
```vyper
# -------------------------- Upkeep price oracle -------------------------
for i in range(MAX_COINS):
if i == N_COINS - 1:
break
if spot_price[i] != 0:
# Update packed prices -----------------
last_prices_packed_new[i] = self.pack_2(
min(spot_price[i], 2 * 10**18), # <----- Cap spot value by 2.
self._calc_moving_average(
last_prices_packed_current[i],
self.ma_exp_time,
ma_last_time_unpacked[0], # index 0 is ma_last_time for prices
)
)
self.last_prices_packed = last_prices_packed_new
```
1. `last_price` which represents the last stored spot price within the AMM is calculated using `_get_p`. Additionally, the value is capped at `2 * 10**18` to prevent price oracle manipulation. Note: It's not actually the spot price which is capped, but rather the spot price that is used in the calculation for the EMA price oracle.
```vyper
@internal
@pure
def _get_p(
xp: DynArray[uint256, MAX_COINS],
amp: uint256,
D: uint256,
) -> DynArray[uint256, MAX_COINS]:
# dx_0 / dx_1 only, however can have any number of coins in pool
ANN: uint256 = unsafe_mul(amp, N_COINS)
Dr: uint256 = unsafe_div(D, pow_mod256(N_COINS, N_COINS))
for i in range(N_COINS_128, bound=MAX_COINS_128):
Dr = Dr * D / xp[i]
p: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp0_A: uint256 = unsafe_div(ANN * xp[0], A_PRECISION)
for i in range(1, MAX_COINS):
if i == N_COINS:
break
p.append(10**18 * (xp0_A + unsafe_div(Dr * xp[0], xp[i])) / (xp0_A + Dr))
return p
```
2. The moving-average price (`ema_price`) is calculated using `_calc_moving_average`. This value can only be updated once per block. If there are two actions which would update the value, only the first action will update it. For the second action, only the `last_price` is updated, while `ema_price` will not be updated and have the same value as in the first action.
```vyper
@internal
@view
def _calc_moving_average(
packed_value: uint256,
averaging_window: uint256,
ma_last_time: uint256
) -> uint256:
last_spot_value: uint256 = packed_value & (2**128 - 1)
last_ema_value: uint256 = (packed_value >> 128)
if ma_last_time < block.timestamp: # calculate new_ema_value and return that.
alpha: uint256 = self.exp(
-convert(
unsafe_div(unsafe_mul(unsafe_sub(block.timestamp, ma_last_time), 10**18), averaging_window), int256
)
)
return unsafe_div(last_spot_value * (10**18 - alpha) + last_ema_value * alpha, 10**18)
return last_ema_value
```
---
### D Oracle
The D oracle is updated when the `upkeep_oracles` method is called. This occurs in response to one of the following actions:
- Token exchange (`__exchange`)
- Liquidity addition (`add_liquidity`)
- Single-sided liquidity (`remove_liquidity_one_coin`)
- Liquidity removal in an imbalanced proportion (`remove_liquidity_imbalance`)
- Balanced proportion liquidity removal. For this action, the `remove_liquidity` function, which executes it, does not directly call the `upkeep_oracles` method. Instead, the D oracle update is performed "manually" within the function. The rationale behind this approach is that updating the price oracle is not necessary in this scenario, because removing in a balanced proportion does not change the prices within the AMM.
*When the D oracle is updated, the code calculates both the "spot" D invariant and the moving-average D invariant value. These values are then packed and stored together in `last_D_packed`.*
```vyper
# ---------------------------- Upkeep D oracle ---------------------------
last_D_packed_current: uint256 = self.last_D_packed
self.last_D_packed = self.pack_2(
D,
self._calc_moving_average(
last_D_packed_current,
self.D_ma_time,
ma_last_time_unpacked[1], # index 1 is ma_last_time for D
)
)
```
---
## [bytes4 method_id][bytes8 ][bytes20 oracle](Pools)
Plain pools are liquidity **exchange contracts which contain at least 2 and up to 8 coins.**
:::deploy[Contract Source & Deployment]
Source code available on [Github](https://github.com/curvefi/stableswap-ng/blob/bff1522b30819b7b240af17ccfb72b0effbf6c47/contracts/main/CurveStableSwapNG.vy).
:::
The deployment of plain pools is permissionless and can be done via the [**`deploy_plain_pool`**](../../factory/stableswap-ng/deployer-api.md#deploy_plain_pool) function within the Stableswap-NG Factory.
:::warning[Examples]
The examples following each code block of the corresponding functions provide a basic illustration of input/output values. **When using the function in production, ensure not to set `_min_dy`, `_min_amount`, etc., to zero or other arbitrary numbers**. Otherwise, MEV bots may frontrun or sandwich your transaction, leading to a potential loss of funds.
:::
:::info[**Oracle Methods Documentation**]
Comprehensive documentation for Oracle Methods is available on a dedicated page, accessible [here](./oracles.md).
:::
---
*The AMM contract utilizes two internal functions to transfer tokens/coins in and out of the pool and then accordingly update `stored_balances`:*
- **`_transfer_in()`**
`expect_optimistic_transfer` is relevant when using the [`exchange_received()`](#exchange_received) function.
| Input | Type | Description |
| ------------------------------ | ---------- | --------------------------------------------------- |
| `coin_idx` | `int128` | Index value of the token to transfer in. |
| `dx` | `uint256` | Amount to transfer in. |
| `sender` | `address` | Address to transfer coins from. |
| `expect_optimistic_transfer` | `bool` | `True` if the contract expects an optimistic coin transfer. |
```vyper
stored_balances: DynArray[uint256, MAX_COINS]
@internal
def _transfer_in(
coin_idx: int128,
dx: uint256,
sender: address,
expect_optimistic_transfer: bool,
) -> uint256:
"""
@notice Contains all logic to handle ERC20 token transfers.
@param coin_idx Index of the coin to transfer in.
@param dx amount of `_coin` to transfer into the pool.
@param dy amount of `_coin` to transfer out of the pool.
@param sender address to transfer `_coin` from.
@param receiver address to transfer `_coin` to.
@param expect_optimistic_transfer True if contract expects an optimistic coin transfer
"""
_dx: uint256 = ERC20(coins[coin_idx]).balanceOf(self)
# ------------------------- Handle Transfers -----------------------------
if expect_optimistic_transfer:
_dx = _dx - self.stored_balances[coin_idx]
assert _dx >= dx
else:
assert dx > 0 # dev : do not transferFrom 0 tokens into the pool
assert ERC20(coins[coin_idx]).transferFrom(
sender, self, dx, default_return_value=True
)
_dx = ERC20(coins[coin_idx]).balanceOf(self) - _dx
# --------------------------- Store transferred in amount ---------------------------
self.stored_balances[coin_idx] += _dx
return _dx
```
- **`_transfer_out()`**
| Input | Type | Description |
| ---------- | --------- | ----------------------------------------- |
| `coin_idx` | `int128` | Index value of the token to transfer out. |
| `_amount` | `uint256` | Amount to transfer out. |
| `receiver` | `address` | Address to send the tokens to. |
```vyper
stored_balances: DynArray[uint256, MAX_COINS]
@internal
def _transfer_out(_coin_idx: int128, _amount: uint256, receiver: address):
"""
@notice Transfer a single token from the pool to receiver.
@dev This function is called by `remove_liquidity` and
`remove_liquidity_one`, `_exchange` and `_withdraw_admin_fees` methods.
@param _coin_idx Index of the token to transfer out
@param _amount Amount of token to transfer out
@param receiver Address to send the tokens to
"""
# 'gulp' coin balance of the pool to stored_balances here to account for
# donations etc.
coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self)
# ------------------------- Handle Transfers -----------------------------
assert ERC20(coins[_coin_idx]).transfer(
receiver, _amount, default_return_value=True
)
# ----------------------- Update Stored Balances -------------------------
self.stored_balances[_coin_idx] = coin_balance - _amount
```
---
## Exchange Methods
*Two functions for token exchanges:*
- The regular `exchange` function.
- A novel `exchange_received` function that executes a token exchange based on the internal balances of the pool.
There is no `exchange_underlying` function, as this implementation is for plain pools and not for metapools, meaning no tokens are paired against other LP tokens.
### `exchange`
::::description[`StableSwap.exchange(i: int128, j: int128, _dx: uint256, _min_dy: uint256, _receiver: address = msg.sender) -> uint256:`]
Function to exchange `_dx` amount of coin `i` for coin `j` and receive a minimum amount of `_min_dy`.
Returns: amount of output coin received (`uint256`).
Emits: `TokenExchange`
| Input | Type | Description |
| ------------ | ---------- | -------------------------------------------------- |
| `i` | `int128` | Index value of input coin. |
| `j` | `int128` | Index value of output coin. |
| `_dx` | `uint256` | Amount of coin `i` being exchanged. |
| `_min_dy` | `uint256` | Minimum amount of coin `j` to receive. |
| `_receiver` | `address` | Receiver of the output tokens; defaults to `msg.sender`. |
```vyper
event TokenExchange:
buyer: indexed(address)
sold_id: int128
tokens_sold: uint256
bought_id: int128
tokens_bought: uint256
@external
@nonreentrant('lock')
def exchange(
i: int128,
j: int128,
_dx: uint256,
_min_dy: uint256,
_receiver: address = msg.sender,
) -> uint256:
"""
@notice Perform an exchange between two coins
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index value of the coin to recieve
@param _dx Amount of `i` being exchanged
@param _min_dy Minimum amount of `j` to receive
@return Actual amount of `j` received
"""
return self._exchange(
msg.sender,
i,
j,
_dx,
_min_dy,
_receiver,
False
)
@internal
def _exchange(
sender: address,
i: int128,
j: int128,
_dx: uint256,
_min_dy: uint256,
receiver: address,
expect_optimistic_transfer: bool
) -> uint256:
assert i != j # dev: coin index out of range
assert _dx > 0 # dev: do not exchange 0 coins
rates: DynArray[uint256, MAX_COINS] = self._stored_rates()
old_balances: DynArray[uint256, MAX_COINS] = self._balances()
xp: DynArray[uint256, MAX_COINS] = self._xp_mem(rates, old_balances)
# --------------------------- Do Transfer in -----------------------------
# `dx` is whatever the pool received after ERC20 transfer:
dx: uint256 = self._transfer_in(
i,
_dx,
sender,
expect_optimistic_transfer
)
# ------------------------------- Exchange -------------------------------
x: uint256 = xp[i] + dx * rates[i] / PRECISION
dy: uint256 = self.__exchange(x, xp, rates, i, j)
assert dy >= _min_dy, "Exchange resulted in fewer coins than expected"
# --------------------------- Do Transfer out ----------------------------
self._transfer_out(j, dy, receiver)
# ------------------------------------------------------------------------
log TokenExchange(msg.sender, i, _dx, j, dy)
return dy
def __exchange(
x: uint256,
_xp: DynArray[uint256, MAX_COINS],
rates: DynArray[uint256, MAX_COINS],
i: int128,
j: int128,
) -> uint256:
amp: uint256 = self._A()
D: uint256 = self.get_D(_xp, amp)
y: uint256 = self.get_y(i, j, x, _xp, amp, D)
dy: uint256 = _xp[j] - y - 1 # -1 just in case there were some rounding errors
dy_fee: uint256 = dy * self._dynamic_fee((_xp[i] + x) / 2, (_xp[j] + y) / 2, self.fee) / FEE_DENOMINATOR
# Convert all to real units
dy = (dy - dy_fee) * PRECISION / rates[j]
self.admin_balances[j] += (
dy_fee * admin_fee / FEE_DENOMINATOR
) * PRECISION / rates[j]
# Calculate and store state prices:
xp: DynArray[uint256, MAX_COINS] = _xp
xp[i] = x
xp[j] = y
# D is not changed because we did not apply a fee
self.upkeep_oracles(xp, amp, D)
return dy
```
```shell
>>> expected_dy = pool.get_dy(0, 1, 10**18) * 0.99
>>> StableSwap.exchange(0, 1, 10**18, expected_dy)
999712
```
:::note
This function exchanges one crvUSD for 0.999712 amount of USDV. `expected_dy` calculates the predicted input amount `j` to receive `dy` of coin `i`. This value can then be used as `_min_dy` in the `exchange` function.
:::
::::
### `exchange_received`
::::description[`StableSwap.exchange_received(i: int128, j: int128, _dx: uint256, _min_dy: uint256, _receiver: address) -> uint256:`]
:::danger
`exchange_received` will revert if the pool contains a rebasing asset. A pool that contains a rebasing token should have an `asset_type` of 2. If this is not the case, the pool is using an incorrect implementation, and rebases can be stolen.
:::
Function to exchange `_dx` amount of coin `i` for coin `j`, receiving a minimum amount of `_min_dy`. This is done without actually transferring the coins into the pool within the same call. The exchange is based on the change in the balance of coin `i`, eliminating the need to grant approval to the contract.
**A detailed article can be found here: https://blog.curvemonitor.com/posts/exchange-received/.**Returns: amount of output coin received (`uint256`).
Emits: `TokenExchange`
| Input | Type | Description |
| ------------ | ---------- | -------------------------------------------------- |
| `i` | `int128` | Index value of input coin. |
| `j` | `int128` | Index value of output coin. |
| `_dx` | `uint256` | Amount of coin `i` being exchanged. |
| `_min_dy` | `uint256` | Minimum amount of coin `j` to receive. |
| `_receiver` | `address` | Receiver of the output tokens. |
```vyper
event TokenExchange:
buyer: indexed(address)
sold_id: int128
tokens_sold: uint256
bought_id: int128
tokens_bought: uint256
stored_balances: DynArray[uint256, MAX_COINS]
@external
@nonreentrant('lock')
def exchange_received(
i: int128,
j: int128,
_dx: uint256,
_min_dy: uint256,
_receiver: address,
) -> uint256:
"""
@notice Perform an exchange between two coins without transferring token in
@dev The contract swaps tokens based on a change in balance of coin[i]. The
dx = ERC20(coin[i]).balanceOf(self) - self.stored_balances[i]. Users of
this method are dex aggregators, arbitrageurs, or other users who do not
wish to grant approvals to the contract: they would instead send tokens
directly to the contract and call `exchange_received`.
Note: This is disabled if pool contains rebasing tokens.
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param _dx Amount of `i` being exchanged
@param _min_dy Minimum amount of `j` to receive
@return Actual amount of `j` received
"""
assert not POOL_IS_REBASING_IMPLEMENTATION # dev: exchange_received not supported if pool contains rebasing tokens
return self._exchange(
msg.sender,
i,
j,
_dx,
_min_dy,
_receiver,
True, # <--------------------------------------- swap optimistically.
)
@internal
def _exchange(
sender: address,
i: int128,
j: int128,
_dx: uint256,
_min_dy: uint256,
receiver: address,
expect_optimistic_transfer: bool
) -> uint256:
assert i != j # dev: coin index out of range
assert _dx > 0 # dev: do not exchange 0 coins
rates: DynArray[uint256, MAX_COINS] = self._stored_rates()
old_balances: DynArray[uint256, MAX_COINS] = self._balances()
xp: DynArray[uint256, MAX_COINS] = self._xp_mem(rates, old_balances)
# --------------------------- Do Transfer in -----------------------------
# `dx` is whatever the pool received after ERC20 transfer:
dx: uint256 = self._transfer_in(
i,
_dx,
sender,
expect_optimistic_transfer
)
# ------------------------------- Exchange -------------------------------
x: uint256 = xp[i] + dx * rates[i] / PRECISION
dy: uint256 = self.__exchange(x, xp, rates, i, j)
assert dy >= _min_dy, "Exchange resulted in fewer coins than expected"
# --------------------------- Do Transfer out ----------------------------
self._transfer_out(j, dy, receiver)
# ------------------------------------------------------------------------
log TokenExchange(msg.sender, i, _dx, j, dy)
return dy
def __exchange(
x: uint256,
_xp: DynArray[uint256, MAX_COINS],
rates: DynArray[uint256, MAX_COINS],
i: int128,
j: int128,
) -> uint256:
amp: uint256 = self._A()
D: uint256 = self.get_D(_xp, amp)
y: uint256 = self.get_y(i, j, x, _xp, amp, D)
dy: uint256 = _xp[j] - y - 1 # -1 just in case there were some rounding errors
dy_fee: uint256 = dy * self._dynamic_fee((_xp[i] + x) / 2, (_xp[j] + y) / 2, self.fee) / FEE_DENOMINATOR
# Convert all to real units
dy = (dy - dy_fee) * PRECISION / rates[j]
self.admin_balances[j] += (
dy_fee * admin_fee / FEE_DENOMINATOR
) * PRECISION / rates[j]
# Calculate and store state prices:
xp: DynArray[uint256, MAX_COINS] = _xp
xp[i] = x
xp[j] = y
# D is not changed because we did not apply a fee
self.upkeep_oracles(xp, amp, D)
return dy
```
```shell
>>> crvusd.transfer("0xe1e77de32fb301ce55871ba095fd6b8e5d9abad8", 10**18)
>>> pool.exchange_received(0, 1, 10**18, 0)
999712
```
:::note
First, there needs to be a token transfer into the pool. Here, one crvUSD is transferred into the pool. Afterwards, `exchange_received` can be called to swap one crvUSD for `dy` USDV.
More information on this method [here](../overview.md#exchange_received).
:::
::::
### `get_dy`
::::description[`StableSwap.get_dy(i: int128, j: int128, dx: uint256) -> uint256:`]
Function to calculate the predicted output amount `j` to receive at the pool's current state given an input of `dx` amount of coin `i`. This is just a simple getter method; the calculation logic is within the CurveStableSwapNGViews contract. See [here](../utility-contracts/views.md#get_dy).
Returns: predicted output amount of `j` (`uint256`).
| Input | Type | Description |
| ------ | -------- | ------------------------------------------ |
| `i` | `int128` | Index value of input coin. |
| `j` | `int128` | Index value of output coin. |
| `dx` | `uint256`| Amount of input coin being exchanged. |
```vyper
interface Factory:
def fee_receiver() -> address: view
def admin() -> address: view
def views_implementation() -> address: view
@view
@external
def get_dy(i: int128, j: int128, dx: uint256) -> uint256:
"""
@notice Calculate the current output dy given input dx
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param dx Amount of `i` being exchanged
@return Amount of `j` predicted
"""
return StableSwapViews(factory.views_implementation()).get_dy(i, j, dx, self)
```
```vyper
@view
@external
def get_dy(i: int128, j: int128, dx: uint256, pool: address) -> uint256:
"""
@notice Calculate the current output dy given input dx
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param dx Amount of `i` being exchanged
@return Amount of `j` predicted
"""
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS)
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION
D: uint256 = self.get_D(xp, amp, N_COINS)
x: uint256 = xp[i] + (dx * rates[i] / PRECISION)
y: uint256 = self.get_y(i, j, x, xp, amp, D, N_COINS)
dy: uint256 = xp[j] - y - 1
base_fee: uint256 = StableSwapNG(pool).fee()
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
fee: uint256 = self._dynamic_fee((xp[i] + x) / 2, (xp[j] + y) / 2, base_fee, fee_multiplier) * dy / FEE_DENOMINATOR
return (dy - fee) * PRECISION / rates[j]
```
```shell
>>> StableSwap.get_dy(0, 1, 10**18)
999712
```
::::
### `get_dx`
::::description[`StableSwap.get_dx(i: int128, j: int128, dy: uint256) -> uint256:`]
Function to calculate the predicted input amount `i` to receive `dy` of coin `j` at the pool's current state. This is just a simple getter method; the calculation logic is within the CurveStableSwapNGViews contract. See [here](../utility-contracts/views.md#get_dx).
Returns: predicted input amount of `i` (`uint256`).
| Input | Type | Description |
| ------ | -------- | ------------------------------------------ |
| `i` | `int128` | Index value of input coin. |
| `j` | `int128` | Index value of output coin. |
| `dy` | `uint256`| Amount of output coin received. |
```vyper
interface StableSwapViews:
def get_dx(i: int128, j: int128, dy: uint256, pool: address) -> uint256: view
def get_dy(i: int128, j: int128, dx: uint256, pool: address) -> uint256: view
def dynamic_fee(i: int128, j: int128, pool: address) -> uint256: view
def calc_token_amount(
_amounts: DynArray[uint256, MAX_COINS],
_is_deposit: bool,
_pool: address
) -> uint256: view
@view
@external
def get_dx(i: int128, j: int128, dy: uint256) -> uint256:
"""
@notice Calculate the current input dx given output dy
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param dy Amount of `j` being received after exchange
@return Amount of `i` predicted
"""
return StableSwapViews(factory.views_implementation()).get_dx(i, j, dy, self)
```
```vyper
@view
@external
def get_dx(i: int128, j: int128, dy: uint256, pool: address) -> uint256:
"""
@notice Calculate the current input dx given output dy
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param dy Amount of `j` being received after exchange
@return Amount of `i` predicted
"""
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
return self._get_dx(i, j, dy, pool, False, N_COINS)
@view
@internal
def _get_dx(
i: int128,
j: int128,
dy: uint256,
pool: address,
static_fee: bool,
N_COINS: uint256
) -> uint256:
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS)
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION
D: uint256 = self.get_D(xp, amp, N_COINS)
base_fee: uint256 = StableSwapNG(pool).fee()
dy_with_fee: uint256 = dy * rates[j] / PRECISION + 1
fee: uint256 = base_fee
if not static_fee:
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
fee = self._dynamic_fee(xp[i], xp[j], base_fee, fee_multiplier)
y: uint256 = xp[j] - dy_with_fee * FEE_DENOMINATOR / (FEE_DENOMINATOR - fee)
x: uint256 = self.get_y(j, i, y, xp, amp, D, N_COINS)
return (x - xp[i]) * PRECISION / rates[i]
```
```shell
>>> StableSwap.get_dx(1, 0, 10**18)
999912
```
::::
---
## Adding and Removing Liquidity
There are no restrictions on how liquidity can be added or removed. Liquidity can be provided or removed in any proportion. However, there are fees associated with adding and removing liquidity that depend on the balances within the pool.
### `add_liquidity`
::::description[`StableSwap.add_liquidity(_amounts: DynArray[uint256, MAX_COINS], _min_mint_amount: uint256, _receiver: address = msg.sender) -> uint256:`]
Function to add liquidity into the pool and mint a minimum of `_min_mint_amount` of the corresponding LP tokens to `_receiver`. A value for the minimum amount is used to prevent being front-run by MEV bots.
Returns: amount of LP tokens received (`uint256`).
Emits: `Transfer` and `AddLiquidity`
| Input | Type | Description |
| ------------ | ------------------------------ | -------------------------------------------------- |
| `_amounts` | `DynArray[uint256, MAX_COINS]`| List of coin amounts to deposit. |
| `_min_amount`| `uint256` | Minimum amount of LP tokens to mint. |
| `_receiver` | `address` | Receiver of the LP tokens; defaults to `msg.sender`.|
```vyper
event Transfer:
sender: indexed(address)
receiver: indexed(address)
value: uint256
event AddLiquidity:
provider: indexed(address)
token_amounts: DynArray[uint256, MAX_COINS]
fees: DynArray[uint256, MAX_COINS]
invariant: uint256
token_supply: uint256
@external
@nonreentrant('lock')
def add_liquidity(
_amounts: DynArray[uint256, MAX_COINS],
_min_mint_amount: uint256,
_receiver: address = msg.sender
) -> uint256:
"""
@notice Deposit coins into the pool
@param _amounts List of amounts of coins to deposit
@param _min_mint_amount Minimum amount of LP tokens to mint from the deposit
@param _receiver Address that owns the minted LP tokens
@return Amount of LP tokens received by depositing
"""
amp: uint256 = self._A()
old_balances: DynArray[uint256, MAX_COINS] = self._balances()
rates: DynArray[uint256, MAX_COINS] = self._stored_rates()
# Initial invariant
D0: uint256 = self.get_D_mem(rates, old_balances, amp)
total_supply: uint256 = self.total_supply
new_balances: DynArray[uint256, MAX_COINS] = old_balances
# -------------------------- Do Transfers In -----------------------------
for i in range(MAX_COINS_128):
if i == N_COINS_128:
break
if _amounts[i] > 0:
new_balances[i] += self._transfer_in(
i,
_amounts[i],
msg.sender,
False, # expect_optimistic_transfer
)
else:
assert total_supply != 0 # dev: initial deposit requires all coins
# ------------------------------------------------------------------------
# Invariant after change
D1: uint256 = self.get_D_mem(rates, new_balances, amp)
assert D1 > D0
# We need to recalculate the invariant accounting for fees
# to calculate fair user's share
fees: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
mint_amount: uint256 = 0
if total_supply > 0:
ideal_balance: uint256 = 0
difference: uint256 = 0
new_balance: uint256 = 0
ys: uint256 = (D0 + D1) / N_COINS
xs: uint256 = 0
_dynamic_fee_i: uint256 = 0
# Only account for fees if we are not the first to deposit
base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
for i in range(MAX_COINS_128):
if i == N_COINS_128:
break
ideal_balance = D1 * old_balances[i] / D0
difference = 0
new_balance = new_balances[i]
if ideal_balance > new_balance:
difference = ideal_balance - new_balance
else:
difference = new_balance - ideal_balance
# fee[i] = _dynamic_fee(i, j) * difference / FEE_DENOMINATOR
xs = unsafe_div(rates[i] * (old_balances[i] + new_balance), PRECISION)
_dynamic_fee_i = self._dynamic_fee(xs, ys, base_fee)
fees.append(_dynamic_fee_i * difference / FEE_DENOMINATOR)
self.admin_balances[i] += fees[i] * admin_fee / FEE_DENOMINATOR
new_balances[i] -= fees[i]
xp: DynArray[uint256, MAX_COINS] = self._xp_mem(rates, new_balances)
D1 = self.get_D(xp, amp) # <--------------- Reuse D1 for new D value.
mint_amount = total_supply * (D1 - D0) / D0
self.upkeep_oracles(xp, amp, D1)
else:
mint_amount = D1 # Take the dust if there was any
# (re)instantiate D oracle if totalSupply is zero.
self.last_D_packed = self.pack_2(D1, D1)
assert mint_amount >= _min_mint_amount, "Slippage screwed you"
# Mint pool tokens
total_supply += mint_amount
self.balanceOf[_receiver] += mint_amount
self.total_supply = total_supply
log Transfer(empty(address), _receiver, mint_amount)
log AddLiquidity(msg.sender, _amounts, fees, D1, total_supply)
return mint_amount
```
```shell
>>> StableSwap.add_liquidity([10000000000000000000, 0], 0)
9997967030080774869
```
::::
### `remove_liquidity`
::::description[`StableSwap.remove_liquidity(_burn_amount: uint256, _min_amounts: DynArray[uint256, MAX_COINS], _receiver: address = msg.sender, _claim_admin_fees: bool = True) -> DynArray[uint256, MAX_COINS]:`]
:::info
When removing liquidity in a balanced ratio, there is no need to update the price oracle, as this function does not alter the balance ratio within the pool. Calling this function only updates `D_oracle`.
The calculation of `D` does not use Newton methods, ensuring that `remove_liquidity` should always work, even if the pool gets borked.
:::
Function to remove `_min_amount` coins from the liquidity pool based on the pools current ratios by burning `_burn_amount` of LP tokens. Admin fees might be claimed after liquidity is removed.
Returns: amount of coins withdrawn (`DynArray[uint256, MAX_COINS]`).
Emits: `RemoveLiquidity`
| Input | Type | Description |
| ------------------ | ------------------------------ | -------------------------------------------------- |
| `_burn_amount` | `uint256` | Amount of LP tokens to be burned. |
| `_min_amounts` | `DynArray[uint256, MAX_COINS]` | Minimum amounts of coins to receive. |
| `_receiver` | `address` | Receiver of the coins; defaults to `msg.sender`. |
| `_claim_admin_fees`| `bool` | If admin fees should be claimed; defaults to `true`.|
```vyper
event RemoveLiquidity:
provider: indexed(address)
token_amounts: DynArray[uint256, MAX_COINS]
fees: DynArray[uint256, MAX_COINS]
token_supply: uint256
@external
@nonreentrant('lock')
def remove_liquidity(
_burn_amount: uint256,
_min_amounts: DynArray[uint256, MAX_COINS],
_receiver: address = msg.sender,
_claim_admin_fees: bool = True,
) -> DynArray[uint256, MAX_COINS]:
"""
@notice Withdraw coins from the pool
@dev Withdrawal amounts are based on current deposit ratios
@param _burn_amount Quantity of LP tokens to burn in the withdrawal
@param _min_amounts Minimum amounts of underlying coins to receive
@param _receiver Address that receives the withdrawn coins
@return List of amounts of coins that were withdrawn
"""
total_supply: uint256 = self.total_supply
assert _burn_amount > 0 # dev: invalid burn amount
amounts: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = self._balances()
value: uint256 = 0
for i in range(MAX_COINS_128):
if i == N_COINS_128:
break
value = balances[i] * _burn_amount / total_supply
assert value >= _min_amounts[i], "Withdrawal resulted in fewer coins than expected"
amounts.append(value)
self._transfer_out(i, value, _receiver)
self._burnFrom(msg.sender, _burn_amount) # <---- Updates self.total_supply
# --------------------------- Upkeep D_oracle ----------------------------
ma_last_time_unpacked: uint256[2] = self.unpack_2(self.ma_last_time)
last_D_packed_current: uint256 = self.last_D_packed
old_D: uint256 = last_D_packed_current & (2**128 - 1)
self.last_D_packed = self.pack_2(
old_D - unsafe_div(old_D * _burn_amount, total_supply), # new_D = proportionally reduce D.
self._calc_moving_average(
last_D_packed_current,
self.D_ma_time,
ma_last_time_unpacked[1]
)
)
if ma_last_time_unpacked[1] < block.timestamp:
ma_last_time_unpacked[1] = block.timestamp
self.ma_last_time = self.pack_2(ma_last_time_unpacked[0], ma_last_time_unpacked[1])
# ------------------------------- Log event ------------------------------
log RemoveLiquidity(
msg.sender,
amounts,
empty(DynArray[uint256, MAX_COINS]),
total_supply - _burn_amount
)
# ------- Withdraw admin fees if _claim_admin_fees is set to True --------
if _claim_admin_fees:
self._withdraw_admin_fees()
return amounts
```
```shell
>>> StableSwap.get_balances()
[1156170050330410764719488, 1052703857490]
>>> StableSwap.remove_liquidity(10**18, [0, 0])
523455207306501616, 476610
```
:::note
`remove_liquidity` removes liquidity in a balanced proportion according to the balances in the pool.
:::
::::
### `remove_liquidity_one_coin`
::::description[`StableSwap.remove_liquidity_one_coin(_burn_amount: uint256, i: int128, _min_received: uint256, _receiver: address = msg.sender) -> uint256:`]
Function to remove a minimum of `_min_received` of coin `i` by burning `_burn_amount` of LP tokens.
Returns: coins received (`uint256`).
Emits: `RemoveLiquidityOne`
| Input | Type | Description |
| --------------- | ---------- | -------------------------------------------------- |
| `_burn_amount` | `uint256` | Amount of LP tokens to burn/withdraw. |
| `i` | `int128` | Index value of the coin to withdraw. |
| `_min_received`| `uint256` | Minimum amount of coin to receive. |
| `_receiver` | `address` | Receiver of the coins; defaults to `msg.sender`. |
```vyper
event RemoveLiquidityOne:
provider: indexed(address)
token_id: int128
token_amount: uint256
coin_amount: uint256
token_supply: uint256
@external
@nonreentrant('lock')
def remove_liquidity_one_coin(
_burn_amount: uint256,
i: int128,
_min_received: uint256,
_receiver: address = msg.sender,
) -> uint256:
"""
@notice Withdraw a single coin from the pool
@param _burn_amount Amount of LP tokens to burn in the withdrawal
@param i Index value of the coin to withdraw
@param _min_received Minimum amount of coin to receive
@param _receiver Address that receives the withdrawn coins
@return Amount of coin received
"""
assert _burn_amount > 0 # dev: do not remove 0 LP tokens
dy: uint256 = 0
fee: uint256 = 0
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
amp: uint256 = empty(uint256)
D: uint256 = empty(uint256)
dy, fee, xp, amp, D = self._calc_withdraw_one_coin(_burn_amount, i)
assert dy >= _min_received, "Not enough coins removed"
self.admin_balances[i] += fee * admin_fee / FEE_DENOMINATOR
self._burnFrom(msg.sender, _burn_amount)
self._transfer_out(i, dy, _receiver)
log RemoveLiquidityOne(msg.sender, i, _burn_amount, dy, self.total_supply)
self.upkeep_oracles(xp, amp, D)
return dy
@view
@internal
def _calc_withdraw_one_coin(
_burn_amount: uint256,
i: int128
) -> (
uint256,
uint256,
DynArray[uint256, MAX_COINS],
uint256,
uint256
):
# First, need to calculate
# * Get current D
# * Solve Eqn against y_i for D - _token_amount
amp: uint256 = self._A()
rates: DynArray[uint256, MAX_COINS] = self._stored_rates()
xp: DynArray[uint256, MAX_COINS] = self._xp_mem(rates, self._balances())
D0: uint256 = self.get_D(xp, amp)
total_supply: uint256 = self.total_supply
D1: uint256 = D0 - _burn_amount * D0 / total_supply
new_y: uint256 = self.get_y_D(amp, i, xp, D1)
base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
ys: uint256 = (D0 + D1) / (2 * N_COINS)
xp_reduced: DynArray[uint256, MAX_COINS] = xp
dx_expected: uint256 = 0
xp_j: uint256 = 0
xavg: uint256 = 0
dynamic_fee: uint256 = 0
for j in range(MAX_COINS_128):
if j == N_COINS_128:
break
dx_expected = 0
xp_j = xp[j]
if j == i:
dx_expected = xp_j * D1 / D0 - new_y
xavg = (xp_j + new_y) / 2
else:
dx_expected = xp_j - xp_j * D1 / D0
xavg = xp_j
dynamic_fee = self._dynamic_fee(xavg, ys, base_fee)
xp_reduced[j] = xp_j - dynamic_fee * dx_expected / FEE_DENOMINATOR
dy: uint256 = xp_reduced[i] - self.get_y_D(amp, i, xp_reduced, D1)
dy_0: uint256 = (xp[i] - new_y) * PRECISION / rates[i] # w/o fees
dy = (dy - 1) * PRECISION / rates[i] # Withdraw less to account for rounding errors
# update xp with new_y for p calculations.
xp[i] = new_y
return dy, dy_0 - dy, xp, amp, D1
```
```shell
>>> StableSwap.remove_liquidity_one_coin(10**18, 0, 9**18)
1000107995665331176
>>> StableSwap.remove_liquidity_one_coin(10**18, 1, 9**18)
999915
```
:::note
Both examples involve removing one LP token. With `remove_liquidity_one_coin` targeted at the higher balanced coin of the pool, a small premium is received. Conversely, when removing liquidity in the form of the lower balance token in the pool, slightly less is received. An estimated value of the output can be obtained via `calc_withdraw_one_coin`.
:::
::::
### `remove_liquidity_imbalance`
::::description[`StableSwap.remove_liquidity_imbalance(_amounts: DynArray[uint256, MAX_COINS], _max_burn_amount: uint256, _receiver: address = msg.sender) -> uint256:`]
Function to burn a maximum of `_max_burn_amount` of LP tokens in order to receive `_amounts` of underlying tokens.
Returns: amount of LP tokens burned (`uint256`).
Emits: `RemoveLiquidityImbalance`
| Input | Type | Description |
| ------------------ | ------------------------------ | -------------------------------------------------- |
| `_amounts` | `DynArray[uint256, MAX_COINS]`| List of amounts of coins to withdraw. |
| `_max_burn_amount` | `uint256` | Maximum amount of LP tokens to burn. |
| `_receiver` | `address` | Receiver of the coins; defaults to `msg.sender`. |
```vyper
event RemoveLiquidityImbalance:
provider: indexed(address)
token_amounts: DynArray[uint256, MAX_COINS]
fees: DynArray[uint256, MAX_COINS]
invariant: uint256
token_supply: uint256
@external
@nonreentrant('lock')
def remove_liquidity_imbalance(
_amounts: DynArray[uint256, MAX_COINS],
_max_burn_amount: uint256,
_receiver: address = msg.sender
) -> uint256:
"""
@notice Withdraw coins from the pool in an imbalanced amount
@param _amounts List of amounts of underlying coins to withdraw
@param _max_burn_amount Maximum amount of LP token to burn in the withdrawal
@param _receiver Address that receives the withdrawn coins
@return Actual amount of the LP token burned in the withdrawal
"""
amp: uint256 = self._A()
rates: DynArray[uint256, MAX_COINS] = self._stored_rates()
old_balances: DynArray[uint256, MAX_COINS] = self._balances()
D0: uint256 = self.get_D_mem(rates, old_balances, amp)
new_balances: DynArray[uint256, MAX_COINS] = old_balances
for i in range(MAX_COINS_128):
if i == N_COINS_128:
break
if _amounts[i] != 0:
new_balances[i] -= _amounts[i]
self._transfer_out(i, _amounts[i], _receiver)
D1: uint256 = self.get_D_mem(rates, new_balances, amp)
base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
ys: uint256 = (D0 + D1) / N_COINS
fees: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
dynamic_fee: uint256 = 0
xs: uint256 = 0
ideal_balance: uint256 = 0
difference: uint256 = 0
new_balance: uint256 = 0
for i in range(MAX_COINS_128):
if i == N_COINS_128:
break
ideal_balance = D1 * old_balances[i] / D0
difference = 0
new_balance = new_balances[i]
if ideal_balance > new_balance:
difference = ideal_balance - new_balance
else:
difference = new_balance - ideal_balance
xs = unsafe_div(rates[i] * (old_balances[i] + new_balance), PRECISION)
dynamic_fee = self._dynamic_fee(xs, ys, base_fee)
fees.append(dynamic_fee * difference / FEE_DENOMINATOR)
self.admin_balances[i] += fees[i] * admin_fee / FEE_DENOMINATOR
new_balances[i] -= fees[i]
D1 = self.get_D_mem(rates, new_balances, amp) # dev: reuse D1 for new D.
self.upkeep_oracles(self._xp_mem(rates, new_balances), amp, D1)
total_supply: uint256 = self.total_supply
burn_amount: uint256 = ((D0 - D1) * total_supply / D0) + 1
assert burn_amount > 1 # dev: zero tokens burned
assert burn_amount <= _max_burn_amount, "Slippage screwed you"
total_supply -= burn_amount
self._burnFrom(msg.sender, burn_amount)
log RemoveLiquidityImbalance(msg.sender, _amounts, fees, D1, total_supply)
return burn_amount
```
```shell
>>> StableSwap.remove_liquidity_imbalance([10**18, 10**6] 10**19)
1999880816717294817
```
::::
### `calc_token_amount`
::::description[`StableSwap.calc_token_amount(_amounts: DynArray[uint256, MAX_COINS], _is_deposit: bool) -> uint256:`]
Function to calculate the addition or reduction of token supply from a deposit (add liquidity) or withdrawal (remove liquidity). This function does take fees into consideration.
Returns: amount of LP tokens (`uint256`).
| Input | Type | Description |
| -------------- | ------------------------------ | -------------------------------------------------- |
| `_amounts` | `DynArray[uint256, MAX_COINS]` | Amount of coins being deposited/withdrawn. |
| `_is_deposit` | `bool` | `true` = deposit, `false` = withdraw. |
```vyper
interface Factory:
def fee_receiver() -> address: view
def admin() -> address: view
def views_implementation() -> address: view
@view
@external
def calc_token_amount(
_amounts: DynArray[uint256, MAX_COINS],
_is_deposit: bool
) -> uint256:
"""
@notice Calculate addition or reduction in token supply from a deposit or withdrawal
@param _amounts Amount of each coin being deposited
@param _is_deposit set True for deposits, False for withdrawals
@return Expected amount of LP tokens received
"""
return StableSwapViews(factory.views_implementation()).calc_token_amount(_amounts, _is_deposit, self)
```
```vyper
@view
@external
def calc_token_amount(
_amounts: DynArray[uint256, MAX_COINS],
_is_deposit: bool,
pool: address
) -> uint256:
"""
@notice Calculate addition or reduction in token supply from a deposit or withdrawal
@param _amounts Amount of each coin being deposited
@param _is_deposit set True for deposits, False for withdrawals
@return Expected amount of LP tokens received
"""
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
old_balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, old_balances, xp = self._get_rates_balances_xp(pool, N_COINS)
# Initial invariant
D0: uint256 = self.get_D(xp, amp, N_COINS)
total_supply: uint256 = StableSwapNG(pool).totalSupply()
new_balances: DynArray[uint256, MAX_COINS] = old_balances
for i in range(MAX_COINS):
if i == N_COINS:
break
amount: uint256 = _amounts[i]
if _is_deposit:
new_balances[i] += amount
else:
new_balances[i] -= amount
# Invariant after change
for idx in range(MAX_COINS):
if idx == N_COINS:
break
xp[idx] = rates[idx] * new_balances[idx] / PRECISION
D1: uint256 = self.get_D(xp, amp, N_COINS)
# We need to recalculate the invariant accounting for fees
# to calculate fair user's share
D2: uint256 = D1
if total_supply > 0:
# Only account for fees if we are not the first to deposit
base_fee: uint256 = StableSwapNG(pool).fee() * N_COINS / (4 * (N_COINS - 1))
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
_dynamic_fee_i: uint256 = 0
xs: uint256 = 0
ys: uint256 = (D0 + D1) / N_COINS
for i in range(MAX_COINS):
if i == N_COINS:
break
ideal_balance: uint256 = D1 * old_balances[i] / D0
difference: uint256 = 0
new_balance: uint256 = new_balances[i]
if ideal_balance > new_balance:
difference = ideal_balance - new_balance
else:
difference = new_balance - ideal_balance
xs = old_balances[i] + new_balance
_dynamic_fee_i = self._dynamic_fee(xs, ys, base_fee, fee_multiplier)
new_balances[i] -= _dynamic_fee_i * difference / FEE_DENOMINATOR
for idx in range(MAX_COINS):
if idx == N_COINS:
break
xp[idx] = rates[idx] * new_balances[idx] / PRECISION
D2 = self.get_D(xp, amp, N_COINS)
else:
return D1 # Take the dust if there was any
diff: uint256 = 0
if _is_deposit:
diff = D2 - D0
else:
diff = D0 - D2
return diff * total_supply / D0
```
```shell
>>> StableSwap.calc_token_amount([10**18, 0], True) # deposit (coin[0])
999701503692424994
>>> StableSwap.calc_token_amount([0, 10**6], True) # deposit (coin[1])
999875942505458416
>>> StableSwap.calc_token_amount([10**18, 10**6], True) # deposit (coin[0] and coin[1])
1999863130101592370
>>> StableSwap.calc_token_amount([10**18, 0], False) # withdraw (coin[1])
999987187514411723
>>> StableSwap.calc_token_amount([10**18, 0], False) # withdraw (coin[0])
1000188312578139610
>>> StableSwap.calc_token_amount([10**18, 10**6], False) # withdraw (coin[0] and coin[1])
1999889816188803581
```
:::note
If `_is_deposit` is True, the method calculates the increase in LP token supply when adding `_amounts` of tokens to the pool. Conversely, when `_is_deposit` is False, the method calculates the decrease in LP token supply when removing `_amounts` of tokens from the pool. This is a `view` function and does not actually alter any states.
:::
::::
### `calc_withdraw_one_coin`
::::description[`StableSwap.calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256:`]
Function to calculate the amount of single token `i` withdrawn when burning `_burn_amount` LP tokens.
Returns: amount of tokens withdrawn (`uint256`).
| Input | Type | Description |
| --------------- | --------- | -------------------------------------------------- |
| `_burn_amount` | `uint256` | Amount of LP tokens to burn. |
| `i` | `int128` | Index value of the coin to withdraw. |
```vyper
@view
@external
def calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256:
"""
@notice Calculate the amount received when withdrawing a single coin
@param _burn_amount Amount of LP tokens to burn in the withdrawal
@param i Index value of the coin to withdraw
@return Amount of coin received
"""
return self._calc_withdraw_one_coin(_burn_amount, i)[0]
@view
@internal
def _calc_withdraw_one_coin(
_burn_amount: uint256,
i: int128
) -> (
uint256,
uint256,
DynArray[uint256, MAX_COINS],
uint256,
uint256
):
# First, need to calculate
# * Get current D
# * Solve Eqn against y_i for D - _token_amount
amp: uint256 = self._A()
rates: DynArray[uint256, MAX_COINS] = self._stored_rates()
xp: DynArray[uint256, MAX_COINS] = self._xp_mem(rates, self._balances())
D0: uint256 = self.get_D(xp, amp)
total_supply: uint256 = self.total_supply
D1: uint256 = D0 - _burn_amount * D0 / total_supply
new_y: uint256 = self.get_y_D(amp, i, xp, D1)
base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
ys: uint256 = (D0 + D1) / (2 * N_COINS)
xp_reduced: DynArray[uint256, MAX_COINS] = xp
dx_expected: uint256 = 0
xp_j: uint256 = 0
xavg: uint256 = 0
dynamic_fee: uint256 = 0
for j in range(MAX_COINS_128):
if j == N_COINS_128:
break
dx_expected = 0
xp_j = xp[j]
if j == i:
dx_expected = xp_j * D1 / D0 - new_y
xavg = (xp_j + new_y) / 2
else:
dx_expected = xp_j - xp_j * D1 / D0
xavg = xp_j
dynamic_fee = self._dynamic_fee(xavg, ys, base_fee)
xp_reduced[j] = xp_j - dynamic_fee * dx_expected / FEE_DENOMINATOR
dy: uint256 = xp_reduced[i] - self.get_y_D(amp, i, xp_reduced, D1)
dy_0: uint256 = (xp[i] - new_y) * PRECISION / rates[i] # w/o fees
dy = (dy - 1) * PRECISION / rates[i] # Withdraw less to account for rounding errors
# update xp with new_y for p calculations.
xp[i] = new_y
return dy, dy_0 - dy, xp, amp, D1
```
```shell
>>> StableSwap.calc_withdraw_one_coin(10**18, 0)
1000107987022361129
>>> StableSwap.calc_withdraw_one_coin(10**18, 1)
999915
>>> StableSwap.get_balances()
[1156160050449617680048138, 1052703857609]
```
::::
---
## Fee Methods
Stableswap-ng introduces a dynamic fee based on the imbalance of the coins within the pool and their pegs:
```vyper
offpeg_fee_multiplier: public(uint256) # * 1e10
@view
@internal
def _dynamic_fee(xpi: uint256, xpj: uint256, _fee: uint256) -> uint256:
_offpeg_fee_multiplier: uint256 = self.offpeg_fee_multiplier
if _offpeg_fee_multiplier <= FEE_DENOMINATOR:
return _fee
xps2: uint256 = (xpi + xpj) **2
return (
(_offpeg_fee_multiplier * _fee) /
((_offpeg_fee_multiplier - FEE_DENOMINATOR) * 4 * xpi * xpj / xps2 + FEE_DENOMINATOR)
)
```
More on dynamic fees [here](../overview.md#dynamic-fees).
### `fee`
::::description[`StableSwap.fee() -> uint256: view`]
Getter method for the fee of the pool. This is the value set when initializing the contract and can be changed via [`set_new_fee`](#set_new_fee).
Returns: fee (`uint256`).
```vyper
fee: public(uint256) # fee * 1e10
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
self.fee = _fee
...
```
```shell
>>> StableSwap.fee()
1000000
```
:::note
The method returns an integer with with 1e10 precision.
:::
::::
### `dynamic_fee`
::::description[`StableSwap.dynamic_fee(i: int128, j: int128) -> uint256:`]
Getter for the swap fee when exchanging between `i` and `j`. The swap fee is expressed as an integer with a 1e10 precision.
Returns: dynamic fee (`uint256`).
| Input | Type | Description |
| ------ | -------- | ------------------------------------------ |
| `i` | `int128` | Index value of input coin. |
| `j` | `int128` | Index value of output coin. |
```vyper
@view
@external
def dynamic_fee(i: int128, j: int128) -> uint256:
"""
@notice Return the fee for swapping between `i` and `j`
@param i Index value for the coin to send
@param j Index value of the coin to recieve
@return Swap fee expressed as an integer with 1e10 precision
"""
return StableSwapViews(factory.views_implementation()).dynamic_fee(i, j, self)
```
```vyper
@view
@external
def dynamic_fee(i: int128, j: int128, pool:address) -> uint256:
"""
@notice Return the fee for swapping between `i` and `j`
@param i Index value for the coin to send
@param j Index value of the coin to recieve
@return Swap fee expressed as an integer with 1e10 precision
"""
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
fee: uint256 = StableSwapNG(pool).fee()
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS)
return self._dynamic_fee(xp[i], xp[j], fee, fee_multiplier)
@view
@internal
def _dynamic_fee(xpi: uint256, xpj: uint256, _fee: uint256, _fee_multiplier: uint256) -> uint256:
if _fee_multiplier <= FEE_DENOMINATOR:
return _fee
xps2: uint256 = (xpi + xpj) **2
return (
(_fee_multiplier * _fee) /
((_fee_multiplier - FEE_DENOMINATOR) * 4 * xpi * xpj / xps2 + FEE_DENOMINATOR)
)
@view
@internal
def _get_rates_balances_xp(pool: address, N_COINS: uint256) -> (
DynArray[uint256, MAX_COINS],
DynArray[uint256, MAX_COINS],
DynArray[uint256, MAX_COINS],
):
rates: DynArray[uint256, MAX_COINS] = StableSwapNG(pool).stored_rates()
balances: DynArray[uint256, MAX_COINS] = StableSwapNG(pool).get_balances()
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
for idx in range(MAX_COINS):
if idx == N_COINS:
break
xp.append(rates[idx] * balances[idx] / PRECISION)
return rates, balances, xp
```
```shell
>>> StableSwap.dynamic_fee(0, 1)
1001758
```
:::note
The method returns an integer with with 1e10 precision.
:::
::::
### `admin_fee`
::::description[`StableSwap.admin_fee() -> uint256: view`]
Getter for the admin fee. It is a constant and is set to 50% (5000000000).
Returns: admin fee (`uint256`).
```vyper
admin_fee: public(constant(uint256)) = 5000000000
```
```shell
>>> StableSwap.admin_fee()
5000000000
```
:::note
The method returns an integer with with 1e10 precision.
:::
::::
### `offpeg_fee_multiplier`
::::description[`StableSwap.offpeg_fee_multiplier() -> uint256: view`]
Getter method for the off-peg fee multiplier. This value determines how much the fee increases when assets within the AMM depeg. This value can be changed via [`set_new_fee`](#set_new_fee).
Returns: offpeg fee multiplier (`uint256`)
```vyper
offpeg_fee_multiplier: public(uint256) # * 1e10
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
self.offpeg_fee_multiplier = _offpeg_fee_multiplier
...
```
```shell
>>> StableSwap.offpeg_fee_multiplier()
50000000000
```
:::note
The method returns an integer with with 1e10 precision.
:::
::::
### `stored_rates`
::::description[`StableSwap.stored_rates() -> DynArray[uint256, MAX_COINS]:`]
Getter for the rate multiplier of each coin.
Returns: stored rates (`DynArray[uint256, MAX_COINS]`).
:::info
If the coin has a rate oracle that has been properly initialized, this method queries that rate by static-calling an external contract.
:::
```vyper
rate_multipliers: immutable(DynArray[uint256, MAX_COINS])
# [bytes4 method_id][bytes8 ][bytes20 oracle]
oracles: DynArray[uint256, MAX_COINS]
@view
@external
def stored_rates() -> DynArray[uint256, MAX_COINS]:
return self._stored_rates()
@view
@internal
def _stored_rates() -> DynArray[uint256, MAX_COINS]:
"""
@notice Gets rate multipliers for each coin.
@dev If the coin has a rate oracle that has been properly initialised,
this method queries that rate by static-calling an external
contract.
"""
rates: DynArray[uint256, MAX_COINS] = rate_multipliers
oracles: DynArray[uint256, MAX_COINS] = self.oracles
for i in range(MAX_COINS_128):
if i == N_COINS_128:
break
if asset_types[i] == 1 and not oracles[i] == 0:
# NOTE: fetched_rate is assumed to be 10**18 precision
fetched_rate: uint256 = convert(
raw_call(
convert(oracles[i] % 2**160, address),
_abi_encode(oracles[i] & ORACLE_BIT_MASK),
max_outsize=32,
is_static_call=True,
),
uint256
)
rates[i] = unsafe_div(rates[i] * fetched_rate, PRECISION)
elif asset_types[i] == 3: # ERC4626
# fetched_rate: uint256 = ERC4626(coins[i]).convertToAssets(call_amount[i]) * scale_factor[i]
# here: call_amount has ERC4626 precision, but the returned value is scaled up to 18
# using scale_factor which is (18 - n) if underlying asset has n decimals.
rates[i] = unsafe_div(
rates[i] * ERC4626(coins[i]).convertToAssets(call_amount[i]) * scale_factor[i],
PRECISION
) # 1e18 precision
return rates
```
```shell
>>> StableSwap.stored_rates()
[1000000000000000000, 1000000000000000000000000000000]
```
::::
### `admin_balances`
::::description[`StableSwap.admin_balances(arg0: uint256) -> uint256: view`]
Getter for the accumulated admin balance of the pool for a coin. These values essentially represent the claimable admin fee.
Returns: admin balances (`uint256`).
| Input | Type | Description |
| ------ | --------- | ------------------------------------------ |
| `arg0` | `uint256` | Index value of the coin. |
```vyper
admin_balances: public(DynArray[uint256, MAX_COINS])
```
```shell
>>> StableSwap.admin_balances(0)
38117658162246205676
>>> StableSwap.admin_balances(1)
10683574
```
::::
### `withdraw_admin_fees`
::::description[`StableSwap.withdraw_admin_fees():`]
Function to withdraw accumulated admin fees from the pool and send them to the `fee_receiver` set in the Factory.
```vyper
interface Factory:
def fee_receiver() -> address: view
def admin() -> address: view
def views_implementation() -> address: view
admin_balances: public(DynArray[uint256, MAX_COINS])
@external
def withdraw_admin_fees():
"""
@notice Claim admin fees. Callable by anyone.
"""
self._withdraw_admin_fees()
@internal
def _withdraw_admin_fees():
fee_receiver: address = factory.fee_receiver()
assert fee_receiver != empty(address) # dev: fee receiver not set
admin_balances: DynArray[uint256, MAX_COINS] = self.admin_balances
for i in range(MAX_COINS_128):
if i == N_COINS_128:
break
if admin_balances[i] > 0:
self._transfer_out(i, admin_balances[i], fee_receiver)
admin_balances[i] = 0
self.admin_balances = admin_balances
```
```shell
>>> StableSwap.withdraw_admin_fees()
```
::::
---
## Amplification Coefficient
The amplification coefficient **`A`** determines a pool’s tolerance for imbalance between the assets within it. A higher value means that trades will incur slippage sooner as the assets within the pool become imbalanced.
The appropriate value for A is dependent upon the type of coin being used within the pool, and is subject to optimisation and pool-parameter update based on the market history of the trading pair. It is possible to modify the amplification coefficient for a pool after it has been deployed. This can be done via the `ramp_A` function. See [admin controls](#ramp_a).
When a ramping of A has been initialized, the process can be stopped by calling the function [`stop_ramp_A()`](#stop_ramp_a).
### `A`
::::description[`StableSwap.A() -> uint256:`]
Getter for the amplification coefficient A.
Returns: A (`uint256`).
```vyper
A_PRECISION: constant(uint256) = 100
MAX_A: constant(uint256) = 10 **6
MAX_A_CHANGE: constant(uint256) = 10
initial_A: public(uint256)
future_A: public(uint256)
initial_A_time: public(uint256)
future_A_time: public(uint256)
@view
@external
def A() -> uint256:
return self._A() / A_PRECISION
@view
@internal
def _A() -> uint256:
"""
Handle ramping A up or down
"""
t1: uint256 = self.future_A_time
A1: uint256 = self.future_A
if block.timestamp < t1:
A0: uint256 = self.initial_A
t0: uint256 = self.initial_A_time
# Expressions in uint256 cannot have negative numbers, thus "if"
if A1 > A0:
return A0 + (A1 - A0) * (block.timestamp - t0) / (t1 - t0)
else:
return A0 - (A0 - A1) * (block.timestamp - t0) / (t1 - t0)
else: # when t1 == 0 or block.timestamp >= t1
return A1
```
```shell
>>> StableSwap.A()
500
```
::::
### `A_precise`
::::description[`StableSwap.A_precise() -> uint256:`]
Getter for the precise A value, which is not divided by `A_PRECISION` unlike `A()`.
Returns: precise A (`uint256`).
```vyper
A_PRECISION: constant(uint256) = 100
MAX_A: constant(uint256) = 10 **6
MAX_A_CHANGE: constant(uint256) = 10
initial_A: public(uint256)
future_A: public(uint256)
initial_A_time: public(uint256)
future_A_time: public(uint256)
@view
@external
def A_precise() -> uint256:
return self._A()
@view
@internal
def _A() -> uint256:
"""
Handle ramping A up or down
"""
t1: uint256 = self.future_A_time
A1: uint256 = self.future_A
if block.timestamp < t1:
A0: uint256 = self.initial_A
t0: uint256 = self.initial_A_time
# Expressions in uint256 cannot have negative numbers, thus "if"
if A1 > A0:
return A0 + (A1 - A0) * (block.timestamp - t0) / (t1 - t0)
else:
return A0 - (A0 - A1) * (block.timestamp - t0) / (t1 - t0)
else: # when t1 == 0 or block.timestamp >= t1
return A1
```
```shell
>>> StableSwap.A_precise()
50000
```
::::
### `initial_A`
::::description[`StableSwap.initial_A() -> uint256: view`]
Getter for the initial A value.
Returns: initial A (`uint256`).
```vyper
A_PRECISION: constant(uint256) = 100
MAX_A: constant(uint256) = 10 **6
MAX_A_CHANGE: constant(uint256) = 10
initial_A: public(uint256)
future_A: public(uint256)
initial_A_time: public(uint256)
future_A_time: public(uint256)
@external
def ramp_A(_future_A: uint256, _future_time: uint256):
assert msg.sender == factory.admin() # dev: only owner
assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME
assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time
_initial_A: uint256 = self._A()
_future_A_p: uint256 = _future_A * A_PRECISION
assert _future_A > 0 and _future_A < MAX_A
if _future_A_p < _initial_A:
assert _future_A_p * MAX_A_CHANGE >= _initial_A
else:
assert _future_A_p <= _initial_A * MAX_A_CHANGE
self.initial_A = _initial_A
self.future_A = _future_A_p
self.initial_A_time = block.timestamp
self.future_A_time = _future_time
log RampA(_initial_A, _future_A_p, block.timestamp, _future_time)
```
```shell
>>> StableSwap.initial_A()
50000
```
::::
### `future_A`
::::description[`StableSwap.future_A() -> uint256: view`]
Getter for the future A value. This value is adjusted when ramping A.
Returns: future A (`uint256`).
```vyper
A_PRECISION: constant(uint256) = 100
MAX_A: constant(uint256) = 10 **6
MAX_A_CHANGE: constant(uint256) = 10
initial_A: public(uint256)
future_A: public(uint256)
initial_A_time: public(uint256)
future_A_time: public(uint256)
@external
def ramp_A(_future_A: uint256, _future_time: uint256):
assert msg.sender == factory.admin() # dev: only owner
assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME
assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time
_initial_A: uint256 = self._A()
_future_A_p: uint256 = _future_A * A_PRECISION
assert _future_A > 0 and _future_A < MAX_A
if _future_A_p < _initial_A:
assert _future_A_p * MAX_A_CHANGE >= _initial_A
else:
assert _future_A_p <= _initial_A * MAX_A_CHANGE
self.initial_A = _initial_A
self.future_A = _future_A_p
self.initial_A_time = block.timestamp
self.future_A_time = _future_time
log RampA(_initial_A, _future_A_p, block.timestamp, _future_time)
```
```shell
>>> StableSwap.future_A()
50000
```
::::
### `initial_A_time`
::::description[`StableSwap.initial_A_time() -> uint256: view`]
Getter for the initial A time. This is the timestamp when ramping A was initialized.
Returns: initial A time (`uint256`).
```vyper
A_PRECISION: constant(uint256) = 100
MAX_A: constant(uint256) = 10 **6
MAX_A_CHANGE: constant(uint256) = 10
initial_A: public(uint256)
future_A: public(uint256)
initial_A_time: public(uint256)
future_A_time: public(uint256)
@external
def ramp_A(_future_A: uint256, _future_time: uint256):
assert msg.sender == factory.admin() # dev: only owner
assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME
assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time
_initial_A: uint256 = self._A()
_future_A_p: uint256 = _future_A * A_PRECISION
assert _future_A > 0 and _future_A < MAX_A
if _future_A_p < _initial_A:
assert _future_A_p * MAX_A_CHANGE >= _initial_A
else:
assert _future_A_p <= _initial_A * MAX_A_CHANGE
self.initial_A = _initial_A
self.future_A = _future_A_p
self.initial_A_time = block.timestamp
self.future_A_time = _future_time
log RampA(_initial_A, _future_A_p, block.timestamp, _future_time)
```
```shell
>>> StableSwap.initial_A_time()
0
```
::::
### `future_A_time`
::::description[`StableSwap.future_A_time() -> uint256: view`]
Getter for the future A time. This is the timestamp when ramping A should be finished.
Returns: future A time (`uint256`).
```vyper
A_PRECISION: constant(uint256) = 100
MAX_A: constant(uint256) = 10 **6
MAX_A_CHANGE: constant(uint256) = 10
initial_A: public(uint256)
future_A: public(uint256)
initial_A_time: public(uint256)
future_A_time: public(uint256)
@external
def ramp_A(_future_A: uint256, _future_time: uint256):
assert msg.sender == factory.admin() # dev: only owner
assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME
assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time
_initial_A: uint256 = self._A()
_future_A_p: uint256 = _future_A * A_PRECISION
assert _future_A > 0 and _future_A < MAX_A
if _future_A_p < _initial_A:
assert _future_A_p * MAX_A_CHANGE >= _initial_A
else:
assert _future_A_p <= _initial_A * MAX_A_CHANGE
self.initial_A = _initial_A
self.future_A = _future_A_p
self.initial_A_time = block.timestamp
self.future_A_time = _future_time
log RampA(_initial_A, _future_A_p, block.timestamp, _future_time)
```
```shell
>>> StableSwap.future_A_time()
0
```
::::
---
## Contract Info Methods
### `coins`
::::description[`StableSwap.coins(arg0: uint256) -> addresss: view`]
Getter for the coin at index `arg0` within the pool.
Returns: coin (`address`).
```vyper
coins: public(immutable(DynArray[address, MAX_COINS]))
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
coins = _coins
...
```
```shell
>>> StableSwap.coins(0)
'0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'
>>> StableSwap.coins(1)
'0x0E573Ce2736Dd9637A0b21058352e1667925C7a8'
```
::::
### `balances`
::::description[`StableSwap.balances(i: uint256) -> uint256:`]
Getter for the current balance of coin `i` within the pool.
Returns: coin balance (`uint256`).
| Input | Type | Description |
| ------ | --------- | --------------------------- |
| `i` | `uint256` | Index value of the coin. |
```vyper
@view
@external
def balances(i: uint256) -> uint256:
"""
@notice Get the current balance of a coin within the
pool, less the accrued admin fees
@param i Index value for the coin to query balance of
@return Token balance
"""
return self._balances()[i]
@view
@internal
def _balances() -> DynArray[uint256, MAX_COINS]:
"""
@notice Calculates the pool's balances _excluding_ the admin's balances.
@dev If the pool contains rebasing tokens, this method ensures LPs keep all
rebases and admin only claims swap fees. This also means that, since
admin's balances are stored in an array and not inferred from read balances,
the fees in the rebasing token that the admin collects is immune to
slashing events.
"""
result: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances_i: uint256 = 0
for i in range(MAX_COINS_128):
if i == N_COINS_128:
break
if POOL_IS_REBASING_IMPLEMENTATION:
balances_i = ERC20(coins[i]).balanceOf(self) - self.admin_balances[i]
else:
balances_i = self.stored_balances[i] - self.admin_balances[i]
result.append(balances_i)
return result
```
```shell
>>> StableSwap.balances(0)
1156160050449617680048138
>>> StableSwap.balances(1)
1052703857609
```
::::
### `get_balances`
::::description[`StableSwap.get_balances() -> DynArray[uint256, MAX_COINS]:`]
Getter for an array with all coin balances in the pool.
Returns: coin balances (`DynArray[uint256, MAX_COINS]`).
```vyper
@view
@external
def get_balances() -> DynArray[uint256, MAX_COINS]:
return self._balances()
@view
@internal
def _balances() -> DynArray[uint256, MAX_COINS]:
"""
@notice Calculates the pool's balances _excluding_ the admin's balances.
@dev If the pool contains rebasing tokens, this method ensures LPs keep all
rebases and admin only claims swap fees. This also means that, since
admin's balances are stored in an array and not inferred from read balances,
the fees in the rebasing token that the admin collects is immune to
slashing events.
"""
result: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances_i: uint256 = 0
for i in range(MAX_COINS_128):
if i == N_COINS_128:
break
if POOL_IS_REBASING_IMPLEMENTATION:
balances_i = ERC20(coins[i]).balanceOf(self) - self.admin_balances[i]
else:
balances_i = self.stored_balances[i] - self.admin_balances[i]
result.append(balances_i)
return result
```
```shell
>>> StableSwap.get_balances()
[1156160050449617680048138, 1052703857609]
```
:::note
The returned values do not take admin fees into account.
:::
::::
### `N_COINS`
::::description[`StableSwap.N_COINS() -> uint256: view`]
Getter for the total number of coins in the pool.
Returns: number of coins (`uint256`).
:::info
There can be a maximum of 8 coins per pool due to `MAX_COINS = 8`.
:::
```vyper
MAX_COINS: constant(uint256) = 8 # max coins is 8 in the factory
N_COINS: public(immutable(uint256))
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
coins = _coins
__n_coins: uint256 = len(_coins)
N_COINS = __n_coins
N_COINS_128 = convert(__n_coins, int128)
...
```
```shell
>>> StableSwap.N_COINS()
2
```
::::
### `totalSupply`
::::description[`StableSwap.totalSupply() -> uint256:`]
Getter for the total supply of the LP token.
Returns: total supply (`uint256`).
```vyper
total_supply: uint256
@view
@external
@nonreentrant('lock')
def totalSupply() -> uint256:
"""
@notice The total supply of pool LP tokens
@return self.total_supply, 18 decimals.
"""
return self.total_supply
```
```shell
>>> StableSwap.totalSupply()
2208717767450789394892159
```
::::
---
## LP Token
**Pool and LP tokens are the same smart contract.** The pool itself acts as an LP Token. When coins are deposited into a Curve pool, the depositor receives pool LP (liquidity provider) tokens in return. Each Curve pool has its unique ERC20 contract representing these LP tokens, making them transferable. Holding these LP tokens allows for their deposit and staking in the pool's liquidity gauge, earning CRV token rewards. Additionally, if a metapool supports the LP token, it can be deposited there to receive the metapool's distinct LP tokens.
### `transfer`
::::description[`StableSwap.transfer(_to : address, _value : uint256) -> bool:`]
Function to transfer `_value` tokens to `_to`.
Returns: true (`bool`).
Emits: `Transfer`
| Input | Type | Description |
| ----------- | -------| ----|
| `_to` | `address` | address to transfer token to |
| `_value` | `uint256` | amount of tokens to transfer |
```vyper
event Transfer:
sender: indexed(address)
receiver: indexed(address)
value: uint256
@external
def transfer(_to : address, _value : uint256) -> bool:
"""
@dev Transfer token for a specified address
@param _to The address to transfer to.
@param _value The amount to be transferred.
"""
self._transfer(msg.sender, _to, _value)
return True
@internal
def _transfer(_from: address, _to: address, _value: uint256):
# # NOTE: vyper does not allow underflows
# # so the following subtraction would revert on insufficient balance
self.balanceOf[_from] -= _value
self.balanceOf[_to] += _value
log Transfer(_from, _to, _value)
```
::::
### `transferFrom`
::::description[`StableSwap.transferFrom(_from : address, _to : address, _value : uint256) -> bool:`]
Function to transfer `_value` tokens from `_from` to `_to`.
Returns: true (`bool`).
Emits: `Transfer`
| Input | Type | Description |
| ----------- | -------| ----|
| `_from` | `address` | address to transfer token from |
| `_to` | `address` | address to transfer token to |
| `_value` | `uint256` | amount of tokens to transfer |
```vyper
event Transfer:
sender: indexed(address)
receiver: indexed(address)
value: uint256
@external
def transferFrom(_from : address, _to : address, _value : uint256) -> bool:
"""
@dev Transfer tokens from one address to another.
@param _from address The address which you want to send tokens from
@param _to address The address which you want to transfer to
@param _value uint256 the amount of tokens to be transferred
"""
self._transfer(_from, _to, _value)
_allowance: uint256 = self.allowance[_from][msg.sender]
if _allowance != max_value(uint256):
self.allowance[_from][msg.sender] = _allowance - _value
return True
@internal
def _transfer(_from: address, _to: address, _value: uint256):
# # NOTE: vyper does not allow underflows
# # so the following subtraction would revert on insufficient balance
self.balanceOf[_from] -= _value
self.balanceOf[_to] += _value
log Transfer(_from, _to, _value)
```
::::
### `allowance`
::::description[`StableSwap.allowance(arg0: address, arg1: address) -> uint256: view`]
Getter method to check the allowance of `arg0` for funds of `arg1`.
Returns: allowed amount (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `address` | Address of the spender |
| `arg1` | `address` | Address of the token owner |
```vyper
allowance: public(HashMap[address, HashMap[address, uint256]])
```
::::
### `approve`
::::description[`StableSwap.approve(_spender : address, _value : uint256) -> bool:`]
Function to approve `_spender` to transfer `_value` of tokens on behalf of `msg.sender`
Returns: true (`bool`).
Emits: `Approval`
| Input | Type | Description |
|-------------|-----------|---------------------------------|
| `_spender` | `address` | Address of the approved spender |
| `_value` | `uint256` | Amount of tokens to approve |
```vyper
event Approval:
owner: indexed(address)
spender: indexed(address)
value: uint256
@external
def approve(_spender : address, _value : uint256) -> bool:
"""
@notice Approve the passed address to transfer the specified amount of
tokens on behalf of msg.sender
@dev Beware that changing an allowance via this method brings the risk that
someone may use both the old and new allowance by unfortunate transaction
ordering: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
@param _spender The address which will transfer the funds
@param _value The amount of tokens that may be transferred
@return bool success
"""
self.allowance[msg.sender][_spender] = _value
log Approval(msg.sender, _spender, _value)
return True
```
::::
### `permit`
::::description[`StableSwap.permit(_owner: address, _spender: address, _value: uint256, _deadline: uint256, _v: uint8, _r: bytes32, _s: bytes32) -> bool:`]
Function to permit `spender` to spend up to `_value` amount of `_owner`'s tokens via a signature.
Returns: true (`bool`).
Emits: `Approval`
| Input | Type | Description |
| ----------- | -------| ----|
| `_owner` | `address` | Account which generated the signature and is granting an allowance |
| `_spender` | `address` | Account which will be granted an allowance |
| `_value` | `uint256` | Amount to approve |
| `_deadline` | `uint256` | Deadline by which signature must be submitted |
| `_v` | `uint8` | The last byte of the ECDSA signature |
| `_r` | `bytes32` | The first 32 bytes of the ECDSA signature |
| `_s` | `bytes32` | The second 32 bytes of the ECDSA signature |
```vyper
event Approval:
owner: indexed(address)
spender: indexed(address)
value: uint256
@external
def permit(
_owner: address,
_spender: address,
_value: uint256,
_deadline: uint256,
_v: uint8,
_r: bytes32,
_s: bytes32
) -> bool:
"""
@notice Approves spender by owner's signature to expend owner's tokens.
See https://eips.ethereum.org/EIPS/eip-2612.
@dev Inspired by https://github.com/yearn/yearn-vaults/blob/main/contracts/Vault.vy#L753-L793
@dev Supports smart contract wallets which implement ERC1271
https://eips.ethereum.org/EIPS/eip-1271
@param _owner The address which is a source of funds and has signed the Permit.
@param _spender The address which is allowed to spend the funds.
@param _value The amount of tokens to be spent.
@param _deadline The timestamp after which the Permit is no longer valid.
@param _v The bytes[64] of the valid secp256k1 signature of permit by owner
@param _r The bytes[0:32] of the valid secp256k1 signature of permit by owner
@param _s The bytes[32:64] of the valid secp256k1 signature of permit by owner
@return True, if transaction completes successfully
"""
assert _owner != empty(address)
assert block.timestamp <= _deadline
nonce: uint256 = self.nonces[_owner]
digest: bytes32 = keccak256(
concat(
b"\x19\x01",
self._domain_separator(),
keccak256(_abi_encode(EIP2612_TYPEHASH, _owner, _spender, _value, nonce, _deadline))
)
)
if _owner.is_contract:
sig: Bytes[65] = concat(_abi_encode(_r, _s), slice(convert(_v, bytes32), 31, 1))
# reentrancy not a concern since this is a staticcall
assert ERC1271(_owner).isValidSignature(digest, sig) == ERC1271_MAGIC_VAL
else:
assert ecrecover(digest, convert(_v, uint256), convert(_r, uint256), convert(_s, uint256)) == _owner
self.allowance[_owner][_spender] = _value
self.nonces[_owner] = nonce + 1
log Approval(_owner, _spender, _value)
return True
```
::::
### `name`
::::description[`StableSwap.name() -> String[64]: view`]
Getter for the name of the LP token.
Returns: name (`String[64]`).
```vyper
name: public(immutable(String[64]))
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
name = _name
...
```
```shell
>>> StableSwap.name()
'USDV-crvUSD'
```
::::
### `symbol`
::::description[`StableSwap.symbol() -> String[32]: view`]
Getter for the symbol of the LP token.
Returns: symbol (`String[32]`).
```vyper
symbol: public(immutable(String[32]))
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
symbol = _symbol
...
```
```shell
>>> StableSwap.symbol()
'USDVcrvUSD'
```
::::
### `decimals`
::::description[`StableSwap.decimals() -> uint8: view`]
Getter for the decimals of the LP token.
Returns: decimals (uint8).
```vyper
decimals: public(constant(uint8)) = 18
```
```shell
>>> StableSwap.decimals()
18
```
::::
### `version`
::::description[`StableSwap.version() -> String[8]: view`]
Getter for the version of the LP token.
Returns: version (`String[8]`).
```vyper
version: public(constant(String[8])) = "v7.0.0"
```
```shell
>>> StableSwap.version()
"v7.0.0"
```
::::
### `balanceOf`
::::description[`StableSwap.balanceOf(arg0: address) -> uint256: view`]
Getter for the LP token balance of `arg0`.
Returns: token balance (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `address` | address to check the balance of |
```vyper
balanceOf: public(HashMap[address, uint256])
```
```shell
>>> StableSwap.balanceOf("0x7a16fF8270133F063aAb6C9977183D9e72835428")
999808484451757093697730
```
::::
### `nonces`
::::description[`StableSwap.nonces(arg0: address) -> uint256: view`]
Getter for the nonce.
Returns: nonces (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `address` | address |
```vyper
nonces: public(HashMap[address, uint256])
```
::::
### `salt`
::::description[`StableSwap.salt() -> bytes32: view`]
Getter for the salt of the LP token.
Returns: salt (`bytes32`).
```vyper
salt: public(immutable(bytes32))
@external
def __init__(
_name: String[32],
_symbol: String[10],
_A: uint256,
_fee: uint256,
_offpeg_fee_multiplier: uint256,
_ma_exp_time: uint256,
_coins: DynArray[address, MAX_COINS],
_rate_multipliers: DynArray[uint256, MAX_COINS],
_asset_types: DynArray[uint8, MAX_COINS],
_method_ids: DynArray[bytes4, MAX_COINS],
_oracles: DynArray[address, MAX_COINS],
):
...
# EIP712 related params -----------------
NAME_HASH = keccak256(name)
salt = block.prevhash
CACHED_CHAIN_ID = chain.id
CACHED_DOMAIN_SEPARATOR = keccak256(
_abi_encode(
EIP712_TYPEHASH,
NAME_HASH,
VERSION_HASH,
chain.id,
self,
salt,
)
)
...
```
```shell
>>> StableSwap.salt()
HexBytes('0x814188b56f08130fe7b283343b64baa08f4d207229dc52776968b62b977c8f46')
```
::::
### `DOMAIN_SEPARATOR`
::::description[`StableSwap.DOMAIN_SEPARATOR() -> bytes32: view`]
Getter for the domain separator.
Returns: domain separator (`bytes32`).
```vyper
CACHED_DOMAIN_SEPARATOR: immutable(bytes32)
@view
@external
def DOMAIN_SEPARATOR() -> bytes32:
"""
@notice EIP712 domain separator.
@return bytes32 Domain Separator set for the current chain.
"""
return self._domain_separator()
@view
@internal
def _domain_separator() -> bytes32:
if chain.id != CACHED_CHAIN_ID:
return keccak256(
_abi_encode(
EIP712_TYPEHASH,
NAME_HASH,
VERSION_HASH,
chain.id,
self,
salt,
)
)
return CACHED_DOMAIN_SEPARATOR
```
```shell
>>> StableSwap.DOMAIN_SEPARATOR()
HexBytes('0xf60903716a331f2ad023b28477aceee88e5180cab4694c497f4f9cefac657989')
```
::::
---
## Admin Controls
The following methods are guarded and may only be called by the **`admin`** of the Stableswap-NG Factory.
### `ramp_A`
::::description[`StableSwap.ramp_A(_future_A: uint256, _future_time: uint256):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory.
:::
Function to ramp amplification coefficient A. Minimum ramp time is 86400 (24h).
*Limitations when ramping A:*
- `block.timestamp` >= `initial_A_time` + `MIN_RAMP_TIME`
- `_future_time` >= `block.timestamp` + `MIN_RAMP_TIME`
- `future_A` > 0
- `future_A` < `MAX_A (1000000)`
Emits: `RampA`
| Input | Type | Description |
| ----------- | -------| ----|
| `_future_A` | `uint256` | future A value |
| `_future_time` | `uint256` | timestamp until ramping should occur; needs to be at least 24h (`MIN_RAMP_TIME`) |
```vyper
A_PRECISION: constant(uint256) = 100
MAX_A: constant(uint256) = 10 **6
MAX_A_CHANGE: constant(uint256) = 10
MIN_RAMP_TIME: constant(uint256) = 86400
event RampA:
old_A: uint256
new_A: uint256
initial_time: uint256
future_time: uint256
@external
def ramp_A(_future_A: uint256, _future_time: uint256):
assert msg.sender == factory.admin() # dev: only owner
assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME
assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time
_initial_A: uint256 = self._A()
_future_A_p: uint256 = _future_A * A_PRECISION
assert _future_A > 0 and _future_A < MAX_A
if _future_A_p < _initial_A:
assert _future_A_p * MAX_A_CHANGE >= _initial_A
else:
assert _future_A_p <= _initial_A * MAX_A_CHANGE
self.initial_A = _initial_A
self.future_A = _future_A_p
self.initial_A_time = block.timestamp
self.future_A_time = _future_time
log RampA(_initial_A, _future_A_p, block.timestamp, _future_time)
```
::::
### `stop_ramp_A`
::::description[`StableSwap.stop_ramp_A():`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory.
:::
Function to immediately stop the ramping A. The current value during the ramping process will be finalized as `A`.
Emits: `StopRampA`
```vyper
event StopRampA:
A: uint256
t: uint256
@external
def stop_ramp_A():
assert msg.sender == factory.admin() # dev: only owner
current_A: uint256 = self._A()
self.initial_A = current_A
self.future_A = current_A
self.initial_A_time = block.timestamp
self.future_A_time = block.timestamp
# now (block.timestamp < t1) is always False, so we return saved A
log StopRampA(current_A, block.timestamp)
```
::::
### `set_new_fee`
::::description[`StableSwap.set_new_fee(_new_fee: uint256, _new_offpeg_fee_multiplier: uint256):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory.
:::
Function to set new values for `fee` and `offpeg_fee_multiplier`.
*Limitations when setting new parameters:*
- `_new_fee` <= `MAX_FEE` (5000000000)
- `_new_offpeg_fee_multiplier` * `_new_fee` <= `MAX_FEE` * `FEE_DENOMINATOR`
Emits: `ApplyNewFee`
| Input | Type | Description |
| ----------- | -------| ----|
| `_new_fee` | `uint256` | new fee |
| `_new_offpeg_fee_multiplier` | `uint256` | new off-peg fee multiplier |
```vyper
MAX_FEE: constant(uint256) = 5 * 10 **9
FEE_DENOMINATOR: constant(uint256) = 10 **10
event ApplyNewFee:
fee: uint256
offpeg_fee_multiplier: uint256
@external
def set_new_fee(_new_fee: uint256, _new_offpeg_fee_multiplier: uint256):
assert msg.sender == factory.admin()
# set new fee:
assert _new_fee <= MAX_FEE
self.fee = _new_fee
# set new offpeg_fee_multiplier:
assert _new_offpeg_fee_multiplier * _new_fee <= MAX_FEE * FEE_DENOMINATOR # dev: offpeg multiplier exceeds maximum
self.offpeg_fee_multiplier = _new_offpeg_fee_multiplier
log ApplyNewFee(_new_fee, _new_offpeg_fee_multiplier)
```
::::
### `set_ma_exp_time`
::::description[`StableSwap.set_ma_exp_time(_ma_exp_time: uint256, _D_ma_time: uint256):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory.
:::
Function to set the moving average window for `ma_exp_time` and `D_ma_time`.
*Limitations when setting new fee parameters:*
- `_ma_exp_time` and `_D_ma_time` > 0
| Input | Type | Description |
| ----------- | -------| ----|
| `_ma_exp_time` | `uint256` | new ma exp time |
| `_D_ma_time` | `uint256` | new D ma time |
```vyper
@external
def set_ma_exp_time(_ma_exp_time: uint256, _D_ma_time: uint256):
"""
@notice Set the moving average window of the price oracles.
@param _ma_exp_time Moving average window. It is time_in_seconds / ln(2)
"""
assert msg.sender == factory.admin() # dev: only owner
assert 0 not in [_ma_exp_time, _D_ma_time]
self.ma_exp_time = _ma_exp_time
self.D_ma_time = _D_ma_time
```
::::
---
## Math Contract
The Math Contract provides **AMM Math** for Stableswap-NG Pools.
:::deploy[Contract Source & Deployment]
Source code available on [GitHub](https://github.com/curvefi/stableswap-ng/blob/bff1522b30819b7b240af17ccfb72b0effbf6c47/contracts/main/CurveStableSwapNGMath.vy).
All Math contract deployments can be found in the [Deployment Addresses](../../../deployments.md) section.
:::
### `get_y`
::::description[`Math.get_y(i: int128, j: int128, x: uint256, xp: DynArray[uint256, MAX_COINS], _amp: uint256, _D: uint256, _n_coins: uint256) -> uint256:`]
Function to calculate how much coins `j` a user receives when providing `x` amount of coin `i`. This is done by solving quadratic equations iteratively.
Returns: amount of output coins to receive (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | index value of input coin |
| `j` | `int128` | index value of output coin |
| `x` | `uint256` | amount of input coin |
| `xp` | `DynArray[uint256, MAX_COINS]` | current balances of the coins in the pool |
| `_amp` | `uint256` | amplification coefficient |
| `_D` | `uint256` | D invariant |
| `_n_coins` | `uint256` | number of coins |
```vyper
MAX_COINS: constant(uint256) = 8
MAX_COINS_128: constant(int128) = 8
A_PRECISION: constant(uint256) = 100
@external
@pure
def get_y(
i: int128,
j: int128,
x: uint256,
xp: DynArray[uint256, MAX_COINS],
_amp: uint256,
_D: uint256,
_n_coins: uint256
) -> uint256:
"""
Calculate x[j] if one makes x[i] = x
Done by solving quadratic equation iteratively.
x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D **(n + 1) / (n **(2 * n) * prod' * A)
x_1**2 + b*x_1 = c
x_1 = (x_1**2 + c) / (2*x_1 + b)
"""
# x in the input is converted to the same price/precision
n_coins_128: int128 = convert(_n_coins, int128)
assert i != j # dev: same coin
assert j >= 0 # dev: j below zero
assert j < n_coins_128 # dev: j above N_COINS
# should be unreachable, but good for safety
assert i >= 0
assert i < n_coins_128
amp: uint256 = _amp
D: uint256 = _D
S_: uint256 = 0
_x: uint256 = 0
y_prev: uint256 = 0
c: uint256 = D
Ann: uint256 = amp * _n_coins
for _i in range(MAX_COINS_128):
if _i == n_coins_128:
break
if _i == i:
_x = x
elif _i != j:
_x = xp[_i]
else:
continue
S_ += _x
c = c * D / (_x * _n_coins)
c = c * D * A_PRECISION / (Ann * _n_coins)
b: uint256 = S_ + D * A_PRECISION / Ann # - D
y: uint256 = D
for _i in range(255):
y_prev = y
y = (y*y + c) / (2 * y + b - D)
# Equality with the precision of 1
if y > y_prev:
if y - y_prev <= 1:
return y
else:
if y_prev - y <= 1:
return y
raise
```
```shell
>>> Math.get_y('todo')
'todo'
```
::::
### `get_D`
::::description[`Math.get_D(_xp: DynArray[uint256, MAX_COINS], _amp: uint256, _n_coins: uint256) -> uint256:`]
Function to iteratively calculate the D invariant in non-overflowing integer operations.
Returns: D invariant (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `_xp` | `DynArray[uint256, MAX_COINS]` | current balances of the coins in the pool |
| `_amp` | `uint256` | amplification coefficient |
| `_n_coins` | `uint256` | number of coins |
```vyper
@external
@pure
def get_D(
_xp: DynArray[uint256, MAX_COINS],
_amp: uint256,
_n_coins: uint256
) -> uint256:
"""
D invariant calculation in non-overflowing integer operations
iteratively
A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i))
Converging solution:
D[j+1] = (A * n**n * sum(x_i) - D[j]**(n+1) / (n**n prod(x_i))) / (A * n**n - 1)
"""
S: uint256 = 0
for x in _xp:
S += x
if S == 0:
return 0
D: uint256 = S
Ann: uint256 = _amp * _n_coins
D_P: uint256 = 0
Dprev: uint256 = 0
for i in range(255):
D_P = D
for x in _xp:
D_P = D_P * D / (x * _n_coins) # If division by 0, this will be borked: only withdrawal will work. And that is good
Dprev = D
# (Ann * S / A_PRECISION + D_P * _n_coins) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (_n_coins + 1) * D_P)
D = (
(unsafe_div(Ann * S, A_PRECISION) + D_P * _n_coins) *
D / (
unsafe_div((Ann - A_PRECISION) * D, A_PRECISION) +
unsafe_add(_n_coins, 1) * D_P
)
)
# Equality with the precision of 1
if D > Dprev:
if D - Dprev <= 1:
return D
else:
if Dprev - D <= 1:
return D
# convergence typically occurs in 4 rounds or less, this should be unreachable!
# if it does happen the pool is borked and LPs can withdraw via `remove_liquidity`
raise
```
```shell
>>> Math.get_D('todo')
'todo'
```
::::
### `get_y_D`
::::description[`Math.get_y_D(A: uint256, i: int128, xp: DynArray[uint256, MAX_COINS], D: uint256, _n_coins: uint256) -> uint256:`]
Function to calculate how much of coin `i` will be in the pool when invariant `D` decreases.
Returns: balance of coin `i` (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `A` | `uint256` | amplification coefficient |
| `i` | `int128` | index value of coin |
| `xp` | `DynArray[uint256, MAX_COINS]` | current balances of the coins in the pool |
| `D` | `uint256` | D invariant |
| `_n_coins` | `uint256` | number of coins |
```vyper
@external
@pure
def get_y_D(
A: uint256,
i: int128,
xp: DynArray[uint256, MAX_COINS],
D: uint256,
_n_coins: uint256
) -> uint256:
"""
Calculate x[i] if one reduces D from being calculated for xp to D
Done by solving quadratic equation iteratively.
x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D **(n + 1) / (n **(2 * n) * prod' * A)
x_1**2 + b*x_1 = c
x_1 = (x_1**2 + c) / (2*x_1 + b)
"""
# x in the input is converted to the same price/precision
n_coins_128: int128 = convert(_n_coins, int128)
assert i >= 0 # dev: i below zero
assert i < n_coins_128 # dev: i above N_COINS
S_: uint256 = 0
_x: uint256 = 0
y_prev: uint256 = 0
c: uint256 = D
Ann: uint256 = A * _n_coins
for _i in range(MAX_COINS_128):
if _i == n_coins_128:
break
if _i != i:
_x = xp[_i]
else:
continue
S_ += _x
c = c * D / (_x * _n_coins)
c = c * D * A_PRECISION / (Ann * _n_coins)
b: uint256 = S_ + D * A_PRECISION / Ann
y: uint256 = D
for _i in range(255):
y_prev = y
y = (y*y + c) / (2 * y + b - D)
# Equality with the precision of 1
if y > y_prev:
if y - y_prev <= 1:
return y
else:
if y_prev - y <= 1:
return y
raise
```
```shell
>>> Math.get_y_D('todo')
'todo'
```
::::
### `exp`
::::description[`Math.exp(x: int256) -> uint256:`]
Function to calculate the natural exponential function of a signed integer with a precision of 1e18.
Returns: calculation result (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `x` | `int256` | 32-byte variable |
```vyper
@external
@pure
def exp(x: int256) -> uint256:
"""
@dev Calculates the natural exponential function of a signed integer with
a precision of 1e18.
@notice Note that this function consumes about 810 gas units. The implementation
is inspired by Remco Bloemen's implementation under the MIT license here:
https://xn--2-umb.com/22/exp-ln.
@dev This implementation is derived from Snekmate, which is authored
by pcaversaccio (Snekmate), distributed under the AGPL-3.0 license.
https://github.com/pcaversaccio/snekmate
@param x The 32-byte variable.
@return int256 The 32-byte calculation result.
"""
value: int256 = x
# If the result is `< 0.5`, we return zero. This happens when we have the following:
# "x <= floor(log(0.5e18) * 1e18) ~ -42e18".
if (x <= -42139678854452767551):
return empty(uint256)
# When the result is "> (2 **255 - 1) / 1e18" we cannot represent it as a signed integer.
# This happens when "x >= floor(log((2 **255 - 1) / 1e18) * 1e18) ~ 135".
assert x < 135305999368893231589, "wad_exp overflow"
# `x` is now in the range "(-42, 136) * 1e18". Convert to "(-42, 136) * 2 **96" for higher
# intermediate precision and a binary base. This base conversion is a multiplication with
# "1e18 / 2 **96 = 5 **18 / 2 **78".
value = unsafe_div(x << 78, 5 **18)
# Reduce the range of `x` to "(-½ ln 2, ½ ln 2) * 2 **96" by factoring out powers of two
# so that "exp(x) = exp(x') * 2 **k", where `k` is a signer integer. Solving this gives
# "k = round(x / log(2))" and "x' = x - k * log(2)". Thus, `k` is in the range "[-61, 195]".
k: int256 = unsafe_add(unsafe_div(value << 96, 54916777467707473351141471128), 2 **95) >> 96
value = unsafe_sub(value, unsafe_mul(k, 54916777467707473351141471128))
# Evaluate using a "(6, 7)"-term rational approximation. Since `p` is monic,
# we will multiply by a scaling factor later.
y: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 1346386616545796478920950773328), value) >> 96, 57155421227552351082224309758442)
p: int256 = unsafe_add(unsafe_mul(unsafe_add(unsafe_mul(unsafe_sub(unsafe_add(y, value), 94201549194550492254356042504812), y) >> 96,\
28719021644029726153956944680412240), value), 4385272521454847904659076985693276 << 96)
# We leave `p` in the "2 **192" base so that we do not have to scale it up
# again for the division.
q: int256 = unsafe_add(unsafe_mul(unsafe_sub(value, 2855989394907223263936484059900), value) >> 96, 50020603652535783019961831881945)
q = unsafe_sub(unsafe_mul(q, value) >> 96, 533845033583426703283633433725380)
q = unsafe_add(unsafe_mul(q, value) >> 96, 3604857256930695427073651918091429)
q = unsafe_sub(unsafe_mul(q, value) >> 96, 14423608567350463180887372962807573)
q = unsafe_add(unsafe_mul(q, value) >> 96, 26449188498355588339934803723976023)
# The polynomial `q` has no zeros in the range because all its roots are complex.
# No scaling is required, as `p` is already "2 **96" too large. Also,
# `r` is in the range "(0.09, 0.25) * 2**96" after the division.
r: int256 = unsafe_div(p, q)
# To finalise the calculation, we have to multiply `r` by:
# - the scale factor "s = ~6.031367120",
# - the factor "2 **k" from the range reduction, and
# - the factor "1e18 / 2 **96" for the base conversion.
# We do this all at once, with an intermediate result in "2**213" base,
# so that the final right shift always gives a positive value.
# Note that to circumvent Vyper's safecast feature for the potentially
# negative parameter value `r`, we first convert `r` to `bytes32` and
# subsequently to `uint256`. Remember that the EVM default behaviour is
# to use two's complement representation to handle signed integers.
return unsafe_mul(convert(convert(r, bytes32), uint256), 3822833074963236453042738258902158003155416615667) >> convert(unsafe_sub(195, k), uint256)
```
```shell
>>> Math.exp('todo')
'todo'
```
::::
---
## Views Contract
This contract contains **view-only external methods** which can be gas-inefficient when called from smart contracts.
:::deploy[Contract Source & Deployment]
Source code available on [GitHub](https://github.com/curvefi/stableswap-ng/blob/bff1522b30819b7b240af17ccfb72b0effbf6c47/contracts/main/CurveStableSwapNGViews.vy).
All Views contract deployments can be found in the [Deployment Addresses](../../../deployments.md) section.
:::
## Token Exchange Methods
### `get_dx`
::::description[`StableSwap.get_dx(i: int128, j: int128, dy: uint256, pool: address) -> uint256:`]
Function to calculate the predicted input amount `i` to receive `dy` of coin `j`.
Returns: predicted amount of `i` (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | index value of input coin |
| `j` | `int128` | index value of output coin |
| `dy` | `uint256` | amount of output coin received |
| `pool` | `address` | pool address |
```vyper
@view
@external
def get_dx(i: int128, j: int128, dy: uint256, pool: address) -> uint256:
"""
@notice Calculate the current input dx given output dy
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index value of the coin to receive
@param dy Amount of `j` being received after exchange
@return Amount of `i` predicted
"""
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
return self._get_dx(i, j, dy, pool, False, N_COINS)
@view
@internal
def _get_dx(
i: int128,
j: int128,
dy: uint256,
pool: address,
static_fee: bool,
N_COINS: uint256
) -> uint256:
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS)
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION
D: uint256 = self.get_D(xp, amp, N_COINS)
base_fee: uint256 = StableSwapNG(pool).fee()
dy_with_fee: uint256 = dy * rates[j] / PRECISION + 1
fee: uint256 = base_fee
if not static_fee:
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
fee = self._dynamic_fee(xp[i], xp[j], base_fee, fee_multiplier)
y: uint256 = xp[j] - dy_with_fee * FEE_DENOMINATOR / (FEE_DENOMINATOR - fee)
x: uint256 = self.get_y(j, i, y, xp, amp, D, N_COINS)
return (x - xp[i]) * PRECISION / rates[i]
```
```shell
>>> StableSwap.get_y('todo')
'todo'
```
::::
### `get_dy`
::::description[`StableSwap.get_dy(i: int128, j: int128, dx: uint256, pool: address) -> uint256:`]
Function to calculate the predicted output amount of coin `j` when exchanging `dx` of coin `i`.
Returns: predicted amount of `j` (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | index value of input coin |
| `j` | `int128` | index value of output coin |
| `dx` | `uint256` | amount of input coin being exchanged |
| `pool` | `address` | pool address |
```vyper
@view
@external
def get_dy(i: int128, j: int128, dx: uint256, pool: address) -> uint256:
"""
@notice Calculate the current output dy given input dx
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param dx Amount of `i` being exchanged
@return Amount of `j` predicted
"""
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS)
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION
D: uint256 = self.get_D(xp, amp, N_COINS)
x: uint256 = xp[i] + (dx * rates[i] / PRECISION)
y: uint256 = self.get_y(i, j, x, xp, amp, D, N_COINS)
dy: uint256 = xp[j] - y - 1
base_fee: uint256 = StableSwapNG(pool).fee()
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
fee: uint256 = self._dynamic_fee((xp[i] + x) / 2, (xp[j] + y) / 2, base_fee, fee_multiplier) * dy / FEE_DENOMINATOR
return (dy - fee) * PRECISION / rates[j]
@view
@internal
def get_y(
i: int128,
j: int128,
x: uint256,
xp: DynArray[uint256, MAX_COINS],
_amp: uint256,
_D: uint256,
N_COINS: uint256
) -> uint256:
"""
Calculate x[j] if one makes x[i] = x
Done by solving quadratic equation iteratively.
x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D **(n + 1) / (n **(2 * n) * prod' * A)
x_1**2 + b*x_1 = c
x_1 = (x_1**2 + c) / (2*x_1 + b)
"""
# x in the input is converted to the same price/precision
assert i != j # dev: same coin
assert j >= 0 # dev: j below zero
assert j < convert(N_COINS, int128) # dev: j above N_COINS
# should be unreachable, but good for safety
assert i >= 0
assert i < convert(N_COINS, int128)
amp: uint256 = _amp
D: uint256 = _D
S_: uint256 = 0
_x: uint256 = 0
c: uint256 = D
Ann: uint256 = amp * N_COINS
for _i in range(MAX_COINS):
if _i == N_COINS:
break
if convert(_i, int128) == i:
_x = x
elif convert(_i, int128) != j:
_x = xp[_i]
else:
continue
S_ += _x
c = c * D / (_x * N_COINS)
c = c * D * A_PRECISION / (Ann * N_COINS)
b: uint256 = S_ + D * A_PRECISION / Ann # - D
y: uint256 = D
return self.newton_y(b, c, D, y)
@pure
@internal
def get_D(_xp: DynArray[uint256, MAX_COINS], _amp: uint256, N_COINS: uint256) -> uint256:
"""
D invariant calculation in non-overflowing integer operations
iteratively
A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i))
Converging solution:
D[j+1] = (A * n**n * sum(x_i) - D[j]**(n+1) / (n**n prod(x_i))) / (A * n**n - 1)
"""
S: uint256 = 0
for i in range(MAX_COINS):
if i == N_COINS:
break
S += _xp[i]
if S == 0:
return 0
D: uint256 = S
Ann: uint256 = _amp * N_COINS
D_P: uint256 = 0
Dprev: uint256 = 0
for i in range(255):
D_P = D
for x in _xp:
D_P = D_P * D / (x * N_COINS)
Dprev = D
D = (Ann * S / A_PRECISION + D_P * N_COINS) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS + 1) * D_P)
# Equality with the precision of 1
if D > Dprev:
if D - Dprev <= 1:
return D
else:
if Dprev - D <= 1:
return D
# convergence typically occurs in 4 rounds or less, this should be unreachable!
# if it does happen the pool is borked and LPs can withdraw via `remove_liquidity`
raise
@internal
@pure
def newton_y(b: uint256, c: uint256, D: uint256, _y: uint256) -> uint256:
y_prev: uint256 = 0
y: uint256 = _y
for _i in range(255):
y_prev = y
y = (y*y + c) / (2 * y + b - D)
# Equality with the precision of 1
if y > y_prev:
if y - y_prev <= 1:
return y
else:
if y_prev - y <= 1:
return y
raise
```
```shell
>>> StableSwap.get_dy('todo')
'todo'
```
::::
### `get_dx_underlying`
::::description[`StableSwap.get_dx_underlying(i: int128, j: int128, dy: uint256, pool: address) -> uint256:`]
Function to calculate the predicted input amount `i` to receive `dy` of coin `j` on the underlying.
Returns: predicted amount of `i` (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | index value of input coin |
| `j` | `int128` | index value of output coin |
| `dy` | `uint256` | amount of output coin received |
| `pool` | `address` | pool address |
```vyper
@view
@external
def get_dx_underlying(
i: int128,
j: int128,
dy: uint256,
pool: address,
) -> uint256:
BASE_POOL: address = StableSwapNG(pool).BASE_POOL()
BASE_N_COINS: uint256 = StableSwapNG(pool).BASE_N_COINS()
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
base_pool_has_static_fee: bool = self._has_static_fee(BASE_POOL)
# CASE 1: Swap does not involve Metapool at all. In this case, we kindly as the user
# to use the right pool for their swaps.
if min(i, j) > 0:
raise "Not a Metapool Swap. Use Base pool."
# CASE 2:
# 1. meta token_0 of (unknown amount) > base pool lp_token
# 2. base pool lp_token > calc_withdraw_one_coin gives dy amount of (j-1)th base coin
# So, need to do the following calculations:
# 1. calc_token_amounts on base pool for depositing liquidity on (j-1)th token > lp_tokens.
# 2. get_dx on metapool for i = 0, and j = 1 (base lp token) with amt calculated in (1).
if i == 0:
# Calculate LP tokens that are burnt to receive dy amount of base_j tokens.
lp_amount_burnt: uint256 = self._base_calc_token_amount(
dy, j - 1, BASE_N_COINS, BASE_POOL, False
)
return self._get_dx(0, 1, lp_amount_burnt, pool, False, N_COINS)
# CASE 3: Swap in token i-1 from base pool and swap out dy amount of token 0 (j) from metapool.
# 1. deposit i-1 token from base pool > receive base pool lp_token
# 2. swap base pool lp token > 0th token of the metapool
# So, need to do the following calculations:
# 1. get_dx on metapool with i = 0, j = 1 > gives how many base lp tokens are required for receiving
# dy amounts of i-1 tokens from the metapool
# 2. We have number of lp tokens: how many i-1 base pool coins are needed to mint that many tokens?
# We don't have a method where user inputs lp tokens and it gives number of coins of (i-1)th token
# is needed to mint that many base_lp_tokens. Instead, we will use calc_withdraw_one_coin. That's
# close enough.
lp_amount_required: uint256 = self._get_dx(1, 0, dy, pool, False, N_COINS)
return StableSwapNG(BASE_POOL).calc_withdraw_one_coin(lp_amount_required, i-1)
@internal
@view
def _base_calc_token_amount(
dx: uint256,
base_i: int128,
base_n_coins: uint256,
base_pool: address,
is_deposit: bool
) -> uint256:
if base_n_coins == 2:
base_inputs: uint256[2] = empty(uint256[2])
base_inputs[base_i] = dx
return StableSwap2(base_pool).calc_token_amount(base_inputs, is_deposit)
elif base_n_coins == 3:
base_inputs: uint256[3] = empty(uint256[3])
base_inputs[base_i] = dx
return StableSwap3(base_pool).calc_token_amount(base_inputs, is_deposit)
else:
raise "base_n_coins > 3 not supported yet."
```
```shell
>>> StableSwap.get_y('todo')
'todo'
```
::::
### `get_dy_underlying`
::::description[`StableSwap.get_dy_underlying(i: int128, j: int128, dx: uint256, pool: address) -> uint256:`]
Function to calculate the predicted output amount of coin `j` when exchanging `dx` of coin `i` on the underlying.
Returns: predicted amount of `j` (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | index value of input coin |
| `j` | `int128` | index value of output coin |
| `dx` | `uint256` | amount of input coin being exchanged |
| `pool` | `address` | pool address |
```vyper
@view
@external
def get_dy_underlying(
i: int128,
j: int128,
dx: uint256,
pool: address,
) -> uint256:
"""
@notice Calculate the current output dy given input dx on underlying
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param dx Amount of `i` being exchanged
@return Amount of `j` predicted
"""
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
MAX_COIN: int128 = convert(N_COINS, int128) - 1
BASE_POOL: address = StableSwapNG(pool).BASE_POOL()
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS)
x: uint256 = 0
base_i: int128 = 0
base_j: int128 = 0
meta_i: int128 = 0
meta_j: int128 = 0
if i != 0:
base_i = i - MAX_COIN
meta_i = 1
if j != 0:
base_j = j - MAX_COIN
meta_j = 1
if i == 0:
x = xp[i] + dx * rates[0] / 10**18
else:
if j == 0:
# i is from BasePool
base_n_coins: uint256 = StableSwapNG(pool).BASE_N_COINS()
x = self._base_calc_token_amount(
dx, base_i, base_n_coins, BASE_POOL, True
) * rates[1] / PRECISION
# Adding number of pool tokens
x += xp[1]
else:
# If both are from the base pool
return StableSwapNG(BASE_POOL).get_dy(base_i, base_j, dx)
# This pool is involved only when in-pool assets are used
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION
D: uint256 = self.get_D(xp, amp, N_COINS)
y: uint256 = self.get_y(meta_i, meta_j, x, xp, amp, D, N_COINS)
dy: uint256 = xp[meta_j] - y - 1
# calculate output after subtracting dynamic fee
base_fee: uint256 = StableSwapNG(pool).fee()
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
dynamic_fee: uint256 = self._dynamic_fee((xp[meta_i] + x) / 2, (xp[meta_j] + y) / 2, base_fee, fee_multiplier)
dy = (dy - dynamic_fee * dy / FEE_DENOMINATOR)
# If output is going via the metapool
if j == 0:
dy = dy * 10**18 / rates[0]
else:
# j is from BasePool
# The fee is already accounted for
dy = StableSwapNG(BASE_POOL).calc_withdraw_one_coin(dy * PRECISION / rates[1], base_j)
return dy
```
```shell
>>> StableSwap.get_y('todo')
'todo'
```
::::
## Deposit / Withdrawal Methods
### `calc_token_amount`
::::description[`StableSwap.calc_token_amount(_amounts: DynArray[uint256, MAX_COINS], _is_deposit: bool, pool: address) -> uint256:`]
Function to calculate the addition or reduction of token supply from a deposit (add liquidity) or withdrawal (remove liquidity) including fees.
Returns: expected amount of LP tokens received (`uint256`)
| Input | Type | Description |
| ----------- | -------| ----|
| `_amounts` | `DynArray[uint256, MAX_COINS]` | amount of coins being deposited/withdrawn |
| `_is_deposit` | `bool` | `true` = deposit, `false` = withdraw |
| `pool` | `address` | pool address |
```vyper
@view
@external
def calc_token_amount(
_amounts: DynArray[uint256, MAX_COINS],
_is_deposit: bool,
pool: address
) -> uint256:
"""
@notice Calculate addition or reduction in token supply from a deposit or withdrawal
@param _amounts Amount of each coin being deposited
@param _is_deposit set True for deposits, False for withdrawals
@return Expected amount of LP tokens received
"""
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
old_balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, old_balances, xp = self._get_rates_balances_xp(pool, N_COINS)
# Initial invariant
D0: uint256 = self.get_D(xp, amp, N_COINS)
total_supply: uint256 = StableSwapNG(pool).totalSupply()
new_balances: DynArray[uint256, MAX_COINS] = old_balances
for i in range(MAX_COINS):
if i == N_COINS:
break
amount: uint256 = _amounts[i]
if _is_deposit:
new_balances[i] += amount
else:
new_balances[i] -= amount
# Invariant after change
for idx in range(MAX_COINS):
if idx == N_COINS:
break
xp[idx] = rates[idx] * new_balances[idx] / PRECISION
D1: uint256 = self.get_D(xp, amp, N_COINS)
# We need to recalculate the invariant accounting for fees
# to calculate fair user's share
D2: uint256 = D1
if total_supply > 0:
# Only account for fees if we are not the first to deposit
base_fee: uint256 = StableSwapNG(pool).fee() * N_COINS / (4 * (N_COINS - 1))
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
_dynamic_fee_i: uint256 = 0
xs: uint256 = 0
ys: uint256 = (D0 + D1) / N_COINS
for i in range(MAX_COINS):
if i == N_COINS:
break
ideal_balance: uint256 = D1 * old_balances[i] / D0
difference: uint256 = 0
new_balance: uint256 = new_balances[i]
if ideal_balance > new_balance:
difference = ideal_balance - new_balance
else:
difference = new_balance - ideal_balance
xs = old_balances[i] + new_balance
_dynamic_fee_i = self._dynamic_fee(xs, ys, base_fee, fee_multiplier)
new_balances[i] -= _dynamic_fee_i * difference / FEE_DENOMINATOR
for idx in range(MAX_COINS):
if idx == N_COINS:
break
xp[idx] = rates[idx] * new_balances[idx] / PRECISION
D2 = self.get_D(xp, amp, N_COINS)
else:
return D1 # Take the dust if there was any
diff: uint256 = 0
if _is_deposit:
diff = D2 - D0
else:
diff = D0 - D2
return diff * total_supply / D0
```
```shell
>>> StableSwap.calc_token_amount('todo')
'todo'
```
::::
### `calc_withdraw_one_coin`
::::description[`StableSwap.calc_withdraw_one_coin(_burn_amount: uint256, i: int128, pool: address) -> uint256:`]
Function to calculate the amount of tokens withdrawn when burning `_burn_amount` amount of LP tokens.
Returns: expected amount of `i` withdrawn (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `_burn_amount` | `uint256` | amount of LP tokens to burn |
| `i` | `int128` | index value of the coin to withdraw |
| `pool` | `address` | pool address |
```vyper
@view
@external
def calc_withdraw_one_coin(_burn_amount: uint256, i: int128, pool: address) -> uint256:
# First, need to calculate
# * Get current D
# * Solve Eqn against y_i for D - _token_amount
amp: uint256 = StableSwapNG(pool).A() * A_PRECISION
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS)
D0: uint256 = self.get_D(xp, amp, N_COINS)
total_supply: uint256 = StableSwapNG(pool).totalSupply()
D1: uint256 = D0 - _burn_amount * D0 / total_supply
new_y: uint256 = self.get_y_D(amp, i, xp, D1, N_COINS)
ys: uint256 = (D0 + D1) / (2 * N_COINS)
base_fee: uint256 = StableSwapNG(pool).fee() * N_COINS / (4 * (N_COINS - 1))
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
xp_reduced: DynArray[uint256, MAX_COINS] = xp
xp_j: uint256 = 0
xavg: uint256 = 0
dynamic_fee: uint256 = 0
for j in range(MAX_COINS):
if j == N_COINS:
break
dx_expected: uint256 = 0
xp_j = xp[j]
if convert(j, int128) == i:
dx_expected = xp_j * D1 / D0 - new_y
xavg = (xp[j] + new_y) / 2
else:
dx_expected = xp_j - xp_j * D1 / D0
xavg = xp[j]
dynamic_fee = self._dynamic_fee(xavg, ys, base_fee, fee_multiplier)
xp_reduced[j] = xp_j - dynamic_fee * dx_expected / FEE_DENOMINATOR
dy: uint256 = xp_reduced[i] - self.get_y_D(amp, i, xp_reduced, D1, N_COINS)
dy = (dy - 1) * PRECISION / rates[i] # Withdraw less to account for rounding errors
return dy
```
```shell
>>> StableSwap.get_y('todo')
'todo'
```
::::
## Dynamic Fee Method
### `dynamic_fee`
::::description[`StableSwap.dynamic_fee(i: int128, j: int128, pool:address) -> uint256:`]
Function to calculate the swap fee when exchanging between `i` and `j`. The swap fee is expressed as a integer with a 1e10 precision.
Returns: swap fee (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `int128` | index value of input coin |
| `j` | `int128` | index value of output coin |
| `pool` | `address` | pool address |
```vyper
@view
@external
def dynamic_fee(i: int128, j: int128, pool:address) -> uint256:
"""
@notice Return the fee for swapping between `i` and `j`
@param i Index value for the coin to send
@param j Index value of the coin to recieve
@return Swap fee expressed as an integer with 1e10 precision
"""
N_COINS: uint256 = StableSwapNG(pool).N_COINS()
fee: uint256 = StableSwapNG(pool).fee()
fee_multiplier: uint256 = StableSwapNG(pool).offpeg_fee_multiplier()
rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
balances: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
rates, balances, xp = self._get_rates_balances_xp(pool, N_COINS)
return self._dynamic_fee(xp[i], xp[j], fee, fee_multiplier)
@view
@internal
def _dynamic_fee(xpi: uint256, xpj: uint256, _fee: uint256, _fee_multiplier: uint256) -> uint256:
if _fee_multiplier <= FEE_DENOMINATOR:
return _fee
xps2: uint256 = (xpi + xpj) ** 2
return (
(_fee_multiplier * _fee) /
((_fee_multiplier - FEE_DENOMINATOR) * 4 * xpi * xpj / xps2 + FEE_DENOMINATOR)
)
@view
@internal
def _get_rates_balances_xp(pool: address, N_COINS: uint256) -> (
DynArray[uint256, MAX_COINS],
DynArray[uint256, MAX_COINS],
DynArray[uint256, MAX_COINS],
):
rates: DynArray[uint256, MAX_COINS] = StableSwapNG(pool).stored_rates()
balances: DynArray[uint256, MAX_COINS] = StableSwapNG(pool).get_balances()
xp: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
for idx in range(MAX_COINS):
if idx == N_COINS:
break
xp.append(rates[idx] * balances[idx] / PRECISION)
return rates, balances, xp
```
```shell
>>> StableSwap.dynamic_fee('todo')
'todo'
```
::::
---
## Tricrypto-NG Overview
:::deploy[Contract Source & Deployment]
Source code is available on [ GitHub](https://github.com/curvefi/tricrypto-ng). The following documentation covers source code up until commit number [`33707fc`](https://github.com/curvefi/tricrypto-ng/commit/33707fc8b84e08786acf184fcfdb744eb4657a99).
All tricrypto-ng deployments can be found in the "Deployment Addresses" section. [↗](../../deployments.md).
:::
For an in-depth understanding of the Cryptoswap invariant design, please refer to the official [Cryptoswap whitepaper](/pdf/whitepapers/whitepaper_cryptoswap.pdf).
---
**The Tricrypto-NG AMM infrastructure represents a technically enhanced iteration of the previous cryptoswap implementations. It comprises the following key components:**
The AMM is a **3-coin, auto-rebalancing Cryptoswap implementation** (version 2.0.0) with several optimizations. Unlike the older version, the **pool contract is an ERC20-compliant LP token**. Two implementations exist: one with **native transfers enabled** and one **disabled**.
The Factory allows the permissionless deployment of liquidity pools and gauges. It can accommodate **multiple blueprints of the AMM** contract. Blueprints are specified by the user while deploying the pool.
A contract which contains different **math utility functions** used in the AMM.
Contains **view methods relevant for integrators** and users. The address of the deployed Views contract is stored in the Factory and is upgradeable by the Factory's admin.
A liquidity gauge blueprint implementation which deploys a liquidity gauge of a pool on Ethereum. Gauges on sidechains must be deployed via the [RootChainGaugeFactory](../../gauges/xchain-gauges/root-gauge-factory.md).
Exponential moving-average oracles for the prices of coins within the AMM.
---
## Applying new parameters or transferring the ownership of the factory involves a **two-step model**. In the first step, changes need to be committed. The second step involves applying these changes.
#
Applying new parameters or transferring the ownership of the factory involves a **two-step model**. In the first step, changes need to be committed. The second step involves applying these changes.
## Pool Ownership
All pools created through the Factory are "owned" by the admin of the Factory, which is the Curve DAO. Ownership can only be changed within the factory contract via `commit_transfer_ownership` and `accept_transfer_ownership`.
---
## Amplification Coefficient and Gamma
More informations about the parameters [here](https://nagaking.substack.com/p/deep-dive-curve-v2-parameters).
The appropriate value for `A` and `gamma` is dependent upon the type of coin being used within the pool, and is subject to optimisation and pool-parameter update based on the market history of the trading pair. It is possible to modify the parameters for a pool after it has been deployed. However, it requires a vote within the Curve DAO and must reach a 15% quorum.
### `ramp_A_gamma`
::::description[`CryptoSwap.ramp_A_gamma(future_A: uint256, future_gamma: uint256, future_time: uint256):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory contract.
:::
Function to ramp A and gamma parameter values linearly. `A` and `gamma` are packed within the same variable.
Emits: `RampAgamma`
| Input | Type | Description |
| ----------- | -------| ----|
| `future_A` | `uint256` | future A value |
| `future_gamma` | `uint256` | future gamma value |
| `future_time` | `uint256` | timestamp at which the ramping will end|
```vyper
event RampAgamma:
initial_A: uint256
future_A: uint256
initial_gamma: uint256
future_gamma: uint256
initial_time: uint256
future_time: uint256
@external
def ramp_A_gamma(
future_A: uint256, future_gamma: uint256, future_time: uint256
):
"""
@notice Initialise Ramping A and gamma parameter values linearly.
@dev Only accessible by factory admin, and only
@param future_A The future A value.
@param future_gamma The future gamma value.
@param future_time The timestamp at which the ramping will end.
"""
assert msg.sender == factory.admin() # dev: only owner
assert block.timestamp > self.initial_A_gamma_time + (MIN_RAMP_TIME - 1) # dev: ramp undergoing
assert future_time > block.timestamp + MIN_RAMP_TIME - 1 # dev: insufficient time
A_gamma: uint256[2] = self._A_gamma()
initial_A_gamma: uint256 = A_gamma[0] << 128
initial_A_gamma = initial_A_gamma | A_gamma[1]
assert future_A > MIN_A - 1
assert future_A < MAX_A + 1
assert future_gamma > MIN_GAMMA - 1
assert future_gamma < MAX_GAMMA + 1
ratio: uint256 = 10**18 * future_A / A_gamma[0]
assert ratio < 10**18 * MAX_A_CHANGE + 1
assert ratio > 10**18 / MAX_A_CHANGE - 1
ratio = 10**18 * future_gamma / A_gamma[1]
assert ratio < 10**18 * MAX_A_CHANGE + 1
assert ratio > 10**18 / MAX_A_CHANGE - 1
self.initial_A_gamma = initial_A_gamma
self.initial_A_gamma_time = block.timestamp
future_A_gamma: uint256 = future_A << 128
future_A_gamma = future_A_gamma | future_gamma
self.future_A_gamma_time = future_time
self.future_A_gamma = future_A_gamma
log RampAgamma(
A_gamma[0],
future_A,
A_gamma[1],
future_gamma,
block.timestamp,
future_time,
)
```
```shell
>>> CryptoSwap.ramp_A_gamma(2700000, 1300000000000, 1693674492)
```
::::
### `stop_ramp_A_gamma`
::::description[`CryptoSwap.stop_ramp_A_gamma():`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory contract.
:::
Function to immediately stop ramping A and gamma parameters and set them to the current value.
Emits: `StopRampA`
```vyper
event StopRampA:
current_A: uint256
current_gamma: uint256
time: uint256
@external
def stop_ramp_A_gamma():
"""
@notice Stop Ramping A and gamma parameters immediately.
@dev Only accessible by factory admin.
"""
assert msg.sender == factory.admin() # dev: only owner
A_gamma: uint256[2] = self._A_gamma()
current_A_gamma: uint256 = A_gamma[0] << 128
current_A_gamma = current_A_gamma | A_gamma[1]
self.initial_A_gamma = current_A_gamma
self.future_A_gamma = current_A_gamma
self.initial_A_gamma_time = block.timestamp
self.future_A_gamma_time = block.timestamp
# ------ Now (block.timestamp < t1) is always False, so we return saved A.
log StopRampA(A_gamma[0], A_gamma[1], block.timestamp)
```
```shell
>>> CryptoSwap.stop_ramp_A_gamma()
```
::::
## Changing Parameters
### `commit_new_parameters`
::::description[`CryptoSwap.commit_new_parameters(_new_mid_fee: uint256, _new_out_fee: uint256, _new_fee_gamma: uint256, _new_allowed_extra_profit: uint256, _new_adjustment_step: uint256, _new_ma_time: uint256):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory contract.
:::
Function to commit new parameters. The new parameters do not take immedaite effect.
Emits: `CommitNewParameters`
| Input | Type | Description |
| ----------- | -------| ----|
| `_new_mid_fee` | `uint256` | new `mid_fee` value |
| `_new_out_fee` | `uint256` | new `out_fee` value |
| `_new_fee_gamma` | `uint256` | new `fee_gamma` value |
| `_new_allowed_extra_profit` | `uint256` | new `allowed_extra_profit` value |
| `_new_adjustment_step` | `uint256` |new `adjustment_step` value |
| `_new_ma_time` | `uint256` | new `ma_time` value |
```vyper
event CommitNewParameters:
deadline: indexed(uint256)
mid_fee: uint256
out_fee: uint256
fee_gamma: uint256
allowed_extra_profit: uint256
adjustment_step: uint256
ma_time: uint256
future_packed_rebalancing_params: uint256
future_packed_fee_params: uint256
ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400
@external
def commit_new_parameters(
_new_mid_fee: uint256,
_new_out_fee: uint256,
_new_fee_gamma: uint256,
_new_allowed_extra_profit: uint256,
_new_adjustment_step: uint256,
_new_ma_time: uint256,
):
"""
@notice Commit new parameters.
@dev Only accessible by factory admin.
@param _new_mid_fee The new mid fee.
@param _new_out_fee The new out fee.
@param _new_fee_gamma The new fee gamma.
@param _new_allowed_extra_profit The new allowed extra profit.
@param _new_adjustment_step The new adjustment step.
@param _new_ma_time The new ma time. ma_time is time_in_seconds/ln(2).
"""
assert msg.sender == Factory(self.factory).admin() # dev: only owner
assert self.admin_actions_deadline == 0 # dev: active action
_deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY
self.admin_actions_deadline = _deadline
# ----------------------------- Set fee params ---------------------------
new_mid_fee: uint256 = _new_mid_fee
new_out_fee: uint256 = _new_out_fee
new_fee_gamma: uint256 = _new_fee_gamma
current_fee_params: uint256[3] = self._unpack(self.packed_fee_params)
if new_out_fee < MAX_FEE + 1:
assert new_out_fee > MIN_FEE - 1 # dev: fee is out of range
else:
new_out_fee = current_fee_params[1]
if new_mid_fee > MAX_FEE:
new_mid_fee = current_fee_params[0]
assert new_mid_fee <= new_out_fee # dev: mid-fee is too high
if new_fee_gamma < 10**18:
assert new_fee_gamma > 0 # dev: fee_gamma out of range [1 .. 10**18]
else:
new_fee_gamma = current_fee_params[2]
self.future_packed_fee_params = self._pack(
[new_mid_fee, new_out_fee, new_fee_gamma]
)
# ----------------- Set liquidity rebalancing parameters -----------------
new_allowed_extra_profit: uint256 = _new_allowed_extra_profit
new_adjustment_step: uint256 = _new_adjustment_step
new_ma_time: uint256 = _new_ma_time
current_rebalancing_params: uint256[3] = self._unpack(self.packed_rebalancing_params)
if new_allowed_extra_profit > 10**18:
new_allowed_extra_profit = current_rebalancing_params[0]
if new_adjustment_step > 10**18:
new_adjustment_step = current_rebalancing_params[1]
if new_ma_time < 872542: # <----- Calculated as: 7 * 24 * 60 * 60 / ln(2)
assert new_ma_time > 86 # dev: MA time should be longer than 60/ln(2)
else:
new_ma_time = current_rebalancing_params[2]
self.future_packed_rebalancing_params = self._pack(
[new_allowed_extra_profit, new_adjustment_step, new_ma_time]
)
# ---------------------------------- LOG ---------------------------------
log CommitNewParameters(
_deadline,
new_mid_fee,
new_out_fee,
new_fee_gamma,
new_allowed_extra_profit,
new_adjustment_step,
new_ma_time,
)
```
```shell
>>> CryptoSwap.commit_new_parameters(20000000, 45000000, 350000000000000, 100000000000, 100000000000, 1800)
```
::::
### `apply_new_parameters`
::::description[`CryptoSwap.apply_new_parameters()`]
Function to apply the parameters from [`commit_new_parameters`](#commit_new_parameters).
Emits: `NewParameters`
```vyper
event NewParameters:
mid_fee: uint256
out_fee: uint256
fee_gamma: uint256
allowed_extra_profit: uint256
adjustment_step: uint256
ma_time: uint256
packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing
# parameters allowed_extra_profit, adjustment_step, and ma_time.
future_packed_rebalancing_params: uint256
packed_fee_params: public(uint256) # <---- Packs mid_fee, out_fee, fee_gamma.
future_packed_fee_params: uint256
@external
@nonreentrant("lock")
def apply_new_parameters():
"""
@notice Apply committed parameters.
@dev Only callable after admin_actions_deadline.
"""
assert block.timestamp >= self.admin_actions_deadline # dev: insufficient time
assert self.admin_actions_deadline != 0 # dev: no active action
self.admin_actions_deadline = 0
packed_fee_params: uint256 = self.future_packed_fee_params
self.packed_fee_params = packed_fee_params
packed_rebalancing_params: uint256 = self.future_packed_rebalancing_params
self.packed_rebalancing_params = packed_rebalancing_params
rebalancing_params: uint256[3] = self._unpack(packed_rebalancing_params)
fee_params: uint256[3] = self._unpack(packed_fee_params)
log NewParameters(
fee_params[0],
fee_params[1],
fee_params[2],
rebalancing_params[0],
rebalancing_params[1],
rebalancing_params[2],
)
```
```shell
>>> CryptoSwap.apply_new_parameters()
```
::::
### `revert_new_parameters`
::::description[`CryptoSwap.revert_new_parameters() -> address: view`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory contract.
:::
Function to revert the parameters changes.
```vyper
@external
def revert_new_parameters():
"""
@notice Revert committed parameters
@dev Only accessible by factory admin. Setting admin_actions_deadline to 0
ensures a revert in apply_new_parameters.
"""
assert msg.sender == Factory(self.factory).admin() # dev: only owner
self.admin_actions_deadline = 0
```
```shell
>>> CryptoSwap.revert_new_parameters()
```
::::
### `admin_actions_deadline`
::::description[`CryptoSwap.admin_actions_deadline() -> uint256: view`]
Getter for the admin actions deadline. This is the deadline until which new parameter changes can be applied. When committing new changes, there is a three-day timespan to apply them (`ADMIN_ACTIONS_DELAY`). If called later, the call will revert.
Returns: timestamp (`uint256`).
```vyper
admin_actions_deadline: public(uint256)
ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400
```
```shell
>>> CryptoSwap.admin_actions_deadline()
0
```
::::
### `initial_A_gamma`
::::description[`CryptoSwap.initial_A_gamma() -> uint256: view`]
Getter for the initial A/gamma.
Returns: A/gamma (`uint256`).
```vyper
initial_A_gamma: public(uint256)
```
```shell
>>> CryptoSwap.initial_A_gamma()
581076037942835227425498917514114728328226821
```
::::
### `initial_A_gamma_time`
::::description[`CryptoSwap.initial_A_gamma_time() -> uint256: view`]
Getter for the initial A/gamma time.
Returns: A/gamma time (`uint256`).
```vyper
initial_A_gamma_time: public(uint256)
```
```shell
>>> CryptoSwap.initial_A_gamma_time()
0
```
::::
### `future_A_gamma`
::::description[`CryptoSwap.future_A_gamma() -> uint256: view`]
Getter for the future A/gamma.
Returns: future A/gamma (`uint256`).
```vyper
future_A_gamma: public(uint256)
```
```shell
>>> CryptoSwap.future_A_gamma()
581076037942835227425498917514114728328226821
```
::::
### `future_A_gamma_time`
::::description[`CryptoSwap.future_A_gamma_time() -> uint256: view`]
Getter for the future A/gamma time.
Returns: future A/gamma time (`uint256`).
```vyper
future_A_gamma_time: public(uint256)
```
```shell
>>> CryptoSwap.future_A_gamma_time()
0
```
::::
---
## Tricrypto-NG Oracles
*Tricrypto-NG pools have the following oracle:*
An exponential moving-average (EMA) price oracle with a periodicity determined by `ma_time`. It returns the price relative to the coin at index 0 in the pool.
:::example[Example: Price Oracle for TriCRV]
The [`TriCRV`](https://etherscan.io/address/0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14) pool consists of `crvUSD <> wETH <> CRV`.
Because `crvUSD` is `coin[0]`, the prices of `wETH` and `CRV` are returned with regard to `crvUSD`.
```vyper
>>> price_oracle(0) = 3670949576287168254655
3670.94957629 # price of wETH w.r.t crvUSD
>>> price_oracle(1) = 724988309167051066
0.72498830916 # price of CRV w.r.t crvUSD
```
*In order to get the reverse EMA (e.g. price of `crvUSD` with regard to `wETH`):*
$\frac{10^{36}}{\text{price\_oracle(0)}} = 2.7240908e+14$
:::
---
*The formula to calculate the exponential moving-average essentially comes down to:*
```vyper
@internal
def tweak_price(
A_gamma: uint256[2],
_xp: uint256[N_COINS],
new_D: uint256,
K0_prev: uint256 = 0,
) -> uint256:
"""
@notice Tweaks price_oracle, last_price and conditionally adjusts
price_scale. This is called whenever there is an unbalanced
liquidity operation: _exchange, add_liquidity, or
remove_liquidity_one_coin.
@dev Contains main liquidity rebalancing logic, by tweaking `price_scale`.
@param A_gamma Array of A and gamma parameters.
@param _xp Array of current balances.
@param new_D New D value.
@param K0_prev Initial guess for `newton_D`.
"""
# ---------------------------- Read storage ------------------------------
rebalancing_params: uint256[3] = self._unpack(
self.packed_rebalancing_params
) # <---------- Contains: allowed_extra_profit, adjustment_step, ma_time.
price_oracle: uint256[N_COINS - 1] = self._unpack_prices(
self.price_oracle_packed
)
last_prices: uint256[N_COINS - 1] = self._unpack_prices(
self.last_prices_packed
)
packed_price_scale: uint256 = self.price_scale_packed
price_scale: uint256[N_COINS - 1] = self._unpack_prices(
packed_price_scale
)
total_supply: uint256 = self.totalSupply
old_xcp_profit: uint256 = self.xcp_profit
old_virtual_price: uint256 = self.virtual_price
last_prices_timestamp: uint256 = self.last_prices_timestamp
# ----------------------- Update MA if needed ----------------------------
if last_prices_timestamp < block.timestamp:
# The moving average price oracle is calculated using the last_price
# of the trade at the previous block, and the price oracle logged
# before that trade. This can happen only once per block.
# ------------------ Calculate moving average params -----------------
alpha: uint256 = MATH.wad_exp(
-convert(
unsafe_div(
(block.timestamp - last_prices_timestamp) * 10**18,
rebalancing_params[2] # <----------------------- ma_time.
),
int256,
)
)
for k in range(N_COINS - 1):
# ----------------- We cap state price that goes into the EMA with
# 2 x price_scale.
price_oracle[k] = unsafe_div(
min(last_prices[k], 2 * price_scale[k]) * (10**18 - alpha) +
price_oracle[k] * alpha, # ^-------- Cap spot price into EMA.
10**18
)
self.price_oracle_packed = self._pack_prices(price_oracle)
self.last_prices_timestamp = block.timestamp # <---- Store timestamp.
# price_oracle is used further on to calculate its vector
# distance from price_scale. This distance is used to calculate
# the amount of adjustment to be done to the price_scale.
# ------------------ If new_D is set to 0, calculate it ------------------
D_unadjusted: uint256 = new_D
if new_D == 0: # <--------------------------- _exchange sets new_D to 0.
D_unadjusted = MATH.newton_D(A_gamma[0], A_gamma[1], _xp, K0_prev)
# ----------------------- Calculate last_prices --------------------------
last_prices = MATH.get_p(_xp, D_unadjusted, A_gamma)
for k in range(N_COINS - 1):
last_prices[k] = unsafe_div(last_prices[k] * price_scale[k], 10**18)
self.last_prices_packed = self._pack_prices(last_prices)
# ---------- Update profit numbers without price adjustment first --------
xp: uint256[N_COINS] = empty(uint256[N_COINS])
xp[0] = unsafe_div(D_unadjusted, N_COINS)
for k in range(N_COINS - 1):
xp[k + 1] = D_unadjusted * 10**18 / (N_COINS * price_scale[k])
# ------------------------- Update xcp_profit ----------------------------
xcp_profit: uint256 = 10**18
virtual_price: uint256 = 10**18
if old_virtual_price > 0:
xcp: uint256 = MATH.geometric_mean(xp)
virtual_price = 10**18 * xcp / total_supply
xcp_profit = unsafe_div(
old_xcp_profit * virtual_price,
old_virtual_price
) # <---------------- Safu to do unsafe_div as old_virtual_price > 0.
# If A and gamma are not undergoing ramps (t < block.timestamp),
# ensure new virtual_price is not less than old virtual_price,
# else the pool suffers a loss.
if self.future_A_gamma_time < block.timestamp:
assert virtual_price > old_virtual_price, "Loss"
self.xcp_profit = xcp_profit
# ------------ Rebalance liquidity if there's enough profits to adjust it:
if virtual_price * 2 - 10**18 > xcp_profit + 2 * rebalancing_params[0]:
# allowed_extra_profit --------^
# ------------------- Get adjustment step ----------------------------
# Calculate the vector distance between price_scale and
# price_oracle.
norm: uint256 = 0
ratio: uint256 = 0
for k in range(N_COINS - 1):
ratio = unsafe_div(price_oracle[k] * 10**18, price_scale[k])
# unsafe_div because we did safediv before ----^
if ratio > 10**18:
ratio = unsafe_sub(ratio, 10**18)
else:
ratio = unsafe_sub(10**18, ratio)
norm = unsafe_add(norm, ratio**2)
norm = isqrt(norm) # <-------------------- isqrt is not in base 1e18.
adjustment_step: uint256 = max(
rebalancing_params[1], unsafe_div(norm, 5)
) # ^------------------------------------- adjustment_step.
if norm > adjustment_step: # <---------- We only adjust prices if the
# vector distance between price_oracle and price_scale is
# large enough. This check ensures that no rebalancing
# occurs if the distance is low i.e. the pool prices are
# pegged to the oracle prices.
# ------------------------------------- Calculate new price scale.
p_new: uint256[N_COINS - 1] = empty(uint256[N_COINS - 1])
for k in range(N_COINS - 1):
p_new[k] = unsafe_div(
price_scale[k] * unsafe_sub(norm, adjustment_step)
+ adjustment_step * price_oracle[k],
norm
) # <- norm is non-zero and gt adjustment_step; unsafe = safe
# ---------------- Update stale xp (using price_scale) with p_new.
xp = _xp
for k in range(N_COINS - 1):
xp[k + 1] = unsafe_div(_xp[k + 1] * p_new[k], price_scale[k])
# unsafe_div because we did safediv before ----^
# ------------------------------------------ Update D with new xp.
D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)
for k in range(N_COINS):
frac: uint256 = xp[k] * 10**18 / D # <----- Check validity of
assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # p_new.
xp[0] = D / N_COINS
for k in range(N_COINS - 1):
xp[k + 1] = D * 10**18 / (N_COINS * p_new[k]) # <---- Convert
# xp to real prices.
# ---------- Calculate new virtual_price using new xp and D. Reuse
# `old_virtual_price` (but it has new virtual_price).
old_virtual_price = unsafe_div(
10**18 * MATH.geometric_mean(xp), total_supply
) # <----- unsafe_div because we did safediv before (if vp>1e18)
# ---------------------------- Proceed if we've got enough profit.
if (
old_virtual_price > 10**18 and
2 * old_virtual_price - 10**18 > xcp_profit
):
packed_price_scale = self._pack_prices(p_new)
self.D = D
self.virtual_price = old_virtual_price
self.price_scale_packed = packed_price_scale
return packed_price_scale
# --------- price_scale was not adjusted. Update the profit counter and D.
self.D = D_unadjusted
self.virtual_price = virtual_price
return packed_price_scale
```
$$\alpha = e^{\text{power}}$$
$$\text{power} = \frac{(\text{block.timestamp} - \text{last\_prices\_timestamp}) \times 10^{18}}{\text{ma\_time}}$$
$$\text{EMA} = \frac{\min(\text{last\_prices}, 2 \times \text{price\_scale}) \times (10^{18} - \alpha) + \text{price\_oracle} \times \alpha}{10^{18}}$$
**Note:** The state price that goes into the EMA is capped with `2 x price_scale` to prevent manipulation.
| Variable | Description |
|-------------------------|-------------------------------------------------------------------------------------------------------|
| `block.timestamp` | Timestamp of the block. Since all transactions within a block share the same timestamp, EMA oracles can only be updated once per block. |
| `last_prices_timestamp` | Timestamp when the EMA oracle was last updated. |
| `ma_time` | Time window for the moving-average oracle. |
| `last_prices` | Last stored spot price of the coin to calculate the price oracle for. |
| `price_scale` | Price scale value of the coin to calculate the price oracle for. |
| `price_oracle` | Price oracle value of the coin to calculate the price oracle for. |
| `alpha` | Weighting multiplier that adjusts the impact of the latest spot value versus the previous EMA in the new EMA calculation. |
---
*The AMM implementation uses several private variables to pack and store key values, which are used for calculating the EMA oracle.*
:::info
Some storage variables pack multiple values into a single entry to save on gas costs. These values are unpacked when needed for use.
```vyper
PRICE_SIZE: constant(uint128) = 256 / (N_COINS - 1)
PRICE_MASK: constant(uint256) = 2**PRICE_SIZE - 1
last_prices_packed: uint256
@internal
@view
def _pack_prices(prices_to_pack: uint256[N_COINS-1]) -> uint256:
"""
@notice Packs N_COINS-1 prices into a uint256.
@param prices_to_pack The prices to pack
@return uint256 An integer that packs prices
"""
packed_prices: uint256 = 0
p: uint256 = 0
for k in range(N_COINS - 1):
packed_prices = packed_prices << PRICE_SIZE
p = prices_to_pack[N_COINS - 2 - k]
assert p < PRICE_MASK
packed_prices = p | packed_prices
return packed_prices
@internal
@view
def _unpack_prices(_packed_prices: uint256) -> uint256[2]:
"""
@notice Unpacks N_COINS-1 prices from a uint256.
@param _packed_prices The packed prices
@return uint256[2] Unpacked prices
"""
unpacked_prices: uint256[N_COINS-1] = empty(uint256[N_COINS-1])
packed_prices: uint256 = _packed_prices
for k in range(N_COINS - 1):
unpacked_prices[k] = packed_prices & PRICE_MASK
packed_prices = packed_prices >> PRICE_SIZE
return unpacked_prices
```
:::
- `last_prices_packed` stores the latest prices of all coins, packaging them into a single variable. When using the prices, they must be unpacked using the `_unpack_prices` method.
- `price_oracle_packed` stores the moving average values of the coins. This variable includes two values: the first represents the moving average for coin(1) relative to coin(0), and the second represents the moving average for coin(2) relative to coin(0). The `price_oracle` method unpacks these values and return the price oracle of the coin at index `k`.
- `price_scale_packed` functions similarly by packing the price scales of coin 1 and 2 with respect to coin 0. The `price_scale` method unpacks these values and returns the price scale of the coin at index `k`.
- `last_prices_timestamp` marks the timestamp when the `price_oracle` for a coin was last updated.
---
## Oracle and Price Methods
### `price_oracle`
::::description[`CurveTricryptoOptimizedWETH.price_oracle(k: uint256) -> uint256: view`]
:::warning[Revert]
This function reverts if `i >= 2`.
:::
Function to calculate the exponential moving average (EMA) price for the coin at index `k` with regard to the coin at index 0. The oracle is an exponential moving average, with a periodicity determined by `ma_time`. The aggregated prices are cached state prices (dy/dx) calculated AFTER the latest trade.
The moving average price oracle is calculated using the last_price of the trade at the previous block, and the price oracle logged before that trade. This can happen only once per block.
| Input | Type | Description |
| ------ | --------- | ------------------ |
| `k` | `uint256` | Index of the coin. |
Returns: EMA price of coin `k` (`uint256`).
```vyper
price_scale_packed: uint256 # <------------------------ Internal price scale.
price_oracle_packed: uint256 # <------- Price target given by moving average.
last_prices_packed: uint256
last_prices_timestamp: public(uint256)
@external
@view
@nonreentrant("lock")
def price_oracle(k: uint256) -> uint256:
"""
@notice Returns the oracle price of the coin at index `k` w.r.t the coin
at index 0.
@dev The oracle is an exponential moving average, with a periodicity
determined by `self.ma_time`. The aggregated prices are cached state
prices (dy/dx) calculated AFTER the latest trade.
@param k The index of the coin.
@return uint256 Price oracle value of kth coin.
"""
price_oracle: uint256 = self._unpack_prices(self.price_oracle_packed)[k]
price_scale: uint256 = self._unpack_prices(self.price_scale_packed)[k]
last_prices_timestamp: uint256 = self.last_prices_timestamp
if last_prices_timestamp < block.timestamp: # <------------ Update moving
# average if needed.
last_prices: uint256 = self._unpack_prices(self.last_prices_packed)[k]
ma_time: uint256 = self._unpack(self.packed_rebalancing_params)[2]
alpha: uint256 = MATH.wad_exp(
-convert(
(block.timestamp - last_prices_timestamp) * 10**18 / ma_time,
int256,
)
)
# ---- We cap state price that goes into the EMA with 2 x price_scale.
return (
min(last_prices, 2 * price_scale) * (10**18 - alpha) +
price_oracle * alpha
) / 10**18
return price_oracle
```
```shell
>>> CurveTricryptoOptimizedWETH.price_oracle(0)
66466761042718407573921
>>> CurveTricryptoOptimizedWETH.price_oracle(1)
3243401255685792725933
```
::::
### `price_scale`
::::description[`CurveTricryptoOptimizedWETH.price_scale(k: uint256) -> uint256: view`]
:::warning[Revert]
This function reverts if `i >= 2`.
:::
Getter for the price scale of the coin at index `k`.
| Input | Type | Description |
| ------ | -------- | ------------------ |
| `k` | `uint256`| Index of the coin. |
Returns: price scale of the coin `k` (`uint256`).
```vyper
price_scale_packed: uint256 # <------------------------ Internal price scale.
@external
@view
def price_scale(k: uint256) -> uint256:
"""
@notice Returns the price scale of the coin at index `k` w.r.t the coin
at index 0.
@dev Price scale determines the price band around which liquidity is
concentrated.
@param k The index of the coin.
@return uint256 Price scale of coin.
"""
return self._unpack_prices(self.price_scale_packed)[k]
```
```shell
>>> CurveTricryptoOptimizedWETH.price_scale(0)
64955165867890305070839
>>> CurveTricryptoOptimizedWETH.price_scale(1)
3133935659389092150237
```
::::
### `last_prices`
::::description[`CurveTricryptoOptimizedWETH.last_prices(k: uint256) -> uint256: view`]
:::warning[Revert]
This function reverts if `i >= 2`.
:::
Getter method for the last stored price for coin at index value `k`, stored in `last_prices_packed`.
| Input | Type | Description |
| ------ | -------- | ------------------ |
| `k` | `uint256`| Index of the coin. |
Returns: last stored spot price of coin `k` (`uint256`).
```vyper
last_prices_packed: uint256
@external
@view
def last_prices(k: uint256) -> uint256:
"""
@notice Returns last price of the coin at index `k` w.r.t the coin
at index 0.
@dev last_prices returns the quote by the AMM for an infinitesimally small swap
after the last trade. It is not equivalent to the last traded price, and
is computed by taking the partial differential of `x` w.r.t `y`. The
derivative is calculated in `get_p` and then multiplied with price_scale
to give last_prices.
@param k The index of the coin.
@return uint256 Last logged price of coin.
"""
return self._unpack_prices(self.last_prices_packed)[k]
@internal
@view
def _unpack_prices(_packed_prices: uint256) -> uint256[2]:
"""
@notice Unpacks N_COINS-1 prices from a uint256.
@param _packed_prices The packed prices
@return uint256[2] Unpacked prices
"""
unpacked_prices: uint256[N_COINS-1] = empty(uint256[N_COINS-1])
packed_prices: uint256 = _packed_prices
for k in range(N_COINS - 1):
unpacked_prices[k] = packed_prices & PRICE_MASK
packed_prices = packed_prices >> PRICE_SIZE
return unpacked_prices
```
```shell
>>> CurveTricryptoOptimizedWETH.last_prices(0)
66512510695325991643669
>>> CurveTricryptoOptimizedWETH.last_prices(1)
3249719806881710136102
```
::::
### `last_prices_timestamp`
::::description[`CurveTricryptoOptimizedWETH.last_prices_timestamp() -> uint256: view`]
Getter for the timestamp when the EMA price oracle was updated the last time.
Returns: timestamp (`uint256`).
```vyper
last_prices_timestamp: public(uint256)
```
```shell
>>> CurveTricryptoOptimizedWETH.last_prices_timestamp()
1713167903
```
::::
### `ma_time`
::::description[`CurveTricryptoOptimizedWETH.ma_time() -> uint256: view`]
Getter for the exponential moving average time for the price oracle. This value can be adjusted via `commit_new_parameters()`, as detailed in the [admin controls](./tricrypto.md#commit_new_parameters) section.
Returns: periodicity of the EMA (`uint256`)
```vyper
packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing
# parameters allowed_extra_profit, adjustment_step, and ma_time.
@view
@external
def ma_time() -> uint256:
"""
@notice Returns the current moving average time in seconds
@dev To get time in seconds, the parameter is multipled by ln(2)
One can expect off-by-one errors here.
@return uint256 ma_time value.
"""
return self._unpack(self.packed_rebalancing_params)[2] * 694 / 1000
```
```shell
>>> CurveTricryptoOptimizedWETH.ma_time()
600
```
::::
### `lp_price`
::::description[`CurveTricryptoOptimizedWETH.lp_price() -> uint256: view`]
Getter for the price of the LP token, calculated as follows:
$$\text{lp token price} = 3 \times \text{virtual\_price} \times \sqrt[3]{\frac{(\text{price\_oracle[0]} \times \text{price\_oracle[1]})}{10^{24}}}$$
Returns: LP token price (`uint256`).
```vyper
price_oracle_packed: uint256 # <------- Price target given by moving average.
@external
@view
@nonreentrant("lock")
def lp_price() -> uint256:
"""
@notice Calculates the current price of the LP token w.r.t coin at the
0th index
@return uint256 LP price.
"""
price_oracle: uint256[N_COINS-1] = self._unpack_prices(
self.price_oracle_packed
)
return (
3 * self.virtual_price * MATH.cbrt(price_oracle[0] * price_oracle[1])
) / 10**24
```
```shell
>>> CurveTricryptoOptimizedWETH.lp_price()
1809325066767569054740
```
::::
### `get_virtual_price`
::::description[`CurveTricryptoOptimizedWETH.get_virtual_price() -> uint256: view`]
Function to calculate the current virtual price of the pool 's LP token.
Returns: virtual price (`uint256`).
```vyper
totalSupply: public(uint256)
@external
@view
@nonreentrant("lock")
def get_virtual_price() -> uint256:
"""
@notice Calculates the current virtual price of the pool LP token.
@dev Not to be confused with `self.virtual_price` which is a cached
virtual price.
@return uint256 Virtual Price.
"""
return 10**18 * self.get_xcp(self.D) / self.totalSupply
```
```shell
>>> CurveTricryptoOptimizedWETH.get_virtual_price()
1005849271542625678
```
::::
### `virtual_price`
::::description[`CurveTricryptoOptimizedWETH.virtual_price() -> uint256: view`]
Getter for the cached virtual price.
Returns: cached virtual price (`uint256`).
```vyper
virtual_price: public(uint256) # <------ Cached (fast to read) virtual price.
# The cached `virtual_price` is also used internally.
```
```shell
>>> CurveTricryptoOptimizedWETH.virtual_price()
1005849271542625678
```
::::
---
## Updating Oracles and Other Storage Variables
The EMA oracle and other storage variables are updated each time the internal `tweak_price` function is called. This function tweaks `price_oracle` and `last_price`, and conditionally adjusts `price_scale`.
The `tweak_price` function is called whenever there is an unbalanced liquidity operation, including:
- `_exchange`
- `add_liquidity`
- `remove_liquidity_one_coin`
```vyper
@internal
def tweak_price(
A_gamma: uint256[2],
_xp: uint256[N_COINS],
new_D: uint256,
K0_prev: uint256 = 0,
) -> uint256:
"""
@notice Tweaks price_oracle, last_price and conditionally adjusts
price_scale. This is called whenever there is an unbalanced
liquidity operation: _exchange, add_liquidity, or
remove_liquidity_one_coin.
@dev Contains main liquidity rebalancing logic, by tweaking `price_scale`.
@param A_gamma Array of A and gamma parameters.
@param _xp Array of current balances.
@param new_D New D value.
@param K0_prev Initial guess for `newton_D`.
"""
# ---------------------------- Read storage ------------------------------
rebalancing_params: uint256[3] = self._unpack(
self.packed_rebalancing_params
) # <---------- Contains: allowed_extra_profit, adjustment_step, ma_time.
price_oracle: uint256[N_COINS - 1] = self._unpack_prices(
self.price_oracle_packed
)
last_prices: uint256[N_COINS - 1] = self._unpack_prices(
self.last_prices_packed
)
packed_price_scale: uint256 = self.price_scale_packed
price_scale: uint256[N_COINS - 1] = self._unpack_prices(
packed_price_scale
)
total_supply: uint256 = self.totalSupply
old_xcp_profit: uint256 = self.xcp_profit
old_virtual_price: uint256 = self.virtual_price
last_prices_timestamp: uint256 = self.last_prices_timestamp
# ----------------------- Update MA if needed ----------------------------
if last_prices_timestamp < block.timestamp:
# The moving average price oracle is calculated using the last_price
# of the trade at the previous block, and the price oracle logged
# before that trade. This can happen only once per block.
# ------------------ Calculate moving average params -----------------
alpha: uint256 = MATH.wad_exp(
-convert(
unsafe_div(
(block.timestamp - last_prices_timestamp) * 10**18,
rebalancing_params[2] # <----------------------- ma_time.
),
int256,
)
)
for k in range(N_COINS - 1):
# ----------------- We cap state price that goes into the EMA with
# 2 x price_scale.
price_oracle[k] = unsafe_div(
min(last_prices[k], 2 * price_scale[k]) * (10**18 - alpha) +
price_oracle[k] * alpha, # ^-------- Cap spot price into EMA.
10**18
)
self.price_oracle_packed = self._pack_prices(price_oracle)
self.last_prices_timestamp = block.timestamp # <---- Store timestamp.
# price_oracle is used further on to calculate its vector
# distance from price_scale. This distance is used to calculate
# the amount of adjustment to be done to the price_scale.
# ------------------ If new_D is set to 0, calculate it ------------------
D_unadjusted: uint256 = new_D
if new_D == 0: # <--------------------------- _exchange sets new_D to 0.
D_unadjusted = MATH.newton_D(A_gamma[0], A_gamma[1], _xp, K0_prev)
# ----------------------- Calculate last_prices --------------------------
last_prices = MATH.get_p(_xp, D_unadjusted, A_gamma)
for k in range(N_COINS - 1):
last_prices[k] = unsafe_div(last_prices[k] * price_scale[k], 10**18)
self.last_prices_packed = self._pack_prices(last_prices)
# ---------- Update profit numbers without price adjustment first --------
xp: uint256[N_COINS] = empty(uint256[N_COINS])
xp[0] = unsafe_div(D_unadjusted, N_COINS)
for k in range(N_COINS - 1):
xp[k + 1] = D_unadjusted * 10**18 / (N_COINS * price_scale[k])
# ------------------------- Update xcp_profit ----------------------------
xcp_profit: uint256 = 10**18
virtual_price: uint256 = 10**18
if old_virtual_price > 0:
xcp: uint256 = MATH.geometric_mean(xp)
virtual_price = 10**18 * xcp / total_supply
xcp_profit = unsafe_div(
old_xcp_profit * virtual_price,
old_virtual_price
) # <---------------- Safu to do unsafe_div as old_virtual_price > 0.
# If A and gamma are not undergoing ramps (t < block.timestamp),
# ensure new virtual_price is not less than old virtual_price,
# else the pool suffers a loss.
if self.future_A_gamma_time < block.timestamp:
assert virtual_price > old_virtual_price, "Loss"
self.xcp_profit = xcp_profit
# ------------ Rebalance liquidity if there's enough profits to adjust it:
if virtual_price * 2 - 10**18 > xcp_profit + 2 * rebalancing_params[0]:
# allowed_extra_profit --------^
# ------------------- Get adjustment step ----------------------------
# Calculate the vector distance between price_scale and
# price_oracle.
norm: uint256 = 0
ratio: uint256 = 0
for k in range(N_COINS - 1):
ratio = unsafe_div(price_oracle[k] * 10**18, price_scale[k])
# unsafe_div because we did safediv before ----^
if ratio > 10**18:
ratio = unsafe_sub(ratio, 10**18)
else:
ratio = unsafe_sub(10**18, ratio)
norm = unsafe_add(norm, ratio**2)
norm = isqrt(norm) # <-------------------- isqrt is not in base 1e18.
adjustment_step: uint256 = max(
rebalancing_params[1], unsafe_div(norm, 5)
) # ^------------------------------------- adjustment_step.
if norm > adjustment_step: # <---------- We only adjust prices if the
# vector distance between price_oracle and price_scale is
# large enough. This check ensures that no rebalancing
# occurs if the distance is low i.e. the pool prices are
# pegged to the oracle prices.
# ------------------------------------- Calculate new price scale.
p_new: uint256[N_COINS - 1] = empty(uint256[N_COINS - 1])
for k in range(N_COINS - 1):
p_new[k] = unsafe_div(
price_scale[k] * unsafe_sub(norm, adjustment_step)
+ adjustment_step * price_oracle[k],
norm
) # <- norm is non-zero and gt adjustment_step; unsafe = safe
# ---------------- Update stale xp (using price_scale) with p_new.
xp = _xp
for k in range(N_COINS - 1):
xp[k + 1] = unsafe_div(_xp[k + 1] * p_new[k], price_scale[k])
# unsafe_div because we did safediv before ----^
# ------------------------------------------ Update D with new xp.
D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)
for k in range(N_COINS):
frac: uint256 = xp[k] * 10**18 / D # <----- Check validity of
assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # p_new.
xp[0] = D / N_COINS
for k in range(N_COINS - 1):
xp[k + 1] = D * 10**18 / (N_COINS * p_new[k]) # <---- Convert
# xp to real prices.
# ---------- Calculate new virtual_price using new xp and D. Reuse
# `old_virtual_price` (but it has new virtual_price).
old_virtual_price = unsafe_div(
10**18 * MATH.geometric_mean(xp), total_supply
) # <----- unsafe_div because we did safediv before (if vp>1e18)
# ---------------------------- Proceed if we've got enough profit.
if (
old_virtual_price > 10**18 and
2 * old_virtual_price - 10**18 > xcp_profit
):
packed_price_scale = self._pack_prices(p_new)
self.D = D
self.virtual_price = old_virtual_price
self.price_scale_packed = packed_price_scale
return packed_price_scale
# --------- price_scale was not adjusted. Update the profit counter and D.
self.D = D_unadjusted
self.virtual_price = virtual_price
return packed_price_scale
```
---
## CurveTricryptoOptimized
A Tricrypto-NG pool consists of **three non-pegged assets**. The LP token is an ERC-20 token integrated directly into the liquidity pool.
:::vyper[`CurveTricryptoOptimized.vy`]
The source code for the `CurveTricryptoOptimized.vy` contract can be found on [GitHub](https://github.com/curvefi/tricrypto-ng/blob/main/contracts/main/CurveTricryptoOptimized.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.3.10`.
This is a **blueprint contract** — individual pools are deployed via the Factory. Pool and LP token share the same address. Full list of all deployments can be found [here](../../../deployments.md).
:::
:::info
The token has the regular ERC-20 methods, which will not be further documented.
:::
For Tricrypto-NG pools, price scaling and fee parameters are bundled and stored as a single unsigned integer. This consolidation reduces storage read and write operations, leading to more cost-efficient calls. When these parameters are accessed, they are subsequently unpacked.
```vyper
@internal
@view
def _pack(x: uint256[3]) -> uint256:
"""
@notice Packs 3 integers with values <= 10**18 into a uint256
@param x The uint256[3] to pack
@return uint256 Integer with packed values
"""
return (x[0] << 128) | (x[1] << 64) | x[2]
```
```vyper
@internal
@view
def _unpack(_packed: uint256) -> uint256[3]:
"""
@notice Unpacks a uint256 into 3 integers (values must be <= 10**18)
@param val The uint256 to unpack
@return uint256[3] A list of length 3 with unpacked integers
"""
return [
(_packed >> 128) & 18446744073709551615,
(_packed >> 64) & 18446744073709551615,
_packed & 18446744073709551615,
]
```
---
## Exchange Methods
*The contract offers two different ways to exchange tokens:*
- A regular `exchange` method.
- A `exchange_underlying` method, which swaps tokens based on native token transfers into the pool. More [here](../../stableswap-ng/overview.md#exchange_received).
### `exchange`
::::description[`TriCrypto.exchange(i: uint256, j: uint256, dx: uint256, min_dy: uint256, receiver: address = msg.sender) -> uint256:`]
Function to exchange `dx` amount of coin `i` for coin `j` and receive a minimum amount of `min_dy`.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint256` | Index value for the input coin |
| `j` | `uint256` | Index value for the output coin |
| `dx` | `uint256` | Amount of input coin being swapped in |
| `min_dy` | `uint256` | Minimum amount of output coin to receive |
| `receiver` | `address` | Address to send output coin to. Defaults to `msg.sender` |
Returns: amount of output coin `j` received (`uint256`).
```vyper
event TokenExchange:
buyer: indexed(address)
sold_id: uint256
tokens_sold: uint256
bought_id: uint256
tokens_bought: uint256
fee: uint256
packed_price_scale: uint256
@payable
@external
@nonreentrant("lock")
def exchange(
i: uint256,
j: uint256,
dx: uint256,
min_dy: uint256,
use_eth: bool = False,
receiver: address = msg.sender
) -> uint256:
"""
@notice Exchange using wrapped native token by default
@param i Index value for the input coin
@param j Index value for the output coin
@param dx Amount of input coin being swapped in
@param min_dy Minimum amount of output coin to receive
@param use_eth True if the input coin is native token, False otherwise
@param receiver Address to send the output coin to. Default is msg.sender
@return uint256 Amount of tokens at index j received by the `receiver
"""
return self._exchange(
msg.sender,
msg.value,
i,
j,
dx,
min_dy,
use_eth,
receiver,
empty(address),
empty(bytes32)
)
@internal
def _exchange(
sender: address,
mvalue: uint256,
i: uint256,
j: uint256,
dx: uint256,
min_dy: uint256,
use_eth: bool,
receiver: address,
callbacker: address,
callback_sig: bytes32
) -> uint256:
assert i != j # dev: coin index out of range
assert dx > 0 # dev: do not exchange 0 coins
A_gamma: uint256[2] = self._A_gamma()
xp: uint256[N_COINS] = self.balances
precisions: uint256[N_COINS] = self._unpack(self.packed_precisions)
dy: uint256 = 0
y: uint256 = xp[j] # <----------------- if j > N_COINS, this will revert.
x0: uint256 = xp[i] # <--------------- if i > N_COINS, this will revert.
xp[i] = x0 + dx
self.balances[i] = xp[i]
packed_price_scale: uint256 = self.price_scale_packed
price_scale: uint256[N_COINS - 1] = self._unpack_prices(
packed_price_scale
)
xp[0] *= precisions[0]
for k in range(1, N_COINS):
xp[k] = unsafe_div(
xp[k] * price_scale[k - 1] * precisions[k],
PRECISION
) # <-------- Safu to do unsafe_div here since PRECISION is not zero.
prec_i: uint256 = precisions[i]
# ----------- Update invariant if A, gamma are undergoing ramps ---------
t: uint256 = self.future_A_gamma_time
if t > block.timestamp:
x0 *= prec_i
if i > 0:
x0 = unsafe_div(x0 * price_scale[i - 1], PRECISION)
x1: uint256 = xp[i] # <------------------ Back up old value in xp ...
xp[i] = x0 # |
self.D = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) # |
xp[i] = x1 # <-------------------------------------- ... and restore.
# ----------------------- Calculate dy and fees --------------------------
D: uint256 = self.D
prec_j: uint256 = precisions[j]
y_out: uint256[2] = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, j)
dy = xp[j] - y_out[0]
xp[j] -= dy
dy -= 1
if j > 0:
dy = dy * PRECISION / price_scale[j - 1]
dy /= prec_j
fee: uint256 = unsafe_div(self._fee(xp) * dy, 10**10)
dy -= fee # <--------------------- Subtract fee from the outgoing amount.
assert dy >= min_dy, "Slippage"
y -= dy
self.balances[j] = y # <----------- Update pool balance of outgoing coin.
y *= prec_j
if j > 0:
y = unsafe_div(y * price_scale[j - 1], PRECISION)
xp[j] = y # <------------------------------------------------- Update xp.
# ---------------------- Do Transfers in and out -------------------------
########################## TRANSFER IN <-------
self._transfer_in(
coins[i], dx, dy, mvalue,
callbacker, callback_sig, # <-------- Callback method is called here.
sender, receiver, use_eth,
)
########################## -------> TRANSFER OUT
self._transfer_out(coins[j], dy, use_eth, receiver)
# ------ Tweak price_scale with good initial guess for newton_D ----------
packed_price_scale = self.tweak_price(A_gamma, xp, 0, y_out[1])
log TokenExchange(sender, i, dx, j, dy, fee, packed_price_scale)
return dy
```
```shell
>>> TriCrypto.exchange("todo")
''
```
::::
### `exchange_underlying`
::::description[`TriCrypto.exchange_underlying(i: uint256, j: uint256, dx: uint256, min_dy: uint256, receiver: address = msg.sender) -> uint256:`]
Function to exchange between two underlying tokens. More [here](../../stableswap-ng/overview.md#exchange_received).
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint256` | Index value for the input coin. |
| `j` | `uint256` | Index value for the output coin. |
| `dx` | `uint256` | Amount of input coin being swapped in. |
| `min_dy` | `uint256` | Minimum amount of output coin to receive. |
| `receiver` | `address` | Receiver Address; defaults to msg.sender. |
Returns: amount of output coin `j` received (`uint256`).
Emits: `TokenExchange`
```vyper
event TokenExchange:
buyer: indexed(address)
sold_id: uint256
tokens_sold: uint256
bought_id: uint256
tokens_bought: uint256
fee: uint256
packed_price_scale: uint256
@payable
@external
@nonreentrant('lock')
def exchange_underlying(
i: uint256,
j: uint256,
dx: uint256,
min_dy: uint256,
receiver: address = msg.sender
) -> uint256:
"""
@notice Exchange using native token transfers.
@param i Index value for the input coin
@param j Index value for the output coin
@param dx Amount of input coin being swapped in
@param min_dy Minimum amount of output coin to receive
@param receiver Address to send the output coin to. Default is msg.sender
@return uint256 Amount of tokens at index j received by the `receiver
"""
return self._exchange(
msg.sender,
msg.value,
i,
j,
dx,
min_dy,
True,
receiver,
empty(address),
empty(bytes32)
)
@internal
def _exchange(
sender: address,
mvalue: uint256,
i: uint256,
j: uint256,
dx: uint256,
min_dy: uint256,
use_eth: bool,
receiver: address,
callbacker: address,
callback_sig: bytes32
) -> uint256:
assert i != j # dev: coin index out of range
assert dx > 0 # dev: do not exchange 0 coins
A_gamma: uint256[2] = self._A_gamma()
xp: uint256[N_COINS] = self.balances
precisions: uint256[N_COINS] = self._unpack(self.packed_precisions)
dy: uint256 = 0
y: uint256 = xp[j] # <----------------- if j > N_COINS, this will revert.
x0: uint256 = xp[i] # <--------------- if i > N_COINS, this will revert.
xp[i] = x0 + dx
self.balances[i] = xp[i]
packed_price_scale: uint256 = self.price_scale_packed
price_scale: uint256[N_COINS - 1] = self._unpack_prices(
packed_price_scale
)
xp[0] *= precisions[0]
for k in range(1, N_COINS):
xp[k] = unsafe_div(
xp[k] * price_scale[k - 1] * precisions[k],
PRECISION
) # <-------- Safu to do unsafe_div here since PRECISION is not zero.
prec_i: uint256 = precisions[i]
# ----------- Update invariant if A, gamma are undergoing ramps ---------
t: uint256 = self.future_A_gamma_time
if t > block.timestamp:
x0 *= prec_i
if i > 0:
x0 = unsafe_div(x0 * price_scale[i - 1], PRECISION)
x1: uint256 = xp[i] # <------------------ Back up old value in xp ...
xp[i] = x0 # |
self.D = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) # |
xp[i] = x1 # <-------------------------------------- ... and restore.
# ----------------------- Calculate dy and fees --------------------------
D: uint256 = self.D
prec_j: uint256 = precisions[j]
y_out: uint256[2] = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, j)
dy = xp[j] - y_out[0]
xp[j] -= dy
dy -= 1
if j > 0:
dy = dy * PRECISION / price_scale[j - 1]
dy /= prec_j
fee: uint256 = unsafe_div(self._fee(xp) * dy, 10**10)
dy -= fee # <--------------------- Subtract fee from the outgoing amount.
assert dy >= min_dy, "Slippage"
y -= dy
self.balances[j] = y # <----------- Update pool balance of outgoing coin.
y *= prec_j
if j > 0:
y = unsafe_div(y * price_scale[j - 1], PRECISION)
xp[j] = y # <------------------------------------------------- Update xp.
# ---------------------- Do Transfers in and out -------------------------
########################## TRANSFER IN <-------
self._transfer_in(
coins[i], dx, dy, mvalue,
callbacker, callback_sig, # <-------- Callback method is called here.
sender, receiver, use_eth,
)
########################## -------> TRANSFER OUT
self._transfer_out(coins[j], dy, use_eth, receiver)
# ------ Tweak price_scale with good initial guess for newton_D ----------
packed_price_scale = self.tweak_price(A_gamma, xp, 0, y_out[1])
log TokenExchange(sender, i, dx, j, dy, fee, packed_price_scale)
return dy
```
```shell
>>> TriCrypto.exchange_underlying('todo')
''
```
::::
### `get_dy`
::::description[`TriCrypto.get_dy(i: uint256, j: uint256, dx: uint256) -> uint256:`]
Getter for the received amount of coin `j` for swapping in `dx` amount of coin `i`. This method includes fees.
| Input | Type | Description |
| ----- | --------- | ------------------------- |
| `i` | `uint256` | Index of input token. |
| `j` | `uint256` | Index of output token. |
| `dx` | `uint256` | Amount of input tokens. |
Returns: exact amount of output coin `j` (`uint256`).
```vyper
interface Factory:
def admin() -> address: view
def fee_receiver() -> address: view
def views_implementation() -> address: view
interface Views:
def calc_token_amount(
amounts: uint256[N_COINS], deposit: bool, swap: address
) -> uint256: view
def get_dy(
i: uint256, j: uint256, dx: uint256, swap: address
) -> uint256: view
def get_dx(
i: uint256, j: uint256, dy: uint256, swap: address
) -> uint256: view
@external
@view
def get_dy(i: uint256, j: uint256, dx: uint256) -> uint256:
"""
@notice Get amount of coin[j] tokens received for swapping in dx amount of coin[i]
@dev Includes fee.
@param i index of input token. Check pool.coins(i) to get coin address at ith index
@param j index of output token
@param dx amount of input coin[i] tokens
@return uint256 Exact amount of output j tokens for dx amount of i input tokens.
"""
view_contract: address = Factory(self.factory).views_implementation()
return Views(view_contract).get_dy(i, j, dx, self)
```
```vyper
@external
@view
def get_dy(
i: uint256, j: uint256, dx: uint256, swap: address
) -> uint256:
dy: uint256 = 0
xp: uint256[N_COINS] = empty(uint256[N_COINS])
# dy = (get_y(x + dx) - y) * (1 - fee)
dy, xp = self._get_dy_nofee(i, j, dx, swap)
dy -= Curve(swap).fee_calc(xp) * dy / 10**10
return dy
@internal
@view
def _get_dy_nofee(
i: uint256, j: uint256, dx: uint256, swap: address
) -> (uint256, uint256[N_COINS]):
assert i != j and i < N_COINS and j < N_COINS, "coin index out of range"
assert dx > 0, "do not exchange 0 coins"
math: Math = Curve(swap).MATH()
xp: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
price_scale: uint256[N_COINS-1] = empty(uint256[N_COINS-1])
D: uint256 = 0
token_supply: uint256 = 0
A: uint256 = 0
gamma: uint256 = 0
xp, D, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap)
# adjust xp with input dx
xp[i] += dx
xp[0] *= precisions[0]
for k in range(N_COINS - 1):
xp[k + 1] = xp[k + 1] * price_scale[k] * precisions[k + 1] / PRECISION
y_out: uint256[2] = math.get_y(A, gamma, xp, D, j)
dy: uint256 = xp[j] - y_out[0] - 1
xp[j] = y_out[0]
if j > 0:
dy = dy * PRECISION / price_scale[j - 1]
dy /= precisions[j]
return dy, xp
```
```shell
>>> TriCrypto.get_dy(0, 1, 10000000)
36134
```
::::
### `get_dx`
::::description[`TriCrypto.get_dx(i: uint256, j: uint256, dy: uint256) -> uint256:`]
Getter for the required amount of coin `i` to input for swapping out `dy` amount of token `j`.
| Input | Type | Description |
| ----- | --------- | ------------------------- |
| `i` | `uint256` | Index of input token. |
| `j` | `uint256` | Index of output token. |
| `dy` | `uint256` | Amount of output tokens. |
Returns: amount of input coin `i` needed (`uint256`).
```vyper
interface Factory:
def admin() -> address: view
def fee_receiver() -> address: view
def views_implementation() -> address: view
interface Views:
def calc_token_amount(
amounts: uint256[N_COINS], deposit: bool, swap: address
) -> uint256: view
def get_dy(
i: uint256, j: uint256, dx: uint256, swap: address
) -> uint256: view
def get_dx(
i: uint256, j: uint256, dy: uint256, swap: address
) -> uint256: view
@external
@view
def get_dx(i: uint256, j: uint256, dy: uint256) -> uint256:
"""
@notice Get amount of coin[i] tokens to input for swapping out dy amount
of coin[j]
@dev This is an approximate method, and returns estimates close to the input
amount. Expensive to call on-chain.
@param i index of input token. Check pool.coins(i) to get coin address at
ith index
@param j index of output token
@param dy amount of input coin[j] tokens received
@return uint256 Approximate amount of input i tokens to get dy amount of j tokens.
"""
view_contract: address = Factory(self.factory).views_implementation()
return Views(view_contract).get_dx(i, j, dy, self)
@external
@view
def fee_calc(xp: uint256[N_COINS]) -> uint256: # <----- For by view contract.
"""
@notice Returns the fee charged by the pool at current state.
@param xp The current balances of the pool multiplied by coin precisions.
@return uint256 Fee value.
"""
return self._fee(xp)
@internal
@view
def _fee(xp: uint256[N_COINS]) -> uint256:
fee_params: uint256[3] = self._unpack(self.packed_fee_params)
f: uint256 = MATH.reduction_coefficient(xp, fee_params[2])
return unsafe_div(
fee_params[0] * f + fee_params[1] * (10**18 - f),
10**18
)
```
```vyper
@view
@external
def get_dx(
i: uint256, j: uint256, dy: uint256, swap: address
) -> uint256:
dx: uint256 = 0
xp: uint256[N_COINS] = empty(uint256[N_COINS])
fee_dy: uint256 = 0
_dy: uint256 = dy
# for more precise dx (but never exact), increase num loops
for k in range(5):
dx, xp = self._get_dx_fee(i, j, _dy, swap)
fee_dy = Curve(swap).fee_calc(xp) * _dy / 10**10
_dy = dy + fee_dy + 1
return dx
@internal
@view
def _get_dx_fee(
i: uint256, j: uint256, dy: uint256, swap: address
) -> (uint256, uint256[N_COINS]):
# here, dy must include fees (and 1 wei offset)
assert i != j and i < N_COINS and j < N_COINS, "coin index out of range"
assert dy > 0, "do not exchange out 0 coins"
math: Math = Curve(swap).MATH()
xp: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
price_scale: uint256[N_COINS-1] = empty(uint256[N_COINS-1])
D: uint256 = 0
token_supply: uint256 = 0
A: uint256 = 0
gamma: uint256 = 0
xp, D, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap)
# adjust xp with output dy. dy contains fee element, which we handle later
# (hence this internal method is called _get_dx_fee)
xp[j] -= dy
xp[0] *= precisions[0]
for k in range(N_COINS - 1):
xp[k + 1] = xp[k + 1] * price_scale[k] * precisions[k + 1] / PRECISION
x_out: uint256[2] = math.get_y(A, gamma, xp, D, i)
dx: uint256 = x_out[0] - xp[i]
xp[i] = x_out[0]
if i > 0:
dx = dx * PRECISION / price_scale[i - 1]
dx /= precisions[i]
return dx, xp
@internal
@view
def _prep_calc(swap: address) -> (
uint256[N_COINS],
uint256,
uint256,
uint256[N_COINS-1],
uint256,
uint256,
uint256[N_COINS]
):
precisions: uint256[N_COINS] = Curve(swap).precisions()
token_supply: uint256 = Curve(swap).totalSupply()
xp: uint256[N_COINS] = empty(uint256[N_COINS])
for k in range(N_COINS):
xp[k] = Curve(swap).balances(k)
price_scale: uint256[N_COINS - 1] = empty(uint256[N_COINS - 1])
for k in range(N_COINS - 1):
price_scale[k] = Curve(swap).price_scale(k)
A: uint256 = Curve(swap).A()
gamma: uint256 = Curve(swap).gamma()
D: uint256 = self._calc_D_ramp(
A, gamma, xp, precisions, price_scale, swap
)
return xp, D, token_supply, price_scale, A, gamma, precisions
```
```vyper
@external
@view
def reduction_coefficient(x: uint256[N_COINS], fee_gamma: uint256) -> uint256:
"""
@notice Calculates the reduction coefficient for the given x and fee_gamma
@dev This method is used for calculating fees.
@param x The x values
@param fee_gamma The fee gamma value
"""
return self._reduction_coefficient(x, fee_gamma)
@internal
@pure
def _reduction_coefficient(x: uint256[N_COINS], fee_gamma: uint256) -> uint256:
# fee_gamma / (fee_gamma + (1 - K))
# where
# K = prod(x) / (sum(x) / N)**N
# (all normalized to 1e18)
S: uint256 = x[0] + x[1] + x[2]
# Could be good to pre-sort x, but it is used only for dynamic fee
K: uint256 = 10**18 * N_COINS * x[0] / S
K = unsafe_div(K * N_COINS * x[1], S) # <- unsafe div is safu.
K = unsafe_div(K * N_COINS * x[2], S)
if fee_gamma > 0:
K = fee_gamma * 10**18 / (fee_gamma + 10**18 - K)
return K
```
```shell
>>> TriCrypto.get_dx(0, 1, 10000000)
2767670393
```
::::
### `fee_calc`
::::description[`TriCrypto.fee_calc(xp: uint256[N_COINS]) -> uint256: view`]
Getter for the charged exchange fee by the pool at the current state.
| Input | Type | Description |
| ----- | ------------------ | ------------------------------------------------ |
| `xp` | `uint256[N_COINS]` | Pool balances multiplied by the coin precisions. |
Returns: fee (`uint256`).
```vyper
@external
@view
def fee_calc(xp: uint256[N_COINS]) -> uint256: # <----- For by view contract.
"""
@notice Returns the fee charged by the pool at current state.
@param xp The current balances of the pool multiplied by coin precisions.
@return uint256 Fee value.
"""
return self._fee(xp)
@internal
@view
def _fee(xp: uint256[N_COINS]) -> uint256:
fee_params: uint256[3] = self._unpack(self.packed_fee_params)
f: uint256 = MATH.reduction_coefficient(xp, fee_params[2])
return unsafe_div(
fee_params[0] * f + fee_params[1] * (10**18 - f),
10**18
)
```
```vyper
@external
@view
def reduction_coefficient(x: uint256[N_COINS], fee_gamma: uint256) -> uint256:
"""
@notice Calculates the reduction coefficient for the given x and fee_gamma
@dev This method is used for calculating fees.
@param x The x values
@param fee_gamma The fee gamma value
"""
return self._reduction_coefficient(x, fee_gamma)
@internal
@pure
def _reduction_coefficient(x: uint256[N_COINS], fee_gamma: uint256) -> uint256:
# fee_gamma / (fee_gamma + (1 - K))
# where
# K = prod(x) / (sum(x) / N)**N
# (all normalized to 1e18)
S: uint256 = x[0] + x[1] + x[2]
# Could be good to pre-sort x, but it is used only for dynamic fee
K: uint256 = 10**18 * N_COINS * x[0] / S
K = unsafe_div(K * N_COINS * x[1], S) # <- unsafe div is safu.
K = unsafe_div(K * N_COINS * x[2], S)
if fee_gamma > 0:
K = fee_gamma * 10**18 / (fee_gamma + 10**18 - K)
return K
```
```shell
>>> TriCrypto.fee_calc('todo')
''
```
::::
---
## Adding and Removing Liquidity
*The tricrypto-ng implementation utilizes the usual methods to add and remove liquidity.*
**Adding liquidity**can be done via the `add_liquidity` method. The code uses a list of unsigned integers `uint256[N_COINS]` as input for the pools underlying tokens to add. **Any proportion is possible**. For example, adding fully single-sided can be done using `[0, 1e18]` or `[1e18, 0]`, but again, any variation is possible, e.g., `[1e18, 1e19]`.
**Removing liquidity**can be done in two different ways. Either withdraw the underlying assets in a **balanced proportion**using the `remove_liquidity` method **or fully single-sided**in a single underlying token using `remove_liquidity_one_coin`.
### `add_liquidity`
::::description[`TriCrypto.add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256, use_eth: bool = False, receiver: address = msg.sender) -> uint256:`]
Function to add liquidity to the pool and mint the corresponding LP tokens.
| Input | Type | Description |
| ---------------- | ------------------- | ----------------------------------------------------- |
| `amounts` | `uint256[N_COINS]` | Amount of each coin to add. |
| `min_mint_amount`| `uint256` | Minimum amount of LP tokens to mint. |
| `use_eth` | `bool` | `True` = native token is added to the pool. |
| `receiver` | `address` | Receiver of the LP tokens; defaults to msg.sender. |
Returns: amount of LP tokens received (`uint256`).
Emits: `AddLiquidity`
```vyper
event AddLiquidity:
provider: indexed(address)
token_amounts: uint256[N_COINS]
fee: uint256
token_supply: uint256
packed_price_scale: uint256
@payable
@external
@nonreentrant("lock")
def add_liquidity(
amounts: uint256[N_COINS],
min_mint_amount: uint256,
use_eth: bool = False,
receiver: address = msg.sender
) -> uint256:
"""
@notice Adds liquidity into the pool.
@param amounts Amounts of each coin to add.
@param min_mint_amount Minimum amount of LP to mint.
@param use_eth True if native token is being added to the pool.
@param receiver Address to send the LP tokens to. Default is msg.sender
@return uint256 Amount of LP tokens received by the `receiver
"""
A_gamma: uint256[2] = self._A_gamma()
xp: uint256[N_COINS] = self.balances
amountsp: uint256[N_COINS] = empty(uint256[N_COINS])
xx: uint256[N_COINS] = empty(uint256[N_COINS])
d_token: uint256 = 0
d_token_fee: uint256 = 0
old_D: uint256 = 0
assert amounts[0] + amounts[1] + amounts[2] > 0 # dev: no coins to add
# --------------------- Get prices, balances -----------------------------
precisions: uint256[N_COINS] = self._unpack(self.packed_precisions)
packed_price_scale: uint256 = self.price_scale_packed
price_scale: uint256[N_COINS-1] = self._unpack_prices(packed_price_scale)
# -------------------------------------- Update balances and calculate xp.
xp_old: uint256[N_COINS] = xp
for i in range(N_COINS):
bal: uint256 = xp[i] + amounts[i]
xp[i] = bal
self.balances[i] = bal
xx = xp
xp[0] *= precisions[0]
xp_old[0] *= precisions[0]
for i in range(1, N_COINS):
xp[i] = unsafe_div(xp[i] * price_scale[i-1] * precisions[i], PRECISION)
xp_old[i] = unsafe_div(
xp_old[i] * unsafe_mul(price_scale[i-1], precisions[i]),
PRECISION
)
# ---------------- transferFrom token into the pool ----------------------
for i in range(N_COINS):
if amounts[i] > 0:
if coins[i] == WETH20:
self._transfer_in(
coins[i],
amounts[i],
0, # <-----------------------------------
msg.value, # | No callbacks
empty(address), # <----------------------| for
empty(bytes32), # <----------------------| add_liquidity.
msg.sender, # |
empty(address), # <-----------------------
use_eth
)
else:
self._transfer_in(
coins[i],
amounts[i],
0,
0, # <----------------- mvalue = 0 if coin is not WETH20.
empty(address),
empty(bytes32),
msg.sender,
empty(address),
False # <-------- use_eth is False if coin is not WETH20.
)
amountsp[i] = xp[i] - xp_old[i]
# -------------------- Calculate LP tokens to mint -----------------------
if self.future_A_gamma_time > block.timestamp: # <--- A_gamma is ramping.
# ----- Recalculate the invariant if A or gamma are undergoing a ramp.
old_D = MATH.newton_D(A_gamma[0], A_gamma[1], xp_old, 0)
else:
old_D = self.D
D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)
token_supply: uint256 = self.totalSupply
if old_D > 0:
d_token = token_supply * D / old_D - token_supply
else:
d_token = self.get_xcp(D) # <------------------------- Making initial
# virtual price equal to 1.
assert d_token > 0 # dev: nothing minted
if old_D > 0:
d_token_fee = (
self._calc_token_fee(amountsp, xp) * d_token / 10**10 + 1
)
d_token -= d_token_fee
token_supply += d_token
self.mint(receiver, d_token)
packed_price_scale = self.tweak_price(A_gamma, xp, D, 0)
else:
self.D = D
self.virtual_price = 10**18
self.xcp_profit = 10**18
self.xcp_profit_a = 10**18
self.mint(receiver, d_token)
assert d_token >= min_mint_amount, "Slippage"
log AddLiquidity(
receiver, amounts, d_token_fee, token_supply, packed_price_scale
)
self._claim_admin_fees() # <--------------------------- Claim admin fees.
return d_token
```
```vyper
@external
@view
def newton_D(
ANN: uint256,
gamma: uint256,
x_unsorted: uint256[N_COINS],
K0_prev: uint256 = 0,
) -> uint256:
"""
@notice Finding the invariant via newtons method using good initial guesses.
@dev ANN is higher by the factor A_MULTIPLIER
@dev ANN is already A * N**N
@param ANN the A * N**N value
@param gamma the gamma value
@param x_unsorted the array of coin balances (not sorted)
@param K0_prev apriori for newton's method derived from get_y_int. Defaults
to zero (no apriori)
"""
x: uint256[N_COINS] = self._sort(x_unsorted)
assert x[0] < max_value(uint256) / 10**18 * N_COINS**N_COINS # dev: out of limits
assert x[0] > 0 # dev: empty pool
# Safe to do unsafe add since we checked largest x's bounds previously
S: uint256 = unsafe_add(unsafe_add(x[0], x[1]), x[2])
D: uint256 = 0
if K0_prev == 0:
# Geometric mean of 3 numbers cannot be larger than the largest number
# so the following is safe to do:
D = unsafe_mul(N_COINS, self._geometric_mean(x))
else:
if S > 10**36:
D = self._cbrt(
unsafe_div(
unsafe_div(x[0] * x[1], 10**36) * x[2],
K0_prev
) * 27 * 10**12
)
elif S > 10**24:
D = self._cbrt(
unsafe_div(
unsafe_div(x[0] * x[1], 10**24) * x[2],
K0_prev
) * 27 * 10**6
)
else:
D = self._cbrt(
unsafe_div(
unsafe_div(x[0] * x[1], 10**18) * x[2],
K0_prev
) * 27
)
# D not zero here if K0_prev > 0, and we checked if x[0] is gt 0.
# initialise variables:
K0: uint256 = 0
_g1k0: uint256 = 0
mul1: uint256 = 0
mul2: uint256 = 0
neg_fprime: uint256 = 0
D_plus: uint256 = 0
D_minus: uint256 = 0
D_prev: uint256 = 0
diff: uint256 = 0
frac: uint256 = 0
for i in range(255):
D_prev = D
# K0 = 10**18 * x[0] * N_COINS / D * x[1] * N_COINS / D * x[2] * N_COINS / D
K0 = unsafe_div(
unsafe_mul(
unsafe_mul(
unsafe_div(
unsafe_mul(
unsafe_mul(
unsafe_div(
unsafe_mul(
unsafe_mul(10**18, x[0]), N_COINS
),
D,
),
x[1],
),
N_COINS,
),
D,
),
x[2],
),
N_COINS,
),
D,
) # <-------- We can convert the entire expression using unsafe math.
# since x_i is not too far from D, so overflow is not expected. Also
# D > 0, since we proved that already. unsafe_div is safe. K0 > 0
# since we can safely assume that D < 10**18 * x[0]. K0 is also
# in the range of 10**18 (it's a property).
_g1k0 = unsafe_add(gamma, 10**18) # <--------- safe to do unsafe_add.
if _g1k0 > K0: # The following operations can safely be unsafe.
_g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1)
else:
_g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1)
# D / (A * N**N) * _g1k0**2 / gamma**2
# mul1 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN
mul1 = unsafe_div(
unsafe_mul(
unsafe_mul(
unsafe_div(
unsafe_mul(
unsafe_div(unsafe_mul(10**18, D), gamma), _g1k0
),
gamma,
),
_g1k0,
),
A_MULTIPLIER,
),
ANN,
) # <------ Since D > 0, gamma is small, _g1k0 is small, the rest are
# non-zero and small constants, and D has a cap in this method,
# we can safely convert everything to unsafe maths.
# 2*N*K0 / _g1k0
# mul2 = (2 * 10**18) * N_COINS * K0 / _g1k0
mul2 = unsafe_div(
unsafe_mul(2 * 10**18 * N_COINS, K0), _g1k0
) # <--------------- K0 is approximately around D, which has a cap of
# 10**15 * 10**18 + 1, since we get that in get_y which is called
# with newton_D. _g1k0 > 0, so the entire expression can be unsafe.
# neg_fprime: uint256 = (S + S * mul2 / 10**18) + mul1 * N_COINS / K0 - mul2 * D / 10**18
neg_fprime = unsafe_sub(
unsafe_add(
unsafe_add(S, unsafe_div(unsafe_mul(S, mul2), 10**18)),
unsafe_div(unsafe_mul(mul1, N_COINS), K0),
),
unsafe_div(unsafe_mul(mul2, D), 10**18),
) # <--- mul1 is a big number but not huge: safe to unsafely multiply
# with N_coins. neg_fprime > 0 if this expression executes.
# mul2 is in the range of 10**18, since K0 is in that range, S * mul2
# is safe. The first three sums can be done using unsafe math safely
# and since the final expression will be small since mul2 is small, we
# can safely do the entire expression unsafely.
# D -= f / fprime
# D * (neg_fprime + S) / neg_fprime
D_plus = unsafe_div(D * unsafe_add(neg_fprime, S), neg_fprime)
# D*D / neg_fprime
D_minus = unsafe_div(D * D, neg_fprime)
# Since we know K0 > 0, and neg_fprime > 0, several unsafe operations
# are possible in the following. Also, (10**18 - K0) is safe to mul.
# So the only expressions we keep safe are (D_minus + ...) and (D * ...)
if 10**18 > K0:
# D_minus += D * (mul1 / neg_fprime) / 10**18 * (10**18 - K0) / K0
D_minus += unsafe_div(
unsafe_mul(
unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18),
unsafe_sub(10**18, K0),
),
K0,
)
else:
# D_minus -= D * (mul1 / neg_fprime) / 10**18 * (K0 - 10**18) / K0
D_minus -= unsafe_div(
unsafe_mul(
unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18),
unsafe_sub(K0, 10**18),
),
K0,
)
if D_plus > D_minus:
D = unsafe_sub(D_plus, D_minus) # <--------- Safe since we check.
else:
D = unsafe_div(unsafe_sub(D_minus, D_plus), 2)
if D > D_prev:
diff = unsafe_sub(D, D_prev)
else:
diff = unsafe_sub(D_prev, D)
# Could reduce precision for gas efficiency here:
if unsafe_mul(diff, 10**14) < max(10**16, D):
# Test that we are safe with the next get_y
for _x in x:
frac = unsafe_div(unsafe_mul(_x, 10**18), D)
assert frac >= 10**16 - 1 and frac < 10**20 + 1, "Unsafe values x[i]"
return D
raise "Did not converge"
```
```shell
>>> TriCrypto.add_liquidity('todo')
''
```
::::
### `calc_token_fee`
::::description[`TriCrypto.calc_token_fee(amounts: uint256[N_COINS], xp: uint256[N_COINS]) -> uint256:`]
Function to calculate the charged fee on `amounts` when adding liquidity.
| Input | Type | Description |
| -------- | ------------------- | ------------------------------------------------ |
| `amounts`| `uint256[N_COINS]` | Amount of coins added to the pool. |
| `xp` | `uint256[N_COINS]` | Pool balances multiplied by the coin precisions. |
Returns: fee (`uint256`).
```vyper
@external
@view
def calc_token_fee(
amounts: uint256[N_COINS], xp: uint256[N_COINS]
) -> uint256:
"""
@notice Returns the fee charged on the given amounts for add_liquidity.
@param amounts The amounts of coins being added to the pool.
@param xp The current balances of the pool multiplied by coin precisions.
@return uint256 Fee charged.
"""
return self._calc_token_fee(amounts, xp)
@view
@internal
def _calc_token_fee(amounts: uint256[N_COINS], xp: uint256[N_COINS]) -> uint256:
# fee = sum(amounts_i - avg(amounts)) * fee' / sum(amounts)
fee: uint256 = unsafe_div(
unsafe_mul(self._fee(xp), N_COINS),
unsafe_mul(4, unsafe_sub(N_COINS, 1))
)
S: uint256 = 0
for _x in amounts:
S += _x
avg: uint256 = unsafe_div(S, N_COINS)
Sdiff: uint256 = 0
for _x in amounts:
if _x > avg:
Sdiff += unsafe_sub(_x, avg)
else:
Sdiff += unsafe_sub(avg, _x)
return fee * Sdiff / S + NOISE_FEE
```
```shell
>>> TriCrypto.calc_token_fee()
'todo'
```
::::
### `remove_liquidity`
::::description[`TriCrypto.remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS], use_eth: bool = False, receiver: address = msg.sender, claim_admin_fees: bool = True) -> uint256[N_COINS]:`]
Function to remove liquidity from the pool and burn the LP tokens. When removing liquidity with this function, no fees are charged as the coins are withdrawn in balanced proportions.
If admin fees are claimed, they are claimed before withdrawing liquidity, ensuring the DAO gets paid first.
| Input | Type | Description |
| -------------- | ---------- | ---------------------------------------- |
| `_amount` | `uint256` | Amount of LP tokens to burn. |
| `min_amounts` | `uint256[N_COINS]` | Minimum amounts of tokens to withdraw. |
| `use_eth` | `bool` | True = withdraw ETH, False = withdraw wETH. |
| `receiver` | `address` | Receiver of the coins; defaults to `msg.sender`. |
| `claim_admin_fees` | `bool` | Whether to claim admin fees; defaults to `True`. |
Returns: withdrawn balances (`uint256[N_COINS]`).
Emits: `RemoveLiquidity`
```vyper
event RemoveLiquidity:
provider: indexed(address)
token_amounts: uint256[N_COINS]
token_supply: uint256
@external
@nonreentrant("lock")
def remove_liquidity(
_amount: uint256,
min_amounts: uint256[N_COINS],
use_eth: bool = False,
receiver: address = msg.sender,
claim_admin_fees: bool = True,
) -> uint256[N_COINS]:
"""
@notice This withdrawal method is very safe, does no complex math since
tokens are withdrawn in balanced proportions. No fees are charged.
@param _amount Amount of LP tokens to burn
@param min_amounts Minimum amounts of tokens to withdraw
@param use_eth Whether to withdraw ETH or not
@param receiver Address to send the withdrawn tokens to
@param claim_admin_fees If True, call self._claim_admin_fees(). Default is True.
@return uint256[3] Amount of pool tokens received by the `receiver`
"""
amount: uint256 = _amount
balances: uint256[N_COINS] = self.balances
d_balances: uint256[N_COINS] = empty(uint256[N_COINS])
if claim_admin_fees:
self._claim_admin_fees() # <------ We claim fees so that the DAO gets
# paid before withdrawal. In emergency cases, set it to False.
# -------------------------------------------------------- Burn LP tokens.
total_supply: uint256 = self.totalSupply # <------ Get totalSupply before
self.burnFrom(msg.sender, _amount) # ---- reducing it with self.burnFrom.
# There are two cases for withdrawing tokens from the pool.
# Case 1. Withdrawal does not empty the pool.
# In this situation, D is adjusted proportional to the amount of
# LP tokens burnt. ERC20 tokens transferred is proportional
# to : (AMM balance * LP tokens in) / LP token total supply
# Case 2. Withdrawal empties the pool.
# In this situation, all tokens are withdrawn and the invariant
# is reset.
if amount == total_supply: # <----------------------------------- Case 2.
for i in range(N_COINS):
d_balances[i] = balances[i]
self.balances[i] = 0 # <------------------------- Empty the pool.
else: # <-------------------------------------------------------- Case 1.
amount -= 1 # <---- To prevent rounding errors, favor LPs a tiny bit.
for i in range(N_COINS):
d_balances[i] = balances[i] * amount / total_supply
assert d_balances[i] >= min_amounts[i]
self.balances[i] = balances[i] - d_balances[i]
balances[i] = d_balances[i] # <-- Now it's the amounts going out.
D: uint256 = self.D
self.D = D - unsafe_div(D * amount, total_supply) # <----------- Reduce D
# proportional to the amount of tokens leaving. Since withdrawals are
# balanced, this is a simple subtraction. If amount == total_supply,
# D will be 0.
# ---------------------------------- Transfers ---------------------------
for i in range(N_COINS):
self._transfer_out(coins[i], d_balances[i], use_eth, receiver)
log RemoveLiquidity(msg.sender, balances, total_supply - _amount)
return d_balances
```
```shell
>>> TriCrypto.remove_liquidity('todo')
''
```
::::
### `remove_liquidity_one_coin`
::::description[`TriCrypto.remove_liquidity_one_coin(token_amount: uint256, i: uint256, min_amount: uint256, use_eth: bool = False, receiver: address = msg.sender) -> uint256:`]
Function to burn `token_amount` LP tokens and withdraw liquidity in a single token `i`.
| Input | Type | Description |
| -------------- | ---------- | ---------------------------------------- |
| `token_amount` | `uint256` | Amount of LP tokens to burn. |
| `i` | `uint256` | Index of the token to withdraw. |
| `min_amount` | `uint256` | Minimum amount of token to withdraw. |
| `use_eth` | `bool` | True = withdraw ETH, False = withdraw wETH. |
| `receiver` | `address` | Receiver of the coins; defaults to `msg.sender`. |
Returns: amount of coins withdrawn (`uint256`).
Emits: `RemoveLiquidityOne`
```vyper
@external
@nonreentrant("lock")
def remove_liquidity_one_coin(
token_amount: uint256,
i: uint256,
min_amount: uint256,
use_eth: bool = False,
receiver: address = msg.sender
) -> uint256:
"""
@notice Withdraw liquidity in a single token.
Involves fees (lower than swap fees).
@dev This operation also involves an admin fee claim.
@param token_amount Amount of LP tokens to burn
@param i Index of the token to withdraw
@param min_amount Minimum amount of token to withdraw.
@param use_eth Whether to withdraw ETH or not
@param receiver Address to send the withdrawn tokens to
@return Amount of tokens at index i received by the `receiver`
"""
A_gamma: uint256[2] = self._A_gamma()
dy: uint256 = 0
D: uint256 = 0
p: uint256 = 0
xp: uint256[N_COINS] = empty(uint256[N_COINS])
approx_fee: uint256 = 0
# ---------------------------- Claim admin fees before removing liquidity.
self._claim_admin_fees()
# ------------------------------------------------------------------------
dy, D, xp, approx_fee = self._calc_withdraw_one_coin(
A_gamma,
token_amount,
i,
(self.future_A_gamma_time > block.timestamp), # <------- During ramps
) # we need to update D.
assert dy >= min_amount, "Slippage"
# ------------------------- Transfers ------------------------------------
self.balances[i] -= dy
self.burnFrom(msg.sender, token_amount)
self._transfer_out(coins[i], dy, use_eth, receiver)
packed_price_scale: uint256 = self.tweak_price(A_gamma, xp, D, 0)
# Safe to use D from _calc_withdraw_one_coin here ---^
log RemoveLiquidityOne(
msg.sender, token_amount, i, dy, approx_fee, packed_price_scale
)
return dy
@internal
@view
def _calc_withdraw_one_coin(
A_gamma: uint256[2],
token_amount: uint256,
i: uint256,
update_D: bool,
) -> (uint256, uint256, uint256[N_COINS], uint256):
token_supply: uint256 = self.totalSupply
assert token_amount <= token_supply # dev: token amount more than supply
assert i < N_COINS # dev: coin out of range
xx: uint256[N_COINS] = self.balances
precisions: uint256[N_COINS] = self._unpack(self.packed_precisions)
xp: uint256[N_COINS] = precisions
D0: uint256 = 0
# -------------------------- Calculate D0 and xp -------------------------
price_scale_i: uint256 = PRECISION * precisions[0]
packed_prices: uint256 = self.price_scale_packed
xp[0] *= xx[0]
for k in range(1, N_COINS):
p: uint256 = (packed_prices & PRICE_MASK)
if i == k:
price_scale_i = p * xp[i]
xp[k] = unsafe_div(xp[k] * xx[k] * p, PRECISION)
packed_prices = packed_prices >> PRICE_SIZE
if update_D: # <-------------- D is updated if pool is undergoing a ramp.
D0 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)
else:
D0 = self.D
D: uint256 = D0
# -------------------------------- Fee Calc ------------------------------
# Charge fees on D. Roughly calculate xp[i] after withdrawal and use that
# to calculate fee. Precision is not paramount here: we just want a
# behavior where the higher the imbalance caused the more fee the AMM
# charges.
# xp is adjusted assuming xp[0] ~= xp[1] ~= x[2], which is usually not the
# case. We charge self._fee(xp), where xp is an imprecise adjustment post
# withdrawal in one coin. If the withdraw is too large: charge max fee by
# default. This is because the fee calculation will otherwise underflow.
xp_imprecise: uint256[N_COINS] = xp
xp_correction: uint256 = xp[i] * N_COINS * token_amount / token_supply
fee: uint256 = self._unpack(self.packed_fee_params)[1] # <- self.out_fee.
if xp_correction < xp_imprecise[i]:
xp_imprecise[i] -= xp_correction
fee = self._fee(xp_imprecise)
dD: uint256 = unsafe_div(token_amount * D, token_supply)
D_fee: uint256 = fee * dD / (2 * 10**10) + 1 # <------- Actual fee on D.
# --------- Calculate `approx_fee` (assuming balanced state) in ith token.
# -------------------------------- We only need this for fee in the event.
approx_fee: uint256 = N_COINS * D_fee * xx[i] / D
# ------------------------------------------------------------------------
D -= (dD - D_fee) # <----------------------------------- Charge fee on D.
# --------------------------------- Calculate `y_out`` with `(D - D_fee)`.
y: uint256 = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, i)[0]
dy: uint256 = (xp[i] - y) * PRECISION / price_scale_i
xp[i] = y
return dy, D, xp, approx_fee
```
```vyper
@external
@view
def newton_D(
ANN: uint256,
gamma: uint256,
x_unsorted: uint256[N_COINS],
K0_prev: uint256 = 0,
) -> uint256:
"""
@notice Finding the invariant via newtons method using good initial guesses.
@dev ANN is higher by the factor A_MULTIPLIER
@dev ANN is already A * N**N
@param ANN the A * N**N value
@param gamma the gamma value
@param x_unsorted the array of coin balances (not sorted)
@param K0_prev apriori for newton's method derived from get_y_int. Defaults
to zero (no apriori)
"""
x: uint256[N_COINS] = self._sort(x_unsorted)
assert x[0] < max_value(uint256) / 10**18 * N_COINS**N_COINS # dev: out of limits
assert x[0] > 0 # dev: empty pool
# Safe to do unsafe add since we checked largest x's bounds previously
S: uint256 = unsafe_add(unsafe_add(x[0], x[1]), x[2])
D: uint256 = 0
if K0_prev == 0:
# Geometric mean of 3 numbers cannot be larger than the largest number
# so the following is safe to do:
D = unsafe_mul(N_COINS, self._geometric_mean(x))
else:
if S > 10**36:
D = self._cbrt(
unsafe_div(
unsafe_div(x[0] * x[1], 10**36) * x[2],
K0_prev
) * 27 * 10**12
)
elif S > 10**24:
D = self._cbrt(
unsafe_div(
unsafe_div(x[0] * x[1], 10**24) * x[2],
K0_prev
) * 27 * 10**6
)
else:
D = self._cbrt(
unsafe_div(
unsafe_div(x[0] * x[1], 10**18) * x[2],
K0_prev
) * 27
)
# D not zero here if K0_prev > 0, and we checked if x[0] is gt 0.
# initialise variables:
K0: uint256 = 0
_g1k0: uint256 = 0
mul1: uint256 = 0
mul2: uint256 = 0
neg_fprime: uint256 = 0
D_plus: uint256 = 0
D_minus: uint256 = 0
D_prev: uint256 = 0
diff: uint256 = 0
frac: uint256 = 0
for i in range(255):
D_prev = D
# K0 = 10**18 * x[0] * N_COINS / D * x[1] * N_COINS / D * x[2] * N_COINS / D
K0 = unsafe_div(
unsafe_mul(
unsafe_mul(
unsafe_div(
unsafe_mul(
unsafe_mul(
unsafe_div(
unsafe_mul(
unsafe_mul(10**18, x[0]), N_COINS
),
D,
),
x[1],
),
N_COINS,
),
D,
),
x[2],
),
N_COINS,
),
D,
) # <-------- We can convert the entire expression using unsafe math.
# since x_i is not too far from D, so overflow is not expected. Also
# D > 0, since we proved that already. unsafe_div is safe. K0 > 0
# since we can safely assume that D < 10**18 * x[0]. K0 is also
# in the range of 10**18 (it's a property).
_g1k0 = unsafe_add(gamma, 10**18) # <--------- safe to do unsafe_add.
if _g1k0 > K0: # The following operations can safely be unsafe.
_g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1)
else:
_g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1)
# D / (A * N**N) * _g1k0**2 / gamma**2
# mul1 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN
mul1 = unsafe_div(
unsafe_mul(
unsafe_mul(
unsafe_div(
unsafe_mul(
unsafe_div(unsafe_mul(10**18, D), gamma), _g1k0
),
gamma,
),
_g1k0,
),
A_MULTIPLIER,
),
ANN,
) # <------ Since D > 0, gamma is small, _g1k0 is small, the rest are
# non-zero and small constants, and D has a cap in this method,
# we can safely convert everything to unsafe maths.
# 2*N*K0 / _g1k0
# mul2 = (2 * 10**18) * N_COINS * K0 / _g1k0
mul2 = unsafe_div(
unsafe_mul(2 * 10**18 * N_COINS, K0), _g1k0
) # <--------------- K0 is approximately around D, which has a cap of
# 10**15 * 10**18 + 1, since we get that in get_y which is called
# with newton_D. _g1k0 > 0, so the entire expression can be unsafe.
# neg_fprime: uint256 = (S + S * mul2 / 10**18) + mul1 * N_COINS / K0 - mul2 * D / 10**18
neg_fprime = unsafe_sub(
unsafe_add(
unsafe_add(S, unsafe_div(unsafe_mul(S, mul2), 10**18)),
unsafe_div(unsafe_mul(mul1, N_COINS), K0),
),
unsafe_div(unsafe_mul(mul2, D), 10**18),
) # <--- mul1 is a big number but not huge: safe to unsafely multiply
# with N_coins. neg_fprime > 0 if this expression executes.
# mul2 is in the range of 10**18, since K0 is in that range, S * mul2
# is safe. The first three sums can be done using unsafe math safely
# and since the final expression will be small since mul2 is small, we
# can safely do the entire expression unsafely.
# D -= f / fprime
# D * (neg_fprime + S) / neg_fprime
D_plus = unsafe_div(D * unsafe_add(neg_fprime, S), neg_fprime)
# D*D / neg_fprime
D_minus = unsafe_div(D * D, neg_fprime)
# Since we know K0 > 0, and neg_fprime > 0, several unsafe operations
# are possible in the following. Also, (10**18 - K0) is safe to mul.
# So the only expressions we keep safe are (D_minus + ...) and (D * ...)
if 10**18 > K0:
# D_minus += D * (mul1 / neg_fprime) / 10**18 * (10**18 - K0) / K0
D_minus += unsafe_div(
unsafe_mul(
unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18),
unsafe_sub(10**18, K0),
),
K0,
)
else:
# D_minus -= D * (mul1 / neg_fprime) / 10**18 * (K0 - 10**18) / K0
D_minus -= unsafe_div(
unsafe_mul(
unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18),
unsafe_sub(K0, 10**18),
),
K0,
)
if D_plus > D_minus:
D = unsafe_sub(D_plus, D_minus) # <--------- Safe since we check.
else:
D = unsafe_div(unsafe_sub(D_minus, D_plus), 2)
if D > D_prev:
diff = unsafe_sub(D, D_prev)
else:
diff = unsafe_sub(D_prev, D)
# Could reduce precision for gas efficiency here:
if unsafe_mul(diff, 10**14) < max(10**16, D):
# Test that we are safe with the next get_y
for _x in x:
frac = unsafe_div(unsafe_mul(_x, 10**18), D)
assert frac >= 10**16 - 1 and frac < 10**20 + 1, "Unsafe values x[i]"
return D
raise "Did not converge"
@external
@view
def get_y(
_ANN: uint256, _gamma: uint256, x: uint256[N_COINS], _D: uint256, i: uint256
) -> uint256[2]:
"""
@notice Calculate x[i] given other balances x[0..N_COINS-1] and invariant D.
@dev ANN = A * N**N.
@param _ANN AMM.A() value.
@param _gamma AMM.gamma() value.
@param x Balances multiplied by prices and precisions of all coins.
@param _D Invariant.
@param i Index of coin to calculate y.
"""
# Safety checks
assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A
assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D
frac: uint256 = 0
for k in range(3):
if k != i:
frac = x[k] * 10**18 / _D
assert frac > 10**16 - 1 and frac < 10**20 + 1, "Unsafe values x[i]"
# if above conditions are met, x[k] > 0
j: uint256 = 0
k: uint256 = 0
if i == 0:
j = 1
k = 2
elif i == 1:
j = 0
k = 2
elif i == 2:
j = 0
k = 1
ANN: int256 = convert(_ANN, int256)
gamma: int256 = convert(_gamma, int256)
D: int256 = convert(_D, int256)
x_j: int256 = convert(x[j], int256)
x_k: int256 = convert(x[k], int256)
gamma2: int256 = unsafe_mul(gamma, gamma)
a: int256 = 10**36 / 27
# 10**36/9 + 2*10**18*gamma/27 - D**2/x_j*gamma**2*ANN/27**2/convert(A_MULTIPLIER, int256)/x_k
b: int256 = (
unsafe_add(
10**36 / 9,
unsafe_div(unsafe_mul(2 * 10**18, gamma), 27)
)
- unsafe_div(
unsafe_div(
unsafe_div(
unsafe_mul(
unsafe_div(unsafe_mul(D, D), x_j),
gamma2
) * ANN,
27**2
),
convert(A_MULTIPLIER, int256)
),
x_k,
)
) # <------- The first two expressions can be unsafe, and unsafely added.
# 10**36/9 + gamma*(gamma + 4*10**18)/27 + gamma**2*(x_j+x_k-D)/D*ANN/27/convert(A_MULTIPLIER, int256)
c: int256 = (
unsafe_add(
10**36 / 9,
unsafe_div(unsafe_mul(gamma, unsafe_add(gamma, 4 * 10**18)), 27)
)
+ unsafe_div(
unsafe_div(
unsafe_mul(
unsafe_div(gamma2 * unsafe_sub(unsafe_add(x_j, x_k), D), D),
ANN
),
27
),
convert(A_MULTIPLIER, int256),
)
) # <--------- Same as above with the first two expressions. In the third
# expression, x_j + x_k will not overflow since we know their range from
# previous assert statements.
# (10**18 + gamma)**2/27
d: int256 = unsafe_div(unsafe_add(10**18, gamma)**2, 27)
# abs(3*a*c/b - b)
d0: int256 = abs(unsafe_mul(3, a) * c / b - b) # <------------ a is smol.
divider: int256 = 0
if d0 > 10**48:
divider = 10**30
elif d0 > 10**44:
divider = 10**26
elif d0 > 10**40:
divider = 10**22
elif d0 > 10**36:
divider = 10**18
elif d0 > 10**32:
divider = 10**14
elif d0 > 10**28:
divider = 10**10
elif d0 > 10**24:
divider = 10**6
elif d0 > 10**20:
divider = 10**2
else:
divider = 1
additional_prec: int256 = 0
if abs(a) > abs(b):
additional_prec = abs(unsafe_div(a, b))
a = unsafe_div(unsafe_mul(a, additional_prec), divider)
b = unsafe_div(b * additional_prec, divider)
c = unsafe_div(c * additional_prec, divider)
d = unsafe_div(d * additional_prec, divider)
else:
additional_prec = abs(unsafe_div(b, a))
a = unsafe_div(a / additional_prec, divider)
b = unsafe_div(unsafe_div(b, additional_prec), divider)
c = unsafe_div(unsafe_div(c, additional_prec), divider)
d = unsafe_div(unsafe_div(d, additional_prec), divider)
# 3*a*c/b - b
_3ac: int256 = unsafe_mul(3, a) * c
delta0: int256 = unsafe_div(_3ac, b) - b
# 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1: int256 = (
unsafe_div(3 * _3ac, b)
- unsafe_mul(2, b)
- unsafe_div(unsafe_div(27 * a**2, b) * d, b)
)
# delta1**2 + 4*delta0**2/b*delta0
sqrt_arg: int256 = (
delta1**2 +
unsafe_div(4 * delta0**2, b) * delta0
)
sqrt_val: int256 = 0
if sqrt_arg > 0:
sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256)
else:
return [self._newton_y(_ANN, _gamma, x, _D, i), 0]
b_cbrt: int256 = 0
if b >= 0:
b_cbrt = convert(self._cbrt(convert(b, uint256)), int256)
else:
b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256)
second_cbrt: int256 = 0
if delta1 > 0:
# convert(self._cbrt(convert((delta1 + sqrt_val), uint256)/2), int256)
second_cbrt = convert(
self._cbrt(unsafe_div(convert(delta1 + sqrt_val, uint256), 2)),
int256
)
else:
second_cbrt = -convert(
self._cbrt(unsafe_div(convert(-(delta1 - sqrt_val), uint256), 2)),
int256
)
# b_cbrt*b_cbrt/10**18*second_cbrt/10**18
C1: int256 = unsafe_div(
unsafe_div(b_cbrt * b_cbrt, 10**18) * second_cbrt,
10**18
)
# (b + b*delta0/C1 - C1)/3
root_K0: int256 = unsafe_div(b + b * delta0 / C1 - C1, 3)
# D*D/27/x_k*D/x_j*root_K0/a
root: int256 = unsafe_div(
unsafe_div(
unsafe_div(unsafe_div(D * D, 27), x_k) * D,
x_j
) * root_K0,
a
)
out: uint256[2] = [
convert(root, uint256),
convert(unsafe_div(10**18 * root_K0, a), uint256)
]
frac = unsafe_div(out[0] * 10**18, _D)
assert frac >= 10**16 - 1 and frac < 10**20 + 1, "Unsafe value for y"
# due to precision issues, get_y can be off by 2 wei or so wrt _newton_y
return out
```
```shell
>>> TriCrypto.remove_liquidity_one_coin('todo')
''
```
::::
### `calc_token_amount`
::::description[`TriCrypto.def calc_token_amount(amounts: uint256[N_COINS], deposit: bool) -> uint256:`]
Function to calculate the LP tokens to be minted or burned for depositing or removing `amounts` of coins. This method takes fees into consideration.
| Input | Type | Description |
| ---------- | ------------------ | ----------------------------------------------- |
| `amounts` | `uint256[N_COINS]` | Amounts of tokens being deposited or withdrawn. |
| `deposit` | `bool` | `true` for deposit, `false` for withdrawal. |
Returns: amount of LP tokens deposited or withdrawn (`uint256`).
```vyper
interface Factory:
def admin() -> address: view
def fee_receiver() -> address: view
def views_implementation() -> address: view
interface Views:
def calc_token_amount(
amounts: uint256[N_COINS], deposit: bool, swap: address
) -> uint256: view
def get_dy(
i: uint256, j: uint256, dx: uint256, swap: address
) -> uint256: view
def get_dx(
i: uint256, j: uint256, dy: uint256, swap: address
) -> uint256: view
@external
@view
def calc_token_amount(amounts: uint256[N_COINS], deposit: bool) -> uint256:
"""
@notice Calculate LP tokens minted or to be burned for depositing or
removing `amounts` of coins
@dev Includes fee.
@param amounts Amounts of tokens being deposited or withdrawn
@param deposit True if it is a deposit action, False if withdrawn.
@return uint256 Amount of LP tokens deposited or withdrawn.
"""
view_contract: address = Factory(self.factory).views_implementation()
return Views(view_contract).calc_token_amount(amounts, deposit, self)
@external
@view
def calc_token_fee(
amounts: uint256[N_COINS], xp: uint256[N_COINS]
) -> uint256:
"""
@notice Returns the fee charged on the given amounts for add_liquidity.
@param amounts The amounts of coins being added to the pool.
@param xp The current balances of the pool multiplied by coin precisions.
@return uint256 Fee charged.
"""
return self._calc_token_fee(amounts, xp)
@view
@internal
def _calc_token_fee(amounts: uint256[N_COINS], xp: uint256[N_COINS]) -> uint256:
# fee = sum(amounts_i - avg(amounts)) * fee' / sum(amounts)
fee: uint256 = unsafe_div(
unsafe_mul(self._fee(xp), N_COINS),
unsafe_mul(4, unsafe_sub(N_COINS, 1))
)
S: uint256 = 0
for _x in amounts:
S += _x
avg: uint256 = unsafe_div(S, N_COINS)
Sdiff: uint256 = 0
for _x in amounts:
if _x > avg:
Sdiff += unsafe_sub(_x, avg)
else:
Sdiff += unsafe_sub(avg, _x)
return fee * Sdiff / S + NOISE_FEE
```
```vyper
@view
@external
def calc_token_amount(
amounts: uint256[N_COINS], deposit: bool, swap: address
) -> uint256:
d_token: uint256 = 0
amountsp: uint256[N_COINS] = empty(uint256[N_COINS])
xp: uint256[N_COINS] = empty(uint256[N_COINS])
d_token, amountsp, xp = self._calc_dtoken_nofee(amounts, deposit, swap)
d_token -= (
Curve(swap).calc_token_fee(amountsp, xp) * d_token / 10**10 + 1
)
return d_token
@view
@external
def calc_fee_token_amount(
amounts: uint256[N_COINS], deposit: bool, swap: address
) -> uint256:
d_token: uint256 = 0
amountsp: uint256[N_COINS] = empty(uint256[N_COINS])
xp: uint256[N_COINS] = empty(uint256[N_COINS])
d_token, amountsp, xp = self._calc_dtoken_nofee(amounts, deposit, swap)
return Curve(swap).calc_token_fee(amountsp, xp) * d_token / 10**10 + 1
```
```shell
>>> TriCrypto.calc_token_amount(todo)
'todo'
```
::::
### `calc_withdraw_one_coin`
::::description[`TriCrypto.calc_withdraw_one_coin(token_amount: uint256, i: uint256) -> uint256:`]
Function to calculate the amount of output token `i` when burning `token_amount` of LP tokens. This method takes fees into consideration.
| Input | Type | Description |
| ------------- | --------- | ---------------------------------------- |
| `token_amount`| `uint256` | Amount of LP tokens burned. |
| `i` | `uint256` | Index of the coin to withdraw. |
Returns: amount of tokens to receive (`uint256`).
```vyper
@view
@external
def calc_withdraw_one_coin(token_amount: uint256, i: uint256) -> uint256:
"""
@notice Calculates output tokens with fee
@param token_amount LP Token amount to burn
@param i token in which liquidity is withdrawn
@return uint256 Amount of ith tokens received for burning token_amount LP tokens.
"""
return self._calc_withdraw_one_coin(
self._A_gamma(),
token_amount,
i,
(self.future_A_gamma_time > block.timestamp)
)[0]
@internal
@view
def _calc_withdraw_one_coin(
A_gamma: uint256[2],
token_amount: uint256,
i: uint256,
update_D: bool,
) -> (uint256, uint256, uint256[N_COINS], uint256):
token_supply: uint256 = self.totalSupply
assert token_amount <= token_supply # dev: token amount more than supply
assert i < N_COINS # dev: coin out of range
xx: uint256[N_COINS] = self.balances
precisions: uint256[N_COINS] = self._unpack(self.packed_precisions)
xp: uint256[N_COINS] = precisions
D0: uint256 = 0
# -------------------------- Calculate D0 and xp -------------------------
price_scale_i: uint256 = PRECISION * precisions[0]
packed_prices: uint256 = self.price_scale_packed
xp[0] *= xx[0]
for k in range(1, N_COINS):
p: uint256 = (packed_prices & PRICE_MASK)
if i == k:
price_scale_i = p * xp[i]
xp[k] = unsafe_div(xp[k] * xx[k] * p, PRECISION)
packed_prices = packed_prices >> PRICE_SIZE
if update_D: # <-------------- D is updated if pool is undergoing a ramp.
D0 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)
else:
D0 = self.D
D: uint256 = D0
# -------------------------------- Fee Calc ------------------------------
# Charge fees on D. Roughly calculate xp[i] after withdrawal and use that
# to calculate fee. Precision is not paramount here: we just want a
# behavior where the higher the imbalance caused the more fee the AMM
# charges.
# xp is adjusted assuming xp[0] ~= xp[1] ~= x[2], which is usually not the
# case. We charge self._fee(xp), where xp is an imprecise adjustment post
# withdrawal in one coin. If the withdraw is too large: charge max fee by
# default. This is because the fee calculation will otherwise underflow.
xp_imprecise: uint256[N_COINS] = xp
xp_correction: uint256 = xp[i] * N_COINS * token_amount / token_supply
fee: uint256 = self._unpack(self.packed_fee_params)[1] # <- self.out_fee.
if xp_correction < xp_imprecise[i]:
xp_imprecise[i] -= xp_correction
fee = self._fee(xp_imprecise)
dD: uint256 = unsafe_div(token_amount * D, token_supply)
D_fee: uint256 = fee * dD / (2 * 10**10) + 1 # <------- Actual fee on D.
# --------- Calculate `approx_fee` (assuming balanced state) in ith token.
# -------------------------------- We only need this for fee in the event.
approx_fee: uint256 = N_COINS * D_fee * xx[i] / D
# ------------------------------------------------------------------------
D -= (dD - D_fee) # <----------------------------------- Charge fee on D.
# --------------------------------- Calculate `y_out`` with `(D - D_fee)`.
y: uint256 = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, i)[0]
dy: uint256 = (xp[i] - y) * PRECISION / price_scale_i
xp[i] = y
return dy, D, xp, approx_fee
```
```vyper
@external
@view
def newton_D(
ANN: uint256,
gamma: uint256,
x_unsorted: uint256[N_COINS],
K0_prev: uint256 = 0,
) -> uint256:
"""
@notice Finding the invariant via newtons method using good initial guesses.
@dev ANN is higher by the factor A_MULTIPLIER
@dev ANN is already A * N**N
@param ANN the A * N**N value
@param gamma the gamma value
@param x_unsorted the array of coin balances (not sorted)
@param K0_prev apriori for newton's method derived from get_y_int. Defaults
to zero (no apriori)
"""
x: uint256[N_COINS] = self._sort(x_unsorted)
assert x[0] < max_value(uint256) / 10**18 * N_COINS**N_COINS # dev: out of limits
assert x[0] > 0 # dev: empty pool
# Safe to do unsafe add since we checked largest x's bounds previously
S: uint256 = unsafe_add(unsafe_add(x[0], x[1]), x[2])
D: uint256 = 0
if K0_prev == 0:
# Geometric mean of 3 numbers cannot be larger than the largest number
# so the following is safe to do:
D = unsafe_mul(N_COINS, self._geometric_mean(x))
else:
if S > 10**36:
D = self._cbrt(
unsafe_div(
unsafe_div(x[0] * x[1], 10**36) * x[2],
K0_prev
) * 27 * 10**12
)
elif S > 10**24:
D = self._cbrt(
unsafe_div(
unsafe_div(x[0] * x[1], 10**24) * x[2],
K0_prev
) * 27 * 10**6
)
else:
D = self._cbrt(
unsafe_div(
unsafe_div(x[0] * x[1], 10**18) * x[2],
K0_prev
) * 27
)
# D not zero here if K0_prev > 0, and we checked if x[0] is gt 0.
# initialise variables:
K0: uint256 = 0
_g1k0: uint256 = 0
mul1: uint256 = 0
mul2: uint256 = 0
neg_fprime: uint256 = 0
D_plus: uint256 = 0
D_minus: uint256 = 0
D_prev: uint256 = 0
diff: uint256 = 0
frac: uint256 = 0
for i in range(255):
D_prev = D
# K0 = 10**18 * x[0] * N_COINS / D * x[1] * N_COINS / D * x[2] * N_COINS / D
K0 = unsafe_div(
unsafe_mul(
unsafe_mul(
unsafe_div(
unsafe_mul(
unsafe_mul(
unsafe_div(
unsafe_mul(
unsafe_mul(10**18, x[0]), N_COINS
),
D,
),
x[1],
),
N_COINS,
),
D,
),
x[2],
),
N_COINS,
),
D,
) # <-------- We can convert the entire expression using unsafe math.
# since x_i is not too far from D, so overflow is not expected. Also
# D > 0, since we proved that already. unsafe_div is safe. K0 > 0
# since we can safely assume that D < 10**18 * x[0]. K0 is also
# in the range of 10**18 (it's a property).
_g1k0 = unsafe_add(gamma, 10**18) # <--------- safe to do unsafe_add.
if _g1k0 > K0: # The following operations can safely be unsafe.
_g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1)
else:
_g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1)
# D / (A * N**N) * _g1k0**2 / gamma**2
# mul1 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN
mul1 = unsafe_div(
unsafe_mul(
unsafe_mul(
unsafe_div(
unsafe_mul(
unsafe_div(unsafe_mul(10**18, D), gamma), _g1k0
),
gamma,
),
_g1k0,
),
A_MULTIPLIER,
),
ANN,
) # <------ Since D > 0, gamma is small, _g1k0 is small, the rest are
# non-zero and small constants, and D has a cap in this method,
# we can safely convert everything to unsafe maths.
# 2*N*K0 / _g1k0
# mul2 = (2 * 10**18) * N_COINS * K0 / _g1k0
mul2 = unsafe_div(
unsafe_mul(2 * 10**18 * N_COINS, K0), _g1k0
) # <--------------- K0 is approximately around D, which has a cap of
# 10**15 * 10**18 + 1, since we get that in get_y which is called
# with newton_D. _g1k0 > 0, so the entire expression can be unsafe.
# neg_fprime: uint256 = (S + S * mul2 / 10**18) + mul1 * N_COINS / K0 - mul2 * D / 10**18
neg_fprime = unsafe_sub(
unsafe_add(
unsafe_add(S, unsafe_div(unsafe_mul(S, mul2), 10**18)),
unsafe_div(unsafe_mul(mul1, N_COINS), K0),
),
unsafe_div(unsafe_mul(mul2, D), 10**18),
) # <--- mul1 is a big number but not huge: safe to unsafely multiply
# with N_coins. neg_fprime > 0 if this expression executes.
# mul2 is in the range of 10**18, since K0 is in that range, S * mul2
# is safe. The first three sums can be done using unsafe math safely
# and since the final expression will be small since mul2 is small, we
# can safely do the entire expression unsafely.
# D -= f / fprime
# D * (neg_fprime + S) / neg_fprime
D_plus = unsafe_div(D * unsafe_add(neg_fprime, S), neg_fprime)
# D*D / neg_fprime
D_minus = unsafe_div(D * D, neg_fprime)
# Since we know K0 > 0, and neg_fprime > 0, several unsafe operations
# are possible in the following. Also, (10**18 - K0) is safe to mul.
# So the only expressions we keep safe are (D_minus + ...) and (D * ...)
if 10**18 > K0:
# D_minus += D * (mul1 / neg_fprime) / 10**18 * (10**18 - K0) / K0
D_minus += unsafe_div(
unsafe_mul(
unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18),
unsafe_sub(10**18, K0),
),
K0,
)
else:
# D_minus -= D * (mul1 / neg_fprime) / 10**18 * (K0 - 10**18) / K0
D_minus -= unsafe_div(
unsafe_mul(
unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18),
unsafe_sub(K0, 10**18),
),
K0,
)
if D_plus > D_minus:
D = unsafe_sub(D_plus, D_minus) # <--------- Safe since we check.
else:
D = unsafe_div(unsafe_sub(D_minus, D_plus), 2)
if D > D_prev:
diff = unsafe_sub(D, D_prev)
else:
diff = unsafe_sub(D_prev, D)
# Could reduce precision for gas efficiency here:
if unsafe_mul(diff, 10**14) < max(10**16, D):
# Test that we are safe with the next get_y
for _x in x:
frac = unsafe_div(unsafe_mul(_x, 10**18), D)
assert frac >= 10**16 - 1 and frac < 10**20 + 1, "Unsafe values x[i]"
return D
raise "Did not converge"
@external
@view
def get_y(
_ANN: uint256, _gamma: uint256, x: uint256[N_COINS], _D: uint256, i: uint256
) -> uint256[2]:
"""
@notice Calculate x[i] given other balances x[0..N_COINS-1] and invariant D.
@dev ANN = A * N**N.
@param _ANN AMM.A() value.
@param _gamma AMM.gamma() value.
@param x Balances multiplied by prices and precisions of all coins.
@param _D Invariant.
@param i Index of coin to calculate y.
"""
# Safety checks
assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A
assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D
frac: uint256 = 0
for k in range(3):
if k != i:
frac = x[k] * 10**18 / _D
assert frac > 10**16 - 1 and frac < 10**20 + 1, "Unsafe values x[i]"
# if above conditions are met, x[k] > 0
j: uint256 = 0
k: uint256 = 0
if i == 0:
j = 1
k = 2
elif i == 1:
j = 0
k = 2
elif i == 2:
j = 0
k = 1
ANN: int256 = convert(_ANN, int256)
gamma: int256 = convert(_gamma, int256)
D: int256 = convert(_D, int256)
x_j: int256 = convert(x[j], int256)
x_k: int256 = convert(x[k], int256)
gamma2: int256 = unsafe_mul(gamma, gamma)
a: int256 = 10**36 / 27
# 10**36/9 + 2*10**18*gamma/27 - D**2/x_j*gamma**2*ANN/27**2/convert(A_MULTIPLIER, int256)/x_k
b: int256 = (
unsafe_add(
10**36 / 9,
unsafe_div(unsafe_mul(2 * 10**18, gamma), 27)
)
- unsafe_div(
unsafe_div(
unsafe_div(
unsafe_mul(
unsafe_div(unsafe_mul(D, D), x_j),
gamma2
) * ANN,
27**2
),
convert(A_MULTIPLIER, int256)
),
x_k,
)
) # <------- The first two expressions can be unsafe, and unsafely added.
# 10**36/9 + gamma*(gamma + 4*10**18)/27 + gamma**2*(x_j+x_k-D)/D*ANN/27/convert(A_MULTIPLIER, int256)
c: int256 = (
unsafe_add(
10**36 / 9,
unsafe_div(unsafe_mul(gamma, unsafe_add(gamma, 4 * 10**18)), 27)
)
+ unsafe_div(
unsafe_div(
unsafe_mul(
unsafe_div(gamma2 * unsafe_sub(unsafe_add(x_j, x_k), D), D),
ANN
),
27
),
convert(A_MULTIPLIER, int256),
)
) # <--------- Same as above with the first two expressions. In the third
# expression, x_j + x_k will not overflow since we know their range from
# previous assert statements.
# (10**18 + gamma)**2/27
d: int256 = unsafe_div(unsafe_add(10**18, gamma)**2, 27)
# abs(3*a*c/b - b)
d0: int256 = abs(unsafe_mul(3, a) * c / b - b) # <------------ a is smol.
divider: int256 = 0
if d0 > 10**48:
divider = 10**30
elif d0 > 10**44:
divider = 10**26
elif d0 > 10**40:
divider = 10**22
elif d0 > 10**36:
divider = 10**18
elif d0 > 10**32:
divider = 10**14
elif d0 > 10**28:
divider = 10**10
elif d0 > 10**24:
divider = 10**6
elif d0 > 10**20:
divider = 10**2
else:
divider = 1
additional_prec: int256 = 0
if abs(a) > abs(b):
additional_prec = abs(unsafe_div(a, b))
a = unsafe_div(unsafe_mul(a, additional_prec), divider)
b = unsafe_div(b * additional_prec, divider)
c = unsafe_div(c * additional_prec, divider)
d = unsafe_div(d * additional_prec, divider)
else:
additional_prec = abs(unsafe_div(b, a))
a = unsafe_div(a / additional_prec, divider)
b = unsafe_div(unsafe_div(b, additional_prec), divider)
c = unsafe_div(unsafe_div(c, additional_prec), divider)
d = unsafe_div(unsafe_div(d, additional_prec), divider)
# 3*a*c/b - b
_3ac: int256 = unsafe_mul(3, a) * c
delta0: int256 = unsafe_div(_3ac, b) - b
# 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1: int256 = (
unsafe_div(3 * _3ac, b)
- unsafe_mul(2, b)
- unsafe_div(unsafe_div(27 * a**2, b) * d, b)
)
# delta1**2 + 4*delta0**2/b*delta0
sqrt_arg: int256 = (
delta1**2 +
unsafe_div(4 * delta0**2, b) * delta0
)
sqrt_val: int256 = 0
if sqrt_arg > 0:
sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256)
else:
return [self._newton_y(_ANN, _gamma, x, _D, i), 0]
b_cbrt: int256 = 0
if b >= 0:
b_cbrt = convert(self._cbrt(convert(b, uint256)), int256)
else:
b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256)
second_cbrt: int256 = 0
if delta1 > 0:
# convert(self._cbrt(convert((delta1 + sqrt_val), uint256)/2), int256)
second_cbrt = convert(
self._cbrt(unsafe_div(convert(delta1 + sqrt_val, uint256), 2)),
int256
)
else:
second_cbrt = -convert(
self._cbrt(unsafe_div(convert(-(delta1 - sqrt_val), uint256), 2)),
int256
)
# b_cbrt*b_cbrt/10**18*second_cbrt/10**18
C1: int256 = unsafe_div(
unsafe_div(b_cbrt * b_cbrt, 10**18) * second_cbrt,
10**18
)
# (b + b*delta0/C1 - C1)/3
root_K0: int256 = unsafe_div(b + b * delta0 / C1 - C1, 3)
# D*D/27/x_k*D/x_j*root_K0/a
root: int256 = unsafe_div(
unsafe_div(
unsafe_div(unsafe_div(D * D, 27), x_k) * D,
x_j
) * root_K0,
a
)
out: uint256[2] = [
convert(root, uint256),
convert(unsafe_div(10**18 * root_K0, a), uint256)
]
frac = unsafe_div(out[0] * 10**18, _D)
assert frac >= 10**16 - 1 and frac < 10**20 + 1, "Unsafe value for y"
# due to precision issues, get_y can be off by 2 wei or so wrt _newton_y
return out
```
```shell
>>> TriCrypto.calc_withdraw_one_coin(1000000000000000000, 0)
1071872163
```
::::
---
## Fees and Pool Profits
The cryptoswap algorithm uses different fees, such as `fee`, `mid_fee`, `out_fee`, or `fee_gamma` to determine the fees charged, more on that [here](../../legacy/cryptoswap-overview.md#fees). All Fee values are denominated in 1e10 and [can be changed](#apply_new_parameters) by the admin.
Additionally, just as for other curve pools, there is an `ADMIN_FEE`, which is hardcoded to 50%. All twocrypto-ng pools share a universal `fee_receiver`, which is determined within the Factory contract.
`xcp_profit` and `xcp_profit_a` are used for tracking pool profits, which is necessary for the pool's rebalancing mechanism. These values are denominated in 1e18.
### `fee`
::::description[`TriCrypto.fee() -> uint256:`]
Getter for the fee charged by the pool at the current state.
Returns: fee (`uint256`).
```vyper
@external
@view
def fee() -> uint256:
"""
@notice Returns the fee charged by the pool at current state.
@dev Not to be confused with the fee charged at liquidity action, since
there the fee is calculated on `xp` AFTER liquidity is added or
removed.
@return uint256 fee bps.
"""
return self._fee(self.xp())
@internal
@view
def _fee(xp: uint256[N_COINS]) -> uint256:
fee_params: uint256[3] = self._unpack(self.packed_fee_params)
f: uint256 = MATH.reduction_coefficient(xp, fee_params[2])
return unsafe_div(
fee_params[0] * f + fee_params[1] * (10**18 - f),
10**18
)
```
```vyper
@external
@view
def reduction_coefficient(x: uint256[N_COINS], fee_gamma: uint256) -> uint256:
"""
@notice Calculates the reduction coefficient for the given x and fee_gamma
@dev This method is used for calculating fees.
@param x The x values
@param fee_gamma The fee gamma value
"""
return self._reduction_coefficient(x, fee_gamma)
@internal
@pure
def _reduction_coefficient(x: uint256[N_COINS], fee_gamma: uint256) -> uint256:
# fee_gamma / (fee_gamma + (1 - K))
# where
# K = prod(x) / (sum(x) / N)**N
# (all normalized to 1e18)
S: uint256 = x[0] + x[1] + x[2]
# Could be good to pre-sort x, but it is used only for dynamic fee
K: uint256 = 10**18 * N_COINS * x[0] / S
K = unsafe_div(K * N_COINS * x[1], S) # <- unsafe div is safu.
K = unsafe_div(K * N_COINS * x[2], S)
if fee_gamma > 0:
K = fee_gamma * 10**18 / (fee_gamma + 10**18 - K)
return K
```
```shell
>>> TriCrypto.fee()
3771992
```
::::
### `mid_fee`
::::description[`TriCrypto.mid_fee() -> uint256:`]
Getter for the current `mid_fee`. This is the minimum fee and is charged when the pool is completely balanced.
Returns: mid fee (`uint256`).
```vyper
packed_fee_params: public(uint256) # <---- Packs mid_fee, out_fee, fee_gamma.
@view
@external
def mid_fee() -> uint256:
"""
@notice Returns the current mid fee
@return uint256 mid_fee value.
"""
return self._unpack(self.packed_fee_params)[0]
```
```shell
>>> TriCrypto.mid_fee()
1499999
```
::::
### `out_fee`
::::description[`TriCrypto.out_fee() -> uint256:`]
Getter for the "out-fee". This is the maximum fee and is charged when the pool is completely imbalanced.
Returns: out fee (`uint256`).
```vyper
packed_fee_params: public(uint256) # <---- Packs mid_fee, out_fee, fee_gamma.
@view
@external
def out_fee() -> uint256:
"""
@notice Returns the current out fee
@return uint256 out_fee value.
"""
return self._unpack(self.packed_fee_params)[1]
```
```shell
>>> TriCrypto.out_fee()
140000000
```
::::
### `fee_gamma`
::::description[`TriCrypto.fee_gamma() -> uint256:`]
Getter for the current "fee-gamma". This parameter modifies the rate at which fees rise as imbalance intensifies. Smaller values result in rapid fee hikes with growing imbalances, while larger values lead to more gradual increments in fees as imbalance expands.
Returns: fee gamma (`uint256`).
```vyper
packed_fee_params: public(uint256) # <---- Packs mid_fee, out_fee, fee_gamma.
@view
@external
def fee_gamma() -> uint256:
"""
@notice Returns the current fee gamma
@return uint256 fee_gamma value.
"""
return self._unpack(self.packed_fee_params)[2]
```
```shell
>>> TriCrypto.fee_gamma()
500000000000000
```
::::
### `packed_fee_params`
::::description[`TriCrypto.packed_fee_params() -> uint256: view`]
Getter for the packed fee parameters.
Returns: packed fee params (`uint256`).
```vyper
packed_fee_params: public(uint256) # <---- Packs mid_fee, out_fee, fee_gamma.
```
```shell
>>> TriCrypto.packed_fee_params()
510423210099040776839142618093032111655788544
```
::::
### `fee_receiver`
::::description[`TriCrypto.fee_receiver() -> address: view`]
Getter for the fee receiver of the admin fees. This address is set within the Tricrypto Factory. Every pool created through the Factory has the same fee receiver.
Returns: fee receiver (`address`).
```vyper
interface Factory:
def admin() -> address: view
def fee_receiver() -> address: view
def views_implementation() -> address: view
@external
@view
def fee_receiver() -> address:
"""
@notice Returns the address of the admin fee receiver.
@return address Fee receiver.
"""
return Factory(self.factory).fee_receiver()
```
```shell
>>> TriCrypto.fee_receiver()
'0xeCb456EA5365865EbAb8a2661B0c503410e9B347'
```
::::
### `ADMIN_FEE`
::::description[`TriCrypto.ADMIN_FEE() -> uint256: view`]
Getter for the admin fee of the pool. This value is hardcoded to 50% (5000000000).
Returns: admin fee (`uint256`).
```vyper
ADMIN_FEE: public(constant(uint256)) = 5 * 10**9 # <----- 50% of earned fees. ```
```
```shell
>>> TriCrypto.ADMIN_FEE()
5000000000
```
::::
### `claim_admin_fees`
::::description[`CryptoSwap.claim_admin_fees() -> uint256:`]
Function to claim the accumulated admin fees from the pool and send them to the fee receiver.
Emits: `ClaimAdminFee`
```vyper
event ClaimAdminFee:
admin: indexed(address)
tokens: uint256
@external
@nonreentrant("lock")
def claim_admin_fees():
"""
@notice Claim admin fees. Callable by anyone.
"""
self._claim_admin_fees()
@internal
def _claim_admin_fees():
"""
@notice Claims admin fees and sends it to fee_receiver set in the factory.
"""
A_gamma: uint256[2] = self._A_gamma()
xcp_profit: uint256 = self.xcp_profit # <---------- Current pool profits.
xcp_profit_a: uint256 = self.xcp_profit_a # <- Profits at previous claim.
total_supply: uint256 = self.totalSupply
# Do not claim admin fees if:
# 1. insufficient profits accrued since last claim, and
# 2. there are less than 10**18 (or 1 unit of) lp tokens, else it can lead
# to manipulated virtual prices.
if xcp_profit <= xcp_profit_a or total_supply < 10**18:
return
# Claim tokens belonging to the admin here. This is done by 'gulping'
# pool tokens that have accrued as fees, but not accounted in pool's
# `self.balances` yet: pool balances only account for incoming and
# outgoing tokens excluding fees. Following 'gulps' fees:
for i in range(N_COINS):
if coins[i] == WETH20:
self.balances[i] = self.balance
else:
self.balances[i] = ERC20(coins[i]).balanceOf(self)
# If the pool has made no profits, `xcp_profit == xcp_profit_a`
# and the pool gulps nothing in the previous step.
vprice: uint256 = self.virtual_price
# Admin fees are calculated as follows.
# 1. Calculate accrued profit since last claim. `xcp_profit`
# is the current profits. `xcp_profit_a` is the profits
# at the previous claim.
# 2. Take out admin's share, which is hardcoded at 5 * 10**9.
# (50% => half of 100% => 10**10 / 2 => 5 * 10**9).
# 3. Since half of the profits go to rebalancing the pool, we
# are left with half; so divide by 2.
fees: uint256 = unsafe_div(
unsafe_sub(xcp_profit, xcp_profit_a) * ADMIN_FEE, 2 * 10**10
)
# ------------------------------ Claim admin fees by minting admin's share
# of the pool in LP tokens.
receiver: address = Factory(self.factory).fee_receiver()
if receiver != empty(address) and fees > 0:
frac: uint256 = vprice * 10**18 / (vprice - fees) - 10**18
claimed: uint256 = self.mint_relative(receiver, frac)
xcp_profit -= fees * 2
self.xcp_profit = xcp_profit
log ClaimAdminFee(receiver, claimed)
# ------------------------------------------- Recalculate D b/c we gulped.
D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], self.xp(), 0)
self.D = D
# ------------------- Recalculate virtual_price following admin fee claim.
# In this instance we do not check if current virtual price is greater
# than old virtual price, since the claim process can result
# in a small decrease in pool's value.
self.virtual_price = 10**18 * self.get_xcp(D) / self.totalSupply
self.xcp_profit_a = xcp_profit # <------------ Cache last claimed profit.
```
```vyper
@external
@view
def newton_D(
ANN: uint256,
gamma: uint256,
x_unsorted: uint256[N_COINS],
K0_prev: uint256 = 0,
) -> uint256:
"""
@notice Finding the invariant via newtons method using good initial guesses.
@dev ANN is higher by the factor A_MULTIPLIER
@dev ANN is already A * N**N
@param ANN the A * N**N value
@param gamma the gamma value
@param x_unsorted the array of coin balances (not sorted)
@param K0_prev apriori for newton's method derived from get_y_int. Defaults
to zero (no apriori)
"""
x: uint256[N_COINS] = self._sort(x_unsorted)
assert x[0] < max_value(uint256) / 10**18 * N_COINS**N_COINS # dev: out of limits
assert x[0] > 0 # dev: empty pool
# Safe to do unsafe add since we checked largest x's bounds previously
S: uint256 = unsafe_add(unsafe_add(x[0], x[1]), x[2])
D: uint256 = 0
if K0_prev == 0:
# Geometric mean of 3 numbers cannot be larger than the largest number
# so the following is safe to do:
D = unsafe_mul(N_COINS, self._geometric_mean(x))
else:
if S > 10**36:
D = self._cbrt(
unsafe_div(
unsafe_div(x[0] * x[1], 10**36) * x[2],
K0_prev
) * 27 * 10**12
)
elif S > 10**24:
D = self._cbrt(
unsafe_div(
unsafe_div(x[0] * x[1], 10**24) * x[2],
K0_prev
) * 27 * 10**6
)
else:
D = self._cbrt(
unsafe_div(
unsafe_div(x[0] * x[1], 10**18) * x[2],
K0_prev
) * 27
)
# D not zero here if K0_prev > 0, and we checked if x[0] is gt 0.
# initialise variables:
K0: uint256 = 0
_g1k0: uint256 = 0
mul1: uint256 = 0
mul2: uint256 = 0
neg_fprime: uint256 = 0
D_plus: uint256 = 0
D_minus: uint256 = 0
D_prev: uint256 = 0
diff: uint256 = 0
frac: uint256 = 0
for i in range(255):
D_prev = D
# K0 = 10**18 * x[0] * N_COINS / D * x[1] * N_COINS / D * x[2] * N_COINS / D
K0 = unsafe_div(
unsafe_mul(
unsafe_mul(
unsafe_div(
unsafe_mul(
unsafe_mul(
unsafe_div(
unsafe_mul(
unsafe_mul(10**18, x[0]), N_COINS
),
D,
),
x[1],
),
N_COINS,
),
D,
),
x[2],
),
N_COINS,
),
D,
) # <-------- We can convert the entire expression using unsafe math.
# since x_i is not too far from D, so overflow is not expected. Also
# D > 0, since we proved that already. unsafe_div is safe. K0 > 0
# since we can safely assume that D < 10**18 * x[0]. K0 is also
# in the range of 10**18 (it's a property).
_g1k0 = unsafe_add(gamma, 10**18) # <--------- safe to do unsafe_add.
if _g1k0 > K0: # The following operations can safely be unsafe.
_g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1)
else:
_g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1)
# D / (A * N**N) * _g1k0**2 / gamma**2
# mul1 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN
mul1 = unsafe_div(
unsafe_mul(
unsafe_mul(
unsafe_div(
unsafe_mul(
unsafe_div(unsafe_mul(10**18, D), gamma), _g1k0
),
gamma,
),
_g1k0,
),
A_MULTIPLIER,
),
ANN,
) # <------ Since D > 0, gamma is small, _g1k0 is small, the rest are
# non-zero and small constants, and D has a cap in this method,
# we can safely convert everything to unsafe maths.
# 2*N*K0 / _g1k0
# mul2 = (2 * 10**18) * N_COINS * K0 / _g1k0
mul2 = unsafe_div(
unsafe_mul(2 * 10**18 * N_COINS, K0), _g1k0
) # <--------------- K0 is approximately around D, which has a cap of
# 10**15 * 10**18 + 1, since we get that in get_y which is called
# with newton_D. _g1k0 > 0, so the entire expression can be unsafe.
# neg_fprime: uint256 = (S + S * mul2 / 10**18) + mul1 * N_COINS / K0 - mul2 * D / 10**18
neg_fprime = unsafe_sub(
unsafe_add(
unsafe_add(S, unsafe_div(unsafe_mul(S, mul2), 10**18)),
unsafe_div(unsafe_mul(mul1, N_COINS), K0),
),
unsafe_div(unsafe_mul(mul2, D), 10**18),
) # <--- mul1 is a big number but not huge: safe to unsafely multiply
# with N_coins. neg_fprime > 0 if this expression executes.
# mul2 is in the range of 10**18, since K0 is in that range, S * mul2
# is safe. The first three sums can be done using unsafe math safely
# and since the final expression will be small since mul2 is small, we
# can safely do the entire expression unsafely.
# D -= f / fprime
# D * (neg_fprime + S) / neg_fprime
D_plus = unsafe_div(D * unsafe_add(neg_fprime, S), neg_fprime)
# D*D / neg_fprime
D_minus = unsafe_div(D * D, neg_fprime)
# Since we know K0 > 0, and neg_fprime > 0, several unsafe operations
# are possible in the following. Also, (10**18 - K0) is safe to mul.
# So the only expressions we keep safe are (D_minus + ...) and (D * ...)
if 10**18 > K0:
# D_minus += D * (mul1 / neg_fprime) / 10**18 * (10**18 - K0) / K0
D_minus += unsafe_div(
unsafe_mul(
unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18),
unsafe_sub(10**18, K0),
),
K0,
)
else:
# D_minus -= D * (mul1 / neg_fprime) / 10**18 * (K0 - 10**18) / K0
D_minus -= unsafe_div(
unsafe_mul(
unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18),
unsafe_sub(K0, 10**18),
),
K0,
)
if D_plus > D_minus:
D = unsafe_sub(D_plus, D_minus) # <--------- Safe since we check.
else:
D = unsafe_div(unsafe_sub(D_minus, D_plus), 2)
if D > D_prev:
diff = unsafe_sub(D, D_prev)
else:
diff = unsafe_sub(D_prev, D)
# Could reduce precision for gas efficiency here:
if unsafe_mul(diff, 10**14) < max(10**16, D):
# Test that we are safe with the next get_y
for _x in x:
frac = unsafe_div(unsafe_mul(_x, 10**18), D)
assert frac >= 10**16 - 1 and frac < 10**20 + 1, "Unsafe values x[i]"
return D
raise "Did not converge"
```
```shell
>>> CryptoSwap.claim_admin_fees()
```
::::
### `xcp_profit`
::::description[`TriCrypto.xcp_profit() -> uint256:`]
Getter for the current pool profits.
Returns: current profits (`uint256`).
```vyper
xcp_profit: public(uint256)
```
```shell
>>> TriCrypto.xcp_profit()
1003213938530958270
```
::::
### `xcp_profit_a`
::::description[`TriCrypto.xcp_profit_a() -> uint256:`]
Getter for the full profit at the last claim of admin fees.
Returns: profit at last claim (`uint256`).
```vyper
xcp_profit_a: public(uint256) # <--- Full profit at last claim of admin fees.
```
```shell
>>> TriCrypto.xcp_profit_a()
1003211094190051384
```
::::
---
## Price Scaling
Curve v2 pools automatically adjust liquidity to optimize depth close to the prevailing market rates, reducing slippage. More [here](../../legacy/cryptoswap-overview.md#price-scaling). Price scaling parameter can be adjusted by the [admin](#apply_new_parameters).
### `price_scale`
::::description[`TriCrypto.price_scale(k: uint256) -> uint256:`]
Getter for the price scale of the coin at index `k` with regard to the coin at index 0. Price scale determines the price band around which liquidity is concentrated and is conditionally updated when calling the functions `add_liquidity`, `remove_liquidity_one_coin`, `exchange`, `exchange_underlying` or `exchange_extended`.
| Input | Type | Description |
| ------ | -------- | ------------------- |
| `k` | `uint256`| Index of the coin. |
Returns: last price (`uint256`).
```vyper
price_scale_packed: uint256 # <------------------------ Internal price scale.
@external
@view
def price_scale(k: uint256) -> uint256:
"""
@notice Returns the price scale of the coin at index `k` w.r.t the coin
at index 0.
@dev Price scale determines the price band around which liquidity is
concentrated.
@param k The index of the coin.
@return uint256 Price scale of coin.
"""
return self._unpack_prices(self.price_scale_packed)[k]
@internal
def tweak_price(
A_gamma: uint256[2],
_xp: uint256[N_COINS],
new_D: uint256,
K0_prev: uint256 = 0,
) -> uint256:
"""
@notice Tweaks price_oracle, last_price and conditionally adjusts
price_scale. This is called whenever there is an unbalanced
liquidity operation: _exchange, add_liquidity, or
remove_liquidity_one_coin.
@dev Contains main liquidity rebalancing logic, by tweaking `price_scale`.
@param A_gamma Array of A and gamma parameters.
@param _xp Array of current balances.
@param new_D New D value.
@param K0_prev Initial guess for `newton_D`.
"""
# ---------------------------- Read storage ------------------------------
rebalancing_params: uint256[3] = self._unpack(
self.packed_rebalancing_params
) # <---------- Contains: allowed_extra_profit, adjustment_step, ma_time.
price_oracle: uint256[N_COINS - 1] = self._unpack_prices(
self.price_oracle_packed
)
last_prices: uint256[N_COINS - 1] = self._unpack_prices(
self.last_prices_packed
)
packed_price_scale: uint256 = self.price_scale_packed
price_scale: uint256[N_COINS - 1] = self._unpack_prices(
packed_price_scale
)
total_supply: uint256 = self.totalSupply
old_xcp_profit: uint256 = self.xcp_profit
old_virtual_price: uint256 = self.virtual_price
last_prices_timestamp: uint256 = self.last_prices_timestamp
# ----------------------- Update MA if needed ----------------------------
if last_prices_timestamp < block.timestamp:
# The moving average price oracle is calculated using the last_price
# of the trade at the previous block, and the price oracle logged
# before that trade. This can happen only once per block.
# ------------------ Calculate moving average params -----------------
alpha: uint256 = MATH.wad_exp(
-convert(
unsafe_div(
(block.timestamp - last_prices_timestamp) * 10**18,
rebalancing_params[2] # <----------------------- ma_time.
),
int256,
)
)
for k in range(N_COINS - 1):
# ----------------- We cap state price that goes into the EMA with
# 2 x price_scale.
price_oracle[k] = unsafe_div(
min(last_prices[k], 2 * price_scale[k]) * (10**18 - alpha) +
price_oracle[k] * alpha, # ^-------- Cap spot price into EMA.
10**18
)
self.price_oracle_packed = self._pack_prices(price_oracle)
self.last_prices_timestamp = block.timestamp # <---- Store timestamp.
# price_oracle is used further on to calculate its vector
# distance from price_scale. This distance is used to calculate
# the amount of adjustment to be done to the price_scale.
# ------------------ If new_D is set to 0, calculate it ------------------
D_unadjusted: uint256 = new_D
if new_D == 0: # <--------------------------- _exchange sets new_D to 0.
D_unadjusted = MATH.newton_D(A_gamma[0], A_gamma[1], _xp, K0_prev)
# ----------------------- Calculate last_prices --------------------------
last_prices = MATH.get_p(_xp, D_unadjusted, A_gamma)
for k in range(N_COINS - 1):
last_prices[k] = unsafe_div(last_prices[k] * price_scale[k], 10**18)
self.last_prices_packed = self._pack_prices(last_prices)
# ---------- Update profit numbers without price adjustment first --------
xp: uint256[N_COINS] = empty(uint256[N_COINS])
xp[0] = unsafe_div(D_unadjusted, N_COINS)
for k in range(N_COINS - 1):
xp[k + 1] = D_unadjusted * 10**18 / (N_COINS * price_scale[k])
# ------------------------- Update xcp_profit ----------------------------
xcp_profit: uint256 = 10**18
virtual_price: uint256 = 10**18
if old_virtual_price > 0:
xcp: uint256 = MATH.geometric_mean(xp)
virtual_price = 10**18 * xcp / total_supply
xcp_profit = unsafe_div(
old_xcp_profit * virtual_price,
old_virtual_price
) # <---------------- Safu to do unsafe_div as old_virtual_price > 0.
# If A and gamma are not undergoing ramps (t < block.timestamp),
# ensure new virtual_price is not less than old virtual_price,
# else the pool suffers a loss.
if self.future_A_gamma_time < block.timestamp:
assert virtual_price > old_virtual_price, "Loss"
self.xcp_profit = xcp_profit
# ------------ Rebalance liquidity if there's enough profits to adjust it:
if virtual_price * 2 - 10**18 > xcp_profit + 2 * rebalancing_params[0]:
# allowed_extra_profit --------^
# ------------------- Get adjustment step ----------------------------
# Calculate the vector distance between price_scale and
# price_oracle.
norm: uint256 = 0
ratio: uint256 = 0
for k in range(N_COINS - 1):
ratio = unsafe_div(price_oracle[k] * 10**18, price_scale[k])
# unsafe_div because we did safediv before ----^
if ratio > 10**18:
ratio = unsafe_sub(ratio, 10**18)
else:
ratio = unsafe_sub(10**18, ratio)
norm = unsafe_add(norm, ratio**2)
norm = isqrt(norm) # <-------------------- isqrt is not in base 1e18.
adjustment_step: uint256 = max(
rebalancing_params[1], unsafe_div(norm, 5)
) # ^------------------------------------- adjustment_step.
if norm > adjustment_step: # <---------- We only adjust prices if the
# vector distance between price_oracle and price_scale is
# large enough. This check ensures that no rebalancing
# occurs if the distance is low i.e. the pool prices are
# pegged to the oracle prices.
# ------------------------------------- Calculate new price scale.
p_new: uint256[N_COINS - 1] = empty(uint256[N_COINS - 1])
for k in range(N_COINS - 1):
p_new[k] = unsafe_div(
price_scale[k] * unsafe_sub(norm, adjustment_step)
+ adjustment_step * price_oracle[k],
norm
) # <- norm is non-zero and gt adjustment_step; unsafe = safe
# ---------------- Update stale xp (using price_scale) with p_new.
xp = _xp
for k in range(N_COINS - 1):
xp[k + 1] = unsafe_div(_xp[k + 1] * p_new[k], price_scale[k])
# unsafe_div because we did safediv before ----^
# ------------------------------------------ Update D with new xp.
D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)
for k in range(N_COINS):
frac: uint256 = xp[k] * 10**18 / D # <----- Check validity of
assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # p_new.
xp[0] = D / N_COINS
for k in range(N_COINS - 1):
xp[k + 1] = D * 10**18 / (N_COINS * p_new[k]) # <---- Convert
# xp to real prices.
# ---------- Calculate new virtual_price using new xp and D. Reuse
# `old_virtual_price` (but it has new virtual_price).
old_virtual_price = unsafe_div(
10**18 * MATH.geometric_mean(xp), total_supply
) # <----- unsafe_div because we did safediv before (if vp>1e18)
# ---------------------------- Proceed if we've got enough profit.
if (
old_virtual_price > 10**18 and
2 * old_virtual_price - 10**18 > xcp_profit
):
packed_price_scale = self._pack_prices(p_new)
self.D = D
self.virtual_price = old_virtual_price
self.price_scale_packed = packed_price_scale
return packed_price_scale
# --------- price_scale was not adjusted. Update the profit counter and D.
self.D = D_unadjusted
self.virtual_price = virtual_price
return packed_price_scale
```
```shell
>>> TriCrypto.price_scale(0)
27902293922834345521086
```
::::
### `allowed_extra_profit`
::::description[`TriCrypto.allowed_extra_profit() -> uint256:`]
Getter for the allowed extra profit value.
Returns: allowed extra profit (`uint256`).
```vyper
packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing
# parameters allowed_extra_profit, adjustment_step, and ma_time.
@view
@external
def allowed_extra_profit() -> uint256:
"""
@notice Returns the current allowed extra profit
@return uint256 allowed_extra_profit value.
"""
return self._unpack(self.packed_rebalancing_params)[0]
```
```shell
>>> TriCrypto.allowed_extra_profit()
100000000
```
::::
### `adjustment_step`
::::description[`TriCrypto.adjustment_step() -> uint256:`]
Getter for the adjustment step value.
Returns: adjustment step (`uint256`).
```vyper
packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing
# parameters allowed_extra_profit, adjustment_step, and ma_time.
@view
@external
def adjustment_step() -> uint256:
"""
@notice Returns the current adjustment step
@return uint256 adjustment_step value.
"""
return self._unpack(self.packed_rebalancing_params)[1]
```
```shell
>>> TriCrypto.adjustment_step()
100000000000
```
::::
### `packed_rebalancing_params`
::::description[`TriCrypto.packed_rebalancing_params() -> uint256: view`]
Getter for the packed rebalancing parameters, consisting of `allowed_extra_profit`, `adjustment_step`, and `ma_time`.
Returns: packed rebalancing parameters (`uint256`).
```vyper
packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing
# parameters allowed_extra_profit, adjustment_step, and ma_time.
```
```shell
>>> TriCrypto.packed_rebalancing_params()
34028236692093848191011868114131982745600000866
```
::::
---
## Bonding Curve Parameters
A bonding curve is used to determine asset prices according to the pool's supply of each asset, more [here](../../legacy/cryptoswap-overview.md#bonding-curve-parameters).
Bonding curve parameters `A` and `gamma` values are [upgradable](#amplification-coefficient-and-gamma) by the the pools admin.
### `A`
::::description[`CryptoSwap.A() -> uint256:`]
Getter for the current pool amplification parameter.
Returns: A (`uint256`).
```vyper
@view
@external
def A() -> uint256:
"""
@notice Returns the current pool amplification parameter.
@return uint256 A param.
"""
return self._A_gamma()[0]
@view
@internal
def _A_gamma() -> uint256[2]:
t1: uint256 = self.future_A_gamma_time
A_gamma_1: uint256 = self.future_A_gamma
gamma1: uint256 = A_gamma_1 & 2**128 - 1
A1: uint256 = A_gamma_1 >> 128
if block.timestamp < t1:
# --------------- Handle ramping up and down of A --------------------
A_gamma_0: uint256 = self.initial_A_gamma
t0: uint256 = self.initial_A_gamma_time
t1 -= t0
t0 = block.timestamp - t0
t2: uint256 = t1 - t0
A1 = ((A_gamma_0 >> 128) * t2 + A1 * t0) / t1
gamma1 = ((A_gamma_0 & 2**128 - 1) * t2 + gamma1 * t0) / t1
return [A1, gamma1]
```
```shell
>>> CryptoSwap.A()
1707629
```
::::
### `gamma`
::::description[`CryptoSwap.gamma() -> uint256:`]
Getter for the current pool gamma parameter.
Returns: gamma (`uint256`).
```vyper
@view
@external
def gamma() -> uint256:
"""
@notice Returns the current pool gamma parameter.
@return uint256 gamma param.
"""
return self._A_gamma()[1]
@view
@internal
def _A_gamma() -> uint256[2]:
t1: uint256 = self.future_A_gamma_time
A_gamma_1: uint256 = self.future_A_gamma
gamma1: uint256 = A_gamma_1 & 2**128 - 1
A1: uint256 = A_gamma_1 >> 128
if block.timestamp < t1:
# --------------- Handle ramping up and down of A --------------------
A_gamma_0: uint256 = self.initial_A_gamma
t0: uint256 = self.initial_A_gamma_time
t1 -= t0
t0 = block.timestamp - t0
t2: uint256 = t1 - t0
A1 = ((A_gamma_0 >> 128) * t2 + A1 * t0) / t1
gamma1 = ((A_gamma_0 & 2**128 - 1) * t2 + gamma1 * t0) / t1
return [A1, gamma1]
```
```shell
>>> CryptoSwap.gamma()
11809167828997
```
::::
---
## Contract Info Methods
### `coins`
::::description[`TriCrypto.coins(arg0: uint256) -> uint256: view`]
Getter for the coin at index `arg0`.
| Input | Type | Description |
| ------ | -------- | ------------------- |
| `k` | `uint256`| Index of the coin. |
Returns: coin (`address`).
```vyper
coins: public(immutable(address[N_COINS]))
```
```shell
>>> TriCrypto.coins(0)
'0xdAC17F958D2ee523a2206206994597C13D831ec7'
```
::::
### `balances`
::::description[`TriCrypto.balances(arg0: uint256) -> uint256: view`]
Getter for the coin balance at index `arg0`.
| Input | Type | Description |
| ------ | -------- | ------------------- |
| `k` | `uint256`| Index of the coin. |
Returns: coin balance (`address`).
```vyper
balances: public(uint256[N_COINS])
```
```shell
>>> TriCrypto.balances(0)
16193303272455
```
::::
### `precisions`
::::description[`TriCrypto.precisions() -> uint256[N_COINS]: view`]
Getter for the precision of each coin in the pool.
Returns: precisions (`uint256[N_COINS]`).
```vyper
N_COINS: constant(uint256) = 3
PRECISION: constant(uint256) = 10**18 # <------- The precision to convert to.
A_MULTIPLIER: constant(uint256) = 10000
packed_precisions: uint256
@view
@external
def precisions() -> uint256[N_COINS]: # <-------------- For by view contract.
"""
@notice Returns the precisions of each coin in the pool.
@return uint256[3] precisions of coins.
"""
return self._unpack(self.packed_precisions)
```
```shell
>>> TriCrypto.precisions()
1000000000000, 10000000000, 1
```
::::
### `factory`
::::description[`TriCrypto.factory() -> address: view`]
Getter for the Factory contract.
Returns: Factory (`address`)
```vyper
factory: public(address)
```
```shell
>>> TriCrypto.factory()
'0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963'
```
::::
### `MATH`
::::description[`TriCrypto.MATH() -> address: view`]
Getter for the [math utility contract](../utility-contracts/math.md).
Returns: math contract (`address`).
```vyper
factory: public(address)
```
```shell
>>> TriCrypto.MATH()
'0xcBFf3004a20dBfE2731543AA38599A526e0fD6eE'
```
::::
### `WETH20`
::::description[`TriCrypto.WETH20() -> address: view`]
Getter for the wETH contract.
Returns: wETH contract (`address`).
```vyper
WETH20: public(immutable(address))
```
```shell
>>> TriCrypto.WETH20()
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
```
::::
## Admin Controls
All pools created through the Factory are "owned" by the admin of the Factory, which is the Curve DAO. Ownership can only be changed within the factory contract via `commit_transfer_ownership` and `accept_transfer_ownership`.
Applying new parameters involves a **two-step model**. In the first step, changes need to be committed. The second step involves applying these changes.
### Amplification Coefficient and Gamma
More information about the parameters [here](https://nagaking.substack.com/p/deep-dive-curve-v2-parameters).
The appropriate value for `A` and `gamma` is dependent upon the type of coin being used within the pool, and is subject to optimisation and pool-parameter update based on the market history of the trading pair. It is possible to modify the parameters for a pool after it has been deployed. However, it requires a vote within the Curve DAO and must reach a 15% quorum.
### `ramp_A_gamma`
::::description[`TriCrypto.ramp_A_gamma(future_A: uint256, future_gamma: uint256, future_time: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory contract.
:::
Function to ramp A and gamma parameter values linearly. `A` and `gamma` are packed within the same variable.
| Input | Type | Description |
| ----------- | -------| ----|
| `future_A` | `uint256` | Future value of `A` |
| `future_gamma` | `uint256` | Future value of `gamma` |
| `future_time` | `uint256` | Timestamp at which the ramping will end |
Emits: `RampAgamma`
```vyper
event RampAgamma:
initial_A: uint256
future_A: uint256
initial_gamma: uint256
future_gamma: uint256
initial_time: uint256
future_time: uint256
@external
def ramp_A_gamma(
future_A: uint256, future_gamma: uint256, future_time: uint256
):
"""
@notice Initialise Ramping A and gamma parameter values linearly.
@dev Only accessible by factory admin, and only
@param future_A The future A value.
@param future_gamma The future gamma value.
@param future_time The timestamp at which the ramping will end.
"""
assert msg.sender == factory.admin() # dev: only owner
assert block.timestamp > self.initial_A_gamma_time + (MIN_RAMP_TIME - 1) # dev: ramp undergoing
assert future_time > block.timestamp + MIN_RAMP_TIME - 1 # dev: insufficient time
A_gamma: uint256[2] = self._A_gamma()
initial_A_gamma: uint256 = A_gamma[0] << 128
initial_A_gamma = initial_A_gamma | A_gamma[1]
assert future_A > MIN_A - 1
assert future_A < MAX_A + 1
assert future_gamma > MIN_GAMMA - 1
assert future_gamma < MAX_GAMMA + 1
ratio: uint256 = 10**18 * future_A / A_gamma[0]
assert ratio < 10**18 * MAX_A_CHANGE + 1
assert ratio > 10**18 / MAX_A_CHANGE - 1
ratio = 10**18 * future_gamma / A_gamma[1]
assert ratio < 10**18 * MAX_A_CHANGE + 1
assert ratio > 10**18 / MAX_A_CHANGE - 1
self.initial_A_gamma = initial_A_gamma
self.initial_A_gamma_time = block.timestamp
future_A_gamma: uint256 = future_A << 128
future_A_gamma = future_A_gamma | future_gamma
self.future_A_gamma_time = future_time
self.future_A_gamma = future_A_gamma
log RampAgamma(
A_gamma[0],
future_A,
A_gamma[1],
future_gamma,
block.timestamp,
future_time,
)
```
```shell
>>> TriCrypto.ramp_A_gamma(2700000, 1300000000000, 1693674492)
```
::::
### `stop_ramp_A_gamma`
::::description[`TriCrypto.stop_ramp_A_gamma()`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory contract.
:::
Function to immediately stop ramping A and gamma parameters and set them to the current value.
Emits: `StopRampA`
```vyper
event StopRampA:
current_A: uint256
current_gamma: uint256
time: uint256
@external
def stop_ramp_A_gamma():
"""
@notice Stop Ramping A and gamma parameters immediately.
@dev Only accessible by factory admin.
"""
assert msg.sender == factory.admin() # dev: only owner
A_gamma: uint256[2] = self._A_gamma()
current_A_gamma: uint256 = A_gamma[0] << 128
current_A_gamma = current_A_gamma | A_gamma[1]
self.initial_A_gamma = current_A_gamma
self.future_A_gamma = current_A_gamma
self.initial_A_gamma_time = block.timestamp
self.future_A_gamma_time = block.timestamp
# ------ Now (block.timestamp < t1) is always False, so we return saved A.
log StopRampA(A_gamma[0], A_gamma[1], block.timestamp)
```
```shell
>>> TriCrypto.stop_ramp_A_gamma()
```
::::
### Changing Parameters
### `commit_new_parameters`
::::description[`TriCrypto.commit_new_parameters(_new_mid_fee: uint256, _new_out_fee: uint256, _new_fee_gamma: uint256, _new_allowed_extra_profit: uint256, _new_adjustment_step: uint256, _new_ma_time: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory contract.
:::
Function to commit new parameters. The new parameters do not take immediate effect.
Emits: `CommitNewParameters`
| Input | Type | Description |
| ----------- | -------| ----|
| `_new_mid_fee` | `uint256` | New `mid_fee` value |
| `_new_out_fee` | `uint256` | New `out_fee` value |
| `_new_fee_gamma` | `uint256` | New `fee_gamma` value |
| `_new_allowed_extra_profit` | `uint256` | New `allowed_extra_profit` value |
| `_new_adjustment_step` | `uint256` | New `adjustment_step` value |
| `_new_ma_time` | `uint256` | New `ma_time` value |
```vyper
event CommitNewParameters:
deadline: indexed(uint256)
mid_fee: uint256
out_fee: uint256
fee_gamma: uint256
allowed_extra_profit: uint256
adjustment_step: uint256
ma_time: uint256
future_packed_rebalancing_params: uint256
future_packed_fee_params: uint256
ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400
@external
def commit_new_parameters(
_new_mid_fee: uint256,
_new_out_fee: uint256,
_new_fee_gamma: uint256,
_new_allowed_extra_profit: uint256,
_new_adjustment_step: uint256,
_new_ma_time: uint256,
):
"""
@notice Commit new parameters.
@dev Only accessible by factory admin.
@param _new_mid_fee The new mid fee.
@param _new_out_fee The new out fee.
@param _new_fee_gamma The new fee gamma.
@param _new_allowed_extra_profit The new allowed extra profit.
@param _new_adjustment_step The new adjustment step.
@param _new_ma_time The new ma time. ma_time is time_in_seconds/ln(2).
"""
assert msg.sender == Factory(self.factory).admin() # dev: only owner
assert self.admin_actions_deadline == 0 # dev: active action
_deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY
self.admin_actions_deadline = _deadline
# ----------------------------- Set fee params ---------------------------
new_mid_fee: uint256 = _new_mid_fee
new_out_fee: uint256 = _new_out_fee
new_fee_gamma: uint256 = _new_fee_gamma
current_fee_params: uint256[3] = self._unpack(self.packed_fee_params)
if new_out_fee < MAX_FEE + 1:
assert new_out_fee > MIN_FEE - 1 # dev: fee is out of range
else:
new_out_fee = current_fee_params[1]
if new_mid_fee > MAX_FEE:
new_mid_fee = current_fee_params[0]
assert new_mid_fee <= new_out_fee # dev: mid-fee is too high
if new_fee_gamma < 10**18:
assert new_fee_gamma > 0 # dev: fee_gamma out of range [1 .. 10**18]
else:
new_fee_gamma = current_fee_params[2]
self.future_packed_fee_params = self._pack(
[new_mid_fee, new_out_fee, new_fee_gamma]
)
# ----------------- Set liquidity rebalancing parameters -----------------
new_allowed_extra_profit: uint256 = _new_allowed_extra_profit
new_adjustment_step: uint256 = _new_adjustment_step
new_ma_time: uint256 = _new_ma_time
current_rebalancing_params: uint256[3] = self._unpack(self.packed_rebalancing_params)
if new_allowed_extra_profit > 10**18:
new_allowed_extra_profit = current_rebalancing_params[0]
if new_adjustment_step > 10**18:
new_adjustment_step = current_rebalancing_params[1]
if new_ma_time < 872542: # <----- Calculated as: 7 * 24 * 60 * 60 / ln(2)
assert new_ma_time > 86 # dev: MA time should be longer than 60/ln(2)
else:
new_ma_time = current_rebalancing_params[2]
self.future_packed_rebalancing_params = self._pack(
[new_allowed_extra_profit, new_adjustment_step, new_ma_time]
)
# ---------------------------------- LOG ---------------------------------
log CommitNewParameters(
_deadline,
new_mid_fee,
new_out_fee,
new_fee_gamma,
new_allowed_extra_profit,
new_adjustment_step,
new_ma_time,
)
```
```shell
>>> TriCrypto.commit_new_parameters(20000000, 45000000, 350000000000000, 100000000000, 100000000000, 1800)
```
::::
### `apply_new_parameters`
::::description[`TriCrypto.apply_new_parameters()`]
:::guard[Guarded Method]
This function can only be called after the `admin_actions_deadline` has passed. The deadline is set when new parameters are committed via [`commit_new_parameters`](#commit_new_parameters).
:::
Function to apply the parameters from [`commit_new_parameters`](#commit_new_parameters).
Emits: `NewParameters`
```vyper
event NewParameters:
mid_fee: uint256
out_fee: uint256
fee_gamma: uint256
allowed_extra_profit: uint256
adjustment_step: uint256
ma_time: uint256
packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing
# parameters allowed_extra_profit, adjustment_step, and ma_time.
future_packed_rebalancing_params: uint256
packed_fee_params: public(uint256) # <---- Packs mid_fee, out_fee, fee_gamma.
future_packed_fee_params: uint256
@external
@nonreentrant("lock")
def apply_new_parameters():
"""
@notice Apply committed parameters.
@dev Only callable after admin_actions_deadline.
"""
assert block.timestamp >= self.admin_actions_deadline # dev: insufficient time
assert self.admin_actions_deadline != 0 # dev: no active action
self.admin_actions_deadline = 0
packed_fee_params: uint256 = self.future_packed_fee_params
self.packed_fee_params = packed_fee_params
packed_rebalancing_params: uint256 = self.future_packed_rebalancing_params
self.packed_rebalancing_params = packed_rebalancing_params
rebalancing_params: uint256[3] = self._unpack(packed_rebalancing_params)
fee_params: uint256[3] = self._unpack(packed_fee_params)
log NewParameters(
fee_params[0],
fee_params[1],
fee_params[2],
rebalancing_params[0],
rebalancing_params[1],
rebalancing_params[2],
)
```
```shell
>>> TriCrypto.apply_new_parameters()
```
::::
### `revert_new_parameters`
::::description[`TriCrypto.revert_new_parameters()`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory contract.
:::
Function to revert the parameters changes.
```vyper
@external
def revert_new_parameters():
"""
@notice Revert committed parameters
@dev Only accessible by factory admin. Setting admin_actions_deadline to 0
ensures a revert in apply_new_parameters.
"""
assert msg.sender == Factory(self.factory).admin() # dev: only owner
self.admin_actions_deadline = 0
```
```shell
>>> TriCrypto.revert_new_parameters()
```
::::
### `admin_actions_deadline`
::::description[`TriCrypto.admin_actions_deadline() -> uint256: view`]
Getter for the admin actions deadline. This is the deadline until which new parameter changes can be applied. When committing new changes, there is a three-day timespan to apply them (`ADMIN_ACTIONS_DELAY`). If called later, the call will revert.
Returns: timestamp (`uint256`).
```vyper
admin_actions_deadline: public(uint256)
ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400
```
```shell
>>> TriCrypto.admin_actions_deadline()
0
```
::::
### `initial_A_gamma`
::::description[`TriCrypto.initial_A_gamma() -> uint256: view`]
Getter for the initial A/gamma.
Returns: A/gamma (`uint256`).
```vyper
initial_A_gamma: public(uint256)
```
```shell
>>> TriCrypto.initial_A_gamma()
581076037942835227425498917514114728328226821
```
::::
### `initial_A_gamma_time`
::::description[`TriCrypto.initial_A_gamma_time() -> uint256: view`]
Getter for the initial A/gamma time.
Returns: A/gamma time (`uint256`).
```vyper
initial_A_gamma_time: public(uint256)
```
```shell
>>> TriCrypto.initial_A_gamma_time()
0
```
::::
### `future_A_gamma`
::::description[`TriCrypto.future_A_gamma() -> uint256: view`]
Getter for the future A/gamma.
Returns: future A/gamma (`uint256`).
```vyper
future_A_gamma: public(uint256)
```
```shell
>>> TriCrypto.future_A_gamma()
581076037942835227425498917514114728328226821
```
::::
### `future_A_gamma_time`
::::description[`TriCrypto.future_A_gamma_time() -> uint256: view`]
Getter for the future A/gamma time.
Returns: future A/gamma time (`uint256`).
```vyper
future_A_gamma_time: public(uint256)
```
```shell
>>> TriCrypto.future_A_gamma_time()
0
```
::::
---
## Math Contract(Utility-contracts)
**The Math Contract provides AMM Math for 3-coin Curve Cryptoswap Pools.**
:::deploy[Contract Source & Deployment]
Source code for this contract is available on [Github](https://github.com/curvefi/tricrypto-ng/blob/main/contracts/main/CurveCryptoMathOptimized3.vy).
Full list of all deployments can be found [here](../../../deployments.md).
:::
---
## AMM Math Functions
### `get_y`
::::description[`Math.get_y(_ANN: uint256, _gamma: uint256, x: uint256[N_COINS], _D: uint256, i: uint256) -> uint256[2]:`]
Function to calculate x[i] given other balances x[0..N_COINS-1] and invariant D.
| Input | Type | Description |
| ----------- | -------| ----|
| `_ANN` | `uint256` | ANN = A * N**N |
| `_gamma` | `_gamma` | AMM.gamma() value |
| `x` | `uint256[N_COINS]` | Balances multiplied by prices and precisions of all coins |
| `_D` | `uint256` | Invariant |
| `i` | `uint256` | Index of coin to calculate y |
Returns: y (`uint256`).
```vyper
N_COINS: constant(uint256) = 3
A_MULTIPLIER: constant(uint256) = 10000
MIN_GAMMA: constant(uint256) = 10**10
MAX_GAMMA: constant(uint256) = 5 * 10**16
MIN_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER / 100
MAX_A: constant(uint256) = N_COINS**N_COINS * A_MULTIPLIER * 1000
@external
@view
def get_y(
_ANN: uint256, _gamma: uint256, x: uint256[N_COINS], _D: uint256, i: uint256
) -> uint256[2]:
"""
@notice Calculate x[i] given other balances x[0..N_COINS-1] and invariant D.
@dev ANN = A * N**N.
@param _ANN AMM.A() value.
@param _gamma AMM.gamma() value.
@param x Balances multiplied by prices and precisions of all coins.
@param _D Invariant.
@param i Index of coin to calculate y.
"""
# Safety checks
assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A
assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D
frac: uint256 = 0
for k in range(3):
if k != i:
frac = x[k] * 10**18 / _D
assert frac > 10**16 - 1 and frac < 10**20 + 1, "Unsafe values x[i]"
# if above conditions are met, x[k] > 0
j: uint256 = 0
k: uint256 = 0
if i == 0:
j = 1
k = 2
elif i == 1:
j = 0
k = 2
elif i == 2:
j = 0
k = 1
ANN: int256 = convert(_ANN, int256)
gamma: int256 = convert(_gamma, int256)
D: int256 = convert(_D, int256)
x_j: int256 = convert(x[j], int256)
x_k: int256 = convert(x[k], int256)
gamma2: int256 = unsafe_mul(gamma, gamma)
a: int256 = 10**36 / 27
# 10**36/9 + 2*10**18*gamma/27 - D**2/x_j*gamma**2*ANN/27**2/convert(A_MULTIPLIER, int256)/x_k
b: int256 = (
unsafe_add(
10**36 / 9,
unsafe_div(unsafe_mul(2 * 10**18, gamma), 27)
)
- unsafe_div(
unsafe_div(
unsafe_div(
unsafe_mul(
unsafe_div(unsafe_mul(D, D), x_j),
gamma2
) * ANN,
27**2
),
convert(A_MULTIPLIER, int256)
),
x_k,
)
) # <------- The first two expressions can be unsafe, and unsafely added.
# 10**36/9 + gamma*(gamma + 4*10**18)/27 + gamma**2*(x_j+x_k-D)/D*ANN/27/convert(A_MULTIPLIER, int256)
c: int256 = (
unsafe_add(
10**36 / 9,
unsafe_div(unsafe_mul(gamma, unsafe_add(gamma, 4 * 10**18)), 27)
)
+ unsafe_div(
unsafe_div(
unsafe_mul(
unsafe_div(gamma2 * unsafe_sub(unsafe_add(x_j, x_k), D), D),
ANN
),
27
),
convert(A_MULTIPLIER, int256),
)
) # <--------- Same as above with the first two expressions. In the third
# expression, x_j + x_k will not overflow since we know their range from
# previous assert statements.
# (10**18 + gamma)**2/27
d: int256 = unsafe_div(unsafe_add(10**18, gamma)**2, 27)
# abs(3*a*c/b - b)
d0: int256 = abs(unsafe_mul(3, a) * c / b - b) # <------------ a is smol.
divider: int256 = 0
if d0 > 10**48:
divider = 10**30
elif d0 > 10**44:
divider = 10**26
elif d0 > 10**40:
divider = 10**22
elif d0 > 10**36:
divider = 10**18
elif d0 > 10**32:
divider = 10**14
elif d0 > 10**28:
divider = 10**10
elif d0 > 10**24:
divider = 10**6
elif d0 > 10**20:
divider = 10**2
else:
divider = 1
additional_prec: int256 = 0
if abs(a) > abs(b):
additional_prec = abs(unsafe_div(a, b))
a = unsafe_div(unsafe_mul(a, additional_prec), divider)
b = unsafe_div(b * additional_prec, divider)
c = unsafe_div(c * additional_prec, divider)
d = unsafe_div(d * additional_prec, divider)
else:
additional_prec = abs(unsafe_div(b, a))
a = unsafe_div(a / additional_prec, divider)
b = unsafe_div(unsafe_div(b, additional_prec), divider)
c = unsafe_div(unsafe_div(c, additional_prec), divider)
d = unsafe_div(unsafe_div(d, additional_prec), divider)
# 3*a*c/b - b
_3ac: int256 = unsafe_mul(3, a) * c
delta0: int256 = unsafe_div(_3ac, b) - b
# 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1: int256 = (
unsafe_div(3 * _3ac, b)
- unsafe_mul(2, b)
- unsafe_div(unsafe_div(27 * a**2, b) * d, b)
)
# delta1**2 + 4*delta0**2/b*delta0
sqrt_arg: int256 = (
delta1**2 +
unsafe_div(4 * delta0**2, b) * delta0
)
sqrt_val: int256 = 0
if sqrt_arg > 0:
sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256)
else:
return [self._newton_y(_ANN, _gamma, x, _D, i), 0]
b_cbrt: int256 = 0
if b >= 0:
b_cbrt = convert(self._cbrt(convert(b, uint256)), int256)
else:
b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256)
second_cbrt: int256 = 0
if delta1 > 0:
# convert(self._cbrt(convert((delta1 + sqrt_val), uint256)/2), int256)
second_cbrt = convert(
self._cbrt(unsafe_div(convert(delta1 + sqrt_val, uint256), 2)),
int256
)
else:
second_cbrt = -convert(
self._cbrt(unsafe_div(convert(-(delta1 - sqrt_val), uint256), 2)),
int256
)
# b_cbrt*b_cbrt/10**18*second_cbrt/10**18
C1: int256 = unsafe_div(
unsafe_div(b_cbrt * b_cbrt, 10**18) * second_cbrt,
10**18
)
# (b + b*delta0/C1 - C1)/3
root_K0: int256 = unsafe_div(b + b * delta0 / C1 - C1, 3)
# D*D/27/x_k*D/x_j*root_K0/a
root: int256 = unsafe_div(
unsafe_div(
unsafe_div(unsafe_div(D * D, 27), x_k) * D,
x_j
) * root_K0,
a
)
out: uint256[2] = [
convert(root, uint256),
convert(unsafe_div(10**18 * root_K0, a), uint256)
]
frac = unsafe_div(out[0] * 10**18, _D)
assert frac >= 10**16 - 1 and frac < 10**20 + 1, "Unsafe value for y"
# due to precision issues, get_y can be off by 2 wei or so wrt _newton_y
return out
```
```shell
>>> Math.get_y(54321000, 14500000000000, [10**18, 10**18, 10**18], 3 * 10**18, 0)
[999999999999999999, 0]
```
::::
### `newton_D`
::::description[`Math.newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS],K0_prev: uint256 = 0,) -> uint256:`]
Function to calculate the invariant with Newtons method using good initial guesses.
| Input | Type | Description |
| ----------- | -------| ----|
| `ANN` | `uint256` | ANN = A * N**N |
| `gamma` | `uint256` | AMM.gamma() value |
| `x_unsorted` | `uint256[N_COINS]` | Unsorted array of coin balances |
| `K0_prev` | `uint256` | apriori for newton's method derived from get_y_int. Defaults to zero (no apriori) |
Returns: D invariant (`uint256`).
```vyper
@external
@view
def newton_D(
ANN: uint256,
gamma: uint256,
x_unsorted: uint256[N_COINS],
K0_prev: uint256 = 0,
) -> uint256:
"""
@notice Finding the invariant via newtons method using good initial guesses.
@dev ANN is higher by the factor A_MULTIPLIER
@dev ANN is already A * N**N
@param ANN the A * N**N value
@param gamma the gamma value
@param x_unsorted the array of coin balances (not sorted)
@param K0_prev apriori for newton's method derived from get_y_int. Defaults
to zero (no apriori)
"""
x: uint256[N_COINS] = self._sort(x_unsorted)
assert x[0] < max_value(uint256) / 10**18 * N_COINS**N_COINS # dev: out of limits
assert x[0] > 0 # dev: empty pool
# Safe to do unsafe add since we checked largest x's bounds previously
S: uint256 = unsafe_add(unsafe_add(x[0], x[1]), x[2])
D: uint256 = 0
if K0_prev == 0:
# Geometric mean of 3 numbers cannot be larger than the largest number
# so the following is safe to do:
D = unsafe_mul(N_COINS, self._geometric_mean(x))
else:
if S > 10**36:
D = self._cbrt(
unsafe_div(
unsafe_div(x[0] * x[1], 10**36) * x[2],
K0_prev
) * 27 * 10**12
)
elif S > 10**24:
D = self._cbrt(
unsafe_div(
unsafe_div(x[0] * x[1], 10**24) * x[2],
K0_prev
) * 27 * 10**6
)
else:
D = self._cbrt(
unsafe_div(
unsafe_div(x[0] * x[1], 10**18) * x[2],
K0_prev
) * 27
)
# D not zero here if K0_prev > 0, and we checked if x[0] is gt 0.
# initialise variables:
K0: uint256 = 0
_g1k0: uint256 = 0
mul1: uint256 = 0
mul2: uint256 = 0
neg_fprime: uint256 = 0
D_plus: uint256 = 0
D_minus: uint256 = 0
D_prev: uint256 = 0
diff: uint256 = 0
frac: uint256 = 0
for i in range(255):
D_prev = D
# K0 = 10**18 * x[0] * N_COINS / D * x[1] * N_COINS / D * x[2] * N_COINS / D
K0 = unsafe_div(
unsafe_mul(
unsafe_mul(
unsafe_div(
unsafe_mul(
unsafe_mul(
unsafe_div(
unsafe_mul(
unsafe_mul(10**18, x[0]), N_COINS
),
D,
),
x[1],
),
N_COINS,
),
D,
),
x[2],
),
N_COINS,
),
D,
) # <-------- We can convert the entire expression using unsafe math.
# since x_i is not too far from D, so overflow is not expected. Also
# D > 0, since we proved that already. unsafe_div is safe. K0 > 0
# since we can safely assume that D < 10**18 * x[0]. K0 is also
# in the range of 10**18 (it's a property).
_g1k0 = unsafe_add(gamma, 10**18) # <--------- safe to do unsafe_add.
if _g1k0 > K0: # The following operations can safely be unsafe.
_g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1)
else:
_g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1)
# D / (A * N**N) * _g1k0**2 / gamma**2
# mul1 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN
mul1 = unsafe_div(
unsafe_mul(
unsafe_mul(
unsafe_div(
unsafe_mul(
unsafe_div(unsafe_mul(10**18, D), gamma), _g1k0
),
gamma,
),
_g1k0,
),
A_MULTIPLIER,
),
ANN,
) # <------ Since D > 0, gamma is small, _g1k0 is small, the rest are
# non-zero and small constants, and D has a cap in this method,
# we can safely convert everything to unsafe maths.
# 2*N*K0 / _g1k0
# mul2 = (2 * 10**18) * N_COINS * K0 / _g1k0
mul2 = unsafe_div(
unsafe_mul(2 * 10**18 * N_COINS, K0), _g1k0
) # <--------------- K0 is approximately around D, which has a cap of
# 10**15 * 10**18 + 1, since we get that in get_y which is called
# with newton_D. _g1k0 > 0, so the entire expression can be unsafe.
# neg_fprime: uint256 = (S + S * mul2 / 10**18) + mul1 * N_COINS / K0 - mul2 * D / 10**18
neg_fprime = unsafe_sub(
unsafe_add(
unsafe_add(S, unsafe_div(unsafe_mul(S, mul2), 10**18)),
unsafe_div(unsafe_mul(mul1, N_COINS), K0),
),
unsafe_div(unsafe_mul(mul2, D), 10**18),
) # <--- mul1 is a big number but not huge: safe to unsafely multiply
# with N_coins. neg_fprime > 0 if this expression executes.
# mul2 is in the range of 10**18, since K0 is in that range, S * mul2
# is safe. The first three sums can be done using unsafe math safely
# and since the final expression will be small since mul2 is small, we
# can safely do the entire expression unsafely.
# D -= f / fprime
# D * (neg_fprime + S) / neg_fprime
D_plus = unsafe_div(D * unsafe_add(neg_fprime, S), neg_fprime)
# D*D / neg_fprime
D_minus = unsafe_div(D * D, neg_fprime)
# Since we know K0 > 0, and neg_fprime > 0, several unsafe operations
# are possible in the following. Also, (10**18 - K0) is safe to mul.
# So the only expressions we keep safe are (D_minus + ...) and (D * ...)
if 10**18 > K0:
# D_minus += D * (mul1 / neg_fprime) / 10**18 * (10**18 - K0) / K0
D_minus += unsafe_div(
unsafe_mul(
unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18),
unsafe_sub(10**18, K0),
),
K0,
)
else:
# D_minus -= D * (mul1 / neg_fprime) / 10**18 * (K0 - 10**18) / K0
D_minus -= unsafe_div(
unsafe_mul(
unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18),
unsafe_sub(K0, 10**18),
),
K0,
)
if D_plus > D_minus:
D = unsafe_sub(D_plus, D_minus) # <--------- Safe since we check.
else:
D = unsafe_div(unsafe_sub(D_minus, D_plus), 2)
if D > D_prev:
diff = unsafe_sub(D, D_prev)
else:
diff = unsafe_sub(D_prev, D)
# Could reduce precision for gas efficiency here:
if unsafe_mul(diff, 10**14) < max(10**16, D):
# Test that we are safe with the next get_y
for _x in x:
frac = unsafe_div(unsafe_mul(_x, 10**18), D)
assert frac >= 10**16 - 1 and frac < 10**20 + 1, "Unsafe values x[i]"
return D
raise "Did not converge"
```
```shell
>>> Math.newton_D(54321000, 14500000000000, [10**18, 10**18, 10**18])
3000000000000000000
```
::::
### `get_p`
::::description[`Math.get_p(_xp: uint256[N_COINS], _D: uint256, _A_gamma: uint256[N_COINS-1]) -> uint256[N_COINS-1]:`]
Function to calculate dx/dy.
| Input | Type | Description |
| ----------- | -------| ----|
| `_xp` | `uint256[N_COINS]` | Balances of the pool |
| `_D` | `uint256` | Current value of D |
| `_A_gamma` | `uint256[N_COINS-1]` | Amplification coefficient and gamma |
Returns: dx/dy (`uint256[N_COINS-1]`).
```vyper
@external
@view
def get_p(
_xp: uint256[N_COINS], _D: uint256, _A_gamma: uint256[N_COINS-1]
) -> uint256[N_COINS-1]:
"""
@notice Calculates dx/dy.
@dev Output needs to be multiplied with price_scale to get the actual value.
@param _xp Balances of the pool.
@param _D Current value of D.
@param _A_gamma Amplification coefficient and gamma.
"""
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe D values
# K0 = P * N**N / D**N.
# K0 is dimensionless and has 10**36 precision:
K0: uint256 = unsafe_div(
unsafe_div(unsafe_div(27 * _xp[0] * _xp[1], _D) * _xp[2], _D) * 10**36,
_D
)
# GK0 is in 10**36 precision and is dimensionless.
# GK0 = (
# 2 * _K0 * _K0 / 10**36 * _K0 / 10**36
# + (gamma + 10**18)**2
# - (_K0 * _K0 / 10**36 * (2 * gamma + 3 * 10**18) / 10**18)
# )
# GK0 is always positive. So the following should never revert:
GK0: uint256 = (
unsafe_div(unsafe_div(2 * K0 * K0, 10**36) * K0, 10**36)
+ pow_mod256(unsafe_add(_A_gamma[1], 10**18), 2)
- unsafe_div(
unsafe_div(pow_mod256(K0, 2), 10**36) * unsafe_add(unsafe_mul(2, _A_gamma[1]), 3 * 10**18),
10**18
)
)
# NNAG2 = N**N * A * gamma**2
NNAG2: uint256 = unsafe_div(unsafe_mul(_A_gamma[0], pow_mod256(_A_gamma[1], 2)), A_MULTIPLIER)
# denominator = (GK0 + NNAG2 * x / D * _K0 / 10**36)
denominator: uint256 = (GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[0], _D) * K0, 10**36) )
# p_xy = x * (GK0 + NNAG2 * y / D * K0 / 10**36) / y * 10**18 / denominator
# p_xz = x * (GK0 + NNAG2 * z / D * K0 / 10**36) / z * 10**18 / denominator
# p is in 10**18 precision.
return [
unsafe_div(
_xp[0] * ( GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[1], _D) * K0, 10**36) ) / _xp[1] * 10**18,
denominator
),
unsafe_div(
_xp[0] * ( GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[2], _D) * K0, 10**36) ) / _xp[2] * 10**18,
denominator
),
]
```
```shell
>>> Math.get_p([10**18, 10**18, 10**18], 3 * 10**18, [54321000, 14500000000000])
[1000000000000000000, 1000000000000000000]
```
::::
## Math Utilities
### `cbrt`
::::description[`Math.cbrt(x: uint256) -> uint256:`]
Function to calculate the cubic root of `x` in 1e18 precision.
| Input | Type | Description |
| ----------- | -------| ----|
| `x` | `uint256` | Value to calculate cubic root for |
Returns: cubic root (`uint256`).
:::tip
More here: https://github.com/pcaversaccio/snekmate.
:::
```vyper
@external
@view
def cbrt(x: uint256) -> uint256:
"""
@notice Calculate the cubic root of a number in 1e18 precision
@dev Consumes around 1500 gas units
@param x The number to calculate the cubic root of
"""
return self._cbrt(x)
@internal
@pure
def _cbrt(x: uint256) -> uint256:
xx: uint256 = 0
if x >= 115792089237316195423570985008687907853269 * 10**18:
xx = x
elif x >= 115792089237316195423570985008687907853269:
xx = unsafe_mul(x, 10**18)
else:
xx = unsafe_mul(x, 10**36)
log2x: int256 = convert(self._snekmate_log_2(xx, False), int256)
# When we divide log2x by 3, the remainder is (log2x % 3).
# So if we just multiply 2**(log2x/3) and discard the remainder to calculate our
# guess, the newton method will need more iterations to converge to a solution,
# since it is missing that precision. It's a few more calculations now to do less
# calculations later:
# pow = log2(x) // 3
# remainder = log2(x) % 3
# initial_guess = 2 **pow * cbrt(2) **remainder
# substituting -> 2 = 1.26 ≈ 1260 / 1000, we get:
#
# initial_guess = 2 **pow * 1260 **remainder // 1000 **remainder
remainder: uint256 = convert(log2x, uint256) % 3
a: uint256 = unsafe_div(
unsafe_mul(
pow_mod256(2, unsafe_div(convert(log2x, uint256), 3)), # <- pow
pow_mod256(1260, remainder),
),
pow_mod256(1000, remainder),
)
# Because we chose good initial values for cube roots, 7 newton raphson iterations
# are just about sufficient. 6 iterations would result in non-convergences, and 8
# would be one too many iterations. Without initial values, the iteration count
# can go up to 20 or greater. The iterations are unrolled. This reduces gas costs
# but takes up more bytecode:
a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3)
a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3)
a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3)
a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3)
a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3)
a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3)
a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3)
if x >= 115792089237316195423570985008687907853269 * 10**18:
a = unsafe_mul(a, 10**12)
elif x >= 115792089237316195423570985008687907853269:
a = unsafe_mul(a, 10**6)
return a
@internal
@pure
def _snekmate_log_2(x: uint256, roundup: bool) -> uint256:
"""
@notice An `internal` helper function that returns the log in base 2
of `x`, following the selected rounding direction.
@dev This implementation is derived from Snekmate, which is authored
by pcaversaccio (Snekmate), distributed under the AGPL-3.0 license.
https://github.com/pcaversaccio/snekmate
@dev Note that it returns 0 if given 0. The implementation is
inspired by OpenZeppelin's implementation here:
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/Math.sol.
@param x The 32-byte variable.
@param roundup The Boolean variable that specifies whether
to round up or not. The default `False` is round down.
@return uint256 The 32-byte calculation result.
"""
value: uint256 = x
result: uint256 = empty(uint256)
# The following lines cannot overflow because we have the well-known
# decay behaviour of `log_2(max_value(uint256)) < max_value(uint256)`.
if x >> 128 != empty(uint256):
value = x >> 128
result = 128
if value >> 64 != empty(uint256):
value = value >> 64
result = unsafe_add(result, 64)
if value >> 32 != empty(uint256):
value = value >> 32
result = unsafe_add(result, 32)
if value >> 16 != empty(uint256):
value = value >> 16
result = unsafe_add(result, 16)
if value >> 8 != empty(uint256):
value = value >> 8
result = unsafe_add(result, 8)
if value >> 4 != empty(uint256):
value = value >> 4
result = unsafe_add(result, 4)
if value >> 2 != empty(uint256):
value = value >> 2
result = unsafe_add(result, 2)
if value >> 1 != empty(uint256):
result = unsafe_add(result, 1)
if (roundup and (1 << result) < x):
result = unsafe_add(result, 1)
return result
```
```shell
>>> Math.cbrt(8000000000000000000)
2000000000000000000
```
::::
### `geometric_mean`
::::description[`Math.geometric_mean(_x: uint256[3]) -> uint256:`]
Function to calculate the geometric mean of a list of numbers in 1e18 precision.
| Input | Type | Description |
| ----------- | -------| ----|
| `_x` | `uint256` | list of three numbers |
Returns: gemoetric mean (`uint256`).
```vyper
@external
@view
def geometric_mean(_x: uint256[3]) -> uint256:
"""
@notice Calculate the geometric mean of a list of numbers in 1e18 precision.
@param _x list of 3 numbers to sort
"""
return self._geometric_mean(_x)
@internal
@view
def _geometric_mean(_x: uint256[3]) -> uint256:
# calculates a geometric mean for three numbers.
prod: uint256 = unsafe_div(
unsafe_div(_x[0] * _x[1], 10**18) * _x[2],
10**18
)
if prod == 0:
return 0
return self._cbrt(prod)
@internal
@pure
def _cbrt(x: uint256) -> uint256:
xx: uint256 = 0
if x >= 115792089237316195423570985008687907853269 * 10**18:
xx = x
elif x >= 115792089237316195423570985008687907853269:
xx = unsafe_mul(x, 10**18)
else:
xx = unsafe_mul(x, 10**36)
log2x: int256 = convert(self._snekmate_log_2(xx, False), int256)
# When we divide log2x by 3, the remainder is (log2x % 3).
# So if we just multiply 2**(log2x/3) and discard the remainder to calculate our
# guess, the newton method will need more iterations to converge to a solution,
# since it is missing that precision. It's a few more calculations now to do less
# calculations later:
# pow = log2(x) // 3
# remainder = log2(x) % 3
# initial_guess = 2 **pow * cbrt(2) **remainder
# substituting -> 2 = 1.26 ≈ 1260 / 1000, we get:
#
# initial_guess = 2 **pow * 1260 **remainder // 1000 **remainder
remainder: uint256 = convert(log2x, uint256) % 3
a: uint256 = unsafe_div(
unsafe_mul(
pow_mod256(2, unsafe_div(convert(log2x, uint256), 3)), # <- pow
pow_mod256(1260, remainder),
),
pow_mod256(1000, remainder),
)
# Because we chose good initial values for cube roots, 7 newton raphson iterations
# are just about sufficient. 6 iterations would result in non-convergences, and 8
# would be one too many iterations. Without initial values, the iteration count
# can go up to 20 or greater. The iterations are unrolled. This reduces gas costs
# but takes up more bytecode:
a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3)
a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3)
a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3)
a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3)
a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3)
a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3)
a = unsafe_div(unsafe_add(unsafe_mul(2, a), unsafe_div(xx, unsafe_mul(a, a))), 3)
if x >= 115792089237316195423570985008687907853269 * 10**18:
a = unsafe_mul(a, 10**12)
elif x >= 115792089237316195423570985008687907853269:
a = unsafe_mul(a, 10**6)
return a
```
```shell
>>> Math.geometric_mean([3000000000000000000,3000000000000000000,3000000000000000000])
3000000000000000000
```
::::
### `reduction_coefficient`
::::description[`Math.reduction_coefficient(x: uint256[N_COINS], fee_gamma: uint256) -> uint256:`]
Function to calculate the reduction coefficient for `x` and `fee_gamma`. This method is used for calculating fees.
| Input | Type | Description |
| ----------- | -------| ----|
| `x` | `uint256[N_COINS]` | Values of x |
| `fee_gamma` | `uint256` | Fee gamma |
Returns: reduction coefficient (`uint256`).
```vyper
@external
@view
def reduction_coefficient(x: uint256[N_COINS], fee_gamma: uint256) -> uint256:
"""
@notice Calculates the reduction coefficient for the given x and fee_gamma
@dev This method is used for calculating fees.
@param x The x values
@param fee_gamma The fee gamma value
"""
return self._reduction_coefficient(x, fee_gamma)
@internal
@pure
def _reduction_coefficient(x: uint256[N_COINS], fee_gamma: uint256) -> uint256:
# fee_gamma / (fee_gamma + (1 - K))
# where
# K = prod(x) / (sum(x) / N)**N
# (all normalized to 1e18)
S: uint256 = x[0] + x[1] + x[2]
# Could be good to pre-sort x, but it is used only for dynamic fee
K: uint256 = 10**18 * N_COINS * x[0] / S
K = unsafe_div(K * N_COINS * x[1], S) # <- unsafe div is safu.
K = unsafe_div(K * N_COINS * x[2], S)
if fee_gamma > 0:
K = fee_gamma * 10**18 / (fee_gamma + 10**18 - K)
return K
```
```shell
>>> Math.reduction_coefficient([1123,1123,11], 500000000000000)
516570424625783
```
::::
### `wad_exp`
::::description[`Math.wad_exp(_power: int256) -> uint256:`]
Function to calculate the natural exponential function of a signed integer with a precision of 1e18.
| Input | Type | Description |
| ----------- | -------| ----|
| `_power` | `int256` | Value to calculate the natural exponential function of|
Returns: natural exponential (`uint256`).
:::tip
More here: https://github.com/pcaversaccio/snekmate.
:::
```vyper
@external
@view
def wad_exp(_power: int256) -> uint256:
"""
@notice Calculates the e**x with 1e18 precision
@param _power The number to calculate the exponential of
"""
return self._snekmate_wad_exp(_power)
@internal
@pure
def _snekmate_wad_exp(x: int256) -> uint256:
"""
@dev Calculates the natural exponential function of a signed integer with
a precision of 1e18.
@notice Note that this function consumes about 810 gas units. The implementation
is inspired by Remco Bloemen's implementation under the MIT license here:
https://xn--2-umb.com/22/exp-ln.
@dev This implementation is derived from Snekmate, which is authored
by pcaversaccio (Snekmate), distributed under the AGPL-3.0 license.
https://github.com/pcaversaccio/snekmate
@param x The 32-byte variable.
@return int256 The 32-byte calculation result.
"""
value: int256 = x
# If the result is `< 0.5`, we return zero. This happens when we have the following:
# "x <= floor(log(0.5e18) * 1e18) ~ -42e18".
if (x <= -42139678854452767551):
return empty(uint256)
# When the result is "> (2 **255 - 1) / 1e18" we cannot represent it as a signed integer.
# This happens when "x >= floor(log((2 **255 - 1) / 1e18) * 1e18) ~ 135".
assert x < 135305999368893231589, "wad_exp overflow"
# `x` is now in the range "(-42, 136) * 1e18". Convert to "(-42, 136) * 2 **96" for higher
# intermediate precision and a binary base. This base conversion is a multiplication with
# "1e18 / 2 **96 = 5 **18 / 2 **78".
value = unsafe_div(x << 78, 5 **18)
# Reduce the range of `x` to "(-½ ln 2, ½ ln 2) * 2 **96" by factoring out powers of two
# so that "exp(x) = exp(x') * 2 **k", where `k` is a signer integer. Solving this gives
# "k = round(x / log(2))" and "x' = x - k * log(2)". Thus, `k` is in the range "[-61, 195]".
k: int256 = unsafe_add(unsafe_div(value << 96, 54916777467707473351141471128), 2 **95) >> 96
value = unsafe_sub(value, unsafe_mul(k, 54916777467707473351141471128))
# Evaluate using a "(6, 7)"-term rational approximation. Since `p` is monic,
# we will multiply by a scaling factor later.
y: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 1346386616545796478920950773328), value) >> 96, 57155421227552351082224309758442)
p: int256 = unsafe_add(unsafe_mul(unsafe_add(unsafe_mul(unsafe_sub(unsafe_add(y, value), 94201549194550492254356042504812), y) >> 96,\
28719021644029726153956944680412240), value), 4385272521454847904659076985693276 << 96)
# We leave `p` in the "2 **192" base so that we do not have to scale it up
# again for the division.
q: int256 = unsafe_add(unsafe_mul(unsafe_sub(value, 2855989394907223263936484059900), value) >> 96, 50020603652535783019961831881945)
q = unsafe_sub(unsafe_mul(q, value) >> 96, 533845033583426703283633433725380)
q = unsafe_add(unsafe_mul(q, value) >> 96, 3604857256930695427073651918091429)
q = unsafe_sub(unsafe_mul(q, value) >> 96, 14423608567350463180887372962807573)
q = unsafe_add(unsafe_mul(q, value) >> 96, 26449188498355588339934803723976023)
# The polynomial `q` has no zeros in the range because all its roots are complex.
# No scaling is required, as `p` is already "2 **96" too large. Also,
# `r` is in the range "(0.09, 0.25) * 2**96" after the division.
r: int256 = unsafe_div(p, q)
# To finalise the calculation, we have to multiply `r` by:
# - the scale factor "s = ~6.031367120",
# - the factor "2 **k" from the range reduction, and
# - the factor "1e18 / 2 **96" for the base conversion.
# We do this all at once, with an intermediate result in "2**213" base,
# so that the final right shift always gives a positive value.
# Note that to circumvent Vyper's safecast feature for the potentially
# negative parameter value `r`, we first convert `r` to `bytes32` and
# subsequently to `uint256`. Remember that the EVM default behaviour is
# to use two's complement representation to handle signed integers.
return unsafe_mul(convert(convert(r, bytes32), uint256), 3822833074963236453042738258902158003155416615667) >> convert(unsafe_sub(195, k), uint256)
```
```shell
>>> Math.wad_exp(1000000)
1000000000001000000
```
::::
### `version`
::::description[`Math.version() -> String[8]: view`]
Getter for the current version of the contract.
Returns: current contract version (`String[8]`).
```vyper
version: public(constant(String[8])) = "v2.0.0"
```
```shell
>>> Math.version()
'v2.0.0'
```
::::
---
## Views Contract(Utility-contracts)
This contract contains **view-only external methods** which can be gas-inefficient when called from smart contracts.
:::deploy[Contract Source & Deployment]
Source code for this contract is available on [Github](https://github.com/curvefi/tricrypto-ng/blob/main/contracts/main/CurveCryptoViews3Optimized.vy).
Full list of all deployments can be found [here](../../../deployments.md).
:::
---
## Exchange Methods
### `get_dy`
::::description[`ViewMethodContract.get_dy(i: uint256, j: uint256, dx: uint256, swap: address) -> uint256:`]
Getter method for the amount of coin `j` tokens received for swapping in `dx` amount of coin `i`. This function includes the fee.
| Input | Type | Description |
| ---------- | --------- | ----------- |
| `i` | `uint256` | Index of input token (use `pool.coins(i)` to get coin address at i-th index) |
| `j` | `uint256` | Index of output token |
| `dx` | `uint256` | Amount of input coin[i] tokens |
| `swap` | `address` | Pool contract address |
Returns: `dy` (`uint256`).
```vyper
@external
@view
def get_dy(
i: uint256, j: uint256, dx: uint256, swap: address
) -> uint256:
dy: uint256 = 0
xp: uint256[N_COINS] = empty(uint256[N_COINS])
# dy = (get_y(x + dx) - y) * (1 - fee)
dy, xp = self._get_dy_nofee(i, j, dx, swap)
dy -= Curve(swap).fee_calc(xp) * dy / 10**10
return dy
```
```shell
>>> ViewMethodContract.get_dy(0, 1, 100000000000, "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4")
384205076
```
::::
### `get_dx`
::::description[`ViewMethodContract.get_dx(i: uint256, j: uint256, dy: uint256, swap: address) -> uint256:`]
Getter method for the amount of coin[i] tokens to input for swapping out dy amount of coin[j]
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint256` | Index of input token (check pool.coins(i) to get coin address at i-th index) |
| `j` | `uint256` | Index of output token |
| `dy` | `uint256` | amount of input coin[j] tokens received |
| `swap` | `address` | Pool contract address |
Returns: dx (`uint256`).
:::note
This is an approximate method, and returns estimates close to the input amount. Expensive to call on-chain.
:::
```vyper
@view
@external
def get_dx(
i: uint256, j: uint256, dy: uint256, swap: address
) -> uint256:
dx: uint256 = 0
xp: uint256[N_COINS] = empty(uint256[N_COINS])
fee_dy: uint256 = 0
_dy: uint256 = dy
# for more precise dx (but never exact), increase num loops
for k in range(5):
dx, xp = self._get_dx_fee(i, j, _dy, swap)
fee_dy = Curve(swap).fee_calc(xp) * _dy / 10**10
_dy = dy + fee_dy + 1
return dx
```
```shell
>>> ViewMethodContract.get_dx(0, 1, 1000, "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4")
259849
```
::::
### `calc_withdraw_one_coin`
::::description[`ViewMethodContract.calc_withdraw_one_coin(token_amount: uint256, i: uint256, swap: address) -> uint256:`]
Getter method for the output tokens (including fees) when withdrawing one coin.
| Input | Type | Description |
| ----------- | -------| ----|
| `token_amount` | `uint256` | LP token amount |
| `i` | `uint256` | Index of the token to withdraw |
| `swap` | `address` | Pool contract address |
Returns: amount of output tokens (`uint256`).
```vyper
@view
@external
def calc_withdraw_one_coin(
token_amount: uint256, i: uint256, swap: address
) -> uint256:
return self._calc_withdraw_one_coin(token_amount, i, swap)[0]
@internal
@view
def _calc_withdraw_one_coin(
token_amount: uint256,
i: uint256,
swap: address
) -> (uint256, uint256):
token_supply: uint256 = Curve(swap).totalSupply()
assert token_amount <= token_supply # dev: token amount more than supply
assert i < N_COINS # dev: coin out of range
math: Math = Curve(swap).MATH()
xx: uint256[N_COINS] = empty(uint256[N_COINS])
price_scale: uint256[N_COINS-1] = empty(uint256[N_COINS-1])
for k in range(N_COINS):
xx[k] = Curve(swap).balances(k)
if k > 0:
price_scale[k - 1] = Curve(swap).price_scale(k - 1)
precisions: uint256[N_COINS] = Curve(swap).precisions()
A: uint256 = Curve(swap).A()
gamma: uint256 = Curve(swap).gamma()
xp: uint256[N_COINS] = precisions
D0: uint256 = 0
p: uint256 = 0
price_scale_i: uint256 = PRECISION * precisions[0]
xp[0] *= xx[0]
for k in range(1, N_COINS):
p = price_scale[k-1]
if i == k:
price_scale_i = p * xp[i]
xp[k] = xp[k] * xx[k] * p / PRECISION
if Curve(swap).future_A_gamma_time() > block.timestamp:
D0 = math.newton_D(A, gamma, xp, 0)
else:
D0 = Curve(swap).D()
D: uint256 = D0
fee: uint256 = self._fee(xp, swap)
dD: uint256 = token_amount * D / token_supply
D_fee: uint256 = fee * dD / (2 * 10**10) + 1
approx_fee: uint256 = N_COINS * D_fee * xx[i] / D
D -= (dD - D_fee)
y_out: uint256[2] = math.get_y(A, gamma, xp, D, i)
dy: uint256 = (xp[i] - y_out[0]) * PRECISION / price_scale_i
xp[i] = y_out[0]
return dy, approx_fee
```
```shell
>>> ViewMethodContract.calc_withdraw_one_coin(1000000000000000, 0, "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4")
1049071
```
::::
### `calc_token_amount`
::::description[`ViewMethodContract.calc_token_amount(amounts: uint256[N_COINS], deposit: bool, swap: address) -> uint256:`]
Function to calculate LP tokens minted or to be burned for depositing or removing `amounts` of coins to or from `swap`.
| Input | Type | Description |
| ----------- | -------| ----|
| `amounts` | `uint256[N_COINS]` | LP token amount |
| `deposit` | `bool` | `True` = deposit, `False` = withdraw |
| `swap` | `address` | Pool contract address |
Returns: LP token amount to be burned/minted (`uint256`).
```vyper
@view
@external
def calc_token_amount(
amounts: uint256[N_COINS], deposit: bool, swap: address
) -> uint256:
d_token: uint256 = 0
amountsp: uint256[N_COINS] = empty(uint256[N_COINS])
xp: uint256[N_COINS] = empty(uint256[N_COINS])
d_token, amountsp, xp = self._calc_dtoken_nofee(amounts, deposit, swap)
d_token -= (
Curve(swap).calc_token_fee(amountsp, xp) * d_token / 10**10 + 1
)
return d_token
@internal
@view
def _calc_dtoken_nofee(
amounts: uint256[N_COINS], deposit: bool, swap: address
) -> (uint256, uint256[N_COINS], uint256[N_COINS]):
math: Math = Curve(swap).MATH()
xp: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
price_scale: uint256[N_COINS-1] = empty(uint256[N_COINS-1])
D0: uint256 = 0
token_supply: uint256 = 0
A: uint256 = 0
gamma: uint256 = 0
xp, D0, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap)
amountsp: uint256[N_COINS] = amounts
if deposit:
for k in range(N_COINS):
xp[k] += amounts[k]
else:
for k in range(N_COINS):
xp[k] -= amounts[k]
xp[0] *= precisions[0]
amountsp[0] *= precisions[0]
for k in range(N_COINS - 1):
p: uint256 = price_scale[k] * precisions[k + 1]
xp[k + 1] = xp[k + 1] * p / PRECISION
amountsp[k + 1] = amountsp[k + 1] * p / PRECISION
D: uint256 = math.newton_D(A, gamma, xp, 0)
d_token: uint256 = token_supply * D / D0
if deposit:
d_token -= token_supply
else:
d_token = token_supply - d_token
return d_token, amountsp, xp
```
```shell
>>> ViewMethodContract.calc_token_amount([1,1,1], 0, "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4")
248287947930
```
::::
## Calculating Fees Methods
Methods to calculate fees for **`get_dy`**, **`withdraw_one_coin`**and **`calc_token_amount`**.
### `calc_fee_get_dy`
::::description[`ViewMethodContract.calc_fee_get_dy(i: uint256, j: uint256, dx: uint256, swap: address) -> uint256:`]
Function to calculate the fees for `get_dy`.
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint256` | Index of input token (check pool.coins(i) to get coin address at i-th index) |
| `j` | `uint256` | Index of output token |
| `dx` | `uint256` | Amount of input coin[i] tokens |
| `swap` | `address` | Pool contract address |
Returns: fee (`uint256`).
```vyper
@external
@view
def calc_fee_get_dy(i: uint256, j: uint256, dx: uint256, swap: address
) -> uint256:
dy: uint256 = 0
xp: uint256[N_COINS] = empty(uint256[N_COINS])
dy, xp = self._get_dy_nofee(i, j, dx, swap)
return Curve(swap).fee_calc(xp) * dy / 10**10
@internal
@view
def _get_dy_nofee(
i: uint256, j: uint256, dx: uint256, swap: address
) -> (uint256, uint256[N_COINS]):
assert i != j and i < N_COINS and j < N_COINS, "coin index out of range"
assert dx > 0, "do not exchange 0 coins"
math: Math = Curve(swap).MATH()
xp: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
price_scale: uint256[N_COINS-1] = empty(uint256[N_COINS-1])
D: uint256 = 0
token_supply: uint256 = 0
A: uint256 = 0
gamma: uint256 = 0
xp, D, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap)
# adjust xp with input dx
xp[i] += dx
xp[0] *= precisions[0]
for k in range(N_COINS - 1):
xp[k + 1] = xp[k + 1] * price_scale[k] * precisions[k + 1] / PRECISION
y_out: uint256[2] = math.get_y(A, gamma, xp, D, j)
dy: uint256 = xp[j] - y_out[0] - 1
xp[j] = y_out[0]
if j > 0:
dy = dy * PRECISION / price_scale[j - 1]
dy /= precisions[j]
return dy, xp
```
```shell
>>> ViewMethodContract.calc_fee_get_dy(0, 1, 100000000, "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4")
142
```
::::
### `calc_fee_withdraw_one_coin`
::::description[`ViewMethodContract.calc_fee_withdraw_one_coin(token_amount: uint256, i: uint256, swap: address) -> uint256:`]
Function to calculate the fees for `withdraw_one_coin`.
| Input | Type | Description |
| ----------- | -------| ----|
| `token_amount` | `uint256` | LP token amount |
| `i` | `uint256` | Index of the token to withdraw |
| `swap` | `address` | Pool contract address |
Returns: fee (`uint256`).
```vyper
@external
@view
def calc_fee_withdraw_one_coin(
token_amount: uint256, i: uint256, swap: address
) -> uint256:
return self._calc_withdraw_one_coin(token_amount, i, swap)[1]
@internal
@view
def _calc_withdraw_one_coin(
token_amount: uint256,
i: uint256,
swap: address
) -> (uint256, uint256):
token_supply: uint256 = Curve(swap).totalSupply()
assert token_amount <= token_supply # dev: token amount more than supply
assert i < N_COINS # dev: coin out of range
math: Math = Curve(swap).MATH()
xx: uint256[N_COINS] = empty(uint256[N_COINS])
price_scale: uint256[N_COINS-1] = empty(uint256[N_COINS-1])
for k in range(N_COINS):
xx[k] = Curve(swap).balances(k)
if k > 0:
price_scale[k - 1] = Curve(swap).price_scale(k - 1)
precisions: uint256[N_COINS] = Curve(swap).precisions()
A: uint256 = Curve(swap).A()
gamma: uint256 = Curve(swap).gamma()
xp: uint256[N_COINS] = precisions
D0: uint256 = 0
p: uint256 = 0
price_scale_i: uint256 = PRECISION * precisions[0]
xp[0] *= xx[0]
for k in range(1, N_COINS):
p = price_scale[k-1]
if i == k:
price_scale_i = p * xp[i]
xp[k] = xp[k] * xx[k] * p / PRECISION
if Curve(swap).future_A_gamma_time() > block.timestamp:
D0 = math.newton_D(A, gamma, xp, 0)
else:
D0 = Curve(swap).D()
D: uint256 = D0
fee: uint256 = self._fee(xp, swap)
dD: uint256 = token_amount * D / token_supply
D_fee: uint256 = fee * dD / (2 * 10**10) + 1
approx_fee: uint256 = N_COINS * D_fee * xx[i] / D
D -= (dD - D_fee)
y_out: uint256[2] = math.get_y(A, gamma, xp, D, i)
dy: uint256 = (xp[i] - y_out[0]) * PRECISION / price_scale_i
xp[i] = y_out[0]
return dy, approx_fee
```
```shell
>>> ViewMethodContract.calc_fee_withdraw_one_coin(10000000000, 2, "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4")
1176331
```
::::
### `calc_fee_token_amount`
::::description[`ViewMethodContract.calc_fee_token_amount(amounts: uint256[N_COINS], deposit: bool, swap: address) -> uint256:`]
Function to calculate the fees for `calc_token_amount`.
| Input | Type | Description |
| ----------- | -------| ----|
| `amounts` | `uint256[N_COINS]` | LP token amount |
| `deposit` | `bool` | `True` = deposit, `False` = withdraw |
| `swap` | `address` | Pool contract address |
Returns: fee (`uint256`).
```vyper
@view
@external
def calc_fee_token_amount(
amounts: uint256[N_COINS], deposit: bool, swap: address
) -> uint256:
d_token: uint256 = 0
amountsp: uint256[N_COINS] = empty(uint256[N_COINS])
xp: uint256[N_COINS] = empty(uint256[N_COINS])
d_token, amountsp, xp = self._calc_dtoken_nofee(amounts, deposit, swap)
return Curve(swap).calc_token_fee(amountsp, xp) * d_token / 10**10 + 1
@internal
@view
def _calc_dtoken_nofee(
amounts: uint256[N_COINS], deposit: bool, swap: address
) -> (uint256, uint256[N_COINS], uint256[N_COINS]):
math: Math = Curve(swap).MATH()
xp: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
price_scale: uint256[N_COINS-1] = empty(uint256[N_COINS-1])
D0: uint256 = 0
token_supply: uint256 = 0
A: uint256 = 0
gamma: uint256 = 0
xp, D0, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap)
amountsp: uint256[N_COINS] = amounts
if deposit:
for k in range(N_COINS):
xp[k] += amounts[k]
else:
for k in range(N_COINS):
xp[k] -= amounts[k]
xp[0] *= precisions[0]
amountsp[0] *= precisions[0]
for k in range(N_COINS - 1):
p: uint256 = price_scale[k] * precisions[k + 1]
xp[k + 1] = xp[k + 1] * p / PRECISION
amountsp[k + 1] = amountsp[k + 1] * p / PRECISION
D: uint256 = math.newton_D(A, gamma, xp, 0)
d_token: uint256 = token_supply * D / D0
if deposit:
d_token -= token_supply
else:
d_token = token_supply - d_token
return d_token, amountsp, xp
```
```shell
>>> ViewMethodContract.calc_fee_token_amount([1,1,1], 0, "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4")
48166379
```
::::
---
## DonationStreamer
The `DonationStreamer` contract enables permissionless, scheduled donations to FXSwap (Twocrypto-NG) pools. Donors deposit tokens and ETH rewards upfront to create "streams" that distribute donations over multiple periods. Anyone can execute due streams and earn ETH bounties for doing so.
This contract is fully permissionless — there is no admin or owner. Once a stream is created, it can only be cancelled by the original donor.
A frontend for creating, viewing, and executing streams is available at [curvefi.github.io/refuel-automation](https://curvefi.github.io/refuel-automation/).
For background on how pool-level donations work, see the [Refuel Mechanism](./refuel.md) and [FXSwap Implementation](./fxswap.md) pages.
:::vyper[`DonationStreamer.vy`]
The source code for the `DonationStreamer.vy` contract can be found on [GitHub](https://github.com/curvefi/refuel-automation). The contract is written in [Vyper](https://vyperlang.org/) version `0.4.3`.
The contract is deployed at the same address on all chains (via CREATE3):
- :logos-ethereum: Ethereum: [`0x2b786BB995978CC2242C567Ae62fd617b0eBC828`](https://etherscan.io/address/0x2b786BB995978CC2242C567Ae62fd617b0eBC828)
- :logos-polygon: Polygon: [`0x2b786BB995978CC2242C567Ae62fd617b0eBC828`](https://polygonscan.com/address/0x2b786BB995978CC2242C567Ae62fd617b0eBC828)
```json
[{"anonymous":false,"inputs":[{"indexed":false,"name":"stream_id","type":"uint256"},{"indexed":true,"name":"donor","type":"address"},{"indexed":true,"name":"pool","type":"address"},{"indexed":false,"name":"amounts","type":"uint256[2]"},{"indexed":false,"name":"period_length","type":"uint256"},{"indexed":false,"name":"n_periods","type":"uint256"},{"indexed":false,"name":"reward_per_period","type":"uint256"}],"name":"StreamCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"stream_id","type":"uint256"},{"indexed":true,"name":"caller","type":"address"},{"indexed":true,"name":"pool","type":"address"},{"indexed":false,"name":"periods","type":"uint256"},{"indexed":false,"name":"amounts","type":"uint256[2]"},{"indexed":false,"name":"reward_paid","type":"uint256"}],"name":"StreamExecuted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"stream_id","type":"uint256"},{"indexed":true,"name":"donor","type":"address"},{"indexed":true,"name":"pool","type":"address"},{"indexed":false,"name":"amounts","type":"uint256[2]"},{"indexed":false,"name":"reward_refund","type":"uint256"}],"name":"StreamCancelled","type":"event"},{"inputs":[{"name":"pool","type":"address"},{"name":"coins","type":"address[2]"},{"name":"amounts","type":"uint256[2]"},{"name":"period_length","type":"uint256"},{"name":"n_periods","type":"uint256"},{"name":"reward_per_period","type":"uint256"}],"name":"create_stream","outputs":[{"name":"","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"name":"stream_id","type":"uint256"}],"name":"cancel_stream","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"stream_id","type":"uint256"}],"name":"execute","outputs":[{"name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"stream_ids","type":"uint256[]"}],"name":"execute_many","outputs":[{"name":"","type":"bool[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"stream_id","type":"uint256"}],"name":"is_due","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"streams_and_rewards_due","outputs":[{"name":"","type":"uint256[]"},{"name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"stream_count","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint256"}],"name":"streams","outputs":[{"components":[{"name":"donor","type":"address"},{"name":"pool","type":"address"},{"name":"coins","type":"address[2]"},{"name":"amounts_per_period","type":"uint256[2]"},{"name":"period_length","type":"uint256"},{"name":"reward_per_period","type":"uint256"},{"name":"next_ts","type":"uint256"},{"name":"reward_remaining","type":"uint256"},{"name":"amounts_remaining","type":"uint256[2]"},{"name":"periods_remaining","type":"uint256"}],"name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"outputs":[],"stateMutability":"nonpayable","type":"constructor"}]
```
:::
## Stream Management
### `create_stream`
::::description[`DonationStreamer.create_stream(pool: address, coins: address[2], amounts: uint256[2], period_length: uint256, n_periods: uint256, reward_per_period: uint256) -> uint256`]
Payable function to create a new donation stream for a pool. The caller deposits the full token amounts and ETH rewards upfront. Tokens are transferred into the contract and donated to the pool in equal portions over `n_periods`. The `coins` parameter must match the pool's coin configuration. Any excess ETH beyond the required `reward_per_period * n_periods` is refunded.
| Input | Type | Description |
| ------------------- | -------------- | ---------------------------------------------------------- |
| `pool` | `address` | Address of the target FXSwap pool |
| `coins` | `address[2]` | Token addresses matching the pool's `coins(0)` and `coins(1)` |
| `amounts` | `uint256[2]` | Total token amounts to donate across all periods |
| `period_length` | `uint256` | Duration of each period in seconds |
| `n_periods` | `uint256` | Number of donation periods |
| `reward_per_period` | `uint256` | ETH reward paid to the executor for each period |
Returns: the stream ID (`uint256`).
Emits: `StreamCreated` event.
```vyper
@external
@payable
@nonreentrant
def create_stream(
pool: address,
coins: address[N_COINS],
amounts: uint256[N_COINS],
period_length: uint256,
n_periods: uint256,
reward_per_period: uint256,
) -> uint256:
"""
@notice Create a donation stream for a pool.
"""
assert pool != empty(address), "pool required"
assert n_periods > 0, "bad n_periods"
assert period_length > 0, "bad period_length"
assert amounts[0] > 0 or amounts[1] > 0, "zero amounts"
# Ensure caller-provided coins match the pool configuration.
assert (
coins[0] == staticcall DonationPoolTarget(pool).coins(0)
and coins[1] == staticcall DonationPoolTarget(pool).coins(1)
), "coin mismatch"
# Rewards are pre-funded for all periods; excess is refunded.
reward_total: uint256 = reward_per_period * n_periods
assert msg.value >= reward_total, "reward mismatch"
# Per-period amounts are truncated; remainders donate on the final period.
amounts_per_period: uint256[N_COINS] = empty(uint256[N_COINS])
for i: uint256 in range(N_COINS):
amount: uint256 = amounts[i]
if amount > 0:
balance_before: uint256 = staticcall IERC20(coins[i]).balanceOf(self)
assert extcall IERC20(coins[i]).transferFrom(
msg.sender, self, amount, default_return_value=True
), "transfer failed"
balance_after: uint256 = staticcall IERC20(coins[i]).balanceOf(self)
assert balance_after - balance_before == amount, "bad token transfer"
amounts_per_period[i] = amount // n_periods
stream_id: uint256 = self.stream_count
self.stream_count = stream_id + 1
self.streams[stream_id] = DonationStream(
donor=msg.sender,
pool=pool,
coins=coins,
amounts_per_period=amounts_per_period,
period_length=period_length,
reward_per_period=reward_per_period,
next_ts=block.timestamp,
reward_remaining=reward_total,
amounts_remaining=amounts,
periods_remaining=n_periods,
)
# Refund excess message value.
if msg.value > reward_total:
send(msg.sender, msg.value - reward_total)
log StreamCreated(
stream_id=stream_id,
donor=msg.sender,
pool=pool,
amounts=amounts,
period_length=period_length,
n_periods=n_periods,
reward_per_period=reward_per_period,
)
return stream_id
```
```shell
>>> DonationStreamer.create_stream(
... pool, # FXSwap pool address
... [coin0, coin1], # pool coin addresses
... [1000e18, 500e18], # total amounts to donate
... 86400, # period length: 1 day
... 7, # number of periods
... 0.001e18, # 0.001 ETH reward per period
... value=0.007e18 # total ETH rewards
... )
0 # stream_id
```
::::
### `cancel_stream`
::::description[`DonationStreamer.cancel_stream(stream_id: uint256)`]
:::guard[Guarded Method]
This function can only be called by the original `donor` who created the stream.
:::
Cancels a stream and refunds all remaining tokens and ETH rewards to the donor. The stream storage is cleared after cancellation.
| Input | Type | Description |
| ----------- | --------- | ---------------------------- |
| `stream_id` | `uint256` | ID of the stream to cancel |
Emits: `StreamCancelled` event.
```vyper
@external
@nonreentrant
def cancel_stream(stream_id: uint256):
"""
@notice Cancel a stream and refund remaining balances.
"""
stream: DonationStream = self.streams[stream_id]
assert stream.donor == msg.sender, "donor only"
pool: address = stream.pool
coins: address[N_COINS] = stream.coins
amounts_refund: uint256[N_COINS] = stream.amounts_remaining
reward_refund: uint256 = stream.reward_remaining
self.streams[stream_id] = empty(DonationStream)
for i: uint256 in range(N_COINS):
if amounts_refund[i] > 0:
assert extcall IERC20(coins[i]).transfer(
msg.sender, amounts_refund[i], default_return_value=True
), "refund failed"
if reward_refund > 0:
send(msg.sender, reward_refund)
log StreamCancelled(
stream_id=stream_id,
donor=msg.sender,
pool=pool,
amounts=amounts_refund,
reward_refund=reward_refund,
)
```
```shell
>>> DonationStreamer.cancel_stream(0)
```
::::
---
## Stream Execution
### `execute`
::::description[`DonationStreamer.execute(stream_id: uint256) -> bool`]
Executes a single stream if it has due periods. The function calculates how many periods are due, donates the proportional token amounts to the pool via `add_liquidity(donation=True)`, and pays the caller the corresponding ETH reward. If all periods are completed, the stream storage is cleared. Returns `False` if no periods are due.
| Input | Type | Description |
| ----------- | --------- | ------------------------------ |
| `stream_id` | `uint256` | ID of the stream to execute |
Returns: whether the stream was executed (`bool`).
Emits: `StreamExecuted` event.
```vyper
@external
@nonreentrant
def execute(stream_id: uint256) -> bool:
"""
@notice Execute a single stream id.
"""
return self._execute_stream(stream_id)
@internal
def _execute_stream(stream_id: uint256) -> bool:
"""
@dev Execute a single stream if due and pay its reward.
"""
stream: DonationStream = self.streams[stream_id]
periods_due: uint256 = self._due_periods(stream)
if periods_due == 0:
return False
is_final: bool = periods_due == stream.periods_remaining
pool: address = stream.pool
coins: address[N_COINS] = stream.coins
# Compute the slice for this execution, using the remainder on the final call.
amounts_to_donate: uint256[N_COINS] = empty(uint256[N_COINS])
for j: uint256 in range(N_COINS):
remaining: uint256 = stream.amounts_remaining[j]
if remaining == 0:
continue
amount: uint256 = stream.amounts_per_period[j] * periods_due
if is_final:
amount = remaining
amounts_to_donate[j] = amount
stream.amounts_remaining[j] = remaining - amount
stream.periods_remaining -= periods_due
stream.next_ts += stream.period_length * periods_due
# Rewards are prorated per period, with final execution paying the remainder.
reward_paid: uint256 = stream.reward_per_period * periods_due
if is_final:
reward_paid = stream.reward_remaining
stream.reward_remaining -= reward_paid
# Clear storage once the stream is finished.
if is_final:
self.streams[stream_id] = empty(DonationStream)
else:
self.streams[stream_id] = stream
# Only approve and add liquidity when there is a non-zero donation.
if amounts_to_donate[0] > 0 or amounts_to_donate[1] > 0:
balances_before: uint256[N_COINS] = empty(uint256[N_COINS])
for j: uint256 in range(N_COINS):
if amounts_to_donate[j] > 0:
balances_before[j] = staticcall IERC20(coins[j]).balanceOf(self)
for j: uint256 in range(N_COINS):
if amounts_to_donate[j] > 0:
self._safe_approve(coins[j], pool, amounts_to_donate[j])
extcall DonationPoolTarget(pool).add_liquidity(
amounts_to_donate,
0,
empty(address),
True,
)
for j: uint256 in range(N_COINS):
if amounts_to_donate[j] > 0:
balance_after: uint256 = staticcall IERC20(coins[j]).balanceOf(self)
assert balances_before[j] >= balance_after, "bad pool pull"
assert balances_before[j] - balance_after == amounts_to_donate[j], "bad pool pull"
self._safe_approve(coins[j], pool, 0)
if reward_paid > 0:
send(msg.sender, reward_paid)
log StreamExecuted(
stream_id=stream_id,
caller=msg.sender,
pool=pool,
periods=periods_due,
amounts=amounts_to_donate,
reward_paid=reward_paid,
)
return True
@internal
def _safe_approve(token: address, spender: address, amount: uint256):
"""
@dev Safely set allowance, resetting to zero when needed.
"""
allowance: uint256 = staticcall IERC20(token).allowance(self, spender)
if allowance == amount:
return
if allowance != 0 and amount != 0:
assert extcall IERC20(token).approve(
spender, 0, default_return_value=True
), "approve failed"
assert extcall IERC20(token).approve(
spender, amount, default_return_value=True
), "approve failed"
@internal
@view
def _due_periods(stream: DonationStream) -> uint256:
"""
@dev Return the number of due periods for a stream.
"""
if (
stream.donor == empty(address)
or stream.periods_remaining == 0
or stream.period_length == 0
or block.timestamp < stream.next_ts
):
return 0
return min(
(block.timestamp - stream.next_ts) // stream.period_length + 1,
stream.periods_remaining,
)
```
```shell
>>> DonationStreamer.execute(0)
True
```
::::
### `execute_many`
::::description[`DonationStreamer.execute_many(stream_ids: DynArray[uint256, 32]) -> DynArray[bool, 32]`]
Executes a batch of streams in a single transaction. Each stream is processed independently — if a stream is not due, its result is `False`. The caller receives the combined ETH rewards for all successfully executed streams. Maximum batch size is 32.
| Input | Type | Description |
| ------------ | ----------------------- | ----------------------------------- |
| `stream_ids` | `DynArray[uint256, 32]` | Array of stream IDs to execute |
Returns: per-stream execution results in input order (`DynArray[bool, 32]`).
```vyper
@external
@nonreentrant
def execute_many(stream_ids: DynArray[uint256, N_MAX_EXECUTE]) -> DynArray[bool, N_MAX_EXECUTE]:
"""
@notice Execute a batch of stream ids.
@return Per-stream execution results in input order.
"""
results: DynArray[bool, N_MAX_EXECUTE] = empty(DynArray[bool, N_MAX_EXECUTE])
for i: uint256 in range(len(stream_ids), bound=N_MAX_EXECUTE):
results.append(self._execute_stream(stream_ids[i]))
return results
```
```shell
>>> DonationStreamer.execute_many([0, 1, 2])
[True, True, False]
```
::::
---
## View Methods
### `is_due`
::::description[`DonationStreamer.is_due(stream_id: uint256) -> bool: view`]
Returns `True` if the stream has one or more periods that can be executed now.
| Input | Type | Description |
| ----------- | --------- | ----------------------------- |
| `stream_id` | `uint256` | ID of the stream to check |
Returns: whether the stream is due for execution (`bool`).
```vyper
@view
@external
def is_due(stream_id: uint256) -> bool:
"""
@notice Return true if the stream can be executed now.
"""
return self._due_periods(self.streams[stream_id]) > 0
@internal
@view
def _due_periods(stream: DonationStream) -> uint256:
"""
@dev Return the number of due periods for a stream.
"""
if (
stream.donor == empty(address)
or stream.periods_remaining == 0
or stream.period_length == 0
or block.timestamp < stream.next_ts
):
return 0
return min(
(block.timestamp - stream.next_ts) // stream.period_length + 1,
stream.periods_remaining,
)
```
::::
### `streams_and_rewards_due`
::::description[`DonationStreamer.streams_and_rewards_due() -> (DynArray[uint256, 1024], DynArray[uint256, 1024]): view`]
Returns two arrays: the IDs of all due streams and their corresponding ETH rewards, ordered from newest to oldest. Iterates over up to 1024 streams starting from the most recent. This function is intended for off-chain use (e.g., by keeper bots) and should not be called on-chain.
Returns: a tuple of (due stream IDs, corresponding rewards) (`DynArray[uint256, 1024]`, `DynArray[uint256, 1024]`).
```vyper
@view
@external
def streams_and_rewards_due(
) -> (DynArray[uint256, N_MAX_VIEW], DynArray[uint256, N_MAX_VIEW]):
"""
@notice Return due stream ids and rewards, newest first.
@dev Not meant to be called onchain; iterates over up to N_MAX_VIEW streams starting from the newest.
"""
due_ids: DynArray[uint256, N_MAX_VIEW] = empty(DynArray[uint256, N_MAX_VIEW])
rewards: DynArray[uint256, N_MAX_VIEW] = empty(DynArray[uint256, N_MAX_VIEW])
count: uint256 = self.stream_count
if count == 0:
return due_ids, rewards
# Walk backward from newest to oldest, capped for view usage.
limit: uint256 = min(count, N_MAX_VIEW)
for i: uint256 in range(limit, bound=N_MAX_VIEW):
stream_id: uint256 = count - 1 - i
stream: DonationStream = self.streams[stream_id]
periods_due: uint256 = self._due_periods(stream)
if periods_due == 0:
continue
due_ids.append(stream_id)
if periods_due == stream.periods_remaining:
rewards.append(stream.reward_remaining)
else:
rewards.append(stream.reward_per_period * periods_due)
return due_ids, rewards
```
```shell
>>> DonationStreamer.streams_and_rewards_due()
([2, 0], [1000000000000000, 3000000000000000])
```
::::
### `stream_count`
::::description[`DonationStreamer.stream_count() -> uint256: view`]
Returns the total number of streams that have been created. Stream IDs are sequential starting from 0.
Returns: total number of streams created (`uint256`).
```vyper
stream_count: public(uint256)
```
::::
### `streams`
::::description[`DonationStreamer.streams(arg0: uint256) -> DonationStream: view`]
Returns the full `DonationStream` struct for a given stream ID. Completed or cancelled streams return a zeroed-out struct.
The `DonationStream` struct contains the following fields:
| Field | Type | Description |
| -------------------- | -------------- | -------------------------------------------------------- |
| `donor` | `address` | Address of the stream creator |
| `pool` | `address` | Target FXSwap pool address |
| `coins` | `address[2]` | Token addresses for the pool |
| `amounts_per_period` | `uint256[2]` | Token amounts donated per period (truncated from total) |
| `period_length` | `uint256` | Duration of each period in seconds |
| `reward_per_period` | `uint256` | ETH reward paid per period execution |
| `next_ts` | `uint256` | Timestamp when the next period becomes due |
| `reward_remaining` | `uint256` | Total ETH rewards still to be paid out |
| `amounts_remaining` | `uint256[2]` | Token amounts still to be donated |
| `periods_remaining` | `uint256` | Number of periods left to execute |
| Input | Type | Description |
| ------ | --------- | ------------- |
| `arg0` | `uint256` | Stream ID |
Returns: the stream data (`DonationStream`).
```vyper
struct DonationStream:
# Static
donor: address
pool: address
coins: address[N_COINS]
amounts_per_period: uint256[N_COINS]
period_length: uint256
reward_per_period: uint256
# Dynamic
next_ts: uint256
reward_remaining: uint256
amounts_remaining: uint256[N_COINS]
periods_remaining: uint256
streams: public(HashMap[uint256, DonationStream])
```
::::
---
## FXSwap Implementation
:::info
This document covers the FXSwap implementation of the Twocrypto-NG pool, which introduces donation (FXSwap) capabilities on top of the base AMM.
:::
:::note[Terminology Note]
The actual contract variables and functions use "donation" terminology (e.g., `donation_shares`, `donation_duration`) as these contracts are already deployed and cannot be changed.
:::
The FXSwap mechanism in Cryptoswap pools allows external parties to contribute liquidity as a buffer that can be burned during rebalancing operations. This system addresses the core challenge of rebalancing costs by providing a liquidity reserve (`donation_shares`) that protects regular LPs from bearing the full impact of rebalancing adjustments.
## What are FXSwaps?
FXSwaps are special LP shares that are not credited to any user but instead serve as a liquidity buffer for the pool. During rebalancing, these shares can be burned to absorb impermanent loss, enabling the pool to adjust its `price_scale` while maintaining virtual price for existing LPs.
```vyper
# Donation shares are tracked separately from regular LP shares
donation_shares: public(uint256)
donation_shares_max_ratio: public(uint256) # Cap on donations (e.g., 10% of total supply)
```
## How are FXSwaps Added?
FXSwaps are added through the `add_liquidity` function with the `donation=True` parameter:
```vyper
@external
@nonreentrant
def add_liquidity(
amounts: uint256[N_COINS],
min_mint_amount: uint256,
receiver: address = msg.sender,
donation: bool = False
) -> uint256:
```
When `donation=True`:
- LP tokens are credited to the donation buffer instead of being minted to a receiver
- Only a minimal `NOISE_FEE` (0.1 BPS) is charged for numerical stability
- The `NOISE_FEE` is absorbed by the pool itself (not distributed to anyone) to ensure numerical precision
- The donation is subject to the `donation_shares_max_ratio` cap
- A `Donation` event is emitted
```vyper
if donation:
assert receiver == empty(address), "nonzero receiver"
new_donation_shares: uint256 = self.donation_shares + d_token
assert new_donation_shares * PRECISION // (token_supply + d_token) <= self.donation_shares_max_ratio, "donation above cap!"
# Credit donation: we don't explicitly mint lp tokens, but increase total supply
self.donation_shares = new_donation_shares
self.totalSupply += d_token
log Donation(donor=msg.sender, token_amounts=amounts_received)
```
## How are FXSwaps Used?
FXSwap donations are automatically burned during pool rebalancing operations when `tweak_price` is called. This occurs after normal pool operations like swaps (`_exchange`), liquidity additions (`add_liquidity`), and imbalanced withdrawals (`remove_liquidity_one_coin` or `remove_liquidity_fixed_out`).
The key logic in `tweak_price`:
```vyper
# Calculate unlocked donations (time-based release + protection damping)
donation_shares: uint256 = self._donation_shares()
# During rebalancing, burn donations to maintain virtual price
donation_shares_to_burn: uint256 = 0
goal_vp: uint256 = max(threshold_vp, virtual_price)
if new_virtual_price < goal_vp:
# Calculate how many donation shares to burn to reach goal_vp
tweaked_supply: uint256 = 10**18 * new_xcp // goal_vp
donation_shares_to_burn = min(
unsafe_sub(total_supply, tweaked_supply),
donation_shares
)
if donation_shares_to_burn > 0:
self.donation_shares -= donation_shares_to_burn
self.totalSupply -= donation_shares_to_burn
self.last_donation_release_ts = block.timestamp
```
## MEV Protection Measures
The FXSwap mechanism includes two key MEV protection measures:
### 1. Time-Based Unlocking
Donations unlock linearly over time (default: 7 days) to prevent immediate extraction:
```vyper
@internal
@view
def _donation_shares(_donation_protection: bool = True) -> uint256:
# Time-based release of donation shares
elapsed: uint256 = block.timestamp - self.last_donation_release_ts
unlocked_shares: uint256 = min(donation_shares, donation_shares * elapsed // self.donation_duration)
```
### 2. Add Liquidity Throttling
When users add liquidity, the protection window is extended to prevent donation extraction via sandwich attacks:
```vyper
# Donation Protection & LP Spam Penalty
relative_lp_add: uint256 = d_token * PRECISION // (token_supply + d_token)
if relative_lp_add > 0 and self.donation_shares > 0:
# Extend protection period
protection_period: uint256 = self.donation_protection_period
extension_seconds: uint256 = min(relative_lp_add * protection_period // self.donation_protection_lp_threshold, protection_period)
current_expiry: uint256 = max(self.donation_protection_expiry_ts, block.timestamp)
new_expiry: uint256 = min(current_expiry + extension_seconds, block.timestamp + protection_period)
self.donation_protection_expiry_ts = new_expiry
```
The protection applies a damping factor to unlocked donations:
```vyper
# Donation protection damping factor
protection_factor: uint256 = 0
expiry: uint256 = self.donation_protection_expiry_ts
if expiry > block.timestamp:
protection_factor = min((expiry - block.timestamp) * PRECISION // self.donation_protection_period, PRECISION)
return unlocked_shares * (PRECISION - protection_factor) // PRECISION
```
This dual protection system ensures that donations cannot be easily extracted by MEV bots while still providing the intended liquidity buffer benefits to the pool.
---
## Contract Functions and Variables
### `add_liquidity`
::::description[`FXSwap.add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256, receiver: address = msg.sender, donation: bool = False) -> uint256:`]
Function to add liquidity to the pool. Can be used for regular liquidity addition or donations when `donation=True`. When `donation=True`, the LP tokens are credited to the donation buffer instead of being minted to a receiver.
| Input | Type | Description |
| ----------------- | ------------------ | ------------------------------------------------ |
| `amounts` | `uint256[N_COINS]` | Amounts of each coin to add. |
| `min_mint_amount` | `uint256` | Minimum amount of LP tokens to mint to `receiver`. |
| `receiver` | `address` | Receiver address of the minted LP tokens; defaults to `msg.sender`. Ignored when `donation=True`. |
| `donation` | `bool` | Whether the liquidity is a donation; defaults to `False`. |
Returns: Amount of LP tokens minted (to receiver or donation buffer) (`uint256`).
Emits: `Donation` or `AddLiquidity` event.
```vyper
cached_price_scale: uint256 # <------------------------ Internal price scale.
cached_price_oracle: uint256 # <------- Price target given by moving average.
balances: public(uint256[N_COINS])
@external
@nonreentrant
def add_liquidity(
amounts: uint256[N_COINS],
min_mint_amount: uint256,
receiver: address = msg.sender,
donation: bool = False
) -> uint256:
"""
@notice Adds liquidity into the pool.
@param amounts Amounts of each coin to add.
@param min_mint_amount Minimum amount of LP to mint.
@param receiver Address to send the LP tokens to. Default is msg.sender
@param donation Whether the liquidity is a donation, if True receiver is ignored.
@return uint256 Amount of LP tokens issued (to receiver or donation buffer).
"""
assert amounts[0] + amounts[1] > 0, "no coins to add"
# --------------------- Get prices, balances -----------------------------
old_balances: uint256[N_COINS] = self.balances
########################## TRANSFER IN <-------
amounts_received: uint256[N_COINS] = empty(uint256[N_COINS])
# This variable will contain the old balances + the amounts received.
balances: uint256[N_COINS] = self.balances
for i: uint256 in range(N_COINS):
if amounts[i] > 0:
# Updates self.balances here:
amounts_received[i] = self._transfer_in(
i,
amounts[i],
msg.sender,
False, # <--------------------- Disable optimistic transfers.
)
balances[i] += amounts_received[i]
price_scale: uint256 = self.cached_price_scale
xp: uint256[N_COINS] = self._xp(balances, price_scale)
old_xp: uint256[N_COINS] = self._xp(old_balances, price_scale)
# --------------------Finalize ramping of empty pool
if self.D == 0:
self.future_A_gamma_time = block.timestamp
# -------------------- Calculate LP tokens to mint -----------------------
A_gamma: uint256[2] = self._A_gamma()
old_D: uint256 = self._get_D(A_gamma, old_xp)
D: uint256 = staticcall self.MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)
token_supply: uint256 = self.totalSupply
d_token: uint256 = 0
if old_D > 0:
d_token = token_supply * D // old_D - token_supply
else:
d_token = self._xcp(D, price_scale) # <----- Making initial virtual price equal to 1.
assert d_token > 0, "nothing minted"
d_token_fee: uint256 = 0
if old_D > 0:
d_token_fee = (
self._calc_token_fee(amounts_received, xp, donation, True) * d_token // 10**10 + 1
) # for donations - we only take NOISE_FEE (check _calc_token_fee)
d_token -= d_token_fee
if donation:
assert receiver == empty(address), "nonzero receiver"
new_donation_shares: uint256 = self.donation_shares + d_token
assert new_donation_shares * PRECISION // (token_supply + d_token) <= self.donation_shares_max_ratio, "donation above cap!"
# When adding donation, if the previous one hasn't been fully released we preserve
# the currently unlocked donation [given by `self._donation_shares()`] by updating
# `self.last_donation_release_ts` as if a single virtual donation of size `new_donation_shares`
# was made in past and linearly unlocked reaching `self._donation_shares()` at the current time.
# We want the following equality to hold:
# self._donation_shares() = new_donation_shares * (new_elapsed / self.donation_duration)
# We can rearrange this to find the new elapsed time (imitating one large virtual donation):
# => new_elapsed = self._donation_shares() * self.donation_duration / new_donation_shares
# edge case: if self.donation_shares = 0, then self._donation_shares() is 0
# and new_elapsed = 0, thus initializing last_donation_release_ts = block.timestamp
new_elapsed: uint256 = self._donation_shares(False) * self.donation_duration // new_donation_shares
# Additional observations:
# new_elapsed = (old_pool * old_elapsed / D) * D / new_pool = old_elapsed * (old_pool / new_pool)
# => new_elapsed is always smaller than old_elapsed
# and self.last_donation_release_ts is carried forward propotionally to new donation size.
self.last_donation_release_ts = block.timestamp - new_elapsed
# Credit donation: we don't explicitly mint lp tokens, but increase total supply
self.donation_shares = new_donation_shares
self.totalSupply += d_token
log Donation(donor=msg.sender, token_amounts=amounts_received)
else:
# --- Donation Protection & LP Spam Penalty ---
# Extend protection to shield against donation extraction via sandwich attacks.
# A penalty is applied for extending the protection to disincentivize spamming.
relative_lp_add: uint256 = d_token * PRECISION // (token_supply + d_token)
if relative_lp_add > 0 and self.donation_shares > 0: # sub-precision additions are expensive to stack
# Extend protection period
protection_period: uint256 = self.donation_protection_period
extension_seconds: uint256 = min(relative_lp_add * protection_period // self.donation_protection_lp_threshold, protection_period)
current_expiry: uint256 = max(self.donation_protection_expiry_ts, block.timestamp)
new_expiry: uint256 = min(current_expiry + extension_seconds, block.timestamp + protection_period)
self.donation_protection_expiry_ts = new_expiry
# Regular liquidity addition
self.mint(receiver, d_token)
price_scale = self.tweak_price(A_gamma, xp, D)
else:
# (re)instatiating an empty pool:
self.D = D
self.virtual_price = 10**18
self.xcp_profit = 10**18
self.xcp_profit_a = 10**18
self.mint(receiver, d_token)
assert d_token >= min_mint_amount, "slippage"
# ---------------------------------------------- Log and claim admin fees.
log AddLiquidity(
receiver=receiver,
token_amounts=amounts_received,
fee=d_token_fee,
token_supply=token_supply+d_token,
price_scale=price_scale
)
return d_token
@internal
@view
def _xp(
balances: uint256[N_COINS],
price_scale: uint256,
) -> uint256[N_COINS]:
return [
balances[0] * PRECISIONS[0],
unsafe_div(balances[1] * PRECISIONS[1] * price_scale, PRECISION)
]
```
```vyper
@external
@pure
def newton_D(_amp: uint256,
gamma: uint256, # unused, present for compatibility with twocrypto
_xp: uint256[N_COINS],
K0_prev: uint256 = 0 # unused, present for compatibility with twocrypto
) -> uint256:
"""
Find D for given x[i] and A.
"""
# gamma and K0_prev are ignored
# _amp is already multiplied by a A_MULTIPLIER and N_COINS
S: uint256 = 0
for x: uint256 in _xp:
S += x
if S == 0:
return 0
D: uint256 = S
Ann: uint256 = _amp * N_COINS
for i: uint256 in range(255):
D_P: uint256 = D
for x: uint256 in _xp:
D_P = D_P * D // x
D_P //= N_COINS**N_COINS
Dprev: uint256 = D
# (Ann * S / A_PRECISION + D_P * N_COINS) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS + 1) * D_P)
D = (
(unsafe_div(Ann * S, A_MULTIPLIER) + D_P * N_COINS) * D
//
(
unsafe_div((Ann - A_MULTIPLIER) * D, A_MULTIPLIER) +
unsafe_add(N_COINS, 1) * D_P
)
)
# Equality with the precision of 1
if D > Dprev:
if D - Dprev <= 1:
return D
else:
if Dprev - D <= 1:
return D
# convergence typically occurs in 4 rounds or less, this should be unreachable!
# if it does happen the pool is borked and LPs can withdraw via `remove_liquidity`
raise "Did not converge"
```
```shell
>>> FXSwap.donation_shares()
0
>>> FXSwap.add_liquidity([10000000000000000000, 0], 0, empty(address), True) # adding 10 USDC as donation to the pool
>>> FXSwap.donation_shares()
11635899407127730908
```
::::
### `donation_shares`
::::description[`FXSwap.donation_shares() -> uint256: view`]
Getter for the current donation shares. Donation shares are the total shares donated to the contract including both "locked" (time-based release) and "throttled" (add_liquidity protection) shares.
Returns: Current donation shares (`uint256`).
```vyper
# Donation shares balance
donation_shares: public(uint256)
```
**Adding Donations**
This example shows how `donation_shares` are added.
```shell
>>> FXSwap.donation_shares()
0
>>> FXSwap.add_liquidity([10000000000000000000, 0], 0, empty(address), True) # adding 10 USDC as donation to the pool
>>> FXSwap.donation_shares()
11635899407127730908
```
**`donation_shares` behaviour**
This example shows how `donation_shares` behave when users interact with a function which calls `tweak_price`. `donation_shares` are decrease as they are used to rebalance the pool.
```shell
>>> FXSwap.donation_shares()
11635899407127730908
>>> FXSwap.exchange(0, 1, 10000000000000000000, 0, user)
>>> FXSwap.donation_shares()
11588763240547931073
```
::::
### `donation_shares_max_ratio`
::::description[`FXSwap.donation_shares_max_ratio() -> uint256: view`]
Getter for the maximum ratio of donation shares allowed in the pool. This parameter prevents the pool from being overwhelmed by donations, ensuring that regular LPs maintain a minimum share of the pool.
Returns: Maximum donation shares ratio (`uint256`).
```vyper
donation_shares_max_ratio: public(uint256)
```
```shell
>>> FXSwap.donation_shares_max_ratio()
100000000000000000 # 10%
```
::::
### `donation_duration`
::::description[`FXSwap.donation_duration() -> uint256: view`]
Getter for the duration required for donations to fully release from locked state. Donations are linearly unlocked over this time period, preventing immediate extraction and ensuring gradual distribution to LPs.
Returns: Donation duration in seconds (`uint256`).
```vyper
donation_duration: public(uint256)
```
```shell
>>> FXSwap.donation_duration()
604800
```
::::
### `last_donation_release_ts`
::::description[`FXSwap.last_donation_release_ts() -> uint256: view`]
Getter for the timestamp of the last donation release. This timestamp is used to calculate how much of the donation shares have been unlocked based on the elapsed time since the last donation was made.
Returns: Last donation release timestamp (`uint256`).
```vyper
last_donation_release_ts: public(uint256)
```
```shell
>>> FXSwap.last_donation_release()
1756389447
```
::::
### `donation_protection_expiry_ts`
::::description[`FXSwap.donation_protection_expiry_ts() -> uint256: view`]
Getter for the timestamp when donation protection expires. This protection mechanism extends the donation lock period when large amounts of liquidity are added, preventing donation extraction via sandwich attacks.
Returns: Donation protection expiry timestamp (`uint256`).
```vyper
donation_protection_expiry_ts: public(uint256)
```
```shell
>>> FXSwap.donation_protection_expiry_ts()
0
```
::::
### `donation_protection_period`
::::description[`FXSwap.donation_protection_period() -> uint256: view`]
Getter for the donation protection period in seconds. This is the maximum duration that donation protection can be extended when large liquidity additions occur, providing a cap on the protection mechanism.
Returns: Donation protection period in seconds (`uint256`).
```vyper
donation_protection_period: public(uint256)
```
```shell
>>> FXSwap.donation_protection_period()
600
```
::::
### `donation_protection_lp_threshold`
::::description[`FXSwap.donation_protection_lp_threshold() -> uint256: view`]
Getter for the LP threshold that triggers donation protection extension. When the relative amount of LP tokens added exceeds this threshold, the donation protection period is extended proportionally to prevent donation extraction attacks.
Returns: Donation protection LP threshold (`uint256`).
```vyper
donation_protection_lp_threshold: public(uint256)
```
```shell
>>> FXSwap.donation_protection_lp_threshold()
200000000000000000 # 20%
```
::::
### `set_donation_duration`
::::description[`FXSwap.set_donation_duration(duration: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory from where the pool was deployed.
:::
Admin function to set the donation duration. This controls how long it takes for donations to fully unlock.
| Input | Type | Description |
| ---------- | --------- | ------------------------------ |
| `duration` | `uint256` | New donation duration in seconds |
```vyper
@external
def set_donation_duration(duration: uint256):
"""
@notice Set the donation duration.
@param duration The new donation duration.
@dev The time required for donations to fully release from locked state.
"""
self._check_admin()
assert duration > 0, "duration must be positive"
self.donation_duration = duration
log SetDonationDuration(duration=duration)
```
```shell
>>> FXSwap.set_donation_duration(86400) # Set to 1 day
```
::::
### `set_donation_protection_params`
::::description[`FXSwap.set_donation_protection_params(_period: uint256, _threshold: uint256, _max_shares_ratio: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory from where the pool was deployed.
:::
Admin function to set donation protection parameters. These parameters control the MEV protection mechanism for donations.
| Input | Type | Description |
| ------------------ | --------- | ---------------------------------------------- |
| `_period` | `uint256` | New donation protection period in seconds |
| `_threshold` | `uint256` | New LP threshold for protection (with 10^18 precision) |
| `_max_shares_ratio`| `uint256` | New maximum donation shares ratio (with 10^18 precision) |
```vyper
@external
def set_donation_protection_params(
_period: uint256,
_threshold: uint256,
_max_shares_ratio: uint256,
):
"""
@notice Set donation protection parameters.
@param _period The new donation protection period in seconds.
@param _threshold The new donation protection threshold with 10**18 precision.
@param _max_shares_ratio The new maximum number of shares.
@dev _threshold = 30 * 10**18//100 means 30%
@dev _max_shares_ratio = 10 * 10**18//100 means 10%
"""
self._check_admin()
assert _period > 0, "period must be positive"
assert _threshold > 0, "threshold must be positive"
assert _max_shares_ratio > 0, "max_shares must be positive"
self.donation_protection_period = _period
self.donation_protection_lp_threshold = _threshold
self.donation_shares_max_ratio = _max_shares_ratio
log SetDonationProtection(
donation_protection_period=_period,
donation_protection_lp_threshold=_threshold,
donation_shares_max_ratio=_max_shares_ratio
)
```
```shell
>>> FXSwap.set_donation_protection_params(600, 200000000000000000, 100000000000000000)
# Set period to 10 minutes, threshold to 20%, max ratio to 10%
```
::::
---
## Refuel Implementation
:::info
This document covers the different implementations of the Twocrypto-NG pool, with a focus on the refuel capabilities that have been introduced.
:::
:::note[Terminology Note]
This document uses **"refuel"** terminology in explanations for consistency with current branding. However, the actual contract variables and functions still use "donation" terminology (e.g., `donation_shares`, `donation_duration`) as these contracts are already deployed and cannot be changed. The functionality is identical - "refuel" and "donation" refer to the same mechanism.
:::
The refuel mechanism in Cryptoswap pools allows external parties to contribute liquidity as a buffer that can be burned during rebalancing operations. This system addresses the core challenge of rebalancing costs by providing a liquidity reserve (`donation_shares`) that protects regular LPs from bearing the full impact of rebalancing adjustments.
## What are Refuels?
Refuels are special LP shares that are not credited to any user but instead serve as a liquidity buffer for the pool. During rebalancing, these shares can be burned to absorb impermanent loss, enabling the pool to adjust its `price_scale` while maintaining virtual price for existing LPs.
```vyper
# Donation shares are tracked separately from regular LP shares
donation_shares: public(uint256)
donation_shares_max_ratio: public(uint256) # Cap on donations (e.g., 10% of total supply)
```
## How are Refuels Added?
Refuels are added through the `add_liquidity` function with the `donation=True` parameter:
```vyper
@external
@nonreentrant
def add_liquidity(
amounts: uint256[N_COINS],
min_mint_amount: uint256,
receiver: address = msg.sender,
donation: bool = False
) -> uint256:
```
When `donation=True`:
- LP tokens are credited to the refuel buffer instead of being minted to a receiver
- Only a minimal `NOISE_FEE` (0.1 BPS) is charged for numerical stability
- The `NOISE_FEE` is absorbed by the pool itself (not distributed to anyone) to ensure numerical precision
- The refuel is subject to the `donation_shares_max_ratio` cap
- A `Donation` event is emitted
```vyper
if donation:
assert receiver == empty(address), "nonzero receiver"
new_donation_shares: uint256 = self.donation_shares + d_token
assert new_donation_shares * PRECISION // (token_supply + d_token) <= self.donation_shares_max_ratio, "donation above cap!"
# Credit donation: we don't explicitly mint lp tokens, but increase total supply
self.donation_shares = new_donation_shares
self.totalSupply += d_token
log Donation(donor=msg.sender, token_amounts=amounts_received)
```
## How are Refuels Used?
Refuels are automatically burned during pool rebalancing operations when `tweak_price` is called. This occurs after normal pool operations like swaps (`_exchange`), liquidity additions (`add_liquidity`), and imbalanced withdrawals (`remove_liquidity_one_coin` or `remove_liquidity_fixed_out`).
The key logic in `tweak_price`:
```vyper
# Calculate unlocked donations (time-based release + protection damping)
donation_shares: uint256 = self._donation_shares()
# During rebalancing, burn donations to maintain virtual price
donation_shares_to_burn: uint256 = 0
goal_vp: uint256 = max(threshold_vp, virtual_price)
if new_virtual_price < goal_vp:
# Calculate how many donation shares to burn to reach goal_vp
tweaked_supply: uint256 = 10**18 * new_xcp // goal_vp
donation_shares_to_burn = min(
unsafe_sub(total_supply, tweaked_supply),
donation_shares
)
if donation_shares_to_burn > 0:
self.donation_shares -= donation_shares_to_burn
self.totalSupply -= donation_shares_to_burn
self.last_donation_release_ts = block.timestamp
```
## MEV Protection Measures
The refuel mechanism includes two key MEV protection measures:
### 1. Time-Based Unlocking
Refuels unlock linearly over time (default: 7 days) to prevent immediate extraction:
```vyper
@internal
@view
def _donation_shares(_donation_protection: bool = True) -> uint256:
# Time-based release of donation shares
elapsed: uint256 = block.timestamp - self.last_donation_release_ts
unlocked_shares: uint256 = min(donation_shares, donation_shares * elapsed // self.donation_duration)
```
### 2. Add Liquidity Throttling
When users add liquidity, the protection window is extended to prevent refuel extraction via sandwich attacks:
```vyper
# Donation Protection & LP Spam Penalty
relative_lp_add: uint256 = d_token * PRECISION // (token_supply + d_token)
if relative_lp_add > 0 and self.donation_shares > 0:
# Extend protection period
protection_period: uint256 = self.donation_protection_period
extension_seconds: uint256 = min(relative_lp_add * protection_period // self.donation_protection_lp_threshold, protection_period)
current_expiry: uint256 = max(self.donation_protection_expiry_ts, block.timestamp)
new_expiry: uint256 = min(current_expiry + extension_seconds, block.timestamp + protection_period)
self.donation_protection_expiry_ts = new_expiry
```
The protection applies a damping factor to unlocked donations:
```vyper
# Donation protection damping factor
protection_factor: uint256 = 0
expiry: uint256 = self.donation_protection_expiry_ts
if expiry > block.timestamp:
protection_factor = min((expiry - block.timestamp) * PRECISION // self.donation_protection_period, PRECISION)
return unlocked_shares * (PRECISION - protection_factor) // PRECISION
```
This dual protection system ensures that refuels cannot be easily extracted by MEV bots while still providing the intended liquidity buffer benefits to the pool.
---
## Contract Functions and Variables
### `add_liquidity`
::::description[`FXSwap.add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256, receiver: address = msg.sender, donation: bool = False) -> uint256:`]
Function to add liquidity to the pool. Can be used for regular liquidity addition or donations when `donation=True`. When `donation=True`, the LP tokens are credited to the donation buffer instead of being minted to a receiver.
Returns: Amount of LP tokens minted (to receiver or donation buffer) (`uint256`).
Emits: `Donation` or `AddLiquidity` event.
| Input | Type | Description |
| ----------------- | ------------------ | ------------------------------------------------ |
| `amounts` | `uint256[N_COINS]` | Amounts of each coin to add. |
| `min_mint_amount` | `uint256` | Minimum amount of LP tokens to mint to `receiver`. |
| `receiver` | `address` | Receiver address of the minted LP tokens; defaults to `msg.sender`. Ignored when `donation=True`. |
| `donation` | `bool` | Whether the liquidity is a donation; defaults to `False`. |
```vyper
cached_price_scale: uint256 # <------------------------ Internal price scale.
cached_price_oracle: uint256 # <------- Price target given by moving average.
balances: public(uint256[N_COINS])
@external
@nonreentrant
def add_liquidity(
amounts: uint256[N_COINS],
min_mint_amount: uint256,
receiver: address = msg.sender,
donation: bool = False
) -> uint256:
"""
@notice Adds liquidity into the pool.
@param amounts Amounts of each coin to add.
@param min_mint_amount Minimum amount of LP to mint.
@param receiver Address to send the LP tokens to. Default is msg.sender
@param donation Whether the liquidity is a donation, if True receiver is ignored.
@return uint256 Amount of LP tokens issued (to receiver or donation buffer).
"""
assert amounts[0] + amounts[1] > 0, "no coins to add"
# --------------------- Get prices, balances -----------------------------
old_balances: uint256[N_COINS] = self.balances
########################## TRANSFER IN <-------
amounts_received: uint256[N_COINS] = empty(uint256[N_COINS])
# This variable will contain the old balances + the amounts received.
balances: uint256[N_COINS] = self.balances
for i: uint256 in range(N_COINS):
if amounts[i] > 0:
# Updates self.balances here:
amounts_received[i] = self._transfer_in(
i,
amounts[i],
msg.sender,
False, # <--------------------- Disable optimistic transfers.
)
balances[i] += amounts_received[i]
price_scale: uint256 = self.cached_price_scale
xp: uint256[N_COINS] = self._xp(balances, price_scale)
old_xp: uint256[N_COINS] = self._xp(old_balances, price_scale)
# --------------------Finalize ramping of empty pool
if self.D == 0:
self.future_A_gamma_time = block.timestamp
# -------------------- Calculate LP tokens to mint -----------------------
A_gamma: uint256[2] = self._A_gamma()
old_D: uint256 = self._get_D(A_gamma, old_xp)
D: uint256 = staticcall self.MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)
token_supply: uint256 = self.totalSupply
d_token: uint256 = 0
if old_D > 0:
d_token = token_supply * D // old_D - token_supply
else:
d_token = self._xcp(D, price_scale) # <----- Making initial virtual price equal to 1.
assert d_token > 0, "nothing minted"
d_token_fee: uint256 = 0
if old_D > 0:
d_token_fee = (
self._calc_token_fee(amounts_received, xp, donation, True) * d_token // 10**10 + 1
) # for donations - we only take NOISE_FEE (check _calc_token_fee)
d_token -= d_token_fee
if donation:
assert receiver == empty(address), "nonzero receiver"
new_donation_shares: uint256 = self.donation_shares + d_token
assert new_donation_shares * PRECISION // (token_supply + d_token) <= self.donation_shares_max_ratio, "donation above cap!"
# When adding donation, if the previous one hasn't been fully released we preserve
# the currently unlocked donation [given by `self._donation_shares()`] by updating
# `self.last_donation_release_ts` as if a single virtual donation of size `new_donation_shares`
# was made in past and linearly unlocked reaching `self._donation_shares()` at the current time.
# We want the following equality to hold:
# self._donation_shares() = new_donation_shares * (new_elapsed / self.donation_duration)
# We can rearrange this to find the new elapsed time (imitating one large virtual donation):
# => new_elapsed = self._donation_shares() * self.donation_duration / new_donation_shares
# edge case: if self.donation_shares = 0, then self._donation_shares() is 0
# and new_elapsed = 0, thus initializing last_donation_release_ts = block.timestamp
new_elapsed: uint256 = self._donation_shares(False) * self.donation_duration // new_donation_shares
# Additional observations:
# new_elapsed = (old_pool * old_elapsed / D) * D / new_pool = old_elapsed * (old_pool / new_pool)
# => new_elapsed is always smaller than old_elapsed
# and self.last_donation_release_ts is carried forward propotionally to new donation size.
self.last_donation_release_ts = block.timestamp - new_elapsed
# Credit donation: we don't explicitly mint lp tokens, but increase total supply
self.donation_shares = new_donation_shares
self.totalSupply += d_token
log Donation(donor=msg.sender, token_amounts=amounts_received)
else:
# --- Donation Protection & LP Spam Penalty ---
# Extend protection to shield against donation extraction via sandwich attacks.
# A penalty is applied for extending the protection to disincentivize spamming.
relative_lp_add: uint256 = d_token * PRECISION // (token_supply + d_token)
if relative_lp_add > 0 and self.donation_shares > 0: # sub-precision additions are expensive to stack
# Extend protection period
protection_period: uint256 = self.donation_protection_period
extension_seconds: uint256 = min(relative_lp_add * protection_period // self.donation_protection_lp_threshold, protection_period)
current_expiry: uint256 = max(self.donation_protection_expiry_ts, block.timestamp)
new_expiry: uint256 = min(current_expiry + extension_seconds, block.timestamp + protection_period)
self.donation_protection_expiry_ts = new_expiry
# Regular liquidity addition
self.mint(receiver, d_token)
price_scale = self.tweak_price(A_gamma, xp, D)
else:
# (re)instatiating an empty pool:
self.D = D
self.virtual_price = 10**18
self.xcp_profit = 10**18
self.xcp_profit_a = 10**18
self.mint(receiver, d_token)
assert d_token >= min_mint_amount, "slippage"
# ---------------------------------------------- Log and claim admin fees.
log AddLiquidity(
receiver=receiver,
token_amounts=amounts_received,
fee=d_token_fee,
token_supply=token_supply+d_token,
price_scale=price_scale
)
return d_token
@internal
@view
def _xp(
balances: uint256[N_COINS],
price_scale: uint256,
) -> uint256[N_COINS]:
return [
balances[0] * PRECISIONS[0],
unsafe_div(balances[1] * PRECISIONS[1] * price_scale, PRECISION)
]
```
```vyper
@external
@pure
def newton_D(_amp: uint256,
gamma: uint256, # unused, present for compatibility with twocrypto
_xp: uint256[N_COINS],
K0_prev: uint256 = 0 # unused, present for compatibility with twocrypto
) -> uint256:
"""
Find D for given x[i] and A.
"""
# gamma and K0_prev are ignored
# _amp is already multiplied by a A_MULTIPLIER and N_COINS
S: uint256 = 0
for x: uint256 in _xp:
S += x
if S == 0:
return 0
D: uint256 = S
Ann: uint256 = _amp * N_COINS
for i: uint256 in range(255):
D_P: uint256 = D
for x: uint256 in _xp:
D_P = D_P * D // x
D_P //= N_COINS**N_COINS
Dprev: uint256 = D
# (Ann * S / A_PRECISION + D_P * N_COINS) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS + 1) * D_P)
D = (
(unsafe_div(Ann * S, A_MULTIPLIER) + D_P * N_COINS) * D
//
(
unsafe_div((Ann - A_MULTIPLIER) * D, A_MULTIPLIER) +
unsafe_add(N_COINS, 1) * D_P
)
)
# Equality with the precision of 1
if D > Dprev:
if D - Dprev <= 1:
return D
else:
if Dprev - D <= 1:
return D
# convergence typically occurs in 4 rounds or less, this should be unreachable!
# if it does happen the pool is borked and LPs can withdraw via `remove_liquidity`
raise "Did not converge"
```
```shell
>>> FXSwap.donation_shares()
0
>>> FXSwap.add_liquidity([10000000000000000000, 0], 0, empty(address), True) # adding 10 USDC as donation to the pool
>>> FXSwap.donation_shares()
11635899407127730908
```
::::
### `donation_shares`
::::description[`FXSwap.donation_shares() -> uint256: view`]
Getter for the current donation shares. Donation shares are the total shares donated to the contract including both "locked" (time-based release) and "throttled" (add_liquidity protection) shares.
Returns: Current donation shares (`uint256`).
```vyper
# Donation shares balance
donation_shares: public(uint256)
```
**Adding Donations**
This example shows how `donation_shares` are added.
```shell
>>> FXSwap.donation_shares()
0
>>> FXSwap.add_liquidity([10000000000000000000, 0], 0, empty(address), True) # adding 10 USDC as donation to the pool
>>> FXSwap.donation_shares()
11635899407127730908
```
**`donation_shares` behaviour**
This example shows how `donation_shares` behave when users interact with a function which calls `tweak_price`. `donation_shares` are decrease as they are used to rebalance the pool.
```shell
>>> FXSwap.donation_shares()
11635899407127730908
>>> FXSwap.exchange(0, 1, 10000000000000000000, 0, user)
>>> FXSwap.donation_shares()
11588763240547931073
```
::::
### `donation_shares_max_ratio`
::::description[`FXSwap.donation_shares_max_ratio() -> uint256: view`]
Getter for the maximum ratio of donation shares allowed in the pool. This parameter prevents the pool from being overwhelmed by donations, ensuring that regular LPs maintain a minimum share of the pool.
Returns: Maximum donation shares ratio (`uint256`).
```vyper
donation_shares_max_ratio: public(uint256)
```
```shell
>>> FXSwap.donation_shares_max_ratio()
100000000000000000 # 10%
```
::::
### `donation_duration`
::::description[`FXSwap.donation_duration() -> uint256: view`]
Getter for the duration required for donations to fully release from locked state. Donations are linearly unlocked over this time period, preventing immediate extraction and ensuring gradual distribution to LPs.
Returns: Donation duration in seconds (`uint256`).
```vyper
donation_duration: public(uint256)
```
```shell
>>> FXSwap.donation_duration()
604800
```
::::
### `last_donation_release_ts`
::::description[`FXSwap.last_donation_release_ts() -> uint256: view`]
Getter for the timestamp of the last donation release. This timestamp is used to calculate how much of the donation shares have been unlocked based on the elapsed time since the last donation was made.
Returns: Last donation release timestamp (`uint256`).
```vyper
last_donation_release_ts: public(uint256)
```
```shell
>>> FXSwap.last_donation_release()
1756389447
```
::::
### `donation_protection_expiry_ts`
::::description[`FXSwap.donation_protection_expiry_ts() -> uint256: view`]
Getter for the timestamp when donation protection expires. This protection mechanism extends the donation lock period when large amounts of liquidity are added, preventing donation extraction via sandwich attacks.
Returns: Donation protection expiry timestamp (`uint256`).
```vyper
donation_protection_expiry_ts: public(uint256)
```
```shell
>>> FXSwap.donation_protection_expiry_ts()
0
```
::::
### `donation_protection_period`
::::description[`FXSwap.donation_protection_period() -> uint256: view`]
Getter for the donation protection period in seconds. This is the maximum duration that donation protection can be extended when large liquidity additions occur, providing a cap on the protection mechanism.
Returns: Donation protection period in seconds (`uint256`).
```vyper
donation_protection_period: public(uint256)
```
```shell
>>> FXSwap.donation_protection_period()
600
```
::::
### `donation_protection_lp_threshold`
::::description[`FXSwap.donation_protection_lp_threshold() -> uint256: view`]
Getter for the LP threshold that triggers donation protection extension. When the relative amount of LP tokens added exceeds this threshold, the donation protection period is extended proportionally to prevent donation extraction attacks.
Returns: Donation protection LP threshold (`uint256`).
```vyper
donation_protection_lp_threshold: public(uint256)
```
```shell
>>> FXSwap.donation_protection_lp_threshold()
200000000000000000 # 20%
```
::::
### `set_donation_duration`
::::description[`FXSwap.set_donation_duration(duration: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory from where the pool was deployed.
:::
Admin function to set the donation duration. This controls how long it takes for donations to fully unlock.
| Input | Type | Description |
| ---------- | --------- | ------------------------------ |
| `duration` | `uint256` | New donation duration in seconds |
```vyper
@external
def set_donation_duration(duration: uint256):
"""
@notice Set the donation duration.
@param duration The new donation duration.
@dev The time required for donations to fully release from locked state.
"""
self._check_admin()
assert duration > 0, "duration must be positive"
self.donation_duration = duration
log SetDonationDuration(duration=duration)
```
```shell
>>> FXSwap.set_donation_duration(86400) # Set to 1 day
```
::::
### `set_donation_protection_params`
::::description[`FXSwap.set_donation_protection_params(_period: uint256, _threshold: uint256, _max_shares_ratio: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory from where the pool was deployed.
:::
Admin function to set donation protection parameters. These parameters control the MEV protection mechanism for donations.
| Input | Type | Description |
| ------------------ | --------- | ---------------------------------------------- |
| `_period` | `uint256` | New donation protection period in seconds |
| `_threshold` | `uint256` | New LP threshold for protection (with 10^18 precision) |
| `_max_shares_ratio`| `uint256` | New maximum donation shares ratio (with 10^18 precision) |
```vyper
@external
def set_donation_protection_params(
_period: uint256,
_threshold: uint256,
_max_shares_ratio: uint256,
):
"""
@notice Set donation protection parameters.
@param _period The new donation protection period in seconds.
@param _threshold The new donation protection threshold with 10**18 precision.
@param _max_shares_ratio The new maximum number of shares.
@dev _threshold = 30 * 10**18//100 means 30%
@dev _max_shares_ratio = 10 * 10**18//100 means 10%
"""
self._check_admin()
assert _period > 0, "period must be positive"
assert _threshold > 0, "threshold must be positive"
assert _max_shares_ratio > 0, "max_shares must be positive"
self.donation_protection_period = _period
self.donation_protection_lp_threshold = _threshold
self.donation_shares_max_ratio = _max_shares_ratio
log SetDonationProtection(
donation_protection_period=_period,
donation_protection_lp_threshold=_threshold,
donation_shares_max_ratio=_max_shares_ratio
)
```
```shell
>>> FXSwap.set_donation_protection_params(600, 200000000000000000, 100000000000000000)
# Set period to 10 minutes, threshold to 20%, max ratio to 10%
```
::::
---
## StreamExecutor
The `StreamExecutor` is a thin convenience contract that batch-executes all due donation streams in a single call and forwards the earned ETH rewards to the caller. It queries the [`DonationStreamer`](./donation-streamer.md) for due streams, executes them in chunks of 32, and sends the accumulated ETH balance to `msg.sender`.
This contract is designed for keeper bots that want to claim all available rewards with a single transaction. Streams can also be executed via the [frontend UI](https://curvefi.github.io/refuel-automation/).
:::vyper[`StreamExecutor.vy`]
The source code for the `StreamExecutor.vy` contract can be found on [GitHub](https://github.com/curvefi/refuel-automation). The contract is written in [Vyper](https://vyperlang.org/) version `0.4.3`.
The contract is deployed at the same address on all chains (via CREATE3):
- :logos-ethereum: Ethereum: [`0x4a8Cc5Cb8f7242be9944E1313793c2E5411c462A`](https://etherscan.io/address/0x4a8Cc5Cb8f7242be9944E1313793c2E5411c462A)
- :logos-polygon: Polygon: [`0x4a8Cc5Cb8f7242be9944E1313793c2E5411c462A`](https://polygonscan.com/address/0x4a8Cc5Cb8f7242be9944E1313793c2E5411c462A)
```json
[{"stateMutability":"payable","type":"fallback"},{"inputs":[],"name":"execute","outputs":[],"stateMutability":"nonpayable","type":"function"}]
```
:::
## Execution
### `execute`
::::description[`StreamExecutor.execute()`]
Queries all due streams from the [`DonationStreamer`](./donation-streamer.md), executes them in batches of 32 via `execute_many`, and sends the accumulated ETH rewards to the caller. If no streams are due, the call succeeds but no ETH is sent.
```vyper
STREAMER: constant(address) = 0x2b786BB995978CC2242C567Ae62fd617b0eBC828
@external
def execute():
self._execute_due()
@internal
def _execute_due():
due_ids: DynArray[uint256, N_MAX_VIEW] = empty(DynArray[uint256, N_MAX_VIEW])
rewards: DynArray[uint256, N_MAX_VIEW] = empty(DynArray[uint256, N_MAX_VIEW])
due_ids, rewards = staticcall DonationStreamer(STREAMER).streams_and_rewards_due()
chunk: DynArray[uint256, N_MAX_EXECUTE] = empty(DynArray[uint256, N_MAX_EXECUTE])
for i: uint256 in range(len(due_ids), bound=N_MAX_VIEW):
chunk.append(due_ids[i])
if len(chunk) == N_MAX_EXECUTE:
extcall DonationStreamer(STREAMER).execute_many(chunk)
chunk = empty(DynArray[uint256, N_MAX_EXECUTE])
if len(chunk) > 0:
extcall DonationStreamer(STREAMER).execute_many(chunk)
if self.balance > 0:
send(msg.sender, self.balance)
```
```shell
>>> StreamExecutor.execute()
```
::::
### `__default__`
::::description[`StreamExecutor.__default__()`]
Payable fallback function that allows the contract to receive ETH from the `DonationStreamer` during stream execution. The `DonationStreamer` sends ETH rewards to `msg.sender` (the `StreamExecutor`) which are then forwarded to the original caller in the `execute` function.
```vyper
@external
@payable
def __default__():
pass
```
```shell
# ETH is received automatically when DonationStreamer pays rewards
# No direct user interaction needed
```
::::
---
## Overview(Twocrypto-ng)
The **Twocrypto-NG contract infrastructure** represents an **optimized version of Curve Finance Crypto pools**.
:::deploy[Contract Source & Deployment]
Source code is available on [GitHub](https://github.com/curvefi/twocrypto-ng).
:::
---
**The AMM infrastructure involves the following parts:**
The AMM is a **2-coin, auto-rebalancing Cryptoswap implementation** (version 2.0.0) with several optimizations. Unlike the older version, the **pool contract is an ERC20-compliant LP token**. The AMMs have a hardcoded `ADMIN_FEE`, set to 50% of the earned profits.
The Factory allows the permissionless deployment of liquidity pools and gauges. It can accommodate **multiple blueprints of the AMM** contract. The admin can implement parameter changes, change the fee recipient, and upgrade implementations.
Contains **view methods relevant for integrators** and users. The address of the deployed Views contract is stored in the Factory and is upgradeable by the Factory's admin.
A contract which contains different **math functions used in the AMM**.
A liquidity gauge blueprint contract which deploys a liquidity gauge of a pool on Ethereum. On sidechains, gauges need to be deployed via the [`RootChainGaugeFactory`](../../gauges/xchain-gauges/root-gauge-factory.md).
---
## New Features
**New features over the regular two-coin CryptoSwap implementation:**
- New fee claiming approach
- [**`exchange_received`**](#exchange_received)
- Overall gas optimizations
### Fee Claiming
Admin fees of a Curve pool are usually claimed through an external function, callable by anyone. **Twocrypto-NG does not have any external function to directly claim fees**. Admin fees are claimed through an internal function, which is called when liquidity is removed single-sidedly via the `_remove_liquidity_one_coin` function, and then sent to the fee receiver determined within the Factory contract.
```vyper
@internal
def _claim_admin_fees():
"""
@notice Claims admin fees and sends it to fee_receiver set in the factory.
@dev Functionally similar to:
1. Calculating admin's share of fees,
2. minting LP tokens,
3. admin claims underlying tokens via remove_liquidity.
"""
# --------------------- Check if fees can be claimed ---------------------
# Disable fee claiming if:
# 1. If time passed since last fee claim is less than
# MIN_ADMIN_FEE_CLAIM_INTERVAL.
# 2. Pool parameters are being ramped.
last_claim_time: uint256 = self.last_admin_fee_claim_timestamp
if (
unsafe_sub(block.timestamp, last_claim_time) < MIN_ADMIN_FEE_CLAIM_INTERVAL or
self.future_A_gamma_time > block.timestamp
):
return
xcp_profit: uint256 = self.xcp_profit # <---------- Current pool profits.
xcp_profit_a: uint256 = self.xcp_profit_a # <- Profits at previous claim.
current_lp_token_supply: uint256 = self.totalSupply
# Do not claim admin fees if:
# 1. insufficient profits accrued since last claim, and
# 2. there are less than 10**18 (or 1 unit of) lp tokens, else it can lead
# to manipulated virtual prices.
if xcp_profit <= xcp_profit_a or current_lp_token_supply < 10**18:
return
# ---------- Conditions met to claim admin fees: compute state. ----------
A_gamma: uint256[2] = self._A_gamma()
D: uint256 = self.D
vprice: uint256 = self.virtual_price
price_scale: uint256 = self.cached_price_scale
fee_receiver: address = factory.fee_receiver()
balances: uint256[N_COINS] = self.balances
# Admin fees are calculated as follows.
# 1. Calculate accrued profit since last claim. `xcp_profit`
# is the current profits. `xcp_profit_a` is the profits
# at the previous claim.
# 2. Take out admin's share, which is hardcoded at 5 * 10**9.
# (50% => half of 100% => 10**10 / 2 => 5 * 10**9).
# 3. Since half of the profits go to rebalancing the pool, we
# are left with half; so divide by 2.
fees: uint256 = unsafe_div(
unsafe_sub(xcp_profit, xcp_profit_a) * ADMIN_FEE, 2 * 10**10
)
# ------------------------------ Claim admin fees by minting admin's share
# of the pool in LP tokens.
# This is the admin fee tokens claimed in self.add_liquidity. We add it to
# the LP token share that the admin needs to claim:
admin_share: uint256 = self.admin_lp_virtual_balance
frac: uint256 = 0
if fee_receiver != empty(address) and fees > 0:
# -------------------------------- Calculate admin share to be minted.
frac = vprice * 10**18 / (vprice - fees) - 10**18
admin_share += current_lp_token_supply * frac / 10**18
# ------ Subtract fees from profits that will be used for rebalancing.
xcp_profit -= fees * 2
# ------------------- Recalculate virtual_price following admin fee claim.
total_supply_including_admin_share: uint256 = (
current_lp_token_supply + admin_share
)
vprice = (
10**18 * self.get_xcp(D, price_scale) /
total_supply_including_admin_share
)
# Do not claim fees if doing so causes virtual price to drop below 10**18.
if vprice < 10**18:
return
# ---------------------------- Update State ------------------------------
# Set admin virtual LP balances to zero because we claimed:
self.admin_lp_virtual_balance = 0
self.xcp_profit = xcp_profit
self.last_admin_fee_claim_timestamp = block.timestamp
# Since we reduce balances: virtual price goes down
self.virtual_price = vprice
# Adjust D after admin seemingly removes liquidity
self.D = D - unsafe_div(D * admin_share, total_supply_including_admin_share)
if xcp_profit > xcp_profit_a:
self.xcp_profit_a = xcp_profit # <-------- Cache last claimed profit.
# --------------------------- Handle Transfers ---------------------------
admin_tokens: uint256[N_COINS] = empty(uint256[N_COINS])
if admin_share > 0:
for i in range(N_COINS):
admin_tokens[i] = (
balances[i] * admin_share /
total_supply_including_admin_share
)
# _transfer_out tokens to admin and update self.balances. State
# update to self.balances occurs before external contract calls:
self._transfer_out(i, admin_tokens[i], fee_receiver)
log ClaimAdminFee(fee_receiver, admin_tokens)
```
### `exchange_received`
This function **allows the exchange of tokens without actually transferring tokens in**, as the exchange is based on the change of the coins balances within the pool.
Users of this method are dex aggregators, arbitrageurs, or other users who **do not wish to grant approvals to the contract**. They can instead send tokens directly to the contract and call **`exchange_received()`**.
```vyper
@internal
def _transfer_in(
_coin_idx: uint256,
_dx: uint256,
sender: address,
expect_optimistic_transfer: bool,
) -> uint256:
"""
@notice Transfers `_coin` from `sender` to `self` and calls `callback_sig`
if it is not empty.
@params _coin_idx uint256 Index of the coin to transfer in.
@params dx amount of `_coin` to transfer into the pool.
@params sender address to transfer `_coin` from.
@params expect_optimistic_transfer bool True if pool expects user to transfer.
This is only enabled for exchange_received.
@return The amount of tokens received.
"""
coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self)
if expect_optimistic_transfer: # Only enabled in exchange_received:
# it expects the caller of exchange_received to have sent tokens to
# the pool before calling this method.
# If someone donates extra tokens to the contract: do not acknowledge.
# We only want to know if there are dx amount of tokens. Anything extra,
# we ignore. This is why we need to check if received_amounts (which
# accounts for coin balances of the contract) is atleast dx.
# If we checked for received_amounts == dx, an extra transfer without a
# call to exchange_received will break the method.
dx: uint256 = coin_balance - self.balances[_coin_idx]
assert dx >= _dx # dev: user didn't give us coins
# Adjust balances
self.balances[_coin_idx] += dx
return dx
# ----------------------------------------------- ERC20 transferFrom flow.
# EXTERNAL CALL
assert ERC20(coins[_coin_idx]).transferFrom(
sender,
self,
_dx,
default_return_value=True
)
dx: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) - coin_balance
self.balances[_coin_idx] += dx
return dx
```
---
:::example
Lets say a user wants to swap **`GOV-TOKEN↔USDC`** through an aggregator. For simplicity we assume, **`GOV-TOKEN↔USDT`** exchange is done via a uniswap pool, **`USDT↔USDC`** via a Curve pool.
``` mermaid
graph LR
u([USER]) --- p1[(UNISWAP)]
p1 -->|"3. transfer out/in"| p2[(CURVE)]
u -..-> |1. approve and transfer| a([AGGREGATOR])
a ==> |"2. exchange"| p1
a -.-|"4. exchange_received"| p2
p2 --> |5. transfer dy out| u
linkStyle 0 stroke-width:0, fill:none;
```
:::
1. User gives approval the `AGGREGATOR`, which then transfers tokens into the aggregator contract
2. Aggregator exchanges `GOV-TOKEN` for `USDT` using Uniswap
3. Transfers the `USDT` directly from Uniswap into the Curve pool
4. Perform a swap on the Curve pool (`USDT↔USDC`) via **`exchange_received`**
5. Transfer `USDC` to the user
:::info
This method saves aggregators one redundant ERC-20 transfer and eliminates the need to grant approval to a curve pool. Without this function, the aggregator would have to conduct an additional transaction, transferring USDT from the Uniswap pool to their aggregator contract after the exchange, and then sending it to the Curve pool for another exchange (USDT↔USDC).
However, with this method in place, the aggregator can transfer the output tokens directly into the next pool and perform an exchange.
:::
---
## Pool: Admin Controls
## Pool Ownership
Liquidity pools are deployed via the [Factory](../../factory/twocrypto-ng/deployer-api.md). All pools deployed **share the same admin**defined within the Factory contract.
Transfering the ownership of a pool is only possible by changing the ownership of the Factory. Admin is the Curve DAO (OwnershipAdmin).
The same applies to the fee receiver of the pools.
[Factory Ownership](../../factory/overview.md#contract-ownership)
---
## Parameter Changes
For more information about parameters: [https://nagaking.substack.com/p/deep-dive-curve-v2-parameters](https://nagaking.substack.com/p/deep-dive-curve-v2-parameters).
The appropriate value for `A` and `gamma` is dependent upon the type of coin being used within the pool, and is subject to optimization and pool-parameter update based on the market history of the trading pair.
It is possible to modify the parameters for a pool after it has been deployed. Again, only the admin of the pool (= Factory admin) can do so.
### `ramp_A_gamma`
::::description[`TwoCrypto.ramp_A_gamma(future_A: uint256, future_gamma: uint256, future_time: uint256):`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the Factory contract.
:::
Function to linearly ramp the values of `A` and `gamma`.
Emits: `RampAgamma`
| Input | Type | Description |
| -------------- | --------- | --------------------- |
| `future_A` | `uint256` | Future value of `A` |
| `future_gamma` | `uint256` | Future value of `gamma` |
| `future_time` | `uint256` | Timestamp at which the ramping will end |
```vyper
event RampAgamma:
initial_A: uint256
future_A: uint256
initial_gamma: uint256
future_gamma: uint256
initial_time: uint256
future_time: uint256
@external
def ramp_A_gamma(
future_A: uint256, future_gamma: uint256, future_time: uint256
):
"""
@notice Initialise Ramping A and gamma parameter values linearly.
@dev Only accessible by factory admin, and only
@param future_A The future A value.
@param future_gamma The future gamma value.
@param future_time The timestamp at which the ramping will end.
"""
assert msg.sender == factory.admin() # dev: only owner
assert block.timestamp > self.initial_A_gamma_time + (MIN_RAMP_TIME - 1) # dev: ramp undergoing
assert future_time > block.timestamp + MIN_RAMP_TIME - 1 # dev: insufficient time
A_gamma: uint256[2] = self._A_gamma()
initial_A_gamma: uint256 = A_gamma[0] << 128
initial_A_gamma = initial_A_gamma | A_gamma[1]
assert future_A > MIN_A - 1
assert future_A < MAX_A + 1
assert future_gamma > MIN_GAMMA - 1
assert future_gamma < MAX_GAMMA + 1
ratio: uint256 = 10**18 * future_A / A_gamma[0]
assert ratio < 10**18 * MAX_A_CHANGE + 1
assert ratio > 10**18 / MAX_A_CHANGE - 1
ratio = 10**18 * future_gamma / A_gamma[1]
assert ratio < 10**18 * MAX_A_CHANGE + 1
assert ratio > 10**18 / MAX_A_CHANGE - 1
self.initial_A_gamma = initial_A_gamma
self.initial_A_gamma_time = block.timestamp
future_A_gamma: uint256 = future_A << 128
future_A_gamma = future_A_gamma | future_gamma
self.future_A_gamma_time = future_time
self.future_A_gamma = future_A_gamma
log RampAgamma(
A_gamma[0],
future_A,
A_gamma[1],
future_gamma,
block.timestamp,
future_time,
)
```
```shell
>>> soon
```
::::
### `stop_ramp_A_gamma`
::::description[`TwoCrypto.stop_ramp_A_gamma():`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the Factory contract.
:::
Function to immediately stop the ramping of A and gamma parameters and set them to their current values.
Emits: `StopRampA`
```vyper
event StopRampA:
current_A: uint256
current_gamma: uint256
time: uint256
@external
def stop_ramp_A_gamma():
"""
@notice Stop Ramping A and gamma parameters immediately.
@dev Only accessible by factory admin.
"""
assert msg.sender == factory.admin() # dev: only owner
A_gamma: uint256[2] = self._A_gamma()
current_A_gamma: uint256 = A_gamma[0] << 128
current_A_gamma = current_A_gamma | A_gamma[1]
self.initial_A_gamma = current_A_gamma
self.future_A_gamma = current_A_gamma
self.initial_A_gamma_time = block.timestamp
self.future_A_gamma_time = block.timestamp
# ------ Now (block.timestamp < t1) is always False, so we return saved A.
log StopRampA(A_gamma[0], A_gamma[1], block.timestamp)
```
```shell
>>> soon
```
::::
### `apply_new_parameters`
::::description[`TwoCrypto.apply_new_parameters(_new_mid_fee: uint256, _new_out_fee: uint256, _new_fee_gamma: uint256, _new_allowed_extra_profit: uint256, _new_adjustment_step: uint256, _new_ma_time: uint256, _new_xcp_ma_time: uint256):`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the Factory contract.
:::
Function to commit new parameters. The new parameters are applied immediately.
Emits: `NewParameters`
| Input | Type | Description |
| ----------------------- | --------- | --------------------------------------------------- |
| `_new_mid_fee` | `uint256` | New `mid_fee` value. |
| `_new_out_fee` | `uint256` | New `out_fee` value. |
| `_new_fee_gamma` | `uint256` | New `fee_gamma` value. |
| `_new_allowed_extra_profit` | `uint256` | New `allowed_extra_profit` value. |
| `_new_adjustment_step` | `uint256` | New `adjustment_step` value. |
| `_new_ma_time` | `uint256` | New `ma_time` value, which is time_in_seconds/ln(2).|
| `_new_xcp_ma_time` | `uint256` | New ma time for xcp oracles. |
```vyper
event NewParameters:
mid_fee: uint256
out_fee: uint256
fee_gamma: uint256
allowed_extra_profit: uint256
adjustment_step: uint256
ma_time: uint256
xcp_ma_time: uint256
@external
@nonreentrant('lock')
def apply_new_parameters(
_new_mid_fee: uint256,
_new_out_fee: uint256,
_new_fee_gamma: uint256,
_new_allowed_extra_profit: uint256,
_new_adjustment_step: uint256,
_new_ma_time: uint256,
_new_xcp_ma_time: uint256,
):
"""
@notice Commit new parameters.
@dev Only accessible by factory admin.
@param _new_mid_fee The new mid fee.
@param _new_out_fee The new out fee.
@param _new_fee_gamma The new fee gamma.
@param _new_allowed_extra_profit The new allowed extra profit.
@param _new_adjustment_step The new adjustment step.
@param _new_ma_time The new ma time. ma_time is time_in_seconds/ln(2).
@param _new_xcp_ma_time The new ma time for xcp oracle.
"""
assert msg.sender == factory.admin() # dev: only owner
# ----------------------------- Set fee params ---------------------------
new_mid_fee: uint256 = _new_mid_fee
new_out_fee: uint256 = _new_out_fee
new_fee_gamma: uint256 = _new_fee_gamma
current_fee_params: uint256[3] = self._unpack_3(self.packed_fee_params)
if new_out_fee < MAX_FEE + 1:
assert new_out_fee > MIN_FEE - 1 # dev: fee is out of range
else:
new_out_fee = current_fee_params[1]
if new_mid_fee > MAX_FEE:
new_mid_fee = current_fee_params[0]
assert new_mid_fee <= new_out_fee # dev: mid-fee is too high
if new_fee_gamma < 10**18:
assert new_fee_gamma > 0 # dev: fee_gamma out of range [1 .. 10**18]
else:
new_fee_gamma = current_fee_params[2]
self.packed_fee_params = self._pack_3([new_mid_fee, new_out_fee, new_fee_gamma])
# ----------------- Set liquidity rebalancing parameters -----------------
new_allowed_extra_profit: uint256 = _new_allowed_extra_profit
new_adjustment_step: uint256 = _new_adjustment_step
new_ma_time: uint256 = _new_ma_time
current_rebalancing_params: uint256[3] = self._unpack_3(self.packed_rebalancing_params)
if new_allowed_extra_profit > 10**18:
new_allowed_extra_profit = current_rebalancing_params[0]
if new_adjustment_step > 10**18:
new_adjustment_step = current_rebalancing_params[1]
if new_ma_time < 872542: # <----- Calculated as: 7 * 24 * 60 * 60 / ln(2)
assert new_ma_time > 86 # dev: MA time should be longer than 60/ln(2)
else:
new_ma_time = current_rebalancing_params[2]
self.packed_rebalancing_params = self._pack_3(
[new_allowed_extra_profit, new_adjustment_step, new_ma_time]
)
# Set xcp oracle moving average window time:
new_xcp_ma_time: uint256 = _new_xcp_ma_time
if new_xcp_ma_time < 872542:
assert new_xcp_ma_time > 86 # dev: xcp MA time should be longer than 60/ln(2)
else:
new_xcp_ma_time = self.xcp_ma_time
self.xcp_ma_time = new_xcp_ma_time
# ---------------------------------- LOG ---------------------------------
log NewParameters(
new_mid_fee,
new_out_fee,
new_fee_gamma,
new_allowed_extra_profit,
new_adjustment_step,
new_ma_time,
_new_xcp_ma_time,
)
```
```shell
>>> soon
```
::::
## Contract Info Methods
### `initial_A_gamma`
::::description[`TwoCrypto.initial_A_gamma -> uint256: view`]
Getter for the initial A/gamma.
Returns: A/gamma (`uint256`).
```vyper
initial_A_gamma: public(uint256)
```
```shell
>>> soon
```
::::
### `initial_A_gamma_time`
::::description[`TwoCrypto.initial_A_gamma_time -> uint256: view`]
Getter for the initial A/gamma time.
Returns: A/gamma time (`uint256`).
```vyper
initial_A_gamma_time: public(uint256)
```
```shell
>>> soon
```
::::
### `future_A_gamma`
::::description[`TwoCrypto.future_A_gamma -> uint256: view`]
Getter for the future A/gamma.
Returns: future A/gamma (`uint256`).
```vyper
future_A_gamma: public(uint256)
```
```shell
>>> soon
```
::::
### `future_A_gamma_time`
::::description[`TwoCrypto.future_A_gamma_time -> uint256: view`]
:::info
This value is initially set to 0 (default) when the pool is first deployed. It only gets populated by `block.timestamp + future_time` in the `ramp_A_gamma` function when the ramping process is initiated. After ramping is finished (i.e., `self.future_A_gamma_time < block.timestamp`), the variable is left as is and not set to 0.
:::
Getter for the future A/gamma time. This is the timestamp when the ramping process is finished.
Returns: future A/gamma time (`uint256`).
```vyper
future_A_gamma_time: public(uint256) # <------ Time when ramping is finished.
# This value is 0 (default) when pool is first deployed, and only gets
# populated by block.timestamp + future_time in `ramp_A_gamma` when the
# ramping process is initiated. After ramping is finished
# (i.e. self.future_A_gamma_time < block.timestamp), the variable is left
# and not set to 0.
```
```shell
>>> soon
```
::::
---
## Twocrypto-NG Oracles
## Price Oracle
*Twocrypto-NG pools contain the following built-in oracles:*
An exponential moving-average (EMA) price oracle with a periodicity determined by `ma_time`. It returns the price of the coin at index 1 with regard to the coin at index 0 in the pool.
An exponential moving-average (EMA) oracle value of the estimated TVL in the pool with a periodicity determined by `xcp_ma_time`.
:::example[Example: Price Oracle for CVG/ETH]
The [`CVG/ETH`](https://etherscan.io/address/0x004c167d27ada24305b76d80762997fa6eb8d9b2) pool consists of `CVG <> wETH`.
Because `wETH` is `coin[0]`, the price of `CVG` is returned with regard to `wETH`.
```shell
>>> price_oracle() = 74644221911389
0.000074644221911389 # price of CVG w.r.t wETH
```
*In order to get the reverse EMA (e.g. price of `wETH` with regard to `CVG`):*
$\frac{10^{36}}{\text{price\_oracle()}} = 1.3396884e+22$
:::
---
*The AMM implementation uses several private variables to pack and store values, which are used for calculating the EMA oracles.*
```vyper
@internal
@pure
def _pack_3(x: uint256[3]) -> uint256:
"""
@notice Packs 3 integers with values <= 10**18 into a uint256
@param x The uint256[3] to pack
@return uint256 Integer with packed values
"""
return (x[0] << 128) | (x[1] << 64) | x[2]
@pure
@internal
def _pack_2(p1: uint256, p2: uint256) -> uint256:
return p1 | (p2 << 128)
@internal
@pure
def _unpack_3(_packed: uint256) -> uint256[3]:
"""
@notice Unpacks a uint256 into 3 integers (values must be <= 10**18)
@param val The uint256 to unpack
@return uint256[3] A list of length 3 with unpacked integers
"""
return [
(_packed >> 128) & 18446744073709551615,
(_packed >> 64) & 18446744073709551615,
_packed & 18446744073709551615,
]
@pure
@internal
def _unpack_2(packed: uint256) -> uint256[2]:
return [packed & (2**128 - 1), packed >> 128]
```
| Variable | Description |
| -------- | ----------- |
| `block.timestamp` | Timestamp of the block. Since all transactions within a block share the same timestamp, EMA oracles can only be updated once per block. |
| `last_prices_timestamp` | Timestamp when the EMA oracle was last updated. |
| `ma_time` | Time window for the moving-average oracle. |
| `last_prices` | Last stored spot price of the coin to calculate the price oracle for. |
| `price_scale` | Price scale value of the coin to calculate the price oracle for. |
| `price_oracle` | Price oracle value of the coin to calculate the price oracle for. |
| `alpha` | Weighting multiplier that adjusts the impact of the latest spot value versus the previous EMA in the new EMA calculation. |
### `price_oracle`
::::description[`CurveTwocryptoOptimized.price_oracle() -> uint256: view`]
:::danger[Oracle Manipulation Prevention]
The state price that goes into the EMA is capped with `2 x price_scale` to prevent oracle manipulation.
:::
Getter for the oracle price of the coin at `index 1` with regard to the coin at `index 0`. The price oracle is an exponential moving-average, with a periodicity determined by `ma_time`. The aggregated prices are cached state prices (dy/dx) calculated AFTER the latest trade.
$$\alpha = e^{\text{power}}$$
$$\text{power} = -\frac{(\text{block.timestamp} - \text{last\_prices\_timestamp}) \times 10^{18}}{\text{ma\_time}}$$
$$\text{EMA} = \frac{\min(\text{last\_prices}, 2 \times \text{price\_scale}) \times (10^{18} - \alpha) + \text{price\_oracle} \times \alpha}{10^{18}}$$
Returns: ema oracle price of coin at index 1 w.r.t coin at index 0 (`uint256`).
```vyper
@external
@view
@nonreentrant("lock")
def price_oracle() -> uint256:
"""
@notice Returns the oracle price of the coin at index `k` w.r.t the coin
at index 0.
@dev The oracle is an exponential moving average, with a periodicity
determined by `self.ma_time`. The aggregated prices are cached state
prices (dy/dx) calculated AFTER the latest trade.
@return uint256 Price oracle value of kth coin.
"""
return self.internal_price_oracle()
@internal
@view
def internal_price_oracle() -> uint256:
"""
@notice Returns the oracle price of the coin at index `k` w.r.t the coin
at index 0.
@dev The oracle is an exponential moving average, with a periodicity
determined by `self.ma_time`. The aggregated prices are cached state
prices (dy/dx) calculated AFTER the latest trade.
@param k The index of the coin.
@return uint256 Price oracle value of kth coin.
"""
price_oracle: uint256 = self.cached_price_oracle
price_scale: uint256 = self.cached_price_scale
last_prices_timestamp: uint256 = self.last_timestamp
if last_prices_timestamp < block.timestamp: # <------------ Update moving
# average if needed.
last_prices: uint256 = self.last_prices
ma_time: uint256 = self._unpack_3(self.packed_rebalancing_params)[2]
alpha: uint256 = MATH.wad_exp(
-convert(
unsafe_sub(block.timestamp, last_prices_timestamp) * 10**18 / ma_time,
int256,
)
)
# ---- We cap state price that goes into the EMA with 2 x price_scale.
return (
min(last_prices, 2 * price_scale) * (10**18 - alpha) +
price_oracle * alpha
) / 10**18
return price_oracle
```
```vyper
@external
@pure
def wad_exp(x: int256) -> int256:
"""
@dev Calculates the natural exponential function of a signed integer with
a precision of 1e18.
@notice Note that this function consumes about 810 gas units. The implementation
is inspired by Remco Bloemen's implementation under the MIT license here:
https://xn--2-umb.com/22/exp-ln.
@param x The 32-byte variable.
@return int256 The 32-byte calculation result.
"""
value: int256 = x
# If the result is `< 0.5`, we return zero. This happens when we have the following:
# "x <= floor(log(0.5e18) * 1e18) ~ -42e18".
if (x <= -42_139_678_854_452_767_551):
return empty(int256)
# When the result is "> (2 **255 - 1) / 1e18" we cannot represent it as a signed integer.
# This happens when "x >= floor(log((2 **255 - 1) / 1e18) * 1e18) ~ 135".
assert x < 135_305_999_368_893_231_589, "Math: wad_exp overflow"
# `x` is now in the range "(-42, 136) * 1e18". Convert to "(-42, 136) * 2 **96" for higher
# intermediate precision and a binary base. This base conversion is a multiplication with
# "1e18 / 2 **96 = 5 **18 / 2 **78".
value = unsafe_div(x << 78, 5 **18)
# Reduce the range of `x` to "(-½ ln 2, ½ ln 2) * 2 **96" by factoring out powers of two
# so that "exp(x) = exp(x') * 2 **k", where `k` is a signer integer. Solving this gives
# "k = round(x / log(2))" and "x' = x - k * log(2)". Thus, `k` is in the range "[-61, 195]".
k: int256 = unsafe_add(unsafe_div(value << 96, 54_916_777_467_707_473_351_141_471_128), 2 **95) >> 96
value = unsafe_sub(value, unsafe_mul(k, 54_916_777_467_707_473_351_141_471_128))
# Evaluate using a "(6, 7)"-term rational approximation. Since `p` is monic,
# we will multiply by a scaling factor later.
y: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 1_346_386_616_545_796_478_920_950_773_328), value) >> 96, 57_155_421_227_552_351_082_224_309_758_442)
p: int256 = unsafe_add(unsafe_mul(unsafe_add(unsafe_mul(unsafe_sub(unsafe_add(y, value), 94_201_549_194_550_492_254_356_042_504_812), y) >> 96,\
28_719_021_644_029_726_153_956_944_680_412_240), value), 4_385_272_521_454_847_904_659_076_985_693_276 << 96)
# We leave `p` in the "2 **192" base so that we do not have to scale it up
# again for the division.
q: int256 = unsafe_add(unsafe_mul(unsafe_sub(value, 2_855_989_394_907_223_263_936_484_059_900), value) >> 96, 50_020_603_652_535_783_019_961_831_881_945)
q = unsafe_sub(unsafe_mul(q, value) >> 96, 533_845_033_583_426_703_283_633_433_725_380)
q = unsafe_add(unsafe_mul(q, value) >> 96, 3_604_857_256_930_695_427_073_651_918_091_429)
q = unsafe_sub(unsafe_mul(q, value) >> 96, 14_423_608_567_350_463_180_887_372_962_807_573)
q = unsafe_add(unsafe_mul(q, value) >> 96, 26_449_188_498_355_588_339_934_803_723_976_023)
# The polynomial `q` has no zeros in the range because all its roots are complex.
# No scaling is required, as `p` is already "2 **96" too large. Also,
# `r` is in the range "(0.09, 0.25) * 2**96" after the division.
r: int256 = unsafe_div(p, q)
# To finalise the calculation, we have to multiply `r` by:
# - the scale factor "s = ~6.031367120",
# - the factor "2 **k" from the range reduction, and
# - the factor "1e18 / 2 **96" for the base conversion.
# We do this all at once, with an intermediate result in "2**213" base,
# so that the final right shift always gives a positive value.
# Note that to circumvent Vyper's safecast feature for the potentially
# negative parameter value `r`, we first convert `r` to `bytes32` and
# subsequently to `uint256`. Remember that the EVM default behaviour is
# to use two's complement representation to handle signed integers.
return convert(unsafe_mul(convert(convert(r, bytes32), uint256), 3_822_833_074_963_236_453_042_738_258_902_158_003_155_416_615_667) >>\
convert(unsafe_sub(195, k), uint256), int256)
```
```shell
>>> CurveTwocryptoOptimized.price_oracle()
176068711374120 # CVG/ETH price
```
::::
### `xcp_oracle`
::::description[`CurveTwocryptoOptimized.xcp_oracle() -> uint256: view`]
Getter for the oracle value for xcp. The oracle is an exponential moving-average, with a periodicity determined by `xcp_ma_time`.
$$\alpha = e^{\text{power}}$$
$$\text{power} = -\frac{(\text{block.timestamp} - \text{last\_prices\_timestamp}) \times 10^{18}}{\text{xcp\_ma\_time}}$$
$$\text{xcp\_oracle} = \frac{\text{last\_xcp} \times (10^{18} - \alpha) + \text{cached\_xcp\_oracle} \times \alpha}{10^{18}}$$
Returns: xcp ema oracle value (`uint256`).
```vyper
cached_xcp_oracle: uint256 # <----------- EMA of totalSupply * virtual_price.
@external
@view
@nonreentrant("lock")
def xcp_oracle() -> uint256:
"""
@notice Returns the oracle value for xcp.
@dev The oracle is an exponential moving average, with a periodicity
determined by `self.xcp_ma_time`.
`TVL` is xcp, calculated as either:
1. virtual_price * total_supply, OR
2. self.get_xcp(...), OR
3. MATH.geometric_mean(xp)
@return uint256 Oracle value of xcp.
"""
last_prices_timestamp: uint256 = self._unpack_2(self.last_timestamp)[1]
cached_xcp_oracle: uint256 = self.cached_xcp_oracle
if last_prices_timestamp < block.timestamp:
alpha: uint256 = MATH.wad_exp(
-convert(
unsafe_div(
unsafe_sub(block.timestamp, last_prices_timestamp) * 10**18,
self.xcp_ma_time
),
int256,
)
)
return (self.last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha) / 10**18
return cached_xcp_oracle
```
```shell
In [1]: CurveTwocryptoOptimized.xcp_oracle()
Out [1]: 3501656271269889041418
```
::::
---
## Updating Oracles
The AMM has an internal `tweak_price` function that updates `price_oracle`, `xcp_oracle`, and `last_prices`, and conditionally adjusts `price_scale` based on the new invariant and xcp profit. The function includes logic to adjust the `price_scale` if certain conditions are met, such as sufficient profits being made within the pool. This mechanism ensures the pool remains balanced.
The function is called whenever `add_liquidity`, `remove_liquidity_one_coin`, or `_exchange` is called. It is not called when removing liquidity in a balanced manner via `remove_liquidity`, as this function does not alter prices. However, the xCP oracle is updated nonetheless.
To prevent oracle manipulation, `price_oracle` and `xcp_oracle` are only **updated once per block**.
*The function takes the following inputs:*
| Input | Type | Description |
| --------- | ------------------ | ----------------------------------- |
| `A_gamma` | `uint256[2]` | Array of `A` and `gamma` values. |
| `_xp` | `uint256[N_COINS]` | Array of the current coin balances. |
| `new_D` | `uint256` | New `D` value. |
| `K0_preb` | `uint256` | Initial guess for `newton_D`. |
```vyper
@internal
def tweak_price(
A_gamma: uint256[2],
_xp: uint256[N_COINS],
new_D: uint256,
K0_prev: uint256 = 0,
) -> uint256:
"""
@notice Updates price_oracle, last_price and conditionally adjusts
price_scale. This is called whenever there is an unbalanced
liquidity operation: _exchange, add_liquidity, or
remove_liquidity_one_coin.
@dev Contains main liquidity rebalancing logic, by tweaking `price_scale`.
@param A_gamma Array of A and gamma parameters.
@param _xp Array of current balances.
@param new_D New D value.
@param K0_prev Initial guess for `newton_D`.
"""
# ---------------------------- Read storage ------------------------------
price_oracle: uint256 = self.cached_price_oracle
last_prices: uint256 = self.last_prices
price_scale: uint256 = self.cached_price_scale
rebalancing_params: uint256[3] = self._unpack_3(self.packed_rebalancing_params)
# Contains: allowed_extra_profit, adjustment_step, ma_time. -----^
total_supply: uint256 = self.totalSupply
old_xcp_profit: uint256 = self.xcp_profit
old_virtual_price: uint256 = self.virtual_price
# ----------------------- Update Oracles if needed -----------------------
last_timestamp: uint256[2] = self._unpack_2(self.last_timestamp)
alpha: uint256 = 0
if last_timestamp[0] < block.timestamp: # 0th index is for price_oracle.
# The moving average price oracle is calculated using the last_price
# of the trade at the previous block, and the price oracle logged
# before that trade. This can happen only once per block.
# ------------------ Calculate moving average params -----------------
alpha = MATH.wad_exp(
-convert(
unsafe_div(
unsafe_sub(block.timestamp, last_timestamp[0]) * 10**18,
rebalancing_params[2] # <----------------------- ma_time.
),
int256,
)
)
# ---------------------------------------------- Update price oracles.
# ----------------- We cap state price that goes into the EMA with
# 2 x price_scale.
price_oracle = unsafe_div(
min(last_prices, 2 * price_scale) * (10**18 - alpha) +
price_oracle * alpha, # ^-------- Cap spot price into EMA.
10**18
)
self.cached_price_oracle = price_oracle
last_timestamp[0] = block.timestamp
# ----------------------------------------------------- Update xcp oracle.
if last_timestamp[1] < block.timestamp:
cached_xcp_oracle: uint256 = self.cached_xcp_oracle
alpha = MATH.wad_exp(
-convert(
unsafe_div(
unsafe_sub(block.timestamp, last_timestamp[1]) * 10**18,
self.xcp_ma_time # <---------- xcp ma time has is longer.
),
int256,
)
)
self.cached_xcp_oracle = unsafe_div(
self.last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha,
10**18
)
# Pack and store timestamps:
last_timestamp[1] = block.timestamp
self.last_timestamp = self._pack_2(last_timestamp[0], last_timestamp[1])
# `price_oracle` is used further on to calculate its vector distance from
# price_scale. This distance is used to calculate the amount of adjustment
# to be done to the price_scale.
# ------------------------------------------------------------------------
# ------------------ If new_D is set to 0, calculate it ------------------
D_unadjusted: uint256 = new_D
if new_D == 0: # <--------------------------- _exchange sets new_D to 0.
D_unadjusted = MATH.newton_D(A_gamma[0], A_gamma[1], _xp, K0_prev)
# ----------------------- Calculate last_prices --------------------------
self.last_prices = unsafe_div(
MATH.get_p(_xp, D_unadjusted, A_gamma) * price_scale,
10**18
)
# ---------- Update profit numbers without price adjustment first --------
xp: uint256[N_COINS] = [
unsafe_div(D_unadjusted, N_COINS),
D_unadjusted * PRECISION / (N_COINS * price_scale) # <------ safediv.
] # with price_scale.
xcp_profit: uint256 = 10**18
virtual_price: uint256 = 10**18
if old_virtual_price > 0:
xcp: uint256 = isqrt(xp[0] * xp[1])
virtual_price = 10**18 * xcp / total_supply
xcp_profit = unsafe_div(
old_xcp_profit * virtual_price,
old_virtual_price
) # <---------------- Safu to do unsafe_div as old_virtual_price > 0.
# If A and gamma are not undergoing ramps (t < block.timestamp),
# ensure new virtual_price is not less than old virtual_price,
# else the pool suffers a loss.
if self.future_A_gamma_time < block.timestamp:
assert virtual_price > old_virtual_price, "Loss"
# -------------------------- Cache last_xcp --------------------------
self.last_xcp = xcp # geometric_mean(D * price_scale)
self.xcp_profit = xcp_profit
# ------------ Rebalance liquidity if there's enough profits to adjust it:
if virtual_price * 2 - 10**18 > xcp_profit + 2 * rebalancing_params[0]:
# allowed_extra_profit --------^
# ------------------- Get adjustment step ----------------------------
# Calculate the vector distance between price_scale and
# price_oracle.
norm: uint256 = unsafe_div(
unsafe_mul(price_oracle, 10**18), price_scale
)
if norm > 10**18:
norm = unsafe_sub(norm, 10**18)
else:
norm = unsafe_sub(10**18, norm)
adjustment_step: uint256 = max(
rebalancing_params[1], unsafe_div(norm, 5)
) # ^------------------------------------- adjustment_step.
if norm > adjustment_step: # <---------- We only adjust prices if the
# vector distance between price_oracle and price_scale is
# large enough. This check ensures that no rebalancing
# occurs if the distance is low i.e. the pool prices are
# pegged to the oracle prices.
# ------------------------------------- Calculate new price scale.
p_new: uint256 = unsafe_div(
price_scale * unsafe_sub(norm, adjustment_step) +
adjustment_step * price_oracle,
norm
) # <---- norm is non-zero and gt adjustment_step; unsafe = safe.
# ---------------- Update stale xp (using price_scale) with p_new.
xp = [
_xp[0],
unsafe_div(_xp[1] * p_new, price_scale)
]
# ------------------------------------------ Update D with new xp.
D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)
for k in range(N_COINS):
frac: uint256 = xp[k] * 10**18 / D # <----- Check validity of
assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # p_new.
# ------------------------------------- Convert xp to real prices.
xp = [
unsafe_div(D, N_COINS),
D * PRECISION / (N_COINS * p_new)
]
# ---------- Calculate new virtual_price using new xp and D. Reuse
# `old_virtual_price` (but it has new virtual_price).
old_virtual_price = unsafe_div(
10**18 * isqrt(xp[0] * xp[1]), total_supply
) # <----- unsafe_div because we did safediv before (if vp>1e18)
# ---------------------------- Proceed if we've got enough profit.
if (
old_virtual_price > 10**18 and
2 * old_virtual_price - 10**18 > xcp_profit
):
self.D = D
self.virtual_price = old_virtual_price
self.cached_price_scale = p_new
return p_new
# --------- price_scale was not adjusted. Update the profit counter and D.
self.D = D_unadjusted
self.virtual_price = virtual_price
return price_scale
```
---
## Other Methods
### `last_prices`
::::description[`CurveTwocryptoOptimized.last_prices() -> uint256: view`]
Getter for the last price of the coin at index 1 with regard to the coin at index 0. This variable is used to calculate the moving average price oracle.
Returns: last price (`uint256`).
```vyper
last_prices: public(uint256)
@internal
def tweak_price(
A_gamma: uint256[2],
_xp: uint256[N_COINS],
new_D: uint256,
K0_prev: uint256 = 0,
) -> uint256:
"""
@notice Updates price_oracle, last_price and conditionally adjusts
price_scale. This is called whenever there is an unbalanced
liquidity operation: _exchange, add_liquidity, or
remove_liquidity_one_coin.
@dev Contains main liquidity rebalancing logic, by tweaking `price_scale`.
@param A_gamma Array of A and gamma parameters.
@param _xp Array of current balances.
@param new_D New D value.
@param K0_prev Initial guess for `newton_D`.
"""
...
# ----------------------- Calculate last_prices --------------------------
self.last_prices = unsafe_div(
MATH.get_p(_xp, D_unadjusted, A_gamma) * price_scale,
10**18
)
...
```
```vyper
@external
@view
def get_p(
_xp: uint256[N_COINS], _D: uint256, _A_gamma: uint256[N_COINS]
) -> uint256:
"""
@notice Calculates dx/dy.
@dev Output needs to be multiplied with price_scale to get the actual value.
@param _xp Balances of the pool.
@param _D Current value of D.
@param _A_gamma Amplification coefficient and gamma.
"""
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe D values
# K0 = P * N**N / D**N.
# K0 is dimensionless and has 10**36 precision:
K0: uint256 = unsafe_div(
unsafe_div(4 * _xp[0] * _xp[1], _D) * 10**36,
_D
)
# GK0 is in 10**36 precision and is dimensionless.
# GK0 = (
# 2 * _K0 * _K0 / 10**36 * _K0 / 10**36
# + (gamma + 10**18)**2
# - (_K0 * _K0 / 10**36 * (2 * gamma + 3 * 10**18) / 10**18)
# )
# GK0 is always positive. So the following should never revert:
GK0: uint256 = (
unsafe_div(unsafe_div(2 * K0 * K0, 10**36) * K0, 10**36)
+ pow_mod256(unsafe_add(_A_gamma[1], 10**18), 2)
- unsafe_div(
unsafe_div(pow_mod256(K0, 2), 10**36) * unsafe_add(unsafe_mul(2, _A_gamma[1]), 3 * 10**18),
10**18
)
)
# NNAG2 = N**N * A * gamma**2
NNAG2: uint256 = unsafe_div(unsafe_mul(_A_gamma[0], pow_mod256(_A_gamma[1], 2)), A_MULTIPLIER)
# denominator = (GK0 + NNAG2 * x / D * _K0 / 10**36)
denominator: uint256 = (GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[0], _D) * K0, 10**36) )
# p_xy = x * (GK0 + NNAG2 * y / D * K0 / 10**36) / y * 10**18 / denominator
# p is in 10**18 precision.
return unsafe_div(
_xp[0] * ( GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[1], _D) * K0, 10**36) ) / _xp[1] * 10**18,
denominator
)
```
```shell
>>> CurveTwocryptoOptimized.last_prices()
'74644221911388'
```
::::
### `last_timestamp`
::::description[`CurveTwocryptoOptimized.last_timestamp() -> uint256: view`]
Getter for the last timestamps when price and xcp oracles were updated. Both timestamps are packed into a single variable. The lower 128 bits represent the timestamp of the price update, the upper 128 bits the timestamps of the xcp update. The distinction between price and xcp is neccessary because these values are not always updated in parallel. Usually they are, but when liquidity is removed in a balanced matter, the price oracle is not updated but the xcp one is.
Returns: packed value of the timestamps of the most recent updated of the price and xcp oracle (`uint256`).
```vyper
last_timestamp: public(uint256) # idx 0 is for prices, idx 1 is for xcp.
```
```shell
>>> CurveTwocryptoOptimized.last_timestamp()
585060874787625947552086540639603571285491911031
# unpacking
>>> 585060874787625947552086540639603571285491911031 & (2**128 - 1)
1719339383
>>> 585060874787625947552086540639603571285491911031 >> 128
1719339383
```
::::
### `ma_time`
::::description[`CurveTwocryptoOptimized.ma_time() -> uint256: view`]
Getter for the moving average time for `price_oracle` denominated in seconds. This variable can be changed using the `apply_new_parameters` method.
Returns: moving average time (`uint256`).
```vyper
packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing
# parameters allowed_extra_profit, adjustment_step, and ma_time.
@view
@external
def ma_time() -> uint256:
"""
@notice Returns the current moving average time in seconds
@dev To get time in seconds, the parameter is multipled by ln(2)
One can expect off-by-one errors here.
@return uint256 ma_time value.
"""
return self._unpack_3(self.packed_rebalancing_params)[2] * 694 / 1000
```
```shell
>>> CurveTwocryptoOptimized.ma_time()
601
```
::::
### `xcp_ma_time`
::::description[`CurveTwocryptoOptimized.xcp_ma_time() -> uint256: view`]
Getter for the moving-average periodicity for `price_oracle` denominated in seconds. This variable can be changed using the `apply_new_parameters` method.
Returns: ma time (`uint256`).
```vyper
xcp_ma_time: public(uint256)
@external
def __init__(
_name: String[64],
_symbol: String[32],
_coins: address[N_COINS],
_math: address,
_salt: bytes32,
packed_precisions: uint256,
packed_gamma_A: uint256,
packed_fee_params: uint256,
packed_rebalancing_params: uint256,
initial_price: uint256,
):
...
self.xcp_ma_time = 62324 # <--------- 12 hours default on contract start.
...
```
```shell
>>> CurveTwocryptoOptimized.xcp_ma_time()
62324
```
::::
### `lp_price`
::::description[`CurveTwocryptoOptimized.lp_price() -> uint256: view`]
Function to calculate the price of the LP token with regard to the coin at `index 0` in the pool. The value is calculate the following:
$$\text{lp\_price} = \frac{2 \times \text{virtual\_price} \times \sqrt{\text{price\_oracle} \times 10^{18}}}{10^{18}}$$
Returns: LP token price (`uint256`).
```vyper
@external
@view
@nonreentrant("lock")
def lp_price() -> uint256:
"""
@notice Calculates the current price of the LP token w.r.t coin at the 0th index
@return uint256 LP price.
"""
return 2 * self.virtual_price * isqrt(self.internal_price_oracle() * 10**18) / 10**18
@internal
@view
def internal_price_oracle() -> uint256:
"""
@notice Returns the oracle price of the coin at index `k` w.r.t the coin
at index 0.
@dev The oracle is an exponential moving average, with a periodicity
determined by `self.ma_time`. The aggregated prices are cached state
prices (dy/dx) calculated AFTER the latest trade.
@param k The index of the coin.
@return uint256 Price oracle value of kth coin.
"""
price_oracle: uint256 = self.cached_price_oracle
price_scale: uint256 = self.cached_price_scale
last_prices_timestamp: uint256 = self._unpack_2(self.last_timestamp)[0]
if last_prices_timestamp < block.timestamp: # <------------ Update moving
# average if needed.
last_prices: uint256 = self.last_prices
ma_time: uint256 = self._unpack_3(self.packed_rebalancing_params)[2]
alpha: uint256 = MATH.wad_exp(
-convert(
unsafe_sub(block.timestamp, last_prices_timestamp) * 10**18 / ma_time,
int256,
)
)
# ---- We cap state price that goes into the EMA with 2 x price_scale.
return (
min(last_prices, 2 * price_scale) * (10**18 - alpha) +
price_oracle * alpha
) / 10**18
return price_oracle
```
```shell
>>> CurveTwocryptoOptimized.lp_price()
26545349102641443 # lp token price in wETH
```
::::
### `virtual_price`
::::description[`CurveTwocryptoOptimized.virtual_price() -> uint256: view`]
:::warning[`get_virtual_price` ≠ `virtual_price`]
`get_virtual_price` should not be confused with `virtual_price`, which is a cached virtual price.
:::
Getter for the cached virtual price. This variable provides a fast read by accessing the cached value instead of recalculating it.
Returns: cached virtual price (`uint256`).
```vyper
virtual_price: public(uint256) # <------ Cached (fast to read) virtual price.
# The cached `virtual_price` is also used internally.
```
```shell
>>> CurveTwocryptoOptimized.virtual_price()
1000270251060292804
```
::::
### `get_virtual_price`
::::description[`CurveTwocryptoOptimized.get_virtual_price() -> uint256: view`]
:::warning[`get_virtual_price` ≠ `virtual_price`]
`get_virtual_price` should not be confused with `virtual_price`, which is a cached virtual price.
:::
Function to dynamically calculate the current virtual price of the pool's LP token. It essentially calculates the virtual price based on the current state of the pool (`D` and `cached_price_scale`) and the total supply of LP tokens.
Returns: virtual price (`uint256`).
```vyper
@external
@view
@nonreentrant("lock")
def get_virtual_price() -> uint256:
"""
@notice Calculates the current virtual price of the pool LP token.
@dev Not to be confused with `self.virtual_price` which is a cached
virtual price.
@return uint256 Virtual Price.
"""
return 10**18 * self.get_xcp(self.D, self.cached_price_scale) / self.totalSupply
@internal
@pure
def get_xcp(D: uint256, price_scale: uint256) -> uint256:
x: uint256[N_COINS] = [
unsafe_div(D, N_COINS),
D * PRECISION / (price_scale * N_COINS)
]
return isqrt(x[0] * x[1]) # <------------------- Geometric Mean.
```
```shell
>>> CurveTwocryptoOptimized.get_virtual_price()
1000270251060292804
```
::::
---
## Pools: Overview
**New Features Over the Regular Two-Coin CryptoSwap Implementation:**- New fee claiming approach
- [**`exchange_received`**](#exchange_received)
- Overall gas optimizations
## Fee Claiming
Admin fees of a Curve pool are usually claimed through an external function, callable by anyone. **Twocrypto-NG does not have any external function to directly claim fees**. Admin fees are claimed through an internal function, which is called when liquidity is removed single-sidedly via the `_remove_liquidity_one_coin` function, and then sent to the fee receiver determined within the Factory contract.
```vyper
@internal
def _claim_admin_fees():
"""
@notice Claims admin fees and sends it to fee_receiver set in the factory.
@dev Functionally similar to:
1. Calculating admin's share of fees,
2. minting LP tokens,
3. admin claims underlying tokens via remove_liquidity.
"""
# --------------------- Check if fees can be claimed ---------------------
# Disable fee claiming if:
# 1. If time passed since last fee claim is less than
# MIN_ADMIN_FEE_CLAIM_INTERVAL.
# 2. Pool parameters are being ramped.
last_claim_time: uint256 = self.last_admin_fee_claim_timestamp
if (
unsafe_sub(block.timestamp, last_claim_time) < MIN_ADMIN_FEE_CLAIM_INTERVAL or
self.future_A_gamma_time > block.timestamp
):
return
xcp_profit: uint256 = self.xcp_profit # <---------- Current pool profits.
xcp_profit_a: uint256 = self.xcp_profit_a # <- Profits at previous claim.
current_lp_token_supply: uint256 = self.totalSupply
# Do not claim admin fees if:
# 1. insufficient profits accrued since last claim, and
# 2. there are less than 10**18 (or 1 unit of) lp tokens, else it can lead
# to manipulated virtual prices.
if xcp_profit <= xcp_profit_a or current_lp_token_supply < 10**18:
return
# ---------- Conditions met to claim admin fees: compute state. ----------
A_gamma: uint256[2] = self._A_gamma()
D: uint256 = self.D
vprice: uint256 = self.virtual_price
price_scale: uint256 = self.cached_price_scale
fee_receiver: address = factory.fee_receiver()
balances: uint256[N_COINS] = self.balances
# Admin fees are calculated as follows.
# 1. Calculate accrued profit since last claim. `xcp_profit`
# is the current profits. `xcp_profit_a` is the profits
# at the previous claim.
# 2. Take out admin's share, which is hardcoded at 5 * 10**9.
# (50% => half of 100% => 10**10 / 2 => 5 * 10**9).
# 3. Since half of the profits go to rebalancing the pool, we
# are left with half; so divide by 2.
fees: uint256 = unsafe_div(
unsafe_sub(xcp_profit, xcp_profit_a) * ADMIN_FEE, 2 * 10**10
)
# ------------------------------ Claim admin fees by minting admin's share
# of the pool in LP tokens.
# This is the admin fee tokens claimed in self.add_liquidity. We add it to
# the LP token share that the admin needs to claim:
admin_share: uint256 = self.admin_lp_virtual_balance
frac: uint256 = 0
if fee_receiver != empty(address) and fees > 0:
# -------------------------------- Calculate admin share to be minted.
frac = vprice * 10**18 / (vprice - fees) - 10**18
admin_share += current_lp_token_supply * frac / 10**18
# ------ Subtract fees from profits that will be used for rebalancing.
xcp_profit -= fees * 2
# ------------------- Recalculate virtual_price following admin fee claim.
total_supply_including_admin_share: uint256 = (
current_lp_token_supply + admin_share
)
vprice = (
10**18 * self.get_xcp(D, price_scale) /
total_supply_including_admin_share
)
# Do not claim fees if doing so causes virtual price to drop below 10**18.
if vprice < 10**18:
return
# ---------------------------- Update State ------------------------------
# Set admin virtual LP balances to zero because we claimed:
self.admin_lp_virtual_balance = 0
self.xcp_profit = xcp_profit
self.last_admin_fee_claim_timestamp = block.timestamp
# Since we reduce balances: virtual price goes down
self.virtual_price = vprice
# Adjust D after admin seemingly removes liquidity
self.D = D - unsafe_div(D * admin_share, total_supply_including_admin_share)
if xcp_profit > xcp_profit_a:
self.xcp_profit_a = xcp_profit # <-------- Cache last claimed profit.
# --------------------------- Handle Transfers ---------------------------
admin_tokens: uint256[N_COINS] = empty(uint256[N_COINS])
if admin_share > 0:
for i in range(N_COINS):
admin_tokens[i] = (
balances[i] * admin_share /
total_supply_including_admin_share
)
# _transfer_out tokens to admin and update self.balances. State
# update to self.balances occurs before external contract calls:
self._transfer_out(i, admin_tokens[i], fee_receiver)
log ClaimAdminFee(fee_receiver, admin_tokens)
```
## `exchange_received`
This new function **allows the exchange of tokens without actually transfering tokens in**, as the exchange is based on the change of the coins balances within the pool (see code below).
Users of this method are dex aggregators, arbitrageurs, or other users who **do not wish to grant approvals to the contract**. They can instead send tokens directly to the contract and call **`exchange_received()`**.
```vyper
@internal
def _transfer_in(
_coin_idx: uint256,
_dx: uint256,
sender: address,
expect_optimistic_transfer: bool,
) -> uint256:
"""
@notice Transfers `_coin` from `sender` to `self` and calls `callback_sig`
if it is not empty.
@params _coin_idx uint256 Index of the coin to transfer in.
@params dx amount of `_coin` to transfer into the pool.
@params sender address to transfer `_coin` from.
@params expect_optimistic_transfer bool True if pool expects user to transfer.
This is only enabled for exchange_received.
@return The amount of tokens received.
"""
coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self)
if expect_optimistic_transfer: # Only enabled in exchange_received:
# it expects the caller of exchange_received to have sent tokens to
# the pool before calling this method.
# If someone donates extra tokens to the contract: do not acknowledge.
# We only want to know if there are dx amount of tokens. Anything extra,
# we ignore. This is why we need to check if received_amounts (which
# accounts for coin balances of the contract) is atleast dx.
# If we checked for received_amounts == dx, an extra transfer without a
# call to exchange_received will break the method.
dx: uint256 = coin_balance - self.balances[_coin_idx]
assert dx >= _dx # dev: user didn't give us coins
# Adjust balances
self.balances[_coin_idx] += dx
return dx
# ----------------------------------------------- ERC20 transferFrom flow.
# EXTERNAL CALL
assert ERC20(coins[_coin_idx]).transferFrom(
sender,
self,
_dx,
default_return_value=True
)
dx: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) - coin_balance
self.balances[_coin_idx] += dx
return dx
```
---
:::example
Lets say a user wants to swap **`GOV-TOKEN<>USDC`**through an aggregator. For simplicity we assume, **`GOV-TOKEN<>USDT`**exchange is done via a uniswap pool, **`USDT<>USDC`**via a Curve pool.
``` mermaid
graph LR
u([USER]) --- p1[(UNISWAP)]
p1 -->|"3. transfer out/in"| p2[(CURVE)]
u -..-> |1. approve and transfer| a([AGGREGATOR])
a ==> |"2. exchange"| p1
a -.-|"4. exchange_received"| p2
p2 --> |5. transfer dy out| u
linkStyle 0 stroke-width:0, fill:none;
```
:::
1. User gives approval the `AGGREGATOR`, which then transfers tokens into the aggregator contract
2. Aggregator exchanges `GOV-TOKEN` for `USDT` using Uniswap
3. Transfers the `USDT` directly from Uniswap into the Curve pool
4. Perform a swap on the Curve pool (`USDT<>USDC`) via **`exchange_received`**5. Transfer `USDC` to the user
:::info
This method saves aggregators one redundant ERC-20 transfer and eliminates the need to grant approval to a curve pool. Without this function, the aggregator would have to conduct an additional transaction, transferring USDT from the Uniswap pool to their aggregator contract after the exchange, and then sending it to the Curve pool for another exchange (USDT<>USDC).
However, with this method in place, the aggregator can transfer the output tokens directly into the next pool and perform an exchange.
:::
---
## CurveTwocryptoOptimized
A Twocrypto-NG pool consists of **two non-pegged assets**. The LP token is an ERC-20 token integrated directly into the liquidity pool.
:::vyper[`CurveTwocryptoOptimized.vy`]
The source code for the `CurveTwocryptoOptimized.vy` contract can be found on [GitHub](https://github.com/curvefi/twocrypto-ng/blob/main/contracts/main/CurveTwocryptoOptimized.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.3.10`.
This is a **blueprint contract** — individual pools are deployed via the [Factory](../../factory/twocrypto-ng/overview.md). Pool and LP token share the same address. Full list of all deployments can be found [here](../../../deployments.md).
:::
:::info
The token has the regular ERC-20 methods, which will not be further documented.
:::
In Twocrypto-NG pools, price scaling and fee **parameters are bundled and stored as a single unsigned integer**. This consolidation reduces storage read and write operations, leading to more cost-efficient calls.
This internal function packs two or three integers into a single uint256.
```vyper
@pure
@internal
def _pack_2(p1: uint256, p2: uint256) -> uint256:
return p1 | (p2 << 128)
@internal
@pure
def _pack_3(x: uint256[3]) -> uint256:
"""
@notice Packs 3 integers with values <= 10**18 into a uint256
@param x The uint256[3] to pack
@return uint256 Integer with packed values
"""
return (x[0] << 128) | (x[1] << 64) | x[2]
```
This internal function unpacks a single uin256 into two or three integers.
```vyper
@pure
@internal
def _unpack_2(packed: uint256) -> uint256[2]:
return [packed & (2**128 - 1), packed >> 128]
@internal
@pure
def _unpack_3(_packed: uint256) -> uint256[3]:
"""
@notice Unpacks a uint256 into 3 integers (values must be <= 10**18)
@param val The uint256 to unpack
@return uint256[3] A list of length 3 with unpacked integers
"""
return [
(_packed >> 128) & 18446744073709551615,
(_packed >> 64) & 18446744073709551615,
_packed & 18446744073709551615,
]
```
*The AMM contract utilizes two internal functions to transfer coins in and out of the pool e.g. when exchanging tokens or adding/removing liquidity:*
**Token transfer into the AMM:**
Internal function to transfer tokens into the AMM, called by `exchange`, `exchange_received` or `add_liquidity`.
| Input | Type | Description |
| -------------------------- | --------- | ------------------------------------------------------ |
| `_coin_idx` | `int128` | Index of the token to transfer in. |
| `_dx` | `uint256` | Amount to transfer in. |
| `sender` | `address` | Address to transfer coins from. |
| `expect_optimistic_transfer` | `bool` | `True` if the contract expects an optimistic coin transfer. |
**`expect_optimistic_transfer`**is only `True` when using the `exchange_received` function.
```vyper
balances: public(uint256[N_COINS])
@internal
def _transfer_in(
_coin_idx: uint256,
_dx: uint256,
sender: address,
expect_optimistic_transfer: bool,
) -> uint256:
"""
@notice Transfers `_coin` from `sender` to `self` and calls `callback_sig`
if it is not empty.
@params _coin_idx uint256 Index of the coin to transfer in.
@params dx amount of `_coin` to transfer into the pool.
@params sender address to transfer `_coin` from.
@params expect_optimistic_transfer bool True if pool expects user to transfer.
This is only enabled for exchange_received.
@return The amount of tokens received.
"""
coin_balance: uint256 = ERC20(coins[_coin_idx]).balanceOf(self)
if expect_optimistic_transfer: # Only enabled in exchange_received:
# it expects the caller of exchange_received to have sent tokens to
# the pool before calling this method.
# If someone donates extra tokens to the contract: do not acknowledge.
# We only want to know if there are dx amount of tokens. Anything extra,
# we ignore. This is why we need to check if received_amounts (which
# accounts for coin balances of the contract) is atleast dx.
# If we checked for received_amounts == dx, an extra transfer without a
# call to exchange_received will break the method.
dx: uint256 = coin_balance - self.balances[_coin_idx]
assert dx >= _dx # dev: user didn't give us coins
# Adjust balances
self.balances[_coin_idx] += dx
return dx
# ----------------------------------------------- ERC20 transferFrom flow.
# EXTERNAL CALL
assert ERC20(coins[_coin_idx]).transferFrom(
sender,
self,
_dx,
default_return_value=True
)
dx: uint256 = ERC20(coins[_coin_idx]).balanceOf(self) - coin_balance
self.balances[_coin_idx] += dx
return dx
```
**Token transfer out of the AMM:**
Internal function to transfer tokens out of the AMM, called by the `remove_liquidity`, `remove_liquidity_one`, `_claim_admin_fees`, and `_exchange` methods.
| Input | Type | Description |
| ------------ | -------- | ------------------------------------- |
| `_coin_idx` | `int128` | Index of the token to transfer out. |
| `_amount` | `uint256`| Amount to transfer out. |
| `receiver` | `address`| Address to send the tokens to. |
```vyper
balances: public(uint256[N_COINS])
@internal
def _transfer_out(_coin_idx: uint256, _amount: uint256, receiver: address):
"""
@notice Transfer a single token from the pool to receiver.
@dev This function is called by `remove_liquidity` and
`remove_liquidity_one`, `_claim_admin_fees` and `_exchange` methods.
@params _coin_idx uint256 Index of the token to transfer out
@params _amount Amount of token to transfer out
@params receiver Address to send the tokens to
"""
# Adjust balances before handling transfers:
self.balances[_coin_idx] -= _amount
# EXTERNAL CALL
assert ERC20(coins[_coin_idx]).transfer(
receiver,
_amount,
default_return_value=True
)
```
---
## Exchange Methods
*The contract offers two different ways to exchange tokens:*
- A regular `exchange` method.
- A novel `exchange_received` method, which swaps tokens based on the *"internal balances"* of the pool. This method is of great use for aggregators, as it **does not require token approval**of the pool, which eliminates certain smart contract risks and *can* remove one redundant ERC-20 transfer. More [here](../../stableswap-ng/overview.md#exchange_received).
### `exchange`
::::description[`TwoCrypto.exchange(i: uint256, j: uint256, dx: uint256, min_dy: uint256, receiver: address = msg.sender) -> uint256:`]
Function to exchange `dx` amount of coin `i` for coin `j` and receive a minimum amount of `min_dy`. Charged fee at current states is `Pool.fee()`.
| Input | Type | Description |
| ----------- | --------- | ---------------------------------------------------------- |
| `i` | `uint256` | Index value for the input coin. |
| `j` | `uint256` | Index value for the output coin. |
| `dx` | `uint256` | Amount of input coin being swapped in. |
| `min_dy` | `uint256` | Minimum amount of output coin to receive. |
| `receiver` | `address` | Address to send output coin to. Defaults to `msg.sender`. |
Returns: amount of output coin `j` received (`uint256`).
Emits: `TokenExchange`
```vyper
event TokenExchange:
buyer: indexed(address)
sold_id: uint256
tokens_sold: uint256
bought_id: uint256
tokens_bought: uint256
fee: uint256
packed_price_scale: uint256
@external
@nonreentrant("lock")
def exchange(
i: uint256,
j: uint256,
dx: uint256,
min_dy: uint256,
receiver: address = msg.sender
) -> uint256:
"""
@notice Exchange using wrapped native token by default
@param i Index value for the input coin
@param j Index value for the output coin
@param dx Amount of input coin being swapped in
@param min_dy Minimum amount of output coin to receive
@param receiver Address to send the output coin to. Default is msg.sender
@return uint256 Amount of tokens at index j received by the `receiver
"""
# _transfer_in updates self.balances here:
dx_received: uint256 = self._transfer_in(
i,
dx,
msg.sender,
False
)
# No ERC20 token transfers occur here:
out: uint256[3] = self._exchange(
i,
j,
dx_received,
min_dy,
)
# _transfer_out updates self.balances here. Update to state occurs before
# external calls:
self._transfer_out(j, out[0], receiver)
# log:
log TokenExchange(msg.sender, i, dx_received, j, out[0], out[1], out[2])
return out[0]
@internal
def _exchange(
i: uint256,
j: uint256,
dx_received: uint256,
min_dy: uint256,
) -> uint256[3]:
assert i != j # dev: coin index out of range
assert dx_received > 0 # dev: do not exchange 0 coins
A_gamma: uint256[2] = self._A_gamma()
xp: uint256[N_COINS] = self.balances
dy: uint256 = 0
y: uint256 = xp[j]
x0: uint256 = xp[i] - dx_received # old xp[i]
price_scale: uint256 = self.cached_price_scale
xp = [
xp[0] * PRECISIONS[0],
unsafe_div(xp[1] * price_scale * PRECISIONS[1], PRECISION)
]
# ----------- Update invariant if A, gamma are undergoing ramps ---------
t: uint256 = self.future_A_gamma_time
if t > block.timestamp:
x0 *= PRECISIONS[i]
if i > 0:
x0 = unsafe_div(x0 * price_scale, PRECISION)
x1: uint256 = xp[i] # <------------------ Back up old value in xp ...
xp[i] = x0 # |
self.D = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) # |
xp[i] = x1 # <-------------------------------------- ... and restore.
# ----------------------- Calculate dy and fees --------------------------
D: uint256 = self.D
y_out: uint256[2] = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, j)
dy = xp[j] - y_out[0]
xp[j] -= dy
dy -= 1
if j > 0:
dy = dy * PRECISION / price_scale
dy /= PRECISIONS[j]
fee: uint256 = unsafe_div(self._fee(xp) * dy, 10**10)
dy -= fee # <--------------------- Subtract fee from the outgoing amount.
assert dy >= min_dy, "Slippage"
y -= dy
y *= PRECISIONS[j]
if j > 0:
y = unsafe_div(y * price_scale, PRECISION)
xp[j] = y # <------------------------------------------------- Update xp.
# ------ Tweak price_scale with good initial guess for newton_D ----------
price_scale = self.tweak_price(A_gamma, xp, 0, y_out[1])
return [dy, fee, price_scale]
```
```vyper
@external
@view
def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev: uint256 = 0) -> uint256:
"""
Finding the invariant using Newton method.
ANN is higher by the factor A_MULTIPLIER
ANN is already A * N**N
"""
# Safety checks
assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A
assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
# Initial value of invariant D is that for constant-product invariant
x: uint256[N_COINS] = x_unsorted
if x[0] < x[1]:
x = [x_unsorted[1], x_unsorted[0]]
assert x[0] > 10**9 - 1 and x[0] < 10**15 * 10**18 + 1 # dev: unsafe values x[0]
assert unsafe_div(x[1] * 10**18, x[0]) > 10**14 - 1 # dev: unsafe values x[i] (input)
S: uint256 = unsafe_add(x[0], x[1]) # can unsafe add here because we checked x[0] bounds
D: uint256 = 0
if K0_prev == 0:
D = N_COINS * isqrt(unsafe_mul(x[0], x[1]))
else:
# D = isqrt(x[0] * x[1] * 4 / K0_prev * 10**18)
D = isqrt(unsafe_mul(unsafe_div(unsafe_mul(unsafe_mul(4, x[0]), x[1]), K0_prev), 10**18))
if S < D:
D = S
__g1k0: uint256 = gamma + 10**18
diff: uint256 = 0
for i in range(255):
D_prev: uint256 = D
assert D > 0
# Unsafe division by D and D_prev is now safe
# K0: uint256 = 10**18
# for _x in x:
# K0 = K0 * _x * N_COINS / D
# collapsed for 2 coins
K0: uint256 = unsafe_div(unsafe_div((10**18 * N_COINS**2) * x[0], D) * x[1], D)
_g1k0: uint256 = __g1k0
if _g1k0 > K0:
_g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1) # > 0
else:
_g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1) # > 0
# D / (A * N**N) * _g1k0**2 / gamma**2
mul1: uint256 = unsafe_div(unsafe_div(unsafe_div(10**18 * D, gamma) * _g1k0, gamma) * _g1k0 * A_MULTIPLIER, ANN)
# 2*N*K0 / _g1k0
mul2: uint256 = unsafe_div(((2 * 10**18) * N_COINS) * K0, _g1k0)
# calculate neg_fprime. here K0 > 0 is being validated (safediv).
neg_fprime: uint256 = (S + unsafe_div(S * mul2, 10**18)) + mul1 * N_COINS / K0 - unsafe_div(mul2 * D, 10**18)
# D -= f / fprime; neg_fprime safediv being validated
D_plus: uint256 = D * (neg_fprime + S) / neg_fprime
D_minus: uint256 = unsafe_div(D * D, neg_fprime)
if 10**18 > K0:
D_minus += unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(10**18, K0), K0)
else:
D_minus -= unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(K0, 10**18), K0)
if D_plus > D_minus:
D = unsafe_sub(D_plus, D_minus)
else:
D = unsafe_div(unsafe_sub(D_minus, D_plus), 2)
if D > D_prev:
diff = unsafe_sub(D, D_prev)
else:
diff = unsafe_sub(D_prev, D)
if diff * 10**14 < max(10**16, D): # Could reduce precision for gas efficiency here
for _x in x:
frac: uint256 = _x * 10**18 / D
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe values x[i]
return D
raise "Did not converge"
@external
@pure
def get_y(
_ANN: uint256,
_gamma: uint256,
_x: uint256[N_COINS],
_D: uint256,
i: uint256
) -> uint256[2]:
# Safety checks
assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A
assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D
ANN: int256 = convert(_ANN, int256)
gamma: int256 = convert(_gamma, int256)
D: int256 = convert(_D, int256)
x_j: int256 = convert(_x[1 - i], int256)
gamma2: int256 = unsafe_mul(gamma, gamma)
# savediv by x_j done here:
y: int256 = D**2 / (x_j * N_COINS**2)
# K0_i: int256 = (10**18 * N_COINS) * x_j / D
K0_i: int256 = unsafe_div(10**18 * N_COINS * x_j, D)
assert (K0_i > 10**16 * N_COINS - 1) and (K0_i < 10**20 * N_COINS + 1) # dev: unsafe values x[i]
ann_gamma2: int256 = ANN * gamma2
# a = 10**36 / N_COINS**2
a: int256 = 10**32
# b = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14
b: int256 = (
D*ann_gamma2/400000000/x_j
- convert(unsafe_mul(10**32, 3), int256)
- unsafe_mul(unsafe_mul(2, gamma), 10**14)
)
# c = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4
c: int256 = (
unsafe_mul(10**32, convert(3, int256))
+ unsafe_mul(unsafe_mul(4, gamma), 10**14)
+ unsafe_div(gamma2, 10**4)
+ unsafe_div(unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) * x_j, D)
- unsafe_div(unsafe_mul(4, ann_gamma2), 400000000)
)
# d = -(10**18+gamma)**2 / 10**4
d: int256 = -unsafe_div(unsafe_add(10**18, gamma) **2, 10**4)
# delta0: int256 = 3*a*c/b - b
delta0: int256 = 3 * a * c / b - b # safediv by b
# delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1: int256 = 3 * delta0 + b - 27*a**2/b*d/b
divider: int256 = 1
threshold: int256 = min(min(abs(delta0), abs(delta1)), a)
if threshold > 10**48:
divider = 10**30
elif threshold > 10**46:
divider = 10**28
elif threshold > 10**44:
divider = 10**26
elif threshold > 10**42:
divider = 10**24
elif threshold > 10**40:
divider = 10**22
elif threshold > 10**38:
divider = 10**20
elif threshold > 10**36:
divider = 10**18
elif threshold > 10**34:
divider = 10**16
elif threshold > 10**32:
divider = 10**14
elif threshold > 10**30:
divider = 10**12
elif threshold > 10**28:
divider = 10**10
elif threshold > 10**26:
divider = 10**8
elif threshold > 10**24:
divider = 10**6
elif threshold > 10**20:
divider = 10**2
a = unsafe_div(a, divider)
b = unsafe_div(b, divider)
c = unsafe_div(c, divider)
d = unsafe_div(d, divider)
# delta0 = 3*a*c/b - b: here we can do more unsafe ops now:
delta0 = unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b) - b
# delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1 = 3 * delta0 + b - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b)
# sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0
sqrt_arg: int256 = delta1**2 + unsafe_mul(unsafe_div(4*delta0**2, b), delta0)
sqrt_val: int256 = 0
if sqrt_arg > 0:
sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256)
else:
return [
self._newton_y(_ANN, _gamma, _x, _D, i),
0
]
b_cbrt: int256 = 0
if b > 0:
b_cbrt = convert(self._cbrt(convert(b, uint256)), int256)
else:
b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256)
second_cbrt: int256 = 0
if delta1 > 0:
# second_cbrt = convert(self._cbrt(convert((delta1 + sqrt_val), uint256) / 2), int256)
second_cbrt = convert(self._cbrt(convert(unsafe_add(delta1, sqrt_val), uint256) / 2), int256)
else:
# second_cbrt = -convert(self._cbrt(convert(unsafe_sub(sqrt_val, delta1), uint256) / 2), int256)
second_cbrt = -convert(self._cbrt(unsafe_div(convert(unsafe_sub(sqrt_val, delta1), uint256), 2)), int256)
# C1: int256 = b_cbrt**2/10**18*second_cbrt/10**18
C1: int256 = unsafe_div(unsafe_mul(unsafe_div(b_cbrt**2, 10**18), second_cbrt), 10**18)
# root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here.
root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_mul(10**18, b)/C1*delta0)/unsafe_mul(3, a)
# y_out: uint256[2] = [
# convert(D**2/x_j*root/4/10**18, uint256), # <--- y
# convert(root, uint256) # <----------------------- K0Prev
# ]
y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)]
frac: uint256 = unsafe_div(y_out[0] * 10**18, _D)
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y
return y_out
```
```shell
>>> soon
```
::::
### `exchange_received`
::::description[`TwoCrypto.exchange_received(i: uint256, j: uint256, dx: uint256, min_dy: uint256, receiver: address = msg.sender) -> uint256:`]
:::warning
The transfer of coins into the pool and then calling `exchange_received` is highly advised to be done in the same transaction. If not, other users or MEV bots may frontrun `exchange_received`, potentially stealing the coins.
:::
Function to exchange `dx` amount of coin `i` for coin `j` and receive a minimum amount of `min_dy`. This function requires a transfer of `dx` amount of coin `i` to the pool prior to calling this function, as this exchange is based on the change of token balances in the pool. The pool will not call `transferFrom` and will only check if a surplus of `coins[i]` is greater than or equal to `dx`. Charged fee at current states is `Pool.fee()`.
| Input | Type | Description |
| ----------- | --------- | ---------------------------------------------------------- |
| `i` | `uint256` | Index value for the input coin. |
| `j` | `uint256` | Index value for the output coin. |
| `dx` | `uint256` | Amount of input coin being swapped in. |
| `min_dy` | `uint256` | Minimum amount of output coin to receive. |
| `receiver` | `address` | Address to send output coin to. Defaults to `msg.sender`. |
Returns: amount of output coin `j` received (`uint256`).
Emits: `TokenExchange`
```vyper
event TokenExchange:
buyer: indexed(address)
sold_id: uint256
tokens_sold: uint256
bought_id: uint256
tokens_bought: uint256
fee: uint256
packed_price_scale: uint256
@external
@nonreentrant('lock')
def exchange_received(
i: uint256,
j: uint256,
dx: uint256,
min_dy: uint256,
receiver: address = msg.sender,
) -> uint256:
"""
@notice Exchange: but user must transfer dx amount of coin[i] tokens to pool first.
Pool will not call transferFrom and will only check if a surplus of
coins[i] is greater than or equal to `dx`.
@dev Use-case is to reduce the number of redundant ERC20 token
transfers in zaps. Primarily for dex-aggregators/arbitrageurs/searchers.
Note for users: please transfer + exchange_received in 1 tx.
@param i Index value for the input coin
@param j Index value for the output coin
@param dx Amount of input coin being swapped in
@param min_dy Minimum amount of output coin to receive
@param receiver Address to send the output coin to
@return uint256 Amount of tokens at index j received by the `receiver`
"""
# _transfer_in updates self.balances here:
dx_received: uint256 = self._transfer_in(
i,
dx,
msg.sender,
True # <---- expect_optimistic_transfer is set to True here.
)
# No ERC20 token transfers occur here:
out: uint256[3] = self._exchange(
i,
j,
dx_received,
min_dy,
)
# _transfer_out updates self.balances here. Update to state occurs before
# external calls:
self._transfer_out(j, out[0], receiver)
# log:
log TokenExchange(msg.sender, i, dx_received, j, out[0], out[1], out[2])
return out[0]
@internal
def _exchange(
i: uint256,
j: uint256,
dx_received: uint256,
min_dy: uint256,
) -> uint256[3]:
assert i != j # dev: coin index out of range
assert dx_received > 0 # dev: do not exchange 0 coins
A_gamma: uint256[2] = self._A_gamma()
xp: uint256[N_COINS] = self.balances
dy: uint256 = 0
y: uint256 = xp[j]
x0: uint256 = xp[i] - dx_received # old xp[i]
price_scale: uint256 = self.cached_price_scale
xp = [
xp[0] * PRECISIONS[0],
unsafe_div(xp[1] * price_scale * PRECISIONS[1], PRECISION)
]
# ----------- Update invariant if A, gamma are undergoing ramps ---------
t: uint256 = self.future_A_gamma_time
if t > block.timestamp:
x0 *= PRECISIONS[i]
if i > 0:
x0 = unsafe_div(x0 * price_scale, PRECISION)
x1: uint256 = xp[i] # <------------------ Back up old value in xp ...
xp[i] = x0 # |
self.D = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0) # |
xp[i] = x1 # <-------------------------------------- ... and restore.
# ----------------------- Calculate dy and fees --------------------------
D: uint256 = self.D
y_out: uint256[2] = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, j)
dy = xp[j] - y_out[0]
xp[j] -= dy
dy -= 1
if j > 0:
dy = dy * PRECISION / price_scale
dy /= PRECISIONS[j]
fee: uint256 = unsafe_div(self._fee(xp) * dy, 10**10)
dy -= fee # <--------------------- Subtract fee from the outgoing amount.
assert dy >= min_dy, "Slippage"
y -= dy
y *= PRECISIONS[j]
if j > 0:
y = unsafe_div(y * price_scale, PRECISION)
xp[j] = y # <------------------------------------------------- Update xp.
# ------ Tweak price_scale with good initial guess for newton_D ----------
price_scale = self.tweak_price(A_gamma, xp, 0, y_out[1])
return [dy, fee, price_scale]
```
```vyper
@external
@view
def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev: uint256 = 0) -> uint256:
"""
Finding the invariant using Newton method.
ANN is higher by the factor A_MULTIPLIER
ANN is already A * N**N
"""
# Safety checks
assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A
assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
# Initial value of invariant D is that for constant-product invariant
x: uint256[N_COINS] = x_unsorted
if x[0] < x[1]:
x = [x_unsorted[1], x_unsorted[0]]
assert x[0] > 10**9 - 1 and x[0] < 10**15 * 10**18 + 1 # dev: unsafe values x[0]
assert unsafe_div(x[1] * 10**18, x[0]) > 10**14 - 1 # dev: unsafe values x[i] (input)
S: uint256 = unsafe_add(x[0], x[1]) # can unsafe add here because we checked x[0] bounds
D: uint256 = 0
if K0_prev == 0:
D = N_COINS * isqrt(unsafe_mul(x[0], x[1]))
else:
# D = isqrt(x[0] * x[1] * 4 / K0_prev * 10**18)
D = isqrt(unsafe_mul(unsafe_div(unsafe_mul(unsafe_mul(4, x[0]), x[1]), K0_prev), 10**18))
if S < D:
D = S
__g1k0: uint256 = gamma + 10**18
diff: uint256 = 0
for i in range(255):
D_prev: uint256 = D
assert D > 0
# Unsafe division by D and D_prev is now safe
# K0: uint256 = 10**18
# for _x in x:
# K0 = K0 * _x * N_COINS / D
# collapsed for 2 coins
K0: uint256 = unsafe_div(unsafe_div((10**18 * N_COINS**2) * x[0], D) * x[1], D)
_g1k0: uint256 = __g1k0
if _g1k0 > K0:
_g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1) # > 0
else:
_g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1) # > 0
# D / (A * N**N) * _g1k0**2 / gamma**2
mul1: uint256 = unsafe_div(unsafe_div(unsafe_div(10**18 * D, gamma) * _g1k0, gamma) * _g1k0 * A_MULTIPLIER, ANN)
# 2*N*K0 / _g1k0
mul2: uint256 = unsafe_div(((2 * 10**18) * N_COINS) * K0, _g1k0)
# calculate neg_fprime. here K0 > 0 is being validated (safediv).
neg_fprime: uint256 = (S + unsafe_div(S * mul2, 10**18)) + mul1 * N_COINS / K0 - unsafe_div(mul2 * D, 10**18)
# D -= f / fprime; neg_fprime safediv being validated
D_plus: uint256 = D * (neg_fprime + S) / neg_fprime
D_minus: uint256 = unsafe_div(D * D, neg_fprime)
if 10**18 > K0:
D_minus += unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(10**18, K0), K0)
else:
D_minus -= unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(K0, 10**18), K0)
if D_plus > D_minus:
D = unsafe_sub(D_plus, D_minus)
else:
D = unsafe_div(unsafe_sub(D_minus, D_plus), 2)
if D > D_prev:
diff = unsafe_sub(D, D_prev)
else:
diff = unsafe_sub(D_prev, D)
if diff * 10**14 < max(10**16, D): # Could reduce precision for gas efficiency here
for _x in x:
frac: uint256 = _x * 10**18 / D
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe values x[i]
return D
raise "Did not converge"
@external
@pure
def get_y(
_ANN: uint256,
_gamma: uint256,
_x: uint256[N_COINS],
_D: uint256,
i: uint256
) -> uint256[2]:
# Safety checks
assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A
assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D
ANN: int256 = convert(_ANN, int256)
gamma: int256 = convert(_gamma, int256)
D: int256 = convert(_D, int256)
x_j: int256 = convert(_x[1 - i], int256)
gamma2: int256 = unsafe_mul(gamma, gamma)
# savediv by x_j done here:
y: int256 = D**2 / (x_j * N_COINS**2)
# K0_i: int256 = (10**18 * N_COINS) * x_j / D
K0_i: int256 = unsafe_div(10**18 * N_COINS * x_j, D)
assert (K0_i > 10**16 * N_COINS - 1) and (K0_i < 10**20 * N_COINS + 1) # dev: unsafe values x[i]
ann_gamma2: int256 = ANN * gamma2
# a = 10**36 / N_COINS**2
a: int256 = 10**32
# b = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14
b: int256 = (
D*ann_gamma2/400000000/x_j
- convert(unsafe_mul(10**32, 3), int256)
- unsafe_mul(unsafe_mul(2, gamma), 10**14)
)
# c = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4
c: int256 = (
unsafe_mul(10**32, convert(3, int256))
+ unsafe_mul(unsafe_mul(4, gamma), 10**14)
+ unsafe_div(gamma2, 10**4)
+ unsafe_div(unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) * x_j, D)
- unsafe_div(unsafe_mul(4, ann_gamma2), 400000000)
)
# d = -(10**18+gamma)**2 / 10**4
d: int256 = -unsafe_div(unsafe_add(10**18, gamma) **2, 10**4)
# delta0: int256 = 3*a*c/b - b
delta0: int256 = 3 * a * c / b - b # safediv by b
# delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1: int256 = 3 * delta0 + b - 27*a**2/b*d/b
divider: int256 = 1
threshold: int256 = min(min(abs(delta0), abs(delta1)), a)
if threshold > 10**48:
divider = 10**30
elif threshold > 10**46:
divider = 10**28
elif threshold > 10**44:
divider = 10**26
elif threshold > 10**42:
divider = 10**24
elif threshold > 10**40:
divider = 10**22
elif threshold > 10**38:
divider = 10**20
elif threshold > 10**36:
divider = 10**18
elif threshold > 10**34:
divider = 10**16
elif threshold > 10**32:
divider = 10**14
elif threshold > 10**30:
divider = 10**12
elif threshold > 10**28:
divider = 10**10
elif threshold > 10**26:
divider = 10**8
elif threshold > 10**24:
divider = 10**6
elif threshold > 10**20:
divider = 10**2
a = unsafe_div(a, divider)
b = unsafe_div(b, divider)
c = unsafe_div(c, divider)
d = unsafe_div(d, divider)
# delta0 = 3*a*c/b - b: here we can do more unsafe ops now:
delta0 = unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b) - b
# delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1 = 3 * delta0 + b - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b)
# sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0
sqrt_arg: int256 = delta1**2 + unsafe_mul(unsafe_div(4*delta0**2, b), delta0)
sqrt_val: int256 = 0
if sqrt_arg > 0:
sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256)
else:
return [
self._newton_y(_ANN, _gamma, _x, _D, i),
0
]
b_cbrt: int256 = 0
if b > 0:
b_cbrt = convert(self._cbrt(convert(b, uint256)), int256)
else:
b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256)
second_cbrt: int256 = 0
if delta1 > 0:
# second_cbrt = convert(self._cbrt(convert((delta1 + sqrt_val), uint256) / 2), int256)
second_cbrt = convert(self._cbrt(convert(unsafe_add(delta1, sqrt_val), uint256) / 2), int256)
else:
# second_cbrt = -convert(self._cbrt(convert(unsafe_sub(sqrt_val, delta1), uint256) / 2), int256)
second_cbrt = -convert(self._cbrt(unsafe_div(convert(unsafe_sub(sqrt_val, delta1), uint256), 2)), int256)
# C1: int256 = b_cbrt**2/10**18*second_cbrt/10**18
C1: int256 = unsafe_div(unsafe_mul(unsafe_div(b_cbrt**2, 10**18), second_cbrt), 10**18)
# root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here.
root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_mul(10**18, b)/C1*delta0)/unsafe_mul(3, a)
# y_out: uint256[2] = [
# convert(D**2/x_j*root/4/10**18, uint256), # <--- y
# convert(root, uint256) # <----------------------- K0Prev
# ]
y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)]
frac: uint256 = unsafe_div(y_out[0] * 10**18, _D)
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y
return y_out
```
```shell
>>> soon
```
::::
### `get_dy`
::::description[`TwoCrypto.get_dy(i: uint256, j: uint256, dx: uint256) -> uint256:`]
Getter for the received amount of coin `j` for swapping in `dx` amount of coin `i`. This method includes fees.
| Input | Type | Description |
| ----- | --------- | ------------------------- |
| `i` | `uint256` | Index of input token. |
| `j` | `uint256` | Index of output token. |
| `dx` | `uint256` | Amount of input tokens. |
Returns: exact amount of output coin `j` (`uint256`).
```vyper
@external
@view
def get_dy(i: uint256, j: uint256, dx: uint256) -> uint256:
"""
@notice Get amount of coin[j] tokens received for swapping in dx amount of coin[i]
@dev Includes fee.
@param i index of input token. Check pool.coins(i) to get coin address at ith index
@param j index of output token
@param dx amount of input coin[i] tokens
@return uint256 Exact amount of output j tokens for dx amount of i input tokens.
"""
view_contract: address = factory.views_implementation()
return Views(view_contract).get_dy(i, j, dx, self)
```
```vyper
@external
@view
def get_dy(
i: uint256, j: uint256, dx: uint256, swap: address
) -> uint256:
dy: uint256 = 0
xp: uint256[N_COINS] = empty(uint256[N_COINS])
# dy = (get_y(x + dx) - y) * (1 - fee)
dy, xp = self._get_dy_nofee(i, j, dx, swap)
dy -= Curve(swap).fee_calc(xp) * dy / 10**10
return dy
@internal
@view
def _get_dy_nofee(
i: uint256, j: uint256, dx: uint256, swap: address
) -> (uint256, uint256[N_COINS]):
assert i != j and i < N_COINS and j < N_COINS, "coin index out of range"
assert dx > 0, "do not exchange 0 coins"
math: Math = Curve(swap).MATH()
xp: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
price_scale: uint256 = 0
D: uint256 = 0
token_supply: uint256 = 0
A: uint256 = 0
gamma: uint256 = 0
xp, D, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap)
# adjust xp with input dx
xp[i] += dx
xp = [
xp[0] * precisions[0],
xp[1] * price_scale * precisions[1] / PRECISION
]
y_out: uint256[2] = math.get_y(A, gamma, xp, D, j)
dy: uint256 = xp[j] - y_out[0] - 1
xp[j] = y_out[0]
if j > 0:
dy = dy * PRECISION / price_scale
dy /= precisions[j]
return dy, xp
```
```vyper
@external
@pure
def get_y(
_ANN: uint256,
_gamma: uint256,
_x: uint256[N_COINS],
_D: uint256,
i: uint256
) -> uint256[2]:
# Safety checks
assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A
assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D
ANN: int256 = convert(_ANN, int256)
gamma: int256 = convert(_gamma, int256)
D: int256 = convert(_D, int256)
x_j: int256 = convert(_x[1 - i], int256)
gamma2: int256 = unsafe_mul(gamma, gamma)
# savediv by x_j done here:
y: int256 = D**2 / (x_j * N_COINS**2)
# K0_i: int256 = (10**18 * N_COINS) * x_j / D
K0_i: int256 = unsafe_div(10**18 * N_COINS * x_j, D)
assert (K0_i > 10**16 * N_COINS - 1) and (K0_i < 10**20 * N_COINS + 1) # dev: unsafe values x[i]
ann_gamma2: int256 = ANN * gamma2
# a = 10**36 / N_COINS**2
a: int256 = 10**32
# b = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14
b: int256 = (
D*ann_gamma2/400000000/x_j
- convert(unsafe_mul(10**32, 3), int256)
- unsafe_mul(unsafe_mul(2, gamma), 10**14)
)
# c = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4
c: int256 = (
unsafe_mul(10**32, convert(3, int256))
+ unsafe_mul(unsafe_mul(4, gamma), 10**14)
+ unsafe_div(gamma2, 10**4)
+ unsafe_div(unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) * x_j, D)
- unsafe_div(unsafe_mul(4, ann_gamma2), 400000000)
)
# d = -(10**18+gamma)**2 / 10**4
d: int256 = -unsafe_div(unsafe_add(10**18, gamma) **2, 10**4)
# delta0: int256 = 3*a*c/b - b
delta0: int256 = 3 * a * c / b - b # safediv by b
# delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1: int256 = 3 * delta0 + b - 27*a**2/b*d/b
divider: int256 = 1
threshold: int256 = min(min(abs(delta0), abs(delta1)), a)
if threshold > 10**48:
divider = 10**30
elif threshold > 10**46:
divider = 10**28
elif threshold > 10**44:
divider = 10**26
elif threshold > 10**42:
divider = 10**24
elif threshold > 10**40:
divider = 10**22
elif threshold > 10**38:
divider = 10**20
elif threshold > 10**36:
divider = 10**18
elif threshold > 10**34:
divider = 10**16
elif threshold > 10**32:
divider = 10**14
elif threshold > 10**30:
divider = 10**12
elif threshold > 10**28:
divider = 10**10
elif threshold > 10**26:
divider = 10**8
elif threshold > 10**24:
divider = 10**6
elif threshold > 10**20:
divider = 10**2
a = unsafe_div(a, divider)
b = unsafe_div(b, divider)
c = unsafe_div(c, divider)
d = unsafe_div(d, divider)
# delta0 = 3*a*c/b - b: here we can do more unsafe ops now:
delta0 = unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b) - b
# delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1 = 3 * delta0 + b - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b)
# sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0
sqrt_arg: int256 = delta1**2 + unsafe_mul(unsafe_div(4*delta0**2, b), delta0)
sqrt_val: int256 = 0
if sqrt_arg > 0:
sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256)
else:
return [
self._newton_y(_ANN, _gamma, _x, _D, i),
0
]
b_cbrt: int256 = 0
if b > 0:
b_cbrt = convert(self._cbrt(convert(b, uint256)), int256)
else:
b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256)
second_cbrt: int256 = 0
if delta1 > 0:
# second_cbrt = convert(self._cbrt(convert((delta1 + sqrt_val), uint256) / 2), int256)
second_cbrt = convert(self._cbrt(convert(unsafe_add(delta1, sqrt_val), uint256) / 2), int256)
else:
# second_cbrt = -convert(self._cbrt(convert(unsafe_sub(sqrt_val, delta1), uint256) / 2), int256)
second_cbrt = -convert(self._cbrt(unsafe_div(convert(unsafe_sub(sqrt_val, delta1), uint256), 2)), int256)
# C1: int256 = b_cbrt**2/10**18*second_cbrt/10**18
C1: int256 = unsafe_div(unsafe_mul(unsafe_div(b_cbrt**2, 10**18), second_cbrt), 10**18)
# root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here.
root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_mul(10**18, b)/C1*delta0)/unsafe_mul(3, a)
# y_out: uint256[2] = [
# convert(D**2/x_j*root/4/10**18, uint256), # <--- y
# convert(root, uint256) # <----------------------- K0Prev
# ]
y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)]
frac: uint256 = unsafe_div(y_out[0] * 10**18, _D)
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y
return y_out
```
```shell
>>> soon
```
::::
### `get_dx`
::::description[`TwoCrypto.get_dx(i: uint256, j: uint256, dy: uint256) -> uint256:`]
Getter for the required amount of coin `i` to input for swapping out `dy` amount of token `j`.
| Input | Type | Description |
| ----- | --------- | ------------------------- |
| `i` | `uint256` | Index of input token. |
| `j` | `uint256` | Index of output token. |
| `dy` | `uint256` | Amount of output tokens. |
Returns: amount of input coin `i` needed (`uint256`).
```vyper
@external
@view
def get_dx(i: uint256, j: uint256, dy: uint256) -> uint256:
"""
@notice Get amount of coin[i] tokens to input for swapping out dy amount
of coin[j]
@dev This is an approximate method, and returns estimates close to the input
amount. Expensive to call on-chain.
@param i index of input token. Check pool.coins(i) to get coin address at
ith index
@param j index of output token
@param dy amount of input coin[j] tokens received
@return uint256 Approximate amount of input i tokens to get dy amount of j tokens.
"""
view_contract: address = factory.views_implementation()
return Views(view_contract).get_dx(i, j, dy, self)
```
```vyper
@view
@external
def get_dx(
i: uint256, j: uint256, dy: uint256, swap: address
) -> uint256:
dx: uint256 = 0
xp: uint256[N_COINS] = empty(uint256[N_COINS])
fee_dy: uint256 = 0
_dy: uint256 = dy
# for more precise dx (but never exact), increase num loops
for k in range(5):
dx, xp = self._get_dx_fee(i, j, _dy, swap)
fee_dy = Curve(swap).fee_calc(xp) * _dy / 10**10
_dy = dy + fee_dy + 1
return dx
@internal
@view
def _get_dx_fee(
i: uint256, j: uint256, dy: uint256, swap: address
) -> (uint256, uint256[N_COINS]):
# here, dy must include fees (and 1 wei offset)
assert i != j and i < N_COINS and j < N_COINS, "coin index out of range"
assert dy > 0, "do not exchange out 0 coins"
math: Math = Curve(swap).MATH()
xp: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
price_scale: uint256 = 0
D: uint256 = 0
token_supply: uint256 = 0
A: uint256 = 0
gamma: uint256 = 0
xp, D, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap)
# adjust xp with output dy. dy contains fee element, which we handle later
# (hence this internal method is called _get_dx_fee)
xp[j] -= dy
xp = [xp[0] * precisions[0], xp[1] * price_scale * precisions[1] / PRECISION]
x_out: uint256[2] = math.get_y(A, gamma, xp, D, i)
dx: uint256 = x_out[0] - xp[i]
xp[i] = x_out[0]
if i > 0:
dx = dx * PRECISION / price_scale
dx /= precisions[i]
return dx, xp
```
```vyper
@external
@pure
def get_y(
_ANN: uint256,
_gamma: uint256,
_x: uint256[N_COINS],
_D: uint256,
i: uint256
) -> uint256[2]:
# Safety checks
assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A
assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D
ANN: int256 = convert(_ANN, int256)
gamma: int256 = convert(_gamma, int256)
D: int256 = convert(_D, int256)
x_j: int256 = convert(_x[1 - i], int256)
gamma2: int256 = unsafe_mul(gamma, gamma)
# savediv by x_j done here:
y: int256 = D**2 / (x_j * N_COINS**2)
# K0_i: int256 = (10**18 * N_COINS) * x_j / D
K0_i: int256 = unsafe_div(10**18 * N_COINS * x_j, D)
assert (K0_i > 10**16 * N_COINS - 1) and (K0_i < 10**20 * N_COINS + 1) # dev: unsafe values x[i]
ann_gamma2: int256 = ANN * gamma2
# a = 10**36 / N_COINS**2
a: int256 = 10**32
# b = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14
b: int256 = (
D*ann_gamma2/400000000/x_j
- convert(unsafe_mul(10**32, 3), int256)
- unsafe_mul(unsafe_mul(2, gamma), 10**14)
)
# c = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4
c: int256 = (
unsafe_mul(10**32, convert(3, int256))
+ unsafe_mul(unsafe_mul(4, gamma), 10**14)
+ unsafe_div(gamma2, 10**4)
+ unsafe_div(unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) * x_j, D)
- unsafe_div(unsafe_mul(4, ann_gamma2), 400000000)
)
# d = -(10**18+gamma)**2 / 10**4
d: int256 = -unsafe_div(unsafe_add(10**18, gamma) **2, 10**4)
# delta0: int256 = 3*a*c/b - b
delta0: int256 = 3 * a * c / b - b # safediv by b
# delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1: int256 = 3 * delta0 + b - 27*a**2/b*d/b
divider: int256 = 1
threshold: int256 = min(min(abs(delta0), abs(delta1)), a)
if threshold > 10**48:
divider = 10**30
elif threshold > 10**46:
divider = 10**28
elif threshold > 10**44:
divider = 10**26
elif threshold > 10**42:
divider = 10**24
elif threshold > 10**40:
divider = 10**22
elif threshold > 10**38:
divider = 10**20
elif threshold > 10**36:
divider = 10**18
elif threshold > 10**34:
divider = 10**16
elif threshold > 10**32:
divider = 10**14
elif threshold > 10**30:
divider = 10**12
elif threshold > 10**28:
divider = 10**10
elif threshold > 10**26:
divider = 10**8
elif threshold > 10**24:
divider = 10**6
elif threshold > 10**20:
divider = 10**2
a = unsafe_div(a, divider)
b = unsafe_div(b, divider)
c = unsafe_div(c, divider)
d = unsafe_div(d, divider)
# delta0 = 3*a*c/b - b: here we can do more unsafe ops now:
delta0 = unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b) - b
# delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1 = 3 * delta0 + b - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b)
# sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0
sqrt_arg: int256 = delta1**2 + unsafe_mul(unsafe_div(4*delta0**2, b), delta0)
sqrt_val: int256 = 0
if sqrt_arg > 0:
sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256)
else:
return [
self._newton_y(_ANN, _gamma, _x, _D, i),
0
]
b_cbrt: int256 = 0
if b > 0:
b_cbrt = convert(self._cbrt(convert(b, uint256)), int256)
else:
b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256)
second_cbrt: int256 = 0
if delta1 > 0:
# second_cbrt = convert(self._cbrt(convert((delta1 + sqrt_val), uint256) / 2), int256)
second_cbrt = convert(self._cbrt(convert(unsafe_add(delta1, sqrt_val), uint256) / 2), int256)
else:
# second_cbrt = -convert(self._cbrt(convert(unsafe_sub(sqrt_val, delta1), uint256) / 2), int256)
second_cbrt = -convert(self._cbrt(unsafe_div(convert(unsafe_sub(sqrt_val, delta1), uint256), 2)), int256)
# C1: int256 = b_cbrt**2/10**18*second_cbrt/10**18
C1: int256 = unsafe_div(unsafe_mul(unsafe_div(b_cbrt**2, 10**18), second_cbrt), 10**18)
# root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here.
root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_mul(10**18, b)/C1*delta0)/unsafe_mul(3, a)
# y_out: uint256[2] = [
# convert(D**2/x_j*root/4/10**18, uint256), # <--- y
# convert(root, uint256) # <----------------------- K0Prev
# ]
y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)]
frac: uint256 = unsafe_div(y_out[0] * 10**18, _D)
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y
return y_out
```
```shell
>>> soon
```
::::
### `fee_calc`
::::description[`TwoCrypto.fee_calc(xp: uint256[N_COINS]) -> uint256:`]
Getter for the charged exchange fee by the pool at the current state.
| Input | Type | Description |
| ----- | ------------------ | ------------------------------------------------ |
| `xp` | `uint256[N_COINS]` | Pool balances multiplied by the coin precisions. |
Returns: fee (`uint256`).
```vyper
@external
@view
def fee_calc(xp: uint256[N_COINS]) -> uint256: # <----- For by view contract.
"""
@notice Returns the fee charged by the pool at current state.
@param xp The current balances of the pool multiplied by coin precisions.
@return uint256 Fee value.
"""
return self._fee(xp)
@internal
@view
def _fee(xp: uint256[N_COINS]) -> uint256:
fee_params: uint256[3] = self._unpack_3(self.packed_fee_params)
f: uint256 = xp[0] + xp[1]
f = fee_params[2] * 10**18 / (
fee_params[2] + 10**18 -
(10**18 * N_COINS**N_COINS) * xp[0] / f * xp[1] / f
)
return unsafe_div(
fee_params[0] * f + fee_params[1] * (10**18 - f),
10**18
)
```
```shell
>>> soon
```
::::
---
## Adding and Removing Liquidity
*The twocrypto-ng implementation utilizes the usual methods to add and remove liquidity.*
**Adding liquidity**can be done via the `add_liquidity` method. The code uses a list of unsigned integers `uint256[N_COINS]` as input for the pools underlying tokens to add. **Any proportion is possible**. For example, adding fully single-sided can be done using `[0, 1e18]` or `[1e18, 0]`, but again, any variation is possible, e.g., `[1e18, 1e19]`.
**Removing liquidity**can be done in two different ways. Either withdraw the underlying assets in a **balanced proportion**using the `remove_liquidity` method **or fully single-sided**in a single underlying token using `remove_liquidity_one_coin`.
### `add_liquidity`
::::description[`TwoCrypto.add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256, receiver: address = msg.sender) -> uint256:`]
Function to add liquidity to the pool and mint the corresponding LP tokens.
| Input | Type | Description |
| ---------------- | ------------------- | ----------------------------------------------------- |
| `amounts` | `uint256[N_COINS]` | Amount of each coin to add. |
| `min_mint_amount`| `uint256` | Minimum amount of LP tokens to mint. |
| `receiver` | `address` | Receiver of the LP tokens; defaults to `msg.sender`. |
Returns: amount of LP tokens received (`uint256`).
Emits: `AddLiquidity`
```vyper
event AddLiquidity:
provider: indexed(address)
token_amounts: uint256[N_COINS]
fee: uint256
token_supply: uint256
packed_price_scale: uint256
@external
@nonreentrant("lock")
def add_liquidity(
amounts: uint256[N_COINS],
min_mint_amount: uint256,
receiver: address = msg.sender
) -> uint256:
"""
@notice Adds liquidity into the pool.
@param amounts Amounts of each coin to add.
@param min_mint_amount Minimum amount of LP to mint.
@param receiver Address to send the LP tokens to. Default is msg.sender
@return uint256 Amount of LP tokens received by the `receiver
"""
A_gamma: uint256[2] = self._A_gamma()
xp: uint256[N_COINS] = self.balances
amountsp: uint256[N_COINS] = empty(uint256[N_COINS])
d_token: uint256 = 0
d_token_fee: uint256 = 0
old_D: uint256 = 0
assert amounts[0] + amounts[1] > 0 # dev: no coins to add
# --------------------- Get prices, balances -----------------------------
price_scale: uint256 = self.cached_price_scale
# -------------------------------------- Update balances and calculate xp.
xp_old: uint256[N_COINS] = xp
amounts_received: uint256[N_COINS] = empty(uint256[N_COINS])
########################## TRANSFER IN <-------
for i in range(N_COINS):
if amounts[i] > 0:
# Updates self.balances here:
amounts_received[i] = self._transfer_in(
i,
amounts[i],
msg.sender,
False, # <--------------------- Disable optimistic transfers.
)
xp[i] = xp[i] + amounts_received[i]
xp = [
xp[0] * PRECISIONS[0],
unsafe_div(xp[1] * price_scale * PRECISIONS[1], PRECISION)
]
xp_old = [
xp_old[0] * PRECISIONS[0],
unsafe_div(xp_old[1] * price_scale * PRECISIONS[1], PRECISION)
]
for i in range(N_COINS):
if amounts_received[i] > 0:
amountsp[i] = xp[i] - xp_old[i]
# -------------------- Calculate LP tokens to mint -----------------------
if self.future_A_gamma_time > block.timestamp: # <--- A_gamma is ramping.
# ----- Recalculate the invariant if A or gamma are undergoing a ramp.
old_D = MATH.newton_D(A_gamma[0], A_gamma[1], xp_old, 0)
else:
old_D = self.D
D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)
token_supply: uint256 = self.totalSupply
if old_D > 0:
d_token = token_supply * D / old_D - token_supply
else:
d_token = self.get_xcp(D, price_scale) # <----- Making initial virtual price equal to 1.
assert d_token > 0 # dev: nothing minted
if old_D > 0:
d_token_fee = (
self._calc_token_fee(amountsp, xp) * d_token / 10**10 + 1
)
d_token -= d_token_fee
token_supply += d_token
self.mint(receiver, d_token)
self.admin_lp_virtual_balance += unsafe_div(ADMIN_FEE * d_token_fee, 10**10)
price_scale = self.tweak_price(A_gamma, xp, D, 0)
else:
# (re)instatiating an empty pool:
self.D = D
self.virtual_price = 10**18
self.xcp_profit = 10**18
self.xcp_profit_a = 10**18
# Initialise xcp oracle here:
self.cached_xcp_oracle = d_token # <--- virtual_price * totalSupply / 10**18
self.mint(receiver, d_token)
assert d_token >= min_mint_amount, "Slippage"
# ---------------------------------------------- Log and claim admin fees.
log AddLiquidity(
receiver,
amounts_received,
d_token_fee,
token_supply,
price_scale
)
return d_token
```
```vyper
@external
@view
def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev: uint256 = 0) -> uint256:
"""
Finding the invariant using Newton method.
ANN is higher by the factor A_MULTIPLIER
ANN is already A * N**N
"""
# Safety checks
assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A
assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
# Initial value of invariant D is that for constant-product invariant
x: uint256[N_COINS] = x_unsorted
if x[0] < x[1]:
x = [x_unsorted[1], x_unsorted[0]]
assert x[0] > 10**9 - 1 and x[0] < 10**15 * 10**18 + 1 # dev: unsafe values x[0]
assert unsafe_div(x[1] * 10**18, x[0]) > 10**14 - 1 # dev: unsafe values x[i] (input)
S: uint256 = unsafe_add(x[0], x[1]) # can unsafe add here because we checked x[0] bounds
D: uint256 = 0
if K0_prev == 0:
D = N_COINS * isqrt(unsafe_mul(x[0], x[1]))
else:
# D = isqrt(x[0] * x[1] * 4 / K0_prev * 10**18)
D = isqrt(unsafe_mul(unsafe_div(unsafe_mul(unsafe_mul(4, x[0]), x[1]), K0_prev), 10**18))
if S < D:
D = S
__g1k0: uint256 = gamma + 10**18
diff: uint256 = 0
for i in range(255):
D_prev: uint256 = D
assert D > 0
# Unsafe division by D and D_prev is now safe
# K0: uint256 = 10**18
# for _x in x:
# K0 = K0 * _x * N_COINS / D
# collapsed for 2 coins
K0: uint256 = unsafe_div(unsafe_div((10**18 * N_COINS**2) * x[0], D) * x[1], D)
_g1k0: uint256 = __g1k0
if _g1k0 > K0:
_g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1) # > 0
else:
_g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1) # > 0
# D / (A * N**N) * _g1k0**2 / gamma**2
mul1: uint256 = unsafe_div(unsafe_div(unsafe_div(10**18 * D, gamma) * _g1k0, gamma) * _g1k0 * A_MULTIPLIER, ANN)
# 2*N*K0 / _g1k0
mul2: uint256 = unsafe_div(((2 * 10**18) * N_COINS) * K0, _g1k0)
# calculate neg_fprime. here K0 > 0 is being validated (safediv).
neg_fprime: uint256 = (S + unsafe_div(S * mul2, 10**18)) + mul1 * N_COINS / K0 - unsafe_div(mul2 * D, 10**18)
# D -= f / fprime; neg_fprime safediv being validated
D_plus: uint256 = D * (neg_fprime + S) / neg_fprime
D_minus: uint256 = unsafe_div(D * D, neg_fprime)
if 10**18 > K0:
D_minus += unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(10**18, K0), K0)
else:
D_minus -= unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(K0, 10**18), K0)
if D_plus > D_minus:
D = unsafe_sub(D_plus, D_minus)
else:
D = unsafe_div(unsafe_sub(D_minus, D_plus), 2)
if D > D_prev:
diff = unsafe_sub(D, D_prev)
else:
diff = unsafe_sub(D_prev, D)
if diff * 10**14 < max(10**16, D): # Could reduce precision for gas efficiency here
for _x in x:
frac: uint256 = _x * 10**18 / D
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe values x[i]
return D
raise "Did not converge"
```
```shell
>>> soon
```
::::
### `calc_token_fee`
::::description[`TwoCrypto.calc_token_fee(amounts: uint256[N_COINS], xp: uint256[N_COINS]) -> uint256:`]
Function to calculate the charged fee on `amounts` when adding liquidity.
| Input | Type | Description |
| -------- | ------------------- | ------------------------------------------------ |
| `amounts`| `uint256[N_COINS]` | Amount of coins added to the pool. |
| `xp` | `uint256[N_COINS]` | Pool balances multiplied by the coin precisions. |
Returns: fee (`uint256`).
```vyper
@external
@view
def calc_token_fee(
amounts: uint256[N_COINS], xp: uint256[N_COINS]
) -> uint256:
"""
@notice Returns the fee charged on the given amounts for add_liquidity.
@param amounts The amounts of coins being added to the pool.
@param xp The current balances of the pool multiplied by coin precisions.
@return uint256 Fee charged.
"""
return self._calc_token_fee(amounts, xp)
@view
@internal
def _calc_token_fee(amounts: uint256[N_COINS], xp: uint256[N_COINS]) -> uint256:
# fee = sum(amounts_i - avg(amounts)) * fee' / sum(amounts)
fee: uint256 = unsafe_div(
unsafe_mul(self._fee(xp), N_COINS),
unsafe_mul(4, unsafe_sub(N_COINS, 1))
)
S: uint256 = 0
for _x in amounts:
S += _x
avg: uint256 = unsafe_div(S, N_COINS)
Sdiff: uint256 = 0
for _x in amounts:
if _x > avg:
Sdiff += unsafe_sub(_x, avg)
else:
Sdiff += unsafe_sub(avg, _x)
return fee * Sdiff / S + NOISE_FEE
```
```shell
>>> soon
```
::::
### `remove_liquidity`
::::description[`TwoCrypto.remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS], receiver: address = msg.sender) -> uint256[N_COINS]:`]
:::info
In case of any issues that result in a malfunctioning AMM state, users can safely withdraw liquidity using **`remove_liquidity`**. Withdrawal is based on balances proportional to the AMM balances, as this function does not perform complex math.
:::
Function to remove liquidity from the pool and burn `_amount` of LP tokens. When removing liquidity with this function, no fees are charged as the coins are withdrawn in balanced proportions. This function also updates the `xcp_oracle` since liquidity was removed.
| Input | Type | Description |
| ------------- | ------------------- | ------------------------------------------------ |
| `_amount` | `uint256` | Amount of LP tokens to burn. |
| `min_amounts` | `uint256[N_COINS]` | Minimum amounts of tokens to withdraw. |
| `receiver` | `address` | Receiver of the coins; defaults to `msg.sender`. |
Returns: withdrawn balances (`uint256[N_COINS]`).
Emits: `RemoveLiquidity`
```vyper
event RemoveLiquidity:
provider: indexed(address)
token_amounts: uint256[N_COINS]
token_supply: uint256
@external
@nonreentrant("lock")
def remove_liquidity(
_amount: uint256,
min_amounts: uint256[N_COINS],
receiver: address = msg.sender,
) -> uint256[N_COINS]:
"""
@notice This withdrawal method is very safe, does no complex math since
tokens are withdrawn in balanced proportions. No fees are charged.
@param _amount Amount of LP tokens to burn
@param min_amounts Minimum amounts of tokens to withdraw
@param receiver Address to send the withdrawn tokens to
@return uint256[3] Amount of pool tokens received by the `receiver`
"""
amount: uint256 = _amount
balances: uint256[N_COINS] = self.balances
withdraw_amounts: uint256[N_COINS] = empty(uint256[N_COINS])
# -------------------------------------------------------- Burn LP tokens.
total_supply: uint256 = self.totalSupply # <------ Get totalSupply before
self.burnFrom(msg.sender, _amount) # ---- reducing it with self.burnFrom.
# There are two cases for withdrawing tokens from the pool.
# Case 1. Withdrawal does not empty the pool.
# In this situation, D is adjusted proportional to the amount of
# LP tokens burnt. ERC20 tokens transferred is proportional
# to : (AMM balance * LP tokens in) / LP token total supply
# Case 2. Withdrawal empties the pool.
# In this situation, all tokens are withdrawn and the invariant
# is reset.
if amount == total_supply: # <----------------------------------- Case 2.
for i in range(N_COINS):
withdraw_amounts[i] = balances[i]
else: # <-------------------------------------------------------- Case 1.
amount -= 1 # <---- To prevent rounding errors, favor LPs a tiny bit.
for i in range(N_COINS):
withdraw_amounts[i] = balances[i] * amount / total_supply
assert withdraw_amounts[i] >= min_amounts[i]
D: uint256 = self.D
self.D = D - unsafe_div(D * amount, total_supply) # <----------- Reduce D
# proportional to the amount of tokens leaving. Since withdrawals are
# balanced, this is a simple subtraction. If amount == total_supply,
# D will be 0.
# ---------------------------------- Transfers ---------------------------
for i in range(N_COINS):
# _transfer_out updates self.balances here. Update to state occurs
# before external calls:
self._transfer_out(i, withdraw_amounts[i], receiver)
log RemoveLiquidity(msg.sender, withdraw_amounts, total_supply - _amount)
# --------------------------- Upkeep xcp oracle --------------------------
# Update xcp since liquidity was removed:
xp: uint256[N_COINS] = self.xp(self.balances, self.cached_price_scale)
last_xcp: uint256 = isqrt(xp[0] * xp[1]) # <----------- Cache it for now.
last_timestamp: uint256[2] = self._unpack_2(self.last_timestamp)
if last_timestamp[1] < block.timestamp:
cached_xcp_oracle: uint256 = self.cached_xcp_oracle
alpha: uint256 = MATH.wad_exp(
-convert(
unsafe_div(
unsafe_sub(block.timestamp, last_timestamp[1]) * 10**18,
self.xcp_ma_time # <---------- xcp ma time has is longer.
),
int256,
)
)
self.cached_xcp_oracle = unsafe_div(
last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha,
10**18
)
last_timestamp[1] = block.timestamp
# Pack and store timestamps:
self.last_timestamp = self._pack_2(last_timestamp[0], last_timestamp[1])
# Store last xcp
self.last_xcp = last_xcp
return withdraw_amounts
```
```vyper
@external
@pure
def wad_exp(x: int256) -> int256:
"""
@dev Calculates the natural exponential function of a signed integer with
a precision of 1e18.
@notice Note that this function consumes about 810 gas units. The implementation
is inspired by Remco Bloemen's implementation under the MIT license here:
https://xn--2-umb.com/22/exp-ln.
@param x The 32-byte variable.
@return int256 The 32-byte calculation result.
"""
value: int256 = x
# If the result is `< 0.5`, we return zero. This happens when we have the following:
# "x <= floor(log(0.5e18) * 1e18) ~ -42e18".
if (x <= -42_139_678_854_452_767_551):
return empty(int256)
# When the result is "> (2 **255 - 1) / 1e18" we cannot represent it as a signed integer.
# This happens when "x >= floor(log((2 **255 - 1) / 1e18) * 1e18) ~ 135".
assert x < 135_305_999_368_893_231_589, "Math: wad_exp overflow"
# `x` is now in the range "(-42, 136) * 1e18". Convert to "(-42, 136) * 2 **96" for higher
# intermediate precision and a binary base. This base conversion is a multiplication with
# "1e18 / 2 **96 = 5 **18 / 2 **78".
value = unsafe_div(x << 78, 5 **18)
# Reduce the range of `x` to "(-½ ln 2, ½ ln 2) * 2 **96" by factoring out powers of two
# so that "exp(x) = exp(x') * 2 **k", where `k` is a signer integer. Solving this gives
# "k = round(x / log(2))" and "x' = x - k * log(2)". Thus, `k` is in the range "[-61, 195]".
k: int256 = unsafe_add(unsafe_div(value << 96, 54_916_777_467_707_473_351_141_471_128), 2 **95) >> 96
value = unsafe_sub(value, unsafe_mul(k, 54_916_777_467_707_473_351_141_471_128))
# Evaluate using a "(6, 7)"-term rational approximation. Since `p` is monic,
# we will multiply by a scaling factor later.
y: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 1_346_386_616_545_796_478_920_950_773_328), value) >> 96, 57_155_421_227_552_351_082_224_309_758_442)
p: int256 = unsafe_add(unsafe_mul(unsafe_add(unsafe_mul(unsafe_sub(unsafe_add(y, value), 94_201_549_194_550_492_254_356_042_504_812), y) >> 96,\
28_719_021_644_029_726_153_956_944_680_412_240), value), 4_385_272_521_454_847_904_659_076_985_693_276 << 96)
# We leave `p` in the "2 **192" base so that we do not have to scale it up
# again for the division.
q: int256 = unsafe_add(unsafe_mul(unsafe_sub(value, 2_855_989_394_907_223_263_936_484_059_900), value) >> 96, 50_020_603_652_535_783_019_961_831_881_945)
q = unsafe_sub(unsafe_mul(q, value) >> 96, 533_845_033_583_426_703_283_633_433_725_380)
q = unsafe_add(unsafe_mul(q, value) >> 96, 3_604_857_256_930_695_427_073_651_918_091_429)
q = unsafe_sub(unsafe_mul(q, value) >> 96, 14_423_608_567_350_463_180_887_372_962_807_573)
q = unsafe_add(unsafe_mul(q, value) >> 96, 26_449_188_498_355_588_339_934_803_723_976_023)
# The polynomial `q` has no zeros in the range because all its roots are complex.
# No scaling is required, as `p` is already "2 **96" too large. Also,
# `r` is in the range "(0.09, 0.25) * 2**96" after the division.
r: int256 = unsafe_div(p, q)
# To finalise the calculation, we have to multiply `r` by:
# - the scale factor "s = ~6.031367120",
# - the factor "2 **k" from the range reduction, and
# - the factor "1e18 / 2 **96" for the base conversion.
# We do this all at once, with an intermediate result in "2**213" base,
# so that the final right shift always gives a positive value.
# Note that to circumvent Vyper's safecast feature for the potentially
# negative parameter value `r`, we first convert `r` to `bytes32` and
# subsequently to `uint256`. Remember that the EVM default behaviour is
# to use two's complement representation to handle signed integers.
return convert(unsafe_mul(convert(convert(r, bytes32), uint256), 3_822_833_074_963_236_453_042_738_258_902_158_003_155_416_615_667) >>\
convert(unsafe_sub(195, k), uint256), int256)
```
```shell
>>> soon
```
::::
### `remove_liquidity_one_coin`
::::description[`TwoCrypto.remove_liquidity_one_coin(token_amount: uint256, i: uint256, min_amount: uint256, receiver: address = msg.sender) -> uint256:`]
Function to burn `token_amount` LP tokens and withdraw liquidity in a single token `i`.
| Input | Type | Description |
| -------------- | --------- | ------------------------------------------------ |
| `token_amount` | `uint256` | Amount of LP tokens to burn. |
| `i` | `uint256` | Index of the token to withdraw. |
| `min_amount` | `uint256` | Minimum amount of token to withdraw. |
| `receiver` | `address` | Receiver of the coins; defaults to `msg.sender`. |
Returns: amount of coins withdrawn (`uint256`).
Emits: `RemoveLiquidityOne`
```vyper
event RemoveLiquidityOne:
provider: indexed(address)
token_amount: uint256
coin_index: uint256
coin_amount: uint256
approx_fee: uint256
packed_price_scale: uint256
@external
@nonreentrant("lock")
def remove_liquidity_one_coin(
token_amount: uint256,
i: uint256,
min_amount: uint256,
receiver: address = msg.sender
) -> uint256:
"""
@notice Withdraw liquidity in a single token.
Involves fees (lower than swap fees).
@dev This operation also involves an admin fee claim.
@param token_amount Amount of LP tokens to burn
@param i Index of the token to withdraw
@param min_amount Minimum amount of token to withdraw.
@param receiver Address to send the withdrawn tokens to
@return Amount of tokens at index i received by the `receiver`
"""
self._claim_admin_fees() # <--------- Auto-claim admin fees occasionally.
A_gamma: uint256[2] = self._A_gamma()
dy: uint256 = 0
D: uint256 = 0
p: uint256 = 0
xp: uint256[N_COINS] = empty(uint256[N_COINS])
approx_fee: uint256 = 0
# ------------------------------------------------------------------------
dy, D, xp, approx_fee = self._calc_withdraw_one_coin(
A_gamma,
token_amount,
i,
(self.future_A_gamma_time > block.timestamp), # <------- During ramps
) # we need to update D.
assert dy >= min_amount, "Slippage"
# ---------------------------- State Updates -----------------------------
# Burn user's tokens:
self.burnFrom(msg.sender, token_amount)
packed_price_scale: uint256 = self.tweak_price(A_gamma, xp, D, 0)
# Safe to use D from _calc_withdraw_one_coin here ---^
# ------------------------- Transfers ------------------------------------
# _transfer_out updates self.balances here. Update to state occurs before
# external calls:
self._transfer_out(i, dy, receiver)
log RemoveLiquidityOne(
msg.sender, token_amount, i, dy, approx_fee, packed_price_scale
)
return dy
@internal
@view
def _calc_withdraw_one_coin(
A_gamma: uint256[2],
token_amount: uint256,
i: uint256,
update_D: bool,
) -> (uint256, uint256, uint256[N_COINS], uint256):
token_supply: uint256 = self.totalSupply
assert token_amount <= token_supply # dev: token amount more than supply
assert i < N_COINS # dev: coin out of range
xx: uint256[N_COINS] = self.balances
D0: uint256 = 0
# -------------------------- Calculate D0 and xp -------------------------
price_scale_i: uint256 = self.cached_price_scale * PRECISIONS[1]
xp: uint256[N_COINS] = [
xx[0] * PRECISIONS[0],
unsafe_div(xx[1] * price_scale_i, PRECISION)
]
if i == 0:
price_scale_i = PRECISION * PRECISIONS[0]
if update_D: # <-------------- D is updated if pool is undergoing a ramp.
D0 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)
else:
D0 = self.D
D: uint256 = D0
# -------------------------------- Fee Calc ------------------------------
# Charge fees on D. Roughly calculate xp[i] after withdrawal and use that
# to calculate fee. Precision is not paramount here: we just want a
# behavior where the higher the imbalance caused the more fee the AMM
# charges.
# xp is adjusted assuming xp[0] ~= xp[1] ~= x[2], which is usually not the
# case. We charge self._fee(xp), where xp is an imprecise adjustment post
# withdrawal in one coin. If the withdraw is too large: charge max fee by
# default. This is because the fee calculation will otherwise underflow.
xp_imprecise: uint256[N_COINS] = xp
xp_correction: uint256 = xp[i] * N_COINS * token_amount / token_supply
fee: uint256 = self._unpack_3(self.packed_fee_params)[1] # <- self.out_fee.
if xp_correction < xp_imprecise[i]:
xp_imprecise[i] -= xp_correction
fee = self._fee(xp_imprecise)
dD: uint256 = unsafe_div(token_amount * D, token_supply)
D_fee: uint256 = fee * dD / (2 * 10**10) + 1 # <------- Actual fee on D.
# --------- Calculate `approx_fee` (assuming balanced state) in ith token.
# -------------------------------- We only need this for fee in the event.
approx_fee: uint256 = N_COINS * D_fee * xx[i] / D # <------------------<---------- TODO: Check math.
# ------------------------------------------------------------------------
D -= (dD - D_fee) # <----------------------------------- Charge fee on D.
# --------------------------------- Calculate `y_out`` with `(D - D_fee)`.
y: uint256 = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, i)[0]
dy: uint256 = (xp[i] - y) * PRECISION / price_scale_i
xp[i] = y
return dy, D, xp, approx_fee
@view
@internal
def _A_gamma() -> uint256[2]:
t1: uint256 = self.future_A_gamma_time
A_gamma_1: uint256 = self.future_A_gamma
gamma1: uint256 = A_gamma_1 & 2**128 - 1
A1: uint256 = A_gamma_1 >> 128
if block.timestamp < t1:
# --------------- Handle ramping up and down of A --------------------
A_gamma_0: uint256 = self.initial_A_gamma
t0: uint256 = self.initial_A_gamma_time
t1 -= t0
t0 = block.timestamp - t0
t2: uint256 = t1 - t0
A1 = ((A_gamma_0 >> 128) * t2 + A1 * t0) / t1
gamma1 = ((A_gamma_0 & 2**128 - 1) * t2 + gamma1 * t0) / t1
return [A1, gamma1]
```
```vyper
@external
@view
def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev: uint256 = 0) -> uint256:
"""
Finding the invariant using Newton method.
ANN is higher by the factor A_MULTIPLIER
ANN is already A * N**N
"""
# Safety checks
assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A
assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
# Initial value of invariant D is that for constant-product invariant
x: uint256[N_COINS] = x_unsorted
if x[0] < x[1]:
x = [x_unsorted[1], x_unsorted[0]]
assert x[0] > 10**9 - 1 and x[0] < 10**15 * 10**18 + 1 # dev: unsafe values x[0]
assert unsafe_div(x[1] * 10**18, x[0]) > 10**14 - 1 # dev: unsafe values x[i] (input)
S: uint256 = unsafe_add(x[0], x[1]) # can unsafe add here because we checked x[0] bounds
D: uint256 = 0
if K0_prev == 0:
D = N_COINS * isqrt(unsafe_mul(x[0], x[1]))
else:
# D = isqrt(x[0] * x[1] * 4 / K0_prev * 10**18)
D = isqrt(unsafe_mul(unsafe_div(unsafe_mul(unsafe_mul(4, x[0]), x[1]), K0_prev), 10**18))
if S < D:
D = S
__g1k0: uint256 = gamma + 10**18
diff: uint256 = 0
for i in range(255):
D_prev: uint256 = D
assert D > 0
# Unsafe division by D and D_prev is now safe
# K0: uint256 = 10**18
# for _x in x:
# K0 = K0 * _x * N_COINS / D
# collapsed for 2 coins
K0: uint256 = unsafe_div(unsafe_div((10**18 * N_COINS**2) * x[0], D) * x[1], D)
_g1k0: uint256 = __g1k0
if _g1k0 > K0:
_g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1) # > 0
else:
_g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1) # > 0
# D / (A * N**N) * _g1k0**2 / gamma**2
mul1: uint256 = unsafe_div(unsafe_div(unsafe_div(10**18 * D, gamma) * _g1k0, gamma) * _g1k0 * A_MULTIPLIER, ANN)
# 2*N*K0 / _g1k0
mul2: uint256 = unsafe_div(((2 * 10**18) * N_COINS) * K0, _g1k0)
# calculate neg_fprime. here K0 > 0 is being validated (safediv).
neg_fprime: uint256 = (S + unsafe_div(S * mul2, 10**18)) + mul1 * N_COINS / K0 - unsafe_div(mul2 * D, 10**18)
# D -= f / fprime; neg_fprime safediv being validated
D_plus: uint256 = D * (neg_fprime + S) / neg_fprime
D_minus: uint256 = unsafe_div(D * D, neg_fprime)
if 10**18 > K0:
D_minus += unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(10**18, K0), K0)
else:
D_minus -= unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(K0, 10**18), K0)
if D_plus > D_minus:
D = unsafe_sub(D_plus, D_minus)
else:
D = unsafe_div(unsafe_sub(D_minus, D_plus), 2)
if D > D_prev:
diff = unsafe_sub(D, D_prev)
else:
diff = unsafe_sub(D_prev, D)
if diff * 10**14 < max(10**16, D): # Could reduce precision for gas efficiency here
for _x in x:
frac: uint256 = _x * 10**18 / D
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe values x[i]
return D
raise "Did not converge"
@external
@pure
def get_y(
_ANN: uint256,
_gamma: uint256,
_x: uint256[N_COINS],
_D: uint256,
i: uint256
) -> uint256[2]:
# Safety checks
assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A
assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D
ANN: int256 = convert(_ANN, int256)
gamma: int256 = convert(_gamma, int256)
D: int256 = convert(_D, int256)
x_j: int256 = convert(_x[1 - i], int256)
gamma2: int256 = unsafe_mul(gamma, gamma)
# savediv by x_j done here:
y: int256 = D**2 / (x_j * N_COINS**2)
# K0_i: int256 = (10**18 * N_COINS) * x_j / D
K0_i: int256 = unsafe_div(10**18 * N_COINS * x_j, D)
assert (K0_i > 10**16 * N_COINS - 1) and (K0_i < 10**20 * N_COINS + 1) # dev: unsafe values x[i]
ann_gamma2: int256 = ANN * gamma2
# a = 10**36 / N_COINS**2
a: int256 = 10**32
# b = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14
b: int256 = (
D*ann_gamma2/400000000/x_j
- convert(unsafe_mul(10**32, 3), int256)
- unsafe_mul(unsafe_mul(2, gamma), 10**14)
)
# c = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4
c: int256 = (
unsafe_mul(10**32, convert(3, int256))
+ unsafe_mul(unsafe_mul(4, gamma), 10**14)
+ unsafe_div(gamma2, 10**4)
+ unsafe_div(unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) * x_j, D)
- unsafe_div(unsafe_mul(4, ann_gamma2), 400000000)
)
# d = -(10**18+gamma)**2 / 10**4
d: int256 = -unsafe_div(unsafe_add(10**18, gamma) **2, 10**4)
# delta0: int256 = 3*a*c/b - b
delta0: int256 = 3 * a * c / b - b # safediv by b
# delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1: int256 = 3 * delta0 + b - 27*a**2/b*d/b
divider: int256 = 1
threshold: int256 = min(min(abs(delta0), abs(delta1)), a)
if threshold > 10**48:
divider = 10**30
elif threshold > 10**46:
divider = 10**28
elif threshold > 10**44:
divider = 10**26
elif threshold > 10**42:
divider = 10**24
elif threshold > 10**40:
divider = 10**22
elif threshold > 10**38:
divider = 10**20
elif threshold > 10**36:
divider = 10**18
elif threshold > 10**34:
divider = 10**16
elif threshold > 10**32:
divider = 10**14
elif threshold > 10**30:
divider = 10**12
elif threshold > 10**28:
divider = 10**10
elif threshold > 10**26:
divider = 10**8
elif threshold > 10**24:
divider = 10**6
elif threshold > 10**20:
divider = 10**2
a = unsafe_div(a, divider)
b = unsafe_div(b, divider)
c = unsafe_div(c, divider)
d = unsafe_div(d, divider)
# delta0 = 3*a*c/b - b: here we can do more unsafe ops now:
delta0 = unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b) - b
# delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1 = 3 * delta0 + b - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b)
# sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0
sqrt_arg: int256 = delta1**2 + unsafe_mul(unsafe_div(4*delta0**2, b), delta0)
sqrt_val: int256 = 0
if sqrt_arg > 0:
sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256)
else:
return [
self._newton_y(_ANN, _gamma, _x, _D, i),
0
]
b_cbrt: int256 = 0
if b > 0:
b_cbrt = convert(self._cbrt(convert(b, uint256)), int256)
else:
b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256)
second_cbrt: int256 = 0
if delta1 > 0:
# second_cbrt = convert(self._cbrt(convert((delta1 + sqrt_val), uint256) / 2), int256)
second_cbrt = convert(self._cbrt(convert(unsafe_add(delta1, sqrt_val), uint256) / 2), int256)
else:
# second_cbrt = -convert(self._cbrt(convert(unsafe_sub(sqrt_val, delta1), uint256) / 2), int256)
second_cbrt = -convert(self._cbrt(unsafe_div(convert(unsafe_sub(sqrt_val, delta1), uint256), 2)), int256)
# C1: int256 = b_cbrt**2/10**18*second_cbrt/10**18
C1: int256 = unsafe_div(unsafe_mul(unsafe_div(b_cbrt**2, 10**18), second_cbrt), 10**18)
# root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here.
root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_mul(10**18, b)/C1*delta0)/unsafe_mul(3, a)
# y_out: uint256[2] = [
# convert(D**2/x_j*root/4/10**18, uint256), # <--- y
# convert(root, uint256) # <----------------------- K0Prev
# ]
y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)]
frac: uint256 = unsafe_div(y_out[0] * 10**18, _D)
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y
return y_out
```
```shell
>>> soon
```
::::
### `calc_token_amount`
::::description[`TwoCrypto.calc_token_amount(amounts: uint256[N_COINS], deposit: bool) -> uint256:`]
Function to calculate the LP tokens to be minted or burned for depositing or removing `amounts` of coins. This method takes fees into consideration.
| Input | Type | Description |
| ---------- | ------------------ | ----------------------------------------------- |
| `amounts` | `uint256[N_COINS]` | Amounts of tokens being deposited or withdrawn. |
| `deposit` | `bool` | `true` for deposit, `false` for withdrawal. |
Returns: amount of LP tokens deposited or withdrawn (`uint256`).
```vyper
interface Factory:
def views_implementation() -> address: view
@external
@view
def calc_token_amount(amounts: uint256[N_COINS], deposit: bool) -> uint256:
"""
@notice Calculate LP tokens minted or to be burned for depositing or
removing `amounts` of coins
@dev Includes fee.
@param amounts Amounts of tokens being deposited or withdrawn
@param deposit True if it is a deposit action, False if withdrawn.
@return uint256 Amount of LP tokens deposited or withdrawn.
"""
view_contract: address = factory.views_implementation()
return Views(view_contract).calc_token_amount(amounts, deposit, self)
@external
@view
def calc_token_fee(
amounts: uint256[N_COINS], xp: uint256[N_COINS]
) -> uint256:
"""
@notice Returns the fee charged on the given amounts for add_liquidity.
@param amounts The amounts of coins being added to the pool.
@param xp The current balances of the pool multiplied by coin precisions.
@return uint256 Fee charged.
"""
return self._calc_token_fee(amounts, xp)
@view
@internal
def _calc_token_fee(amounts: uint256[N_COINS], xp: uint256[N_COINS]) -> uint256:
# fee = sum(amounts_i - avg(amounts)) * fee' / sum(amounts)
fee: uint256 = unsafe_div(
unsafe_mul(self._fee(xp), N_COINS),
unsafe_mul(4, unsafe_sub(N_COINS, 1))
)
S: uint256 = 0
for _x in amounts:
S += _x
avg: uint256 = unsafe_div(S, N_COINS)
Sdiff: uint256 = 0
for _x in amounts:
if _x > avg:
Sdiff += unsafe_sub(_x, avg)
else:
Sdiff += unsafe_sub(avg, _x)
return fee * Sdiff / S + NOISE_FEE
```
```vyper
@view
@external
def calc_token_amount(
amounts: uint256[N_COINS], deposit: bool, swap: address
) -> uint256:
d_token: uint256 = 0
amountsp: uint256[N_COINS] = empty(uint256[N_COINS])
xp: uint256[N_COINS] = empty(uint256[N_COINS])
d_token, amountsp, xp = self._calc_dtoken_nofee(amounts, deposit, swap)
d_token -= (
Curve(swap).calc_token_fee(amountsp, xp) * d_token / 10**10 + 1
)
return d_token
@internal
@view
def _calc_dtoken_nofee(
amounts: uint256[N_COINS], deposit: bool, swap: address
) -> (uint256, uint256[N_COINS], uint256[N_COINS]):
math: Math = Curve(swap).MATH()
xp: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
price_scale: uint256 = 0
D0: uint256 = 0
token_supply: uint256 = 0
A: uint256 = 0
gamma: uint256 = 0
xp, D0, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap)
amountsp: uint256[N_COINS] = amounts
if deposit:
for k in range(N_COINS):
xp[k] += amounts[k]
else:
for k in range(N_COINS):
xp[k] -= amounts[k]
xp = [
xp[0] * precisions[0],
xp[1] * price_scale * precisions[1] / PRECISION
]
amountsp = [
amountsp[0]* precisions[0],
amountsp[1] * price_scale * precisions[1] / PRECISION
]
D: uint256 = math.newton_D(A, gamma, xp, 0)
d_token: uint256 = token_supply * D / D0
if deposit:
d_token -= token_supply
else:
d_token = token_supply - d_token
return d_token, amountsp, xp
```
```shell
In [1]: Pool.calc_token_amount([1000000000000000000, 0], True)
Out [1]: 37590681591977081154
In [1]: Pool.calc_token_amount([0, 1000000000000000000], True)
Out [1]: 6622263874240447
In [1]: Pool.calc_token_amount([1000000000000000000, 0], False)
Out [1]: 37707043389433059543
```
::::
### `calc_withdraw_one_coin`
::::description[`TwoCrypto.calc_withdraw_one_coin(token_amount: uint256, i: uint256) -> uint256:`]
Function to calculate the amount of output token `i` when burning `token_amount` of LP tokens. This method takes fees into consideration.
| Input | Type | Description |
| ------------- | --------- | ---------------------------------------- |
| `token_amount`| `uint256` | Amount of LP tokens burned. |
| `i` | `uint256` | Index of the coin to withdraw. |
Returns: amount of tokens to receive (`uint256`).
```vyper
@view
@external
def calc_withdraw_one_coin(token_amount: uint256, i: uint256) -> uint256:
"""
@notice Calculates output tokens with fee
@param token_amount LP Token amount to burn
@param i token in which liquidity is withdrawn
@return uint256 Amount of ith tokens received for burning token_amount LP tokens.
"""
return self._calc_withdraw_one_coin(
self._A_gamma(),
token_amount,
i,
(self.future_A_gamma_time > block.timestamp)
)[0]
@internal
@view
def _calc_withdraw_one_coin(
A_gamma: uint256[2],
token_amount: uint256,
i: uint256,
update_D: bool,
) -> (uint256, uint256, uint256[N_COINS], uint256):
token_supply: uint256 = self.totalSupply
assert token_amount <= token_supply # dev: token amount more than supply
assert i < N_COINS # dev: coin out of range
xx: uint256[N_COINS] = self.balances
D0: uint256 = 0
# -------------------------- Calculate D0 and xp -------------------------
price_scale_i: uint256 = self.cached_price_scale * PRECISIONS[1]
xp: uint256[N_COINS] = [
xx[0] * PRECISIONS[0],
unsafe_div(xx[1] * price_scale_i, PRECISION)
]
if i == 0:
price_scale_i = PRECISION * PRECISIONS[0]
if update_D: # <-------------- D is updated if pool is undergoing a ramp.
D0 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)
else:
D0 = self.D
D: uint256 = D0
# -------------------------------- Fee Calc ------------------------------
# Charge fees on D. Roughly calculate xp[i] after withdrawal and use that
# to calculate fee. Precision is not paramount here: we just want a
# behavior where the higher the imbalance caused the more fee the AMM
# charges.
# xp is adjusted assuming xp[0] ~= xp[1] ~= x[2], which is usually not the
# case. We charge self._fee(xp), where xp is an imprecise adjustment post
# withdrawal in one coin. If the withdraw is too large: charge max fee by
# default. This is because the fee calculation will otherwise underflow.
xp_imprecise: uint256[N_COINS] = xp
xp_correction: uint256 = xp[i] * N_COINS * token_amount / token_supply
fee: uint256 = self._unpack_3(self.packed_fee_params)[1] # <- self.out_fee.
if xp_correction < xp_imprecise[i]:
xp_imprecise[i] -= xp_correction
fee = self._fee(xp_imprecise)
dD: uint256 = unsafe_div(token_amount * D, token_supply)
D_fee: uint256 = fee * dD / (2 * 10**10) + 1 # <------- Actual fee on D.
# --------- Calculate `approx_fee` (assuming balanced state) in ith token.
# -------------------------------- We only need this for fee in the event.
approx_fee: uint256 = N_COINS * D_fee * xx[i] / D # <------------------<---------- TODO: Check math.
# ------------------------------------------------------------------------
D -= (dD - D_fee) # <----------------------------------- Charge fee on D.
# --------------------------------- Calculate `y_out`` with `(D - D_fee)`.
y: uint256 = MATH.get_y(A_gamma[0], A_gamma[1], xp, D, i)[0]
dy: uint256 = (xp[i] - y) * PRECISION / price_scale_i
xp[i] = y
return dy, D, xp, approx_fee
```
```vyper
@external
@view
def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev: uint256 = 0) -> uint256:
"""
Finding the invariant using Newton method.
ANN is higher by the factor A_MULTIPLIER
ANN is already A * N**N
"""
# Safety checks
assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A
assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
# Initial value of invariant D is that for constant-product invariant
x: uint256[N_COINS] = x_unsorted
if x[0] < x[1]:
x = [x_unsorted[1], x_unsorted[0]]
assert x[0] > 10**9 - 1 and x[0] < 10**15 * 10**18 + 1 # dev: unsafe values x[0]
assert unsafe_div(x[1] * 10**18, x[0]) > 10**14 - 1 # dev: unsafe values x[i] (input)
S: uint256 = unsafe_add(x[0], x[1]) # can unsafe add here because we checked x[0] bounds
D: uint256 = 0
if K0_prev == 0:
D = N_COINS * isqrt(unsafe_mul(x[0], x[1]))
else:
# D = isqrt(x[0] * x[1] * 4 / K0_prev * 10**18)
D = isqrt(unsafe_mul(unsafe_div(unsafe_mul(unsafe_mul(4, x[0]), x[1]), K0_prev), 10**18))
if S < D:
D = S
__g1k0: uint256 = gamma + 10**18
diff: uint256 = 0
for i in range(255):
D_prev: uint256 = D
assert D > 0
# Unsafe division by D and D_prev is now safe
# K0: uint256 = 10**18
# for _x in x:
# K0 = K0 * _x * N_COINS / D
# collapsed for 2 coins
K0: uint256 = unsafe_div(unsafe_div((10**18 * N_COINS**2) * x[0], D) * x[1], D)
_g1k0: uint256 = __g1k0
if _g1k0 > K0:
_g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1) # > 0
else:
_g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1) # > 0
# D / (A * N**N) * _g1k0**2 / gamma**2
mul1: uint256 = unsafe_div(unsafe_div(unsafe_div(10**18 * D, gamma) * _g1k0, gamma) * _g1k0 * A_MULTIPLIER, ANN)
# 2*N*K0 / _g1k0
mul2: uint256 = unsafe_div(((2 * 10**18) * N_COINS) * K0, _g1k0)
# calculate neg_fprime. here K0 > 0 is being validated (safediv).
neg_fprime: uint256 = (S + unsafe_div(S * mul2, 10**18)) + mul1 * N_COINS / K0 - unsafe_div(mul2 * D, 10**18)
# D -= f / fprime; neg_fprime safediv being validated
D_plus: uint256 = D * (neg_fprime + S) / neg_fprime
D_minus: uint256 = unsafe_div(D * D, neg_fprime)
if 10**18 > K0:
D_minus += unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(10**18, K0), K0)
else:
D_minus -= unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(K0, 10**18), K0)
if D_plus > D_minus:
D = unsafe_sub(D_plus, D_minus)
else:
D = unsafe_div(unsafe_sub(D_minus, D_plus), 2)
if D > D_prev:
diff = unsafe_sub(D, D_prev)
else:
diff = unsafe_sub(D_prev, D)
if diff * 10**14 < max(10**16, D): # Could reduce precision for gas efficiency here
for _x in x:
frac: uint256 = _x * 10**18 / D
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe values x[i]
return D
raise "Did not converge"
@external
@pure
def get_y(
_ANN: uint256,
_gamma: uint256,
_x: uint256[N_COINS],
_D: uint256,
i: uint256
) -> uint256[2]:
# Safety checks
assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A
assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D
ANN: int256 = convert(_ANN, int256)
gamma: int256 = convert(_gamma, int256)
D: int256 = convert(_D, int256)
x_j: int256 = convert(_x[1 - i], int256)
gamma2: int256 = unsafe_mul(gamma, gamma)
# savediv by x_j done here:
y: int256 = D**2 / (x_j * N_COINS**2)
# K0_i: int256 = (10**18 * N_COINS) * x_j / D
K0_i: int256 = unsafe_div(10**18 * N_COINS * x_j, D)
assert (K0_i > 10**16 * N_COINS - 1) and (K0_i < 10**20 * N_COINS + 1) # dev: unsafe values x[i]
ann_gamma2: int256 = ANN * gamma2
# a = 10**36 / N_COINS**2
a: int256 = 10**32
# b = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14
b: int256 = (
D*ann_gamma2/400000000/x_j
- convert(unsafe_mul(10**32, 3), int256)
- unsafe_mul(unsafe_mul(2, gamma), 10**14)
)
# c = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4
c: int256 = (
unsafe_mul(10**32, convert(3, int256))
+ unsafe_mul(unsafe_mul(4, gamma), 10**14)
+ unsafe_div(gamma2, 10**4)
+ unsafe_div(unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) * x_j, D)
- unsafe_div(unsafe_mul(4, ann_gamma2), 400000000)
)
# d = -(10**18+gamma)**2 / 10**4
d: int256 = -unsafe_div(unsafe_add(10**18, gamma) **2, 10**4)
# delta0: int256 = 3*a*c/b - b
delta0: int256 = 3 * a * c / b - b # safediv by b
# delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1: int256 = 3 * delta0 + b - 27*a**2/b*d/b
divider: int256 = 1
threshold: int256 = min(min(abs(delta0), abs(delta1)), a)
if threshold > 10**48:
divider = 10**30
elif threshold > 10**46:
divider = 10**28
elif threshold > 10**44:
divider = 10**26
elif threshold > 10**42:
divider = 10**24
elif threshold > 10**40:
divider = 10**22
elif threshold > 10**38:
divider = 10**20
elif threshold > 10**36:
divider = 10**18
elif threshold > 10**34:
divider = 10**16
elif threshold > 10**32:
divider = 10**14
elif threshold > 10**30:
divider = 10**12
elif threshold > 10**28:
divider = 10**10
elif threshold > 10**26:
divider = 10**8
elif threshold > 10**24:
divider = 10**6
elif threshold > 10**20:
divider = 10**2
a = unsafe_div(a, divider)
b = unsafe_div(b, divider)
c = unsafe_div(c, divider)
d = unsafe_div(d, divider)
# delta0 = 3*a*c/b - b: here we can do more unsafe ops now:
delta0 = unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b) - b
# delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1 = 3 * delta0 + b - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b)
# sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0
sqrt_arg: int256 = delta1**2 + unsafe_mul(unsafe_div(4*delta0**2, b), delta0)
sqrt_val: int256 = 0
if sqrt_arg > 0:
sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256)
else:
return [
self._newton_y(_ANN, _gamma, _x, _D, i),
0
]
b_cbrt: int256 = 0
if b > 0:
b_cbrt = convert(self._cbrt(convert(b, uint256)), int256)
else:
b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256)
second_cbrt: int256 = 0
if delta1 > 0:
# second_cbrt = convert(self._cbrt(convert((delta1 + sqrt_val), uint256) / 2), int256)
second_cbrt = convert(self._cbrt(convert(unsafe_add(delta1, sqrt_val), uint256) / 2), int256)
else:
# second_cbrt = -convert(self._cbrt(convert(unsafe_sub(sqrt_val, delta1), uint256) / 2), int256)
second_cbrt = -convert(self._cbrt(unsafe_div(convert(unsafe_sub(sqrt_val, delta1), uint256), 2)), int256)
# C1: int256 = b_cbrt**2/10**18*second_cbrt/10**18
C1: int256 = unsafe_div(unsafe_mul(unsafe_div(b_cbrt**2, 10**18), second_cbrt), 10**18)
# root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here.
root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_mul(10**18, b)/C1*delta0)/unsafe_mul(3, a)
# y_out: uint256[2] = [
# convert(D**2/x_j*root/4/10**18, uint256), # <--- y
# convert(root, uint256) # <----------------------- K0Prev
# ]
y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)]
frac: uint256 = unsafe_div(y_out[0] * 10**18, _D)
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y
return y_out
```
```shell
In [1]: Pool.calc_withdraw_one_coin(1000000000000000000, 0)
Out [1]: 26501860406190437
In [2]: Pool.calc_withdraw_one_coin(1000000000000000000, 1)
Out [2]: 150537307454780254829
```
::::
---
## Fees and Pool Profits
The cryptoswap algorithm uses different fees, such as `fee`, `mid_fee`, `out_fee`, or `fee_gamma` to determine the fees charged, more on that [here](../../legacy/cryptoswap-overview.md#fees). All Fee values are denominated in 1e10 and [can be changed](#apply_new_parameters) by the admin.
Additionally, just as for other curve pools, there is an `ADMIN_FEE`, which is hardcoded to 50%. All twocrypto-ng pools share a universal `fee_receiver`, which is determined within the Factory contract. Unlike for most other Curve pools, there is no external method to claim the admin fees. They are claimed when removing liquidity single sided.
`xcp_profit`, `xcp_profit_a`, and `last_xcp` are used for tracking pool profits, which is necessary for the pool's rebalancing mechanism. These values are denominated in 1e18.
### `fee`
::::description[`TwoCrypto.fee() -> uint256:`]
Getter for the fee charged by the pool at the current state.
Returns: fee in bps (`uint256`).
```vyper
@external
@view
def fee() -> uint256:
"""
@notice Returns the fee charged by the pool at current state.
@dev Not to be confused with the fee charged at liquidity action, since
there the fee is calculated on `xp` AFTER liquidity is added or
removed.
@return uint256 fee bps.
"""
return self._fee(self.xp(self.balances, self.cached_price_scale))
@internal
@view
def _fee(xp: uint256[N_COINS]) -> uint256:
fee_params: uint256[3] = self._unpack_3(self.packed_fee_params)
f: uint256 = xp[0] + xp[1]
f = fee_params[2] * 10**18 / (
fee_params[2] + 10**18 -
(10**18 * N_COINS**N_COINS) * xp[0] / f * xp[1] / f
)
return unsafe_div(
fee_params[0] * f + fee_params[1] * (10**18 - f),
10**18
)
```
```shell
In [1]: Pool.fee()
Out [1]: 30622026
```
::::
### `mid_fee`
::::description[`TwoCrypto.mid_fee() -> uint256:`]
Getter for the `mid_fee`. This fee is the minimum fee and is charged when the pool is completely balanced.
Returns: mid fee (`uint256`).
```vyper
packed_fee_params: public(uint256) # <---- Packs mid_fee, out_fee, fee_gamma.
@view
@external
def mid_fee() -> uint256:
"""
@notice Returns the current mid fee
@return uint256 mid_fee value.
"""
return self._unpack_3(self.packed_fee_params)[0]
```
```shell
In [1]: Pool.mid_fee()
Out [1]: 26000000
```
::::
### `out_fee`
::::description[`TwoCrypto.out_fee() -> uint256:`]
Getter for the `out_fee`. This fee is the maximum fee and is charged when the pool is completely imbalanced.
Returns: out fee (`uint256`).
```vyper
packed_fee_params: public(uint256) # <---- Packs mid_fee, out_fee, fee_gamma.
@view
@external
def out_fee() -> uint256:
"""
@notice Returns the current out fee
@return uint256 out_fee value.
"""
return self._unpack_3(self.packed_fee_params)[1]
```
```shell
In [1]: Pool.out_fee()
Out [1]: 45000000
```
::::
### `fee_gamma`
::::description[`TwoCrypto.fee_gamma() -> uint256:`]
Getter for the current `fee_gamma`. This parameter modifies the rate at which fees rise as imbalance intensifies. Smaller values result in rapid fee hikes with growing imbalances, while larger values lead to more gradual increments in fees as imbalance expands.
Returns: fee gamma (`uint256`).
```vyper
packed_fee_params: public(uint256) # <---- Packs mid_fee, out_fee, fee_gamma.
@view
@external
def fee_gamma() -> uint256:
"""
@notice Returns the current fee gamma
@return uint256 fee_gamma value.
"""
return self._unpack_3(self.packed_fee_params)[2]
```
```shell
In [1]: Pool.fee_gamma()
Out [1]: 230000000000000
```
::::
### `packed_fee_params`
::::description[`TwoCrypto.packed_fee_params() -> uint256: view`]
Getter for the packed fee parameters.
Returns: packed fee params (`uint256`).
```vyper
# Fee params that determine dynamic fees:
packed_fee_params: public(uint256) # <---- Packs mid_fee, out_fee, fee_gamma.
@external
def __init__(
_name: String[64],
_symbol: String[32],
_coins: address[N_COINS],
_math: address,
_salt: bytes32,
packed_precisions: uint256,
packed_gamma_A: uint256,
packed_fee_params: uint256,
packed_rebalancing_params: uint256,
initial_price: uint256,
):
...
self.packed_fee_params = packed_fee_params # <-------------- Contains Fee
# params: mid_fee, out_fee and fee_gamma.
...
```
```shell
In [1]: Pool.packed_fee_params()
Out [1]: 8847341539944400050877843276543133320576000000
```
::::
### `ADMIN_FEE`
::::description[`TwoCrypto.packed_fee_params() -> uint256: view`]
Getter for the admin fee of the pool. This value is hardcoded to 50% (5000000000) of the earned fees and can not be changed.
Returns: admin fee (`uint256`).
```vyper
ADMIN_FEE: public(constant(uint256)) = 5 * 10**9 # <----- 50% of earned fees.
```
```shell
In [1]: Pool.ADMIN_FEE()
Out [1]: 5000000000
```
::::
### `fee_receiver`
::::description[`TwoCrypto.fee_receiver() -> address:`]
Getter for the fee receiver of the admin fees. This address is set within the [Twocrypto-NG Factory](../../factory/twocrypto-ng/overview.md). Every pool created through the Factory has the same fee receiver.
Returns: fee receiver (`address`).
```vyper
interface Factory:
def fee_receiver() -> address: view
@external
@view
def fee_receiver() -> address:
"""
@notice Returns the address of the admin fee receiver.
@return address Fee receiver.
"""
return factory.fee_receiver()
```
```shell
In [1]: Pool.fee_receiver()
Out [1]: '0xeCb456EA5365865EbAb8a2661B0c503410e9B347'
```
::::
### `xcp_profit`
::::description[`TwoCrypto.xcp_profit() -> uint256: view`]
Getter for the current pool profits.
Returns: current profits (`uint256`).
```vyper
xcp_profit: public(uint256)
```
```shell
In [1]: Pool.xcp_profit()
Out [1]: 1000280532115852216
```
::::
### `xcp_profit_a`
::::description[`TwoCrypto.xcp_profit_a() -> uint256: view`]
Getter for the full profit at the last claim of admin fees.
Returns: profit at last claim (`uint256`).
```vyper
xcp_profit_a: public(uint256) # <--- Full profit at last claim of admin fees.
@external
def __init__(
_name: String[64],
_symbol: String[32],
_coins: address[N_COINS],
_math: address,
_salt: bytes32,
packed_precisions: uint256,
packed_gamma_A: uint256,
packed_fee_params: uint256,
packed_rebalancing_params: uint256,
initial_price: uint256,
):
...
self.xcp_profit_a = 10**18
...
```
```shell
In [1]: Pool.xcp_profit_a()
Out [1]: 1000000000000000000
```
::::
### `last_xcp`
::::description[`TwoCrypto.last_xcp() -> uint256: view`]
Getter for the last xcp action. This variable is updated by calling `tweak_price` or `remove_liquidity`.
Returns: timestamp of last xcp action (`uint256`).
```vyper
last_xcp: public(uint256)
```
```shell
In [1]: Pool.last_xcp()
Out [1]: 4177413767556756716238
```
::::
---
## Price Scaling
Curve v2 pools automatically adjust liquidity to optimize depth close to the prevailing market rates, reducing slippage. More [here](../../legacy/cryptoswap-overview.md#price-scaling). Price scaling parameter can be adjusted by the [admin](#apply_new_parameters).
### `price_scale`
::::description[`TwoCrypto.price_scale() -> uint256:`]
Getter for the price scale of the coin at index 1 with regard to the coin at index 0. The price scale determines the price band around which liquidity is concentrated.
Returns: price scale (`uint256`).
```vyper
cached_price_scale: uint256 # <------------------------ Internal price scale.
@external
@view
@nonreentrant("lock")
def price_scale() -> uint256:
"""
@notice Returns the price scale of the coin at index `k` w.r.t the coin
at index 0.
@dev Price scale determines the price band around which liquidity is
concentrated.
@return uint256 Price scale of coin.
"""
return self.cached_price_scale
```
```shell
In [1]: Pool.price_scale()
Out [1]: 176501696719232
```
::::
### `allowed_extra_profit`
::::description[`TwoCrypto.allowed_extra_profit() -> uint256:`]
Getter for the allowed extra profit value.
Returns: allowed extra profit (`uint256`).
```vyper
packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing
# parameters allowed_extra_profit, adjustment_step, and ma_time.
@view
@external
def allowed_extra_profit() -> uint256:
"""
@notice Returns the current allowed extra profit
@return uint256 allowed_extra_profit value.
"""
return self._unpack_3(self.packed_rebalancing_params)[0]
```
```shell
In [1]: Pool.allowed_extra_profit()
Out [1]: 2000000000000
```
::::
### `adjustment_step`
::::description[`TwoCrypto.allowed_extra_profit() -> uint256:`]
Getter for the adjustment step value.
Returns: adjustment step (`uint256`).
```vyper
packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing
# parameters allowed_extra_profit, adjustment_step, and ma_time.
@view
@external
def adjustment_step() -> uint256:
"""
@notice Returns the current adjustment step
@return uint256 adjustment_step value.
"""
return self._unpack_3(self.packed_rebalancing_params)[1]
```
```shell
In [1]: Pool.adjustment_step()
Out [1]: 146000000000000
```
::::
### `packed_rebalancing_params`
::::description[`TwoCrypto.packed_rebalancing_params() -> uint256: view`]
Getter for the packed rebalancing parameters, consisting of `allowed_extra_profit`, `adjustment_step`, and `ma_time`.
Returns: packed rebalancing parameters (`uint256`).
```vyper
packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing
# parameters allowed_extra_profit, adjustment_step, and ma_time.
```
```shell
In [1]: Pool.packed_rebalancing_params()
Out [1]: 680564733841876929619973849625130958848000000000600
```
::::
---
## Bonding Curve Parameters
A bonding curve is used to determine asset prices according to the pool's supply of each asset, more [here](../../legacy/cryptoswap-overview.md#bonding-curve-parameters).
Bonding curve parameters `A` and `gamma` values are [upgradable](#parameter-changes) by the the pools admin.
### `A`
::::description[`TwoCrypto.A() -> uint256:`]
Getter for the current pool amplification parameter.
Returns: A (`uint256`).
```vyper
@view
@external
def A() -> uint256:
"""
@notice Returns the current pool amplification parameter.
@return uint256 A param.
"""
return self._A_gamma()[0]
@view
@internal
def _A_gamma() -> uint256[2]:
t1: uint256 = self.future_A_gamma_time
A_gamma_1: uint256 = self.future_A_gamma
gamma1: uint256 = A_gamma_1 & 2**128 - 1
A1: uint256 = A_gamma_1 >> 128
if block.timestamp < t1:
# --------------- Handle ramping up and down of A --------------------
A_gamma_0: uint256 = self.initial_A_gamma
t0: uint256 = self.initial_A_gamma_time
t1 -= t0
t0 = block.timestamp - t0
t2: uint256 = t1 - t0
A1 = ((A_gamma_0 >> 128) * t2 + A1 * t0) / t1
gamma1 = ((A_gamma_0 & 2**128 - 1) * t2 + gamma1 * t0) / t1
return [A1, gamma1]
```
```shell
In [1]: Pool.A()
Out [1]: 400000
```
::::
### `gamma`
::::description[`TwoCrypto.gamma() -> uint256:`]
Getter for the current pool gamma parameter.
Returns: gamma (`uint256`).
```vyper
@view
@external
def gamma() -> uint256:
"""
@notice Returns the current pool gamma parameter.
@return uint256 gamma param.
"""
return self._A_gamma()[1]
@view
@internal
def _A_gamma() -> uint256[2]:
t1: uint256 = self.future_A_gamma_time
A_gamma_1: uint256 = self.future_A_gamma
gamma1: uint256 = A_gamma_1 & 2**128 - 1
A1: uint256 = A_gamma_1 >> 128
if block.timestamp < t1:
# --------------- Handle ramping up and down of A --------------------
A_gamma_0: uint256 = self.initial_A_gamma
t0: uint256 = self.initial_A_gamma_time
t1 -= t0
t0 = block.timestamp - t0
t2: uint256 = t1 - t0
A1 = ((A_gamma_0 >> 128) * t2 + A1 * t0) / t1
gamma1 = ((A_gamma_0 & 2**128 - 1) * t2 + gamma1 * t0) / t1
return [A1, gamma1]
```
```shell
In [1]: Pool.gamma()
Out [1]: 145000000000000
```
::::
---
## Oracle Methods
*All pools have their own built in exponential moving average price oracle.*
Prices and oracles are adjusted by when calling the internal `tweak_price` method, which happens at `add_liquidity`, `remove_liquidity_one_coin` and `_exchange`.
It is not called when removing liquidity one sided with `remove_liquidity` as this function does not alter prices.
```vyper
@internal
def tweak_price(
A_gamma: uint256[2],
_xp: uint256[N_COINS],
new_D: uint256,
K0_prev: uint256 = 0,
) -> uint256:
"""
@notice Updates price_oracle, last_price and conditionally adjusts
price_scale. This is called whenever there is an unbalanced
liquidity operation: _exchange, add_liquidity, or
remove_liquidity_one_coin.
@dev Contains main liquidity rebalancing logic, by tweaking `price_scale`.
@param A_gamma Array of A and gamma parameters.
@param _xp Array of current balances.
@param new_D New D value.
@param K0_prev Initial guess for `newton_D`.
"""
# ---------------------------- Read storage ------------------------------
price_oracle: uint256 = self.cached_price_oracle
last_prices: uint256 = self.last_prices
price_scale: uint256 = self.cached_price_scale
rebalancing_params: uint256[3] = self._unpack_3(self.packed_rebalancing_params)
# Contains: allowed_extra_profit, adjustment_step, ma_time. -----^
total_supply: uint256 = self.totalSupply
old_xcp_profit: uint256 = self.xcp_profit
old_virtual_price: uint256 = self.virtual_price
# ----------------------- Update Oracles if needed -----------------------
last_timestamp: uint256[2] = self._unpack_2(self.last_timestamp)
alpha: uint256 = 0
if last_timestamp[0] < block.timestamp: # 0th index is for price_oracle.
# The moving average price oracle is calculated using the last_price
# of the trade at the previous block, and the price oracle logged
# before that trade. This can happen only once per block.
# ------------------ Calculate moving average params -----------------
alpha = MATH.wad_exp(
-convert(
unsafe_div(
unsafe_sub(block.timestamp, last_timestamp[0]) * 10**18,
rebalancing_params[2] # <----------------------- ma_time.
),
int256,
)
)
# ---------------------------------------------- Update price oracles.
# ----------------- We cap state price that goes into the EMA with
# 2 x price_scale.
price_oracle = unsafe_div(
min(last_prices, 2 * price_scale) * (10**18 - alpha) +
price_oracle * alpha, # ^-------- Cap spot price into EMA.
10**18
)
self.cached_price_oracle = price_oracle
last_timestamp[0] = block.timestamp
# ----------------------------------------------------- Update xcp oracle.
if last_timestamp[1] < block.timestamp:
cached_xcp_oracle: uint256 = self.cached_xcp_oracle
alpha = MATH.wad_exp(
-convert(
unsafe_div(
unsafe_sub(block.timestamp, last_timestamp[1]) * 10**18,
self.xcp_ma_time # <---------- xcp ma time has is longer.
),
int256,
)
)
self.cached_xcp_oracle = unsafe_div(
self.last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha,
10**18
)
# Pack and store timestamps:
last_timestamp[1] = block.timestamp
self.last_timestamp = self._pack_2(last_timestamp[0], last_timestamp[1])
# `price_oracle` is used further on to calculate its vector distance from
# price_scale. This distance is used to calculate the amount of adjustment
# to be done to the price_scale.
# ------------------------------------------------------------------------
# ------------------ If new_D is set to 0, calculate it ------------------
D_unadjusted: uint256 = new_D
if new_D == 0: # <--------------------------- _exchange sets new_D to 0.
D_unadjusted = MATH.newton_D(A_gamma[0], A_gamma[1], _xp, K0_prev)
# ----------------------- Calculate last_prices --------------------------
self.last_prices = unsafe_div(
MATH.get_p(_xp, D_unadjusted, A_gamma) * price_scale,
10**18
)
# ---------- Update profit numbers without price adjustment first --------
xp: uint256[N_COINS] = [
unsafe_div(D_unadjusted, N_COINS),
D_unadjusted * PRECISION / (N_COINS * price_scale) # <------ safediv.
] # with price_scale.
xcp_profit: uint256 = 10**18
virtual_price: uint256 = 10**18
if old_virtual_price > 0:
xcp: uint256 = isqrt(xp[0] * xp[1])
virtual_price = 10**18 * xcp / total_supply
xcp_profit = unsafe_div(
old_xcp_profit * virtual_price,
old_virtual_price
) # <---------------- Safu to do unsafe_div as old_virtual_price > 0.
# If A and gamma are not undergoing ramps (t < block.timestamp),
# ensure new virtual_price is not less than old virtual_price,
# else the pool suffers a loss.
if self.future_A_gamma_time < block.timestamp:
assert virtual_price > old_virtual_price, "Loss"
# -------------------------- Cache last_xcp --------------------------
self.last_xcp = xcp # geometric_mean(D * price_scale)
self.xcp_profit = xcp_profit
# ------------ Rebalance liquidity if there's enough profits to adjust it:
if virtual_price * 2 - 10**18 > xcp_profit + 2 * rebalancing_params[0]:
# allowed_extra_profit --------^
# ------------------- Get adjustment step ----------------------------
# Calculate the vector distance between price_scale and
# price_oracle.
norm: uint256 = unsafe_div(
unsafe_mul(price_oracle, 10**18), price_scale
)
if norm > 10**18:
norm = unsafe_sub(norm, 10**18)
else:
norm = unsafe_sub(10**18, norm)
adjustment_step: uint256 = max(
rebalancing_params[1], unsafe_div(norm, 5)
) # ^------------------------------------- adjustment_step.
if norm > adjustment_step: # <---------- We only adjust prices if the
# vector distance between price_oracle and price_scale is
# large enough. This check ensures that no rebalancing
# occurs if the distance is low i.e. the pool prices are
# pegged to the oracle prices.
# ------------------------------------- Calculate new price scale.
p_new: uint256 = unsafe_div(
price_scale * unsafe_sub(norm, adjustment_step) +
adjustment_step * price_oracle,
norm
) # <---- norm is non-zero and gt adjustment_step; unsafe = safe.
# ---------------- Update stale xp (using price_scale) with p_new.
xp = [
_xp[0],
unsafe_div(_xp[1] * p_new, price_scale)
]
# ------------------------------------------ Update D with new xp.
D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)
for k in range(N_COINS):
frac: uint256 = xp[k] * 10**18 / D # <----- Check validity of
assert (frac > 10**16 - 1) and (frac < 10**20 + 1) # p_new.
# ------------------------------------- Convert xp to real prices.
xp = [
unsafe_div(D, N_COINS),
D * PRECISION / (N_COINS * p_new)
]
# ---------- Calculate new virtual_price using new xp and D. Reuse
# `old_virtual_price` (but it has new virtual_price).
old_virtual_price = unsafe_div(
10**18 * isqrt(xp[0] * xp[1]), total_supply
) # <----- unsafe_div because we did safediv before (if vp>1e18)
# ---------------------------- Proceed if we've got enough profit.
if (
old_virtual_price > 10**18 and
2 * old_virtual_price - 10**18 > xcp_profit
):
self.D = D
self.virtual_price = old_virtual_price
self.cached_price_scale = p_new
return p_new
# --------- price_scale was not adjusted. Update the profit counter and D.
self.D = D_unadjusted
self.virtual_price = virtual_price
return price_scale
```
### `price_oracle`
::::description[`TwoCrypto.price_oracle() -> uint256:`]
:::info
The aggregated prices are cached state prices (dx/dy) calculated **AFTER**the last trade.
:::
Getter for the oracle price of the coin at index 1 with regard to the coin at index 0. The price oracle is an exponential moving average with a periodicity determined by `ma_time`.
Returns: oracle price (`uint256`).
```vyper
@external
@view
@nonreentrant("lock")
def price_oracle() -> uint256:
"""
@notice Returns the oracle price of the coin at index `k` w.r.t the coin
at index 0.
@dev The oracle is an exponential moving average, with a periodicity
determined by `self.ma_time`. The aggregated prices are cached state
prices (dy/dx) calculated AFTER the latest trade.
@return uint256 Price oracle value of kth coin.
"""
return self.internal_price_oracle()
@internal
@view
def internal_price_oracle() -> uint256:
"""
@notice Returns the oracle price of the coin at index `k` w.r.t the coin
at index 0.
@dev The oracle is an exponential moving average, with a periodicity
determined by `self.ma_time`. The aggregated prices are cached state
prices (dy/dx) calculated AFTER the latest trade.
@param k The index of the coin.
@return uint256 Price oracle value of kth coin.
"""
price_oracle: uint256 = self.cached_price_oracle
price_scale: uint256 = self.cached_price_scale
last_prices_timestamp: uint256 = self._unpack_2(self.last_timestamp)[0]
if last_prices_timestamp < block.timestamp: # <------------ Update moving
# average if needed.
last_prices: uint256 = self.last_prices
ma_time: uint256 = self._unpack_3(self.packed_rebalancing_params)[2]
alpha: uint256 = MATH.wad_exp(
-convert(
unsafe_sub(block.timestamp, last_prices_timestamp) * 10**18 / ma_time,
int256,
)
)
# ---- We cap state price that goes into the EMA with 2 x price_scale.
return (
min(last_prices, 2 * price_scale) * (10**18 - alpha) +
price_oracle * alpha
) / 10**18
return price_oracle
```
```vyper
@external
@pure
def wad_exp(x: int256) -> int256:
"""
@dev Calculates the natural exponential function of a signed integer with
a precision of 1e18.
@notice Note that this function consumes about 810 gas units. The implementation
is inspired by Remco Bloemen's implementation under the MIT license here:
https://xn--2-umb.com/22/exp-ln.
@param x The 32-byte variable.
@return int256 The 32-byte calculation result.
"""
value: int256 = x
# If the result is `< 0.5`, we return zero. This happens when we have the following:
# "x <= floor(log(0.5e18) * 1e18) ~ -42e18".
if (x <= -42_139_678_854_452_767_551):
return empty(int256)
# When the result is "> (2 **255 - 1) / 1e18" we cannot represent it as a signed integer.
# This happens when "x >= floor(log((2 **255 - 1) / 1e18) * 1e18) ~ 135".
assert x < 135_305_999_368_893_231_589, "Math: wad_exp overflow"
# `x` is now in the range "(-42, 136) * 1e18". Convert to "(-42, 136) * 2 **96" for higher
# intermediate precision and a binary base. This base conversion is a multiplication with
# "1e18 / 2 **96 = 5 **18 / 2 **78".
value = unsafe_div(x << 78, 5 **18)
# Reduce the range of `x` to "(-½ ln 2, ½ ln 2) * 2 **96" by factoring out powers of two
# so that "exp(x) = exp(x') * 2 **k", where `k` is a signer integer. Solving this gives
# "k = round(x / log(2))" and "x' = x - k * log(2)". Thus, `k` is in the range "[-61, 195]".
k: int256 = unsafe_add(unsafe_div(value << 96, 54_916_777_467_707_473_351_141_471_128), 2 **95) >> 96
value = unsafe_sub(value, unsafe_mul(k, 54_916_777_467_707_473_351_141_471_128))
# Evaluate using a "(6, 7)"-term rational approximation. Since `p` is monic,
# we will multiply by a scaling factor later.
y: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 1_346_386_616_545_796_478_920_950_773_328), value) >> 96, 57_155_421_227_552_351_082_224_309_758_442)
p: int256 = unsafe_add(unsafe_mul(unsafe_add(unsafe_mul(unsafe_sub(unsafe_add(y, value), 94_201_549_194_550_492_254_356_042_504_812), y) >> 96,\
28_719_021_644_029_726_153_956_944_680_412_240), value), 4_385_272_521_454_847_904_659_076_985_693_276 << 96)
# We leave `p` in the "2 **192" base so that we do not have to scale it up
# again for the division.
q: int256 = unsafe_add(unsafe_mul(unsafe_sub(value, 2_855_989_394_907_223_263_936_484_059_900), value) >> 96, 50_020_603_652_535_783_019_961_831_881_945)
q = unsafe_sub(unsafe_mul(q, value) >> 96, 533_845_033_583_426_703_283_633_433_725_380)
q = unsafe_add(unsafe_mul(q, value) >> 96, 3_604_857_256_930_695_427_073_651_918_091_429)
q = unsafe_sub(unsafe_mul(q, value) >> 96, 14_423_608_567_350_463_180_887_372_962_807_573)
q = unsafe_add(unsafe_mul(q, value) >> 96, 26_449_188_498_355_588_339_934_803_723_976_023)
# The polynomial `q` has no zeros in the range because all its roots are complex.
# No scaling is required, as `p` is already "2 **96" too large. Also,
# `r` is in the range "(0.09, 0.25) * 2**96" after the division.
r: int256 = unsafe_div(p, q)
# To finalise the calculation, we have to multiply `r` by:
# - the scale factor "s = ~6.031367120",
# - the factor "2 **k" from the range reduction, and
# - the factor "1e18 / 2 **96" for the base conversion.
# We do this all at once, with an intermediate result in "2**213" base,
# so that the final right shift always gives a positive value.
# Note that to circumvent Vyper's safecast feature for the potentially
# negative parameter value `r`, we first convert `r` to `bytes32` and
# subsequently to `uint256`. Remember that the EVM default behaviour is
# to use two's complement representation to handle signed integers.
return convert(unsafe_mul(convert(convert(r, bytes32), uint256), 3_822_833_074_963_236_453_042_738_258_902_158_003_155_416_615_667) >>\
convert(unsafe_sub(195, k), uint256), int256)
```
```shell
In [1]: Pool.price_oracle()
Out [1]: 176068711374120 # CVG/ETH price
```
::::
### `ma_time`
::::description[`TwoCrypto.ma_time() -> uint256:`]
Getter for the moving average time in seconds.
Returns: moving average time (`uint256`).
```vyper
packed_rebalancing_params: public(uint256) # <---------- Contains rebalancing
# parameters allowed_extra_profit, adjustment_step, and ma_time.
@view
@external
def ma_time() -> uint256:
"""
@notice Returns the current moving average time in seconds
@dev To get time in seconds, the parameter is multipled by ln(2)
One can expect off-by-one errors here.
@return uint256 ma_time value.
"""
return self._unpack_3(self.packed_rebalancing_params)[2] * 694 / 1000
```
```shell
In [1]: Pool.ma_time()
Out [1]: 416
```
::::
### `lp_price`
::::description[`TwoCrypto.lp_price() -> uint256:`]
Function to calculate the current price of the LP token with regard to the coin at index 0.
Returns: LP token price (`uint256`).
```vyper
@external
@view
@nonreentrant("lock")
def lp_price() -> uint256:
"""
@notice Calculates the current price of the LP token w.r.t coin at the 0th index
@return uint256 LP price.
"""
return 2 * self.virtual_price * isqrt(self.internal_price_oracle() * 10**18) / 10**18
@internal
@view
def internal_price_oracle() -> uint256:
"""
@notice Returns the oracle price of the coin at index `k` w.r.t the coin
at index 0.
@dev The oracle is an exponential moving average, with a periodicity
determined by `self.ma_time`. The aggregated prices are cached state
prices (dy/dx) calculated AFTER the latest trade.
@param k The index of the coin.
@return uint256 Price oracle value of kth coin.
"""
price_oracle: uint256 = self.cached_price_oracle
price_scale: uint256 = self.cached_price_scale
last_prices_timestamp: uint256 = self._unpack_2(self.last_timestamp)[0]
if last_prices_timestamp < block.timestamp: # <------------ Update moving
# average if needed.
last_prices: uint256 = self.last_prices
ma_time: uint256 = self._unpack_3(self.packed_rebalancing_params)[2]
alpha: uint256 = MATH.wad_exp(
-convert(
unsafe_sub(block.timestamp, last_prices_timestamp) * 10**18 / ma_time,
int256,
)
)
# ---- We cap state price that goes into the EMA with 2 x price_scale.
return (
min(last_prices, 2 * price_scale) * (10**18 - alpha) +
price_oracle * alpha
) / 10**18
return price_oracle
```
```shell
In [1]: Pool.lp_price()
Out [1]: 26545349102641443 # lp token price in wETH
```
::::
### `virtual_price`
::::description[`TwoCrypto.virtual_price -> uint256: view`]
:::warning[`get_virtual_price` ≠ `virtual_price`]
`get_virtual_price` should not be confused with `virtual_price`, which is a cached virtual price.
:::
Getter for the cached virtual price. This variable provides a fast read by accessing the cached value instead of recalculating it.
Returns: cached virtual price (`uint256`).
```vyper
virtual_price: public(uint256) # <------ Cached (fast to read) virtual price.
# The cached `virtual_price` is also used internally.
```
```shell
In [1]: Pool.virtual_price()
Out [1]: 1000270251060292804
```
::::
### `get_virtual_price`
::::description[`TwoCrypto.virtual_price -> uint256: view`]
:::warning[`get_virtual_price` ≠ `virtual_price`]
`get_virtual_price` should not be confused with `virtual_price`, which is a cached virtual price.
:::
Function to calculate the current virtual price of the pool's LP token.
Returns: virtual price (`uint256`).
```vyper
@external
@view
@nonreentrant("lock")
def get_virtual_price() -> uint256:
"""
@notice Calculates the current virtual price of the pool LP token.
@dev Not to be confused with `self.virtual_price` which is a cached
virtual price.
@return uint256 Virtual Price.
"""
return 10**18 * self.get_xcp(self.D, self.cached_price_scale) / self.totalSupply
```
```shell
In [1]: Pool.get_virtual_price()
Out [1]: 1000270251060292804
```
::::
### `last_prices`
::::description[`TwoCrypto.last_prices -> uint256: view`]
Getter for the last price. This variable is used to calculate the moving average price oracle.
Returns: last price (`uint256`).
```vyper
last_prices: public(uint256)
@internal
def tweak_price(
A_gamma: uint256[2],
_xp: uint256[N_COINS],
new_D: uint256,
K0_prev: uint256 = 0,
) -> uint256:
"""
@notice Updates price_oracle, last_price and conditionally adjusts
price_scale. This is called whenever there is an unbalanced
liquidity operation: _exchange, add_liquidity, or
remove_liquidity_one_coin.
@dev Contains main liquidity rebalancing logic, by tweaking `price_scale`.
@param A_gamma Array of A and gamma parameters.
@param _xp Array of current balances.
@param new_D New D value.
@param K0_prev Initial guess for `newton_D`.
"""
...
# ----------------------- Calculate last_prices --------------------------
self.last_prices = unsafe_div(
MATH.get_p(_xp, D_unadjusted, A_gamma) * price_scale,
10**18
)
...
```
```shell
In [1]: Pool.last_prices()
Out [1]: 176068709329068
```
::::
### `xcp_oracle`
::::description[`TwoCrypto.xcp_oracle() -> uint256`]
Getter for the oracle value for xcp. The oracle is an exponential moving average, with a periodicity determined by `xcp_ma_time`.
Returns: xcp oracle value (`uint256`).
```vyper
cached_xcp_oracle: uint256 # <----------- EMA of totalSupply * virtual_price.
@external
@view
@nonreentrant("lock")
def xcp_oracle() -> uint256:
"""
@notice Returns the oracle value for xcp.
@dev The oracle is an exponential moving average, with a periodicity
determined by `self.xcp_ma_time`.
`TVL` is xcp, calculated as either:
1. virtual_price * total_supply, OR
2. self.get_xcp(...), OR
3. MATH.geometric_mean(xp)
@return uint256 Oracle value of xcp.
"""
last_prices_timestamp: uint256 = self._unpack_2(self.last_timestamp)[1]
cached_xcp_oracle: uint256 = self.cached_xcp_oracle
if last_prices_timestamp < block.timestamp:
alpha: uint256 = MATH.wad_exp(
-convert(
unsafe_div(
unsafe_sub(block.timestamp, last_prices_timestamp) * 10**18,
self.xcp_ma_time
),
int256,
)
)
return (self.last_xcp * (10**18 - alpha) + cached_xcp_oracle * alpha) / 10**18
return cached_xcp_oracle
```
```shell
In [1]: Pool.xcp_oracle()
Out [1]: 3501656271269889041418
```
::::
### `xcp_ma_time`
::::description[`TwoCrypto.xcp_ma_time() -> uint256: view`]
Getter for the moving average time window for `xcp_oracle`.
Returns: ma time (`uint256`).
```vyper
xcp_ma_time: public(uint256)
@external
def __init__(
_name: String[64],
_symbol: String[32],
_coins: address[N_COINS],
_math: address,
_salt: bytes32,
packed_precisions: uint256,
packed_gamma_A: uint256,
packed_fee_params: uint256,
packed_rebalancing_params: uint256,
initial_price: uint256,
):
...
self.xcp_ma_time = 62324 # <--------- 12 hours default on contract start.
...
```
```shell
In [1]: Pool.xcp_ma_time()
Out [1]: 62324
```
::::
### `D`
::::description[`TwoCrypto.D() -> uint256: view`]
Getter for the D invariant.
Returns: D (`uint256`).
```vyper
D: public(uint256)
```
```shell
In [1]: Pool.D()
Out [1]: 110997117004824612212
```
::::
### `last_timestamp`
::::description[`TwoCrypto.last_timestamp() -> uint256: view`]
Getter for the last timestamp of prices and xcp. The two values are packed into a `uint256`. Need to unpack them: Index 0 is for prices, index 1 is for xcp.
Returns: last timestamp (`uint256`).
```vyper
last_timestamp: public(uint256) # idx 0 is for prices, idx 1 is for xcp.
```
```shell
In [1]: Pool.last_timestamp()
Out [1]: 581843605731969082977825518885220002649556567303
```
::::
---
## Contract Info Methods
### `admin`
::::description[`TwoCrypto.admin() -> address:`]
Getter for the admin of the pool. All deployed pools share the same admin, which is specified within the Factory contract.
Returns: admin (`address`).
```vyper
interface Factory:
def admin() -> address: view
@external
@view
def admin() -> address:
"""
@notice Returns the address of the pool's admin.
@return address Admin.
"""
return factory.admin()
```
```shell
In [1]: Pool.admin()
Out [1]: todo
```
::::
### `precisions`
::::description[`TwoCrypto.precisions() -> uint256[N_COINS]:`]
Getter for the precisions of each coin in the pool. Precisions are used to make sure the pool is compatible with any coins with decimals up to 18.
Returns: precision of coins (`uint256[N_COINS]`).
```vyper
PRECISION: constant(uint256) = 10**18 # <------- The precision to convert to.
PRECISIONS: immutable(uint256[N_COINS])
@view
@external
def precisions() -> uint256[N_COINS]: # <-------------- For by view contract.
"""
@notice Returns the precisions of each coin in the pool.
@return uint256[3] precisions of coins.
"""
return PRECISIONS
```
```shell
In [1]: Pool.precisions()
Out [1]: [1, 1]
```
::::
### `MATH`
::::description[`TwoCrypto.MATH() -> address: view`]
Getter for the [math utility contract](../utility-contracts/math.md).
Returns: math contract (`address`).
```vyper
MATH: public(immutable(Math))
@external
def __init__(
_name: String[64],
_symbol: String[32],
_coins: address[N_COINS],
_math: address,
_salt: bytes32,
packed_precisions: uint256,
packed_gamma_A: uint256,
packed_fee_params: uint256,
packed_rebalancing_params: uint256,
initial_price: uint256,
):
MATH = Math(_math)
...
```
```shell
In [1]: Pool.MATH()
Out [1]: '0x2005995a71243be9FB995DaB4742327dc76564Df'
```
::::
### `coins`
::::description[`TwoCrypto.coins(arg0: uint256) -> address: view`]
Getter for the coin at index `arg0` in the pool.
| Input | Type | Description |
| ----- | --------- | ------------ |
| `arg0`| `uint256` | Coin index. |
Returns: precision of coins (`uint256[N_COINS]`).
```vyper
coins: public(immutable(address[N_COINS]))
@external
def __init__(
_name: String[64],
_symbol: String[32],
_coins: address[N_COINS],
_math: address,
_salt: bytes32,
packed_precisions: uint256,
packed_gamma_A: uint256,
packed_fee_params: uint256,
packed_rebalancing_params: uint256,
initial_price: uint256,
):
MATH = Math(_math)
factory = Factory(msg.sender)
name = _name
symbol = _symbol
coins = _coins
...
```
```shell
In [1]: Pool.coins(0)
Out [1]: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
In [2]: Pool.coins(1)
Out [2]: '0x97efFB790f2fbB701D88f89DB4521348A2B77be8'
```
::::
### `factory`
::::description[`TwoCrypto.factory() -> address: view`]
Getter for the [Factory contract](../../factory/twocrypto-ng/overview.md), which allows the permissionless deployment of liquidity pools and gauges.
Returns: factory (`address`).
```vyper
factory: public(immutable(Factory))
@external
def __init__(
_name: String[64],
_symbol: String[32],
_coins: address[N_COINS],
_math: address,
_salt: bytes32,
packed_precisions: uint256,
packed_gamma_A: uint256,
packed_fee_params: uint256,
packed_rebalancing_params: uint256,
initial_price: uint256,
):
...
factory = Factory(msg.sender)
...
```
```shell
In [1]: Pool.factory()
Out [1]: '0x98EE851a00abeE0d95D08cF4CA2BdCE32aeaAF7F'
```
::::
### `balances`
::::description[`TwoCrypto.balances(arg0: uint256) -> uint256: view`]
Getter for the current coin balances in the pool.
| Input | Type | Description |
| ----- | --------- | ------------ |
| `arg0`| `uint256` | Coin index. |
Returns: coin balances (`uint256`).
```vyper
balances: public(uint256[N_COINS])
```
```shell
In [1]: Pool.balances(0)
Out [1]: 55021540803117067316
In [2]: Pool.balances(1)
Out [2]: 317141267512073253038923
```
::::
## Admin Controls
Liquidity pools are deployed via the [Factory](../../factory/twocrypto-ng/overview.md). All pools deployed **share the same admin** defined within the Factory contract.
Transferring the ownership of a pool is only possible by changing the ownership of the Factory. Admin is the Curve DAO (OwnershipAdmin).
The same applies to the fee receiver of the pools. See [Factory Ownership](../../factory/overview.md#contract-ownership).
### Parameter Changes
For more information about parameters: [https://nagaking.substack.com/p/deep-dive-curve-v2-parameters](https://nagaking.substack.com/p/deep-dive-curve-v2-parameters).
The appropriate value for `A` and `gamma` is dependent upon the type of coin being used within the pool, and is subject to optimization and pool-parameter update based on the market history of the trading pair.
It is possible to modify the parameters for a pool after it has been deployed. Again, only the admin of the pool (= Factory admin) can do so.
### `ramp_A_gamma`
::::description[`TwoCrypto.ramp_A_gamma(future_A: uint256, future_gamma: uint256, future_time: uint256)`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the Factory contract.
:::
Function to linearly ramp the values of `A` and `gamma`.
| Input | Type | Description |
| -------------- | --------- | --------------------- |
| `future_A` | `uint256` | Future value of `A` |
| `future_gamma` | `uint256` | Future value of `gamma` |
| `future_time` | `uint256` | Timestamp at which the ramping will end |
Emits: `RampAgamma`
```vyper
event RampAgamma:
initial_A: uint256
future_A: uint256
initial_gamma: uint256
future_gamma: uint256
initial_time: uint256
future_time: uint256
@external
def ramp_A_gamma(
future_A: uint256, future_gamma: uint256, future_time: uint256
):
"""
@notice Initialise Ramping A and gamma parameter values linearly.
@dev Only accessible by factory admin, and only
@param future_A The future A value.
@param future_gamma The future gamma value.
@param future_time The timestamp at which the ramping will end.
"""
assert msg.sender == factory.admin() # dev: only owner
assert block.timestamp > self.initial_A_gamma_time + (MIN_RAMP_TIME - 1) # dev: ramp undergoing
assert future_time > block.timestamp + MIN_RAMP_TIME - 1 # dev: insufficient time
A_gamma: uint256[2] = self._A_gamma()
initial_A_gamma: uint256 = A_gamma[0] << 128
initial_A_gamma = initial_A_gamma | A_gamma[1]
assert future_A > MIN_A - 1
assert future_A < MAX_A + 1
assert future_gamma > MIN_GAMMA - 1
assert future_gamma < MAX_GAMMA + 1
ratio: uint256 = 10**18 * future_A / A_gamma[0]
assert ratio < 10**18 * MAX_A_CHANGE + 1
assert ratio > 10**18 / MAX_A_CHANGE - 1
ratio = 10**18 * future_gamma / A_gamma[1]
assert ratio < 10**18 * MAX_A_CHANGE + 1
assert ratio > 10**18 / MAX_A_CHANGE - 1
self.initial_A_gamma = initial_A_gamma
self.initial_A_gamma_time = block.timestamp
future_A_gamma: uint256 = future_A << 128
future_A_gamma = future_A_gamma | future_gamma
self.future_A_gamma_time = future_time
self.future_A_gamma = future_A_gamma
log RampAgamma(
A_gamma[0],
future_A,
A_gamma[1],
future_gamma,
block.timestamp,
future_time,
)
```
```shell
>>> TwoCrypto.ramp_A_gamma(2700000, 1300000000000, 1693674492)
```
::::
### `stop_ramp_A_gamma`
::::description[`TwoCrypto.stop_ramp_A_gamma()`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the Factory contract.
:::
Function to immediately stop the ramping of A and gamma parameters and set them to their current values.
Emits: `StopRampA`
```vyper
event StopRampA:
current_A: uint256
current_gamma: uint256
time: uint256
@external
def stop_ramp_A_gamma():
"""
@notice Stop Ramping A and gamma parameters immediately.
@dev Only accessible by factory admin.
"""
assert msg.sender == factory.admin() # dev: only owner
A_gamma: uint256[2] = self._A_gamma()
current_A_gamma: uint256 = A_gamma[0] << 128
current_A_gamma = current_A_gamma | A_gamma[1]
self.initial_A_gamma = current_A_gamma
self.future_A_gamma = current_A_gamma
self.initial_A_gamma_time = block.timestamp
self.future_A_gamma_time = block.timestamp
# ------ Now (block.timestamp < t1) is always False, so we return saved A.
log StopRampA(A_gamma[0], A_gamma[1], block.timestamp)
```
```shell
>>> TwoCrypto.stop_ramp_A_gamma()
```
::::
### `apply_new_parameters`
::::description[`TwoCrypto.apply_new_parameters(_new_mid_fee: uint256, _new_out_fee: uint256, _new_fee_gamma: uint256, _new_allowed_extra_profit: uint256, _new_adjustment_step: uint256, _new_ma_time: uint256, _new_xcp_ma_time: uint256)`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the Factory contract.
:::
Function to commit new parameters. The new parameters are applied immediately.
| Input | Type | Description |
| ----------------------- | --------- | --------------------------------------------------- |
| `_new_mid_fee` | `uint256` | New `mid_fee` value. |
| `_new_out_fee` | `uint256` | New `out_fee` value. |
| `_new_fee_gamma` | `uint256` | New `fee_gamma` value. |
| `_new_allowed_extra_profit` | `uint256` | New `allowed_extra_profit` value. |
| `_new_adjustment_step` | `uint256` | New `adjustment_step` value. |
| `_new_ma_time` | `uint256` | New `ma_time` value, which is time_in_seconds/ln(2).|
| `_new_xcp_ma_time` | `uint256` | New ma time for xcp oracles. |
Emits: `NewParameters`
```vyper
event NewParameters:
mid_fee: uint256
out_fee: uint256
fee_gamma: uint256
allowed_extra_profit: uint256
adjustment_step: uint256
ma_time: uint256
xcp_ma_time: uint256
@external
@nonreentrant('lock')
def apply_new_parameters(
_new_mid_fee: uint256,
_new_out_fee: uint256,
_new_fee_gamma: uint256,
_new_allowed_extra_profit: uint256,
_new_adjustment_step: uint256,
_new_ma_time: uint256,
_new_xcp_ma_time: uint256,
):
"""
@notice Commit new parameters.
@dev Only accessible by factory admin.
@param _new_mid_fee The new mid fee.
@param _new_out_fee The new out fee.
@param _new_fee_gamma The new fee gamma.
@param _new_allowed_extra_profit The new allowed extra profit.
@param _new_adjustment_step The new adjustment step.
@param _new_ma_time The new ma time. ma_time is time_in_seconds/ln(2).
@param _new_xcp_ma_time The new ma time for xcp oracle.
"""
assert msg.sender == factory.admin() # dev: only owner
# ----------------------------- Set fee params ---------------------------
new_mid_fee: uint256 = _new_mid_fee
new_out_fee: uint256 = _new_out_fee
new_fee_gamma: uint256 = _new_fee_gamma
current_fee_params: uint256[3] = self._unpack_3(self.packed_fee_params)
if new_out_fee < MAX_FEE + 1:
assert new_out_fee > MIN_FEE - 1 # dev: fee is out of range
else:
new_out_fee = current_fee_params[1]
if new_mid_fee > MAX_FEE:
new_mid_fee = current_fee_params[0]
assert new_mid_fee <= new_out_fee # dev: mid-fee is too high
if new_fee_gamma < 10**18:
assert new_fee_gamma > 0 # dev: fee_gamma out of range [1 .. 10**18]
else:
new_fee_gamma = current_fee_params[2]
self.packed_fee_params = self._pack_3([new_mid_fee, new_out_fee, new_fee_gamma])
# ----------------- Set liquidity rebalancing parameters -----------------
new_allowed_extra_profit: uint256 = _new_allowed_extra_profit
new_adjustment_step: uint256 = _new_adjustment_step
new_ma_time: uint256 = _new_ma_time
current_rebalancing_params: uint256[3] = self._unpack_3(self.packed_rebalancing_params)
if new_allowed_extra_profit > 10**18:
new_allowed_extra_profit = current_rebalancing_params[0]
if new_adjustment_step > 10**18:
new_adjustment_step = current_rebalancing_params[1]
if new_ma_time < 872542: # <----- Calculated as: 7 * 24 * 60 * 60 / ln(2)
assert new_ma_time > 86 # dev: MA time should be longer than 60/ln(2)
else:
new_ma_time = current_rebalancing_params[2]
self.packed_rebalancing_params = self._pack_3(
[new_allowed_extra_profit, new_adjustment_step, new_ma_time]
)
# Set xcp oracle moving average window time:
new_xcp_ma_time: uint256 = _new_xcp_ma_time
if new_xcp_ma_time < 872542:
assert new_xcp_ma_time > 86 # dev: xcp MA time should be longer than 60/ln(2)
else:
new_xcp_ma_time = self.xcp_ma_time
self.xcp_ma_time = new_xcp_ma_time
# ---------------------------------- LOG ---------------------------------
log NewParameters(
new_mid_fee,
new_out_fee,
new_fee_gamma,
new_allowed_extra_profit,
new_adjustment_step,
new_ma_time,
_new_xcp_ma_time,
)
```
```shell
>>> TwoCrypto.apply_new_parameters(20000000, 45000000, 350000000000000, 100000000000, 100000000000, 1800, 1800)
```
::::
### `initial_A_gamma`
::::description[`TwoCrypto.initial_A_gamma -> uint256: view`]
Getter for the initial A/gamma.
Returns: A/gamma (`uint256`).
```vyper
initial_A_gamma: public(uint256)
```
```shell
>>> TwoCrypto.initial_A_gamma()
581076037942835227425498917514114728328226821
```
::::
### `initial_A_gamma_time`
::::description[`TwoCrypto.initial_A_gamma_time -> uint256: view`]
Getter for the initial A/gamma time.
Returns: A/gamma time (`uint256`).
```vyper
initial_A_gamma_time: public(uint256)
```
```shell
>>> TwoCrypto.initial_A_gamma_time()
0
```
::::
### `future_A_gamma`
::::description[`TwoCrypto.future_A_gamma -> uint256: view`]
Getter for the future A/gamma.
Returns: future A/gamma (`uint256`).
```vyper
future_A_gamma: public(uint256)
```
```shell
>>> TwoCrypto.future_A_gamma()
581076037942835227425498917514114728328226821
```
::::
### `future_A_gamma_time`
::::description[`TwoCrypto.future_A_gamma_time -> uint256: view`]
:::info
This value is initially set to 0 (default) when the pool is first deployed. It only gets populated by `block.timestamp + future_time` in the `ramp_A_gamma` function when the ramping process is initiated. After ramping is finished (i.e., `self.future_A_gamma_time < block.timestamp`), the variable is left as is and not set to 0.
:::
Getter for the future A/gamma time. This is the timestamp when the ramping process is finished.
Returns: future A/gamma time (`uint256`).
```vyper
future_A_gamma_time: public(uint256) # <------ Time when ramping is finished.
# This value is 0 (default) when pool is first deployed, and only gets
# populated by block.timestamp + future_time in `ramp_A_gamma` when the
# ramping process is initiated. After ramping is finished
# (i.e. self.future_A_gamma_time < block.timestamp), the variable is left
# and not set to 0.
```
```shell
>>> TwoCrypto.future_A_gamma_time()
0
```
::::
---
## Math Contract(3)
**The Math Contract provides AMM Math for 2-coin Curve Twocrypto-NG Pools.**
:::deploy[Contract Source & Deployment]
Source code for this contract is available on [Github](https://github.com/curvefi/twocrypto-ng/blob/main/contracts/main/CurveCryptoMathOptimized2.vy).
Full list of all deployments can be found [here](../../../deployments.md).
:::
---
## AMM Math Functions
### `snekmate_log_2`
::::description[`Math._snekmate_log_2(x: uint256, roundup: bool) -> uint256:`]
:::info
This implementation is derived from Snekmate, which is authored by pcaversaccio ([Snekmate](https://github.com/pcaversaccio/snekmate)), distributed under the AGPL-3.0 license.
Note that it returns 0 if given 0. The implementation is inspired by OpenZeppelin's implementation here: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/Math.sol.
:::
Function to calculate the logarithm base 2 of `x`, following the selected rounding direction.
| Input | Type | Description |
| ------- | --------- | ------------------------------------------------ |
| `x` | `uint256` | 32-byte variable |
| `roundup` | `bool` | Whether to round up or not. Default is `False` to round down |
Returns: 32-byte calculation result (`uint256`).
```vyper
@internal
@pure
def _snekmate_log_2(x: uint256, roundup: bool) -> uint256:
"""
@notice An `internal` helper function that returns the log in base 2
of `x`, following the selected rounding direction.
@dev This implementation is derived from Snekmate, which is authored
by pcaversaccio (Snekmate), distributed under the AGPL-3.0 license.
https://github.com/pcaversaccio/snekmate
@dev Note that it returns 0 if given 0. The implementation is
inspired by OpenZeppelin's implementation here:
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/Math.sol.
@param x The 32-byte variable.
@param roundup The Boolean variable that specifies whether
to round up or not. The default `False` is round down.
@return uint256 The 32-byte calculation result.
"""
value: uint256 = x
result: uint256 = empty(uint256)
# The following lines cannot overflow because we have the well-known
# decay behaviour of `log_2(max_value(uint256)) < max_value(uint256)`.
if x >> 128 != empty(uint256):
value = x >> 128
result = 128
if value >> 64 != empty(uint256):
value = value >> 64
result = unsafe_add(result, 64)
if value >> 32 != empty(uint256):
value = value >> 32
result = unsafe_add(result, 32)
if value >> 16 != empty(uint256):
value = value >> 16
result = unsafe_add(result, 16)
if value >> 8 != empty(uint256):
value = value >> 8
result = unsafe_add(result, 8)
if value >> 4 != empty(uint256):
value = value >> 4
result = unsafe_add(result, 4)
if value >> 2 != empty(uint256):
value = value >> 2
result = unsafe_add(result, 2)
if value >> 1 != empty(uint256):
result = unsafe_add(result, 1)
if (roundup and (1 << result) < x):
result = unsafe_add(result, 1)
return result
```
::::
### `newton_y`
::::description[`Math.newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> uint256:`]
Function to calculate `y` given `ANN`, `gamma`, `x`, `D`, and `i`.
| Input | Type | Description |
|---------|------------------|---------------------------------------------------------------------|
| `ANN` | `uint256` | Amplification coefficient adjusted by N; `ANN = A * N^N` |
| `gamma` | `uint256` | Gamma |
| `x` | `uint256[N_COINS]` | Balances multiplied by prices and precisions of the coins |
| `D` | `uint256` | D invariant. |
| `i` | `uint256` | Index of the coin for which to calculate `y`. |
Returns: `y` (`uint256`).
```vyper
@external
@pure
def newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> uint256:
# Safety checks
assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A
assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert D > 10**17 - 1 and D < 10**15 * 10**18 + 1 # dev: unsafe values D
y: uint256 = self._newton_y(ANN, gamma, x, D, i)
frac: uint256 = y * 10**18 / D
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y
return y
@internal
@pure
def _newton_y(ANN: uint256, gamma: uint256, x: uint256[N_COINS], D: uint256, i: uint256) -> uint256:
"""
Calculating x[i] given other balances x[0..N_COINS-1] and invariant D
ANN = A * N**N
This is computationally expensive.
"""
x_j: uint256 = x[1 - i]
y: uint256 = D**2 / (x_j * N_COINS**2)
K0_i: uint256 = (10**18 * N_COINS) * x_j / D
assert (K0_i > 10**16*N_COINS - 1) and (K0_i < 10**20*N_COINS + 1) # dev: unsafe values x[i]
convergence_limit: uint256 = max(max(x_j / 10**14, D / 10**14), 100)
for j in range(255):
y_prev: uint256 = y
K0: uint256 = K0_i * y * N_COINS / D
S: uint256 = x_j + y
_g1k0: uint256 = gamma + 10**18
if _g1k0 > K0:
_g1k0 = _g1k0 - K0 + 1
else:
_g1k0 = K0 - _g1k0 + 1
# D / (A * N**N) * _g1k0**2 / gamma**2
mul1: uint256 = 10**18 * D / gamma * _g1k0 / gamma * _g1k0 * A_MULTIPLIER / ANN
# 2*K0 / _g1k0
mul2: uint256 = 10**18 + (2 * 10**18) * K0 / _g1k0
yfprime: uint256 = 10**18 * y + S * mul2 + mul1
_dyfprime: uint256 = D * mul2
if yfprime < _dyfprime:
y = y_prev / 2
continue
else:
yfprime -= _dyfprime
fprime: uint256 = yfprime / y
# y -= f / f_prime; y = (y * fprime - f) / fprime
# y = (yfprime + 10**18 * D - 10**18 * S) // fprime + mul1 // fprime * (10**18 - K0) // K0
y_minus: uint256 = mul1 / fprime
y_plus: uint256 = (yfprime + 10**18 * D) / fprime + y_minus * 10**18 / K0
y_minus += 10**18 * S / fprime
if y_plus < y_minus:
y = y_prev / 2
else:
y = y_plus - y_minus
diff: uint256 = 0
if y > y_prev:
diff = y - y_prev
else:
diff = y_prev - y
if diff < max(convergence_limit, y / 10**14):
return y
raise "Did not converge"
```
::::
### `get_y`
::::description[`Math.get_y(_ANN: uint256, _gamma: uint256, _x: uint256[N_COINS], _D: uint256, i: uint256) -> uint256[2]:`]
Function to calculate `y` given `_ANN`, `_gamma`, `_x`, `_D`, and `_i`.
| Input | Type | Description |
|---------|------------------|---------------------------------------------------------------------|
| `_ANN` | `uint256` | Amplification coefficient adjusted by N; `ANN = A * N^N` |
| `_gamma` | `uint256` | Gamma |
| `_x` | `uint256[N_COINS]` | Balances multiplied by prices and precisions of the coins |
| `_D` | `uint256` | D invariant. |
| `_i` | `uint256` | Index of the coin for which to calculate `y`. |
Returns: `y` (`uint256`).
```vyper
@external
@pure
def get_y(
_ANN: uint256,
_gamma: uint256,
_x: uint256[N_COINS],
_D: uint256,
i: uint256
) -> uint256[2]:
# Safety checks
assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A
assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D
ANN: int256 = convert(_ANN, int256)
gamma: int256 = convert(_gamma, int256)
D: int256 = convert(_D, int256)
x_j: int256 = convert(_x[1 - i], int256)
gamma2: int256 = unsafe_mul(gamma, gamma)
# savediv by x_j done here:
y: int256 = D**2 / (x_j * N_COINS**2)
# K0_i: int256 = (10**18 * N_COINS) * x_j / D
K0_i: int256 = unsafe_div(10**18 * N_COINS * x_j, D)
assert (K0_i > 10**16 * N_COINS - 1) and (K0_i < 10**20 * N_COINS + 1) # dev: unsafe values x[i]
ann_gamma2: int256 = ANN * gamma2
# a = 10**36 / N_COINS**2
a: int256 = 10**32
# b = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14
b: int256 = (
D*ann_gamma2/400000000/x_j
- convert(unsafe_mul(10**32, 3), int256)
- unsafe_mul(unsafe_mul(2, gamma), 10**14)
)
# c = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4
c: int256 = (
unsafe_mul(10**32, convert(3, int256))
+ unsafe_mul(unsafe_mul(4, gamma), 10**14)
+ unsafe_div(gamma2, 10**4)
+ unsafe_div(unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) * x_j, D)
- unsafe_div(unsafe_mul(4, ann_gamma2), 400000000)
)
# d = -(10**18+gamma)**2 / 10**4
d: int256 = -unsafe_div(unsafe_add(10**18, gamma) **2, 10**4)
# delta0: int256 = 3*a*c/b - b
delta0: int256 = 3 * a * c / b - b # safediv by b
# delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1: int256 = 3 * delta0 + b - 27*a**2/b*d/b
divider: int256 = 1
threshold: int256 = min(min(abs(delta0), abs(delta1)), a)
if threshold > 10**48:
divider = 10**30
elif threshold > 10**46:
divider = 10**28
elif threshold > 10**44:
divider = 10**26
elif threshold > 10**42:
divider = 10**24
elif threshold > 10**40:
divider = 10**22
elif threshold > 10**38:
divider = 10**20
elif threshold > 10**36:
divider = 10**18
elif threshold > 10**34:
divider = 10**16
elif threshold > 10**32:
divider = 10**14
elif threshold > 10**30:
divider = 10**12
elif threshold > 10**28:
divider = 10**10
elif threshold > 10**26:
divider = 10**8
elif threshold > 10**24:
divider = 10**6
elif threshold > 10**20:
divider = 10**2
a = unsafe_div(a, divider)
b = unsafe_div(b, divider)
c = unsafe_div(c, divider)
d = unsafe_div(d, divider)
# delta0 = 3*a*c/b - b: here we can do more unsafe ops now:
delta0 = unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b) - b
# delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1 = 3 * delta0 + b - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b)
# sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0
sqrt_arg: int256 = delta1**2 + unsafe_mul(unsafe_div(4*delta0**2, b), delta0)
sqrt_val: int256 = 0
if sqrt_arg > 0:
sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256)
else:
return [
self._newton_y(_ANN, _gamma, _x, _D, i),
0
]
b_cbrt: int256 = 0
if b > 0:
b_cbrt = convert(self._cbrt(convert(b, uint256)), int256)
else:
b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256)
second_cbrt: int256 = 0
if delta1 > 0:
# second_cbrt = convert(self._cbrt(convert((delta1 + sqrt_val), uint256) / 2), int256)
second_cbrt = convert(self._cbrt(convert(unsafe_add(delta1, sqrt_val), uint256) / 2), int256)
else:
# second_cbrt = -convert(self._cbrt(convert(unsafe_sub(sqrt_val, delta1), uint256) / 2), int256)
second_cbrt = -convert(self._cbrt(unsafe_div(convert(unsafe_sub(sqrt_val, delta1), uint256), 2)), int256)
# C1: int256 = b_cbrt**2/10**18*second_cbrt/10**18
C1: int256 = unsafe_div(unsafe_mul(unsafe_div(b_cbrt**2, 10**18), second_cbrt), 10**18)
# root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here.
root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_mul(10**18, b)/C1*delta0)/unsafe_mul(3, a)
# y_out: uint256[2] = [
# convert(D**2/x_j*root/4/10**18, uint256), # <--- y
# convert(root, uint256) # <----------------------- K0Prev
# ]
y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)]
frac: uint256 = unsafe_div(y_out[0] * 10**18, _D)
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y
return y_out
```
::::
### `newton_D`
::::description[`Math.newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev: uint256 = 0) -> uint256:`]
Function to find the D invariant using the Newton method.
| Input | Type | Description |
|--------------|---------------------|-----------------------------------------------------------------------------|
| `_ANN` | `uint256` | Amplification coefficient adjusted by N; `_ANN = A * N^N`. |
| `gamma` | `uint256` | Gamma value. |
| `x_unsorted` | `uint256[N_COINS]` | Unsorted array of the pool balances. |
| `K0_prev` | `uint256` | A priori for Newton's method derived from `get_y_int`. Defaults to zero if no a priori is provided. |
Returns: D invariant (`uint256`).
```vyper
@external
@view
def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev: uint256 = 0) -> uint256:
"""
Finding the invariant using Newton method.
ANN is higher by the factor A_MULTIPLIER
ANN is already A * N**N
"""
# Safety checks
assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A
assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
# Initial value of invariant D is that for constant-product invariant
x: uint256[N_COINS] = x_unsorted
if x[0] < x[1]:
x = [x_unsorted[1], x_unsorted[0]]
assert x[0] > 10**9 - 1 and x[0] < 10**15 * 10**18 + 1 # dev: unsafe values x[0]
assert unsafe_div(x[1] * 10**18, x[0]) > 10**14 - 1 # dev: unsafe values x[i] (input)
S: uint256 = unsafe_add(x[0], x[1]) # can unsafe add here because we checked x[0] bounds
D: uint256 = 0
if K0_prev == 0:
D = N_COINS * isqrt(unsafe_mul(x[0], x[1]))
else:
# D = isqrt(x[0] * x[1] * 4 / K0_prev * 10**18)
D = isqrt(unsafe_mul(unsafe_div(unsafe_mul(unsafe_mul(4, x[0]), x[1]), K0_prev), 10**18))
if S < D:
D = S
__g1k0: uint256 = gamma + 10**18
diff: uint256 = 0
for i in range(255):
D_prev: uint256 = D
assert D > 0
# Unsafe division by D and D_prev is now safe
# K0: uint256 = 10**18
# for _x in x:
# K0 = K0 * _x * N_COINS / D
# collapsed for 2 coins
K0: uint256 = unsafe_div(unsafe_div((10**18 * N_COINS**2) * x[0], D) * x[1], D)
_g1k0: uint256 = __g1k0
if _g1k0 > K0:
_g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1) # > 0
else:
_g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1) # > 0
# D / (A * N**N) * _g1k0**2 / gamma**2
mul1: uint256 = unsafe_div(unsafe_div(unsafe_div(10**18 * D, gamma) * _g1k0, gamma) * _g1k0 * A_MULTIPLIER, ANN)
# 2*N*K0 / _g1k0
mul2: uint256 = unsafe_div(((2 * 10**18) * N_COINS) * K0, _g1k0)
# calculate neg_fprime. here K0 > 0 is being validated (safediv).
neg_fprime: uint256 = (S + unsafe_div(S * mul2, 10**18)) + mul1 * N_COINS / K0 - unsafe_div(mul2 * D, 10**18)
# D -= f / fprime; neg_fprime safediv being validated
D_plus: uint256 = D * (neg_fprime + S) / neg_fprime
D_minus: uint256 = unsafe_div(D * D, neg_fprime)
if 10**18 > K0:
D_minus += unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(10**18, K0), K0)
else:
D_minus -= unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(K0, 10**18), K0)
if D_plus > D_minus:
D = unsafe_sub(D_plus, D_minus)
else:
D = unsafe_div(unsafe_sub(D_minus, D_plus), 2)
if D > D_prev:
diff = unsafe_sub(D, D_prev)
else:
diff = unsafe_sub(D_prev, D)
if diff * 10**14 < max(10**16, D): # Could reduce precision for gas efficiency here
for _x in x:
frac: uint256 = _x * 10**18 / D
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe values x[i]
return D
raise "Did not converge"
```
::::
### `get_p`
::::description[`Math.get_p(_xp: uint256[N_COINS], _D: uint256, _A_gamma: uint256[N_COINS]) -> uint256:`]
Function to calculate dx/dy. The output needs to be multiplied with `price_scale` to get the actual value. The function is externally called form a twocrypto-ng pools when prices are tweaked via [`tweak_price`](../pools/twocrypto.md#oracle-methods).
| Input | Type | Description |
| ----- | ---- | ----------- |
| `_xp` | `uint256[N_COINS]` | Balances of the pool. |
| `_D` | `uint256` | Current value of D. |
| `_A_gamma` | `uint256[N_COINS]` | Amplification coefficient and gamma. |
Returns: dx/dy (`uint256`).
```vyper
@external
@view
def get_p(
_xp: uint256[N_COINS], _D: uint256, _A_gamma: uint256[N_COINS]
) -> uint256:
"""
@notice Calculates dx/dy.
@dev Output needs to be multiplied with price_scale to get the actual value.
@param _xp Balances of the pool.
@param _D Current value of D.
@param _A_gamma Amplification coefficient and gamma.
"""
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe D values
# K0 = P * N**N / D**N.
# K0 is dimensionless and has 10**36 precision:
K0: uint256 = unsafe_div(
unsafe_div(4 * _xp[0] * _xp[1], _D) * 10**36,
_D
)
# GK0 is in 10**36 precision and is dimensionless.
# GK0 = (
# 2 * _K0 * _K0 / 10**36 * _K0 / 10**36
# + (gamma + 10**18)**2
# - (_K0 * _K0 / 10**36 * (2 * gamma + 3 * 10**18) / 10**18)
# )
# GK0 is always positive. So the following should never revert:
GK0: uint256 = (
unsafe_div(unsafe_div(2 * K0 * K0, 10**36) * K0, 10**36)
+ pow_mod256(unsafe_add(_A_gamma[1], 10**18), 2)
- unsafe_div(
unsafe_div(pow_mod256(K0, 2), 10**36) * unsafe_add(unsafe_mul(2, _A_gamma[1]), 3 * 10**18),
10**18
)
)
# NNAG2 = N**N * A * gamma**2
NNAG2: uint256 = unsafe_div(unsafe_mul(_A_gamma[0], pow_mod256(_A_gamma[1], 2)), A_MULTIPLIER)
# denominator = (GK0 + NNAG2 * x / D * _K0 / 10**36)
denominator: uint256 = (GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[0], _D) * K0, 10**36) )
# p_xy = x * (GK0 + NNAG2 * y / D * K0 / 10**36) / y * 10**18 / denominator
# p is in 10**18 precision.
return unsafe_div(
_xp[0] * ( GK0 + unsafe_div(unsafe_div(NNAG2 * _xp[1], _D) * K0, 10**36) ) / _xp[1] * 10**18,
denominator
)
```
::::
### `wad_exp`
::::description[`Math.wad_exp(x: int256) -> int256:`]
Function to calculate the natural exponential function of a signed integer with a precision of 1e18.
| Input | Type | Description |
| ----- | ---- | ----------- |
| `x` | `int256` | 32-byte variable |
Returns: 32-byte calculation result (`int256`).
```vyper
@external
@pure
def wad_exp(x: int256) -> int256:
"""
@dev Calculates the natural exponential function of a signed integer with
a precision of 1e18.
@notice Note that this function consumes about 810 gas units. The implementation
is inspired by Remco Bloemen's implementation under the MIT license here:
https://xn--2-umb.com/22/exp-ln.
@param x The 32-byte variable.
@return int256 The 32-byte calculation result.
"""
value: int256 = x
# If the result is `< 0.5`, we return zero. This happens when we have the following:
# "x <= floor(log(0.5e18) * 1e18) ~ -42e18".
if (x <= -42_139_678_854_452_767_551):
return empty(int256)
# When the result is "> (2 **255 - 1) / 1e18" we cannot represent it as a signed integer.
# This happens when "x >= floor(log((2 **255 - 1) / 1e18) * 1e18) ~ 135".
assert x < 135_305_999_368_893_231_589, "Math: wad_exp overflow"
# `x` is now in the range "(-42, 136) * 1e18". Convert to "(-42, 136) * 2 **96" for higher
# intermediate precision and a binary base. This base conversion is a multiplication with
# "1e18 / 2 **96 = 5 **18 / 2 **78".
value = unsafe_div(x << 78, 5 **18)
# Reduce the range of `x` to "(-½ ln 2, ½ ln 2) * 2 **96" by factoring out powers of two
# so that "exp(x) = exp(x') * 2 **k", where `k` is a signer integer. Solving this gives
# "k = round(x / log(2))" and "x' = x - k * log(2)". Thus, `k` is in the range "[-61, 195]".
k: int256 = unsafe_add(unsafe_div(value << 96, 54_916_777_467_707_473_351_141_471_128), 2 **95) >> 96
value = unsafe_sub(value, unsafe_mul(k, 54_916_777_467_707_473_351_141_471_128))
# Evaluate using a "(6, 7)"-term rational approximation. Since `p` is monic,
# we will multiply by a scaling factor later.
y: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 1_346_386_616_545_796_478_920_950_773_328), value) >> 96, 57_155_421_227_552_351_082_224_309_758_442)
p: int256 = unsafe_add(unsafe_mul(unsafe_add(unsafe_mul(unsafe_sub(unsafe_add(y, value), 94_201_549_194_550_492_254_356_042_504_812), y) >> 96,\
28_719_021_644_029_726_153_956_944_680_412_240), value), 4_385_272_521_454_847_904_659_076_985_693_276 << 96)
# We leave `p` in the "2 **192" base so that we do not have to scale it up
# again for the division.
q: int256 = unsafe_add(unsafe_mul(unsafe_sub(value, 2_855_989_394_907_223_263_936_484_059_900), value) >> 96, 50_020_603_652_535_783_019_961_831_881_945)
q = unsafe_sub(unsafe_mul(q, value) >> 96, 533_845_033_583_426_703_283_633_433_725_380)
q = unsafe_add(unsafe_mul(q, value) >> 96, 3_604_857_256_930_695_427_073_651_918_091_429)
q = unsafe_sub(unsafe_mul(q, value) >> 96, 14_423_608_567_350_463_180_887_372_962_807_573)
q = unsafe_add(unsafe_mul(q, value) >> 96, 26_449_188_498_355_588_339_934_803_723_976_023)
# The polynomial `q` has no zeros in the range because all its roots are complex.
# No scaling is required, as `p` is already "2 **96" too large. Also,
# `r` is in the range "(0.09, 0.25) * 2**96" after the division.
r: int256 = unsafe_div(p, q)
# To finalise the calculation, we have to multiply `r` by:
# - the scale factor "s = ~6.031367120",
# - the factor "2 **k" from the range reduction, and
# - the factor "1e18 / 2 **96" for the base conversion.
# We do this all at once, with an intermediate result in "2**213" base,
# so that the final right shift always gives a positive value.
# Note that to circumvent Vyper's safecast feature for the potentially
# negative parameter value `r`, we first convert `r` to `bytes32` and
# subsequently to `uint256`. Remember that the EVM default behaviour is
# to use two's complement representation to handle signed integers.
return convert(unsafe_mul(convert(convert(r, bytes32), uint256), 3_822_833_074_963_236_453_042_738_258_902_158_003_155_416_615_667) >>\
convert(unsafe_sub(195, k), uint256), int256)
```
::::
### `version`
::::description[`Math.version() -> String[8]: view `]
Getter for the version of the contract.
Returns: Version of the contract (`String[8]`).
```vyper
version: public(constant(String[8])) = "v2.0.0"
```
```shell
>>> Math.version()
'v2.0.0'
```
::::
---
## Views Contract(3)
The Views Contract contains **view-only external methods**, which **may be gas-inefficient when called from within smart contracts**. However, it can be highly useful for searches, aggregators, or other entities looking to integrate with twocrypto-ng pools.
:::deploy[Contract Source & Deployment]
Source code for this contract is available on [Github](https://github.com/curvefi/twocrypto-ng/blob/main/contracts/main/CurveCryptoViews2Optimized.vy).
Full list of all deployments can be found [here](../../../deployments.md).
:::
---
## Exchange Methods
### `get_dy`
::::description[`Views.get_dy(i: uint256, j: uint256, dx: uint256, swap: address) -> uint256: view`]
Function to calculate the amount of coin `j` tokens received for swapping in `dx` amount of coin `i` tokens. This function includes fees.
| Input | Type | Description |
|-------|-----------|---------------------------------------------------------------------------|
| `i` | `uint256` | Index of the input token (use `pool.coins(i)` to get the coin address at the i-th index). |
| `j` | `uint256` | Index of the output token. |
| `dx` | `uint256` | Amount of input coin[i] tokens to be swapped. |
| `swap`| `address` | Address of the pool contract where the swap will occur. |
Returns: `dy` (`uint256`).
```vyper
@external
@view
def get_dy(
i: uint256, j: uint256, dx: uint256, swap: address
) -> uint256:
dy: uint256 = 0
xp: uint256[N_COINS] = empty(uint256[N_COINS])
# dy = (get_y(x + dx) - y) * (1 - fee)
dy, xp = self._get_dy_nofee(i, j, dx, swap)
dy -= Curve(swap).fee_calc(xp) * dy / 10**10
return dy
@internal
@view
def _get_dy_nofee(
i: uint256, j: uint256, dx: uint256, swap: address
) -> (uint256, uint256[N_COINS]):
assert i != j and i < N_COINS and j < N_COINS, "coin index out of range"
assert dx > 0, "do not exchange 0 coins"
math: Math = Curve(swap).MATH()
xp: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
price_scale: uint256 = 0
D: uint256 = 0
token_supply: uint256 = 0
A: uint256 = 0
gamma: uint256 = 0
xp, D, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap)
# adjust xp with input dx
xp[i] += dx
xp = [
xp[0] * precisions[0],
xp[1] * price_scale * precisions[1] / PRECISION
]
y_out: uint256[2] = math.get_y(A, gamma, xp, D, j)
dy: uint256 = xp[j] - y_out[0] - 1
xp[j] = y_out[0]
if j > 0:
dy = dy * PRECISION / price_scale
dy /= precisions[j]
return dy, xp
```
```vyper
@external
@view
def fee_calc(xp: uint256[N_COINS]) -> uint256: # <----- For by view contract.
"""
@notice Returns the fee charged by the pool at current state.
@param xp The current balances of the pool multiplied by coin precisions.
@return uint256 Fee value.
"""
return self._fee(xp)
@internal
@view
def _fee(xp: uint256[N_COINS]) -> uint256:
fee_params: uint256[3] = self._unpack_3(self.packed_fee_params)
f: uint256 = xp[0] + xp[1]
f = fee_params[2] * 10**18 / (
fee_params[2] + 10**18 -
(10**18 * N_COINS**N_COINS) * xp[0] / f * xp[1] / f
)
return unsafe_div(
fee_params[0] * f + fee_params[1] * (10**18 - f),
10**18
)
```
```vyper
@external
@pure
def get_y(
_ANN: uint256,
_gamma: uint256,
_x: uint256[N_COINS],
_D: uint256,
i: uint256
) -> uint256[2]:
# Safety checks
assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A
assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D
ANN: int256 = convert(_ANN, int256)
gamma: int256 = convert(_gamma, int256)
D: int256 = convert(_D, int256)
x_j: int256 = convert(_x[1 - i], int256)
gamma2: int256 = unsafe_mul(gamma, gamma)
# savediv by x_j done here:
y: int256 = D**2 / (x_j * N_COINS**2)
# K0_i: int256 = (10**18 * N_COINS) * x_j / D
K0_i: int256 = unsafe_div(10**18 * N_COINS * x_j, D)
assert (K0_i > 10**16 * N_COINS - 1) and (K0_i < 10**20 * N_COINS + 1) # dev: unsafe values x[i]
ann_gamma2: int256 = ANN * gamma2
# a = 10**36 / N_COINS**2
a: int256 = 10**32
# b = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14
b: int256 = (
D*ann_gamma2/400000000/x_j
- convert(unsafe_mul(10**32, 3), int256)
- unsafe_mul(unsafe_mul(2, gamma), 10**14)
)
# c = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4
c: int256 = (
unsafe_mul(10**32, convert(3, int256))
+ unsafe_mul(unsafe_mul(4, gamma), 10**14)
+ unsafe_div(gamma2, 10**4)
+ unsafe_div(unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) * x_j, D)
- unsafe_div(unsafe_mul(4, ann_gamma2), 400000000)
)
# d = -(10**18+gamma)**2 / 10**4
d: int256 = -unsafe_div(unsafe_add(10**18, gamma) **2, 10**4)
# delta0: int256 = 3*a*c/b - b
delta0: int256 = 3 * a * c / b - b # safediv by b
# delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1: int256 = 3 * delta0 + b - 27*a**2/b*d/b
divider: int256 = 1
threshold: int256 = min(min(abs(delta0), abs(delta1)), a)
if threshold > 10**48:
divider = 10**30
elif threshold > 10**46:
divider = 10**28
elif threshold > 10**44:
divider = 10**26
elif threshold > 10**42:
divider = 10**24
elif threshold > 10**40:
divider = 10**22
elif threshold > 10**38:
divider = 10**20
elif threshold > 10**36:
divider = 10**18
elif threshold > 10**34:
divider = 10**16
elif threshold > 10**32:
divider = 10**14
elif threshold > 10**30:
divider = 10**12
elif threshold > 10**28:
divider = 10**10
elif threshold > 10**26:
divider = 10**8
elif threshold > 10**24:
divider = 10**6
elif threshold > 10**20:
divider = 10**2
a = unsafe_div(a, divider)
b = unsafe_div(b, divider)
c = unsafe_div(c, divider)
d = unsafe_div(d, divider)
# delta0 = 3*a*c/b - b: here we can do more unsafe ops now:
delta0 = unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b) - b
# delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1 = 3 * delta0 + b - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b)
# sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0
sqrt_arg: int256 = delta1**2 + unsafe_mul(unsafe_div(4*delta0**2, b), delta0)
sqrt_val: int256 = 0
if sqrt_arg > 0:
sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256)
else:
return [
self._newton_y(_ANN, _gamma, _x, _D, i),
0
]
b_cbrt: int256 = 0
if b > 0:
b_cbrt = convert(self._cbrt(convert(b, uint256)), int256)
else:
b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256)
second_cbrt: int256 = 0
if delta1 > 0:
# second_cbrt = convert(self._cbrt(convert((delta1 + sqrt_val), uint256) / 2), int256)
second_cbrt = convert(self._cbrt(convert(unsafe_add(delta1, sqrt_val), uint256) / 2), int256)
else:
# second_cbrt = -convert(self._cbrt(convert(unsafe_sub(sqrt_val, delta1), uint256) / 2), int256)
second_cbrt = -convert(self._cbrt(unsafe_div(convert(unsafe_sub(sqrt_val, delta1), uint256), 2)), int256)
# C1: int256 = b_cbrt**2/10**18*second_cbrt/10**18
C1: int256 = unsafe_div(unsafe_mul(unsafe_div(b_cbrt**2, 10**18), second_cbrt), 10**18)
# root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here.
root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_mul(10**18, b)/C1*delta0)/unsafe_mul(3, a)
# y_out: uint256[2] = [
# convert(D**2/x_j*root/4/10**18, uint256), # <--- y
# convert(root, uint256) # <----------------------- K0Prev
# ]
y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)]
frac: uint256 = unsafe_div(y_out[0] * 10**18, _D)
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y
return y_out
```
```shell
>>> Views.get_dy(0, 1, 100, pool) # swaps 100 tokens coin(0) to coin(1) within "pool"
returns dy # tokens received
```
::::
### `get_dx`
::::description[`Views.get_dx(i: uint256, j: uint256, dy: uint256, swap: address) -> uint256: view`]
Getter method for the amount of coin `i` tokens required to input for swapping out `dy` amount of coin `j`.
| Input | Type | Description |
|-------|-----------|----------------------------------------------------------------------------|
| `i` | `uint256` | Index of the input token (use `pool.coins(i)` to get the coin address at the i-th index). |
| `j` | `uint256` | Index of the output token. |
| `dy` | `uint256` | Desired amount of output coin[j] tokens to receive. |
| `swap`| `address` | Address of the pool contract where the swap will occur. |
Returns: `dx` (`uint256`).
```vyper
@view
@external
def get_dx(
i: uint256, j: uint256, dy: uint256, swap: address
) -> uint256:
dx: uint256 = 0
xp: uint256[N_COINS] = empty(uint256[N_COINS])
fee_dy: uint256 = 0
_dy: uint256 = dy
# for more precise dx (but never exact), increase num loops
for k in range(5):
dx, xp = self._get_dx_fee(i, j, _dy, swap)
fee_dy = Curve(swap).fee_calc(xp) * _dy / 10**10
_dy = dy + fee_dy + 1
return dx
@internal
@view
def _get_dx_fee(
i: uint256, j: uint256, dy: uint256, swap: address
) -> (uint256, uint256[N_COINS]):
# here, dy must include fees (and 1 wei offset)
assert i != j and i < N_COINS and j < N_COINS, "coin index out of range"
assert dy > 0, "do not exchange out 0 coins"
math: Math = Curve(swap).MATH()
xp: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
price_scale: uint256 = 0
D: uint256 = 0
token_supply: uint256 = 0
A: uint256 = 0
gamma: uint256 = 0
xp, D, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap)
# adjust xp with output dy. dy contains fee element, which we handle later
# (hence this internal method is called _get_dx_fee)
xp[j] -= dy
xp = [xp[0] * precisions[0], xp[1] * price_scale * precisions[1] / PRECISION]
x_out: uint256[2] = math.get_y(A, gamma, xp, D, i)
dx: uint256 = x_out[0] - xp[i]
xp[i] = x_out[0]
if i > 0:
dx = dx * PRECISION / price_scale
dx /= precisions[i]
return dx, xp
```
```vyper
@external
@view
def fee_calc(xp: uint256[N_COINS]) -> uint256: # <----- For by view contract.
"""
@notice Returns the fee charged by the pool at current state.
@param xp The current balances of the pool multiplied by coin precisions.
@return uint256 Fee value.
"""
return self._fee(xp)
@internal
@view
def _fee(xp: uint256[N_COINS]) -> uint256:
fee_params: uint256[3] = self._unpack_3(self.packed_fee_params)
f: uint256 = xp[0] + xp[1]
f = fee_params[2] * 10**18 / (
fee_params[2] + 10**18 -
(10**18 * N_COINS**N_COINS) * xp[0] / f * xp[1] / f
)
return unsafe_div(
fee_params[0] * f + fee_params[1] * (10**18 - f),
10**18
)
```
```vyper
@external
@pure
def get_y(
_ANN: uint256,
_gamma: uint256,
_x: uint256[N_COINS],
_D: uint256,
i: uint256
) -> uint256[2]:
# Safety checks
assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A
assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D
ANN: int256 = convert(_ANN, int256)
gamma: int256 = convert(_gamma, int256)
D: int256 = convert(_D, int256)
x_j: int256 = convert(_x[1 - i], int256)
gamma2: int256 = unsafe_mul(gamma, gamma)
# savediv by x_j done here:
y: int256 = D**2 / (x_j * N_COINS**2)
# K0_i: int256 = (10**18 * N_COINS) * x_j / D
K0_i: int256 = unsafe_div(10**18 * N_COINS * x_j, D)
assert (K0_i > 10**16 * N_COINS - 1) and (K0_i < 10**20 * N_COINS + 1) # dev: unsafe values x[i]
ann_gamma2: int256 = ANN * gamma2
# a = 10**36 / N_COINS**2
a: int256 = 10**32
# b = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14
b: int256 = (
D*ann_gamma2/400000000/x_j
- convert(unsafe_mul(10**32, 3), int256)
- unsafe_mul(unsafe_mul(2, gamma), 10**14)
)
# c = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4
c: int256 = (
unsafe_mul(10**32, convert(3, int256))
+ unsafe_mul(unsafe_mul(4, gamma), 10**14)
+ unsafe_div(gamma2, 10**4)
+ unsafe_div(unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) * x_j, D)
- unsafe_div(unsafe_mul(4, ann_gamma2), 400000000)
)
# d = -(10**18+gamma)**2 / 10**4
d: int256 = -unsafe_div(unsafe_add(10**18, gamma) **2, 10**4)
# delta0: int256 = 3*a*c/b - b
delta0: int256 = 3 * a * c / b - b # safediv by b
# delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1: int256 = 3 * delta0 + b - 27*a**2/b*d/b
divider: int256 = 1
threshold: int256 = min(min(abs(delta0), abs(delta1)), a)
if threshold > 10**48:
divider = 10**30
elif threshold > 10**46:
divider = 10**28
elif threshold > 10**44:
divider = 10**26
elif threshold > 10**42:
divider = 10**24
elif threshold > 10**40:
divider = 10**22
elif threshold > 10**38:
divider = 10**20
elif threshold > 10**36:
divider = 10**18
elif threshold > 10**34:
divider = 10**16
elif threshold > 10**32:
divider = 10**14
elif threshold > 10**30:
divider = 10**12
elif threshold > 10**28:
divider = 10**10
elif threshold > 10**26:
divider = 10**8
elif threshold > 10**24:
divider = 10**6
elif threshold > 10**20:
divider = 10**2
a = unsafe_div(a, divider)
b = unsafe_div(b, divider)
c = unsafe_div(c, divider)
d = unsafe_div(d, divider)
# delta0 = 3*a*c/b - b: here we can do more unsafe ops now:
delta0 = unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b) - b
# delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1 = 3 * delta0 + b - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b)
# sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0
sqrt_arg: int256 = delta1**2 + unsafe_mul(unsafe_div(4*delta0**2, b), delta0)
sqrt_val: int256 = 0
if sqrt_arg > 0:
sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256)
else:
return [
self._newton_y(_ANN, _gamma, _x, _D, i),
0
]
b_cbrt: int256 = 0
if b > 0:
b_cbrt = convert(self._cbrt(convert(b, uint256)), int256)
else:
b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256)
second_cbrt: int256 = 0
if delta1 > 0:
# second_cbrt = convert(self._cbrt(convert((delta1 + sqrt_val), uint256) / 2), int256)
second_cbrt = convert(self._cbrt(convert(unsafe_add(delta1, sqrt_val), uint256) / 2), int256)
else:
# second_cbrt = -convert(self._cbrt(convert(unsafe_sub(sqrt_val, delta1), uint256) / 2), int256)
second_cbrt = -convert(self._cbrt(unsafe_div(convert(unsafe_sub(sqrt_val, delta1), uint256), 2)), int256)
# C1: int256 = b_cbrt**2/10**18*second_cbrt/10**18
C1: int256 = unsafe_div(unsafe_mul(unsafe_div(b_cbrt**2, 10**18), second_cbrt), 10**18)
# root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here.
root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_mul(10**18, b)/C1*delta0)/unsafe_mul(3, a)
# y_out: uint256[2] = [
# convert(D**2/x_j*root/4/10**18, uint256), # <--- y
# convert(root, uint256) # <----------------------- K0Prev
# ]
y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)]
frac: uint256 = unsafe_div(y_out[0] * 10**18, _D)
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y
return y_out
```
```shell
>>> Views.get_dx(0, 1, 100, pool) # how much of coin(0) to input in order to get 100 of coin(1)
returns dx
```
::::
### `calc_fee_get_dy`
::::description[`Views.calc_fee_get_dy(i: uint256, j: uint256, dx: uint256, swap: address) -> uint256: view`]
Function to calculate the fees for `get_dy`.
| Input | Type | Description |
|-------|-----------|---------------------------------------------------------------------------|
| `i` | `uint256` | Index of the input token (use `pool.coins(i)` to get the coin address at the i-th index). |
| `j` | `uint256` | Index of the output token. |
| `dx` | `uint256` | Amount of input coin[i] tokens. |
| `swap`| `address` | Address of the pool contract. |
Returns: Approximate fee (`uint256`).
```vyper
@external
@view
def calc_fee_get_dy(i: uint256, j: uint256, dx: uint256, swap: address
) -> uint256:
dy: uint256 = 0
xp: uint256[N_COINS] = empty(uint256[N_COINS])
dy, xp = self._get_dy_nofee(i, j, dx, swap)
return Curve(swap).fee_calc(xp) * dy / 10**10
@internal
@view
def _get_dy_nofee(
i: uint256, j: uint256, dx: uint256, swap: address
) -> (uint256, uint256[N_COINS]):
assert i != j and i < N_COINS and j < N_COINS, "coin index out of range"
assert dx > 0, "do not exchange 0 coins"
math: Math = Curve(swap).MATH()
xp: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
price_scale: uint256 = 0
D: uint256 = 0
token_supply: uint256 = 0
A: uint256 = 0
gamma: uint256 = 0
xp, D, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap)
# adjust xp with input dx
xp[i] += dx
xp = [
xp[0] * precisions[0],
xp[1] * price_scale * precisions[1] / PRECISION
]
y_out: uint256[2] = math.get_y(A, gamma, xp, D, j)
dy: uint256 = xp[j] - y_out[0] - 1
xp[j] = y_out[0]
if j > 0:
dy = dy * PRECISION / price_scale
dy /= precisions[j]
return dy, xp
```
```vyper
@external
@view
def fee_calc(xp: uint256[N_COINS]) -> uint256: # <----- For by view contract.
"""
@notice Returns the fee charged by the pool at current state.
@param xp The current balances of the pool multiplied by coin precisions.
@return uint256 Fee value.
"""
return self._fee(xp)
@internal
@view
def _fee(xp: uint256[N_COINS]) -> uint256:
fee_params: uint256[3] = self._unpack_3(self.packed_fee_params)
f: uint256 = xp[0] + xp[1]
f = fee_params[2] * 10**18 / (
fee_params[2] + 10**18 -
(10**18 * N_COINS**N_COINS) * xp[0] / f * xp[1] / f
)
return unsafe_div(
fee_params[0] * f + fee_params[1] * (10**18 - f),
10**18
)
```
```vyper
@external
@pure
def get_y(
_ANN: uint256,
_gamma: uint256,
_x: uint256[N_COINS],
_D: uint256,
i: uint256
) -> uint256[2]:
# Safety checks
assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A
assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D
ANN: int256 = convert(_ANN, int256)
gamma: int256 = convert(_gamma, int256)
D: int256 = convert(_D, int256)
x_j: int256 = convert(_x[1 - i], int256)
gamma2: int256 = unsafe_mul(gamma, gamma)
# savediv by x_j done here:
y: int256 = D**2 / (x_j * N_COINS**2)
# K0_i: int256 = (10**18 * N_COINS) * x_j / D
K0_i: int256 = unsafe_div(10**18 * N_COINS * x_j, D)
assert (K0_i > 10**16 * N_COINS - 1) and (K0_i < 10**20 * N_COINS + 1) # dev: unsafe values x[i]
ann_gamma2: int256 = ANN * gamma2
# a = 10**36 / N_COINS**2
a: int256 = 10**32
# b = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14
b: int256 = (
D*ann_gamma2/400000000/x_j
- convert(unsafe_mul(10**32, 3), int256)
- unsafe_mul(unsafe_mul(2, gamma), 10**14)
)
# c = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4
c: int256 = (
unsafe_mul(10**32, convert(3, int256))
+ unsafe_mul(unsafe_mul(4, gamma), 10**14)
+ unsafe_div(gamma2, 10**4)
+ unsafe_div(unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) * x_j, D)
- unsafe_div(unsafe_mul(4, ann_gamma2), 400000000)
)
# d = -(10**18+gamma)**2 / 10**4
d: int256 = -unsafe_div(unsafe_add(10**18, gamma) **2, 10**4)
# delta0: int256 = 3*a*c/b - b
delta0: int256 = 3 * a * c / b - b # safediv by b
# delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1: int256 = 3 * delta0 + b - 27*a**2/b*d/b
divider: int256 = 1
threshold: int256 = min(min(abs(delta0), abs(delta1)), a)
if threshold > 10**48:
divider = 10**30
elif threshold > 10**46:
divider = 10**28
elif threshold > 10**44:
divider = 10**26
elif threshold > 10**42:
divider = 10**24
elif threshold > 10**40:
divider = 10**22
elif threshold > 10**38:
divider = 10**20
elif threshold > 10**36:
divider = 10**18
elif threshold > 10**34:
divider = 10**16
elif threshold > 10**32:
divider = 10**14
elif threshold > 10**30:
divider = 10**12
elif threshold > 10**28:
divider = 10**10
elif threshold > 10**26:
divider = 10**8
elif threshold > 10**24:
divider = 10**6
elif threshold > 10**20:
divider = 10**2
a = unsafe_div(a, divider)
b = unsafe_div(b, divider)
c = unsafe_div(c, divider)
d = unsafe_div(d, divider)
# delta0 = 3*a*c/b - b: here we can do more unsafe ops now:
delta0 = unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b) - b
# delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1 = 3 * delta0 + b - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b)
# sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0
sqrt_arg: int256 = delta1**2 + unsafe_mul(unsafe_div(4*delta0**2, b), delta0)
sqrt_val: int256 = 0
if sqrt_arg > 0:
sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256)
else:
return [
self._newton_y(_ANN, _gamma, _x, _D, i),
0
]
b_cbrt: int256 = 0
if b > 0:
b_cbrt = convert(self._cbrt(convert(b, uint256)), int256)
else:
b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256)
second_cbrt: int256 = 0
if delta1 > 0:
# second_cbrt = convert(self._cbrt(convert((delta1 + sqrt_val), uint256) / 2), int256)
second_cbrt = convert(self._cbrt(convert(unsafe_add(delta1, sqrt_val), uint256) / 2), int256)
else:
# second_cbrt = -convert(self._cbrt(convert(unsafe_sub(sqrt_val, delta1), uint256) / 2), int256)
second_cbrt = -convert(self._cbrt(unsafe_div(convert(unsafe_sub(sqrt_val, delta1), uint256), 2)), int256)
# C1: int256 = b_cbrt**2/10**18*second_cbrt/10**18
C1: int256 = unsafe_div(unsafe_mul(unsafe_div(b_cbrt**2, 10**18), second_cbrt), 10**18)
# root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here.
root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_mul(10**18, b)/C1*delta0)/unsafe_mul(3, a)
# y_out: uint256[2] = [
# convert(D**2/x_j*root/4/10**18, uint256), # <--- y
# convert(root, uint256) # <----------------------- K0Prev
# ]
y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)]
frac: uint256 = unsafe_div(y_out[0] * 10**18, _D)
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y
return y_out
```
```shell
>>> Views.calc_fee_get_dy(0, 1, 100, pool) # calculate fees for swapping 100 of coin 0 for coin 1
returns approx_fee
```
::::
## Methods for Adding/Removing Liquidity
### `calc_withdraw_one_coin`
::::description[`Views.calc_withdraw_one_coin(token_amount: uint256, i: uint256, swap: address) -> uint256: view`]
Function to calculate the output tokens (including fees) received when withdrawing LP tokens as a single coin.
| Input | Type | Description |
|---------------|-----------|------------------------------------------------------------------|
| `token_amount`| `uint256` | Amount of LP tokens to be withdrawn. |
| `i` | `uint256` | Index of the coin to withdraw in (use `Pool.coins(i)` to get the coin address at the i-th index). |
| `swap` | `address` | Address of the pool from which to withdraw. |
Returns: `dy` (`uint256`).
```vyper
@view
@external
def calc_withdraw_one_coin(
token_amount: uint256, i: uint256, swap: address
) -> uint256:
return self._calc_withdraw_one_coin(token_amount, i, swap)[0]
@internal
@view
def _calc_withdraw_one_coin(
token_amount: uint256,
i: uint256,
swap: address
) -> (uint256, uint256):
token_supply: uint256 = Curve(swap).totalSupply()
assert token_amount <= token_supply # dev: token amount more than supply
assert i < N_COINS # dev: coin out of range
math: Math = Curve(swap).MATH()
xx: uint256[N_COINS] = empty(uint256[N_COINS])
for k in range(N_COINS):
xx[k] = Curve(swap).balances(k)
precisions: uint256[N_COINS] = Curve(swap).precisions()
A: uint256 = Curve(swap).A()
gamma: uint256 = Curve(swap).gamma()
D0: uint256 = 0
p: uint256 = 0
price_scale_i: uint256 = Curve(swap).price_scale() * precisions[1]
xp: uint256[N_COINS] = [
xx[0] * precisions[0],
unsafe_div(xx[1] * price_scale_i, PRECISION)
]
if i == 0:
price_scale_i = PRECISION * precisions[0]
if Curve(swap).future_A_gamma_time() > block.timestamp:
D0 = math.newton_D(A, gamma, xp, 0)
else:
D0 = Curve(swap).D()
D: uint256 = D0
fee: uint256 = self._fee(xp, swap)
dD: uint256 = token_amount * D / token_supply
D_fee: uint256 = fee * dD / (2 * 10**10) + 1
approx_fee: uint256 = N_COINS * D_fee * xx[i] / D
D -= (dD - D_fee)
y_out: uint256[2] = math.get_y(A, gamma, xp, D, i)
dy: uint256 = (xp[i] - y_out[0]) * PRECISION / price_scale_i
xp[i] = y_out[0]
return dy, approx_fee
```
```vyper
@external
@pure
def get_y(
_ANN: uint256,
_gamma: uint256,
_x: uint256[N_COINS],
_D: uint256,
i: uint256
) -> uint256[2]:
# Safety checks
assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A
assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D
ANN: int256 = convert(_ANN, int256)
gamma: int256 = convert(_gamma, int256)
D: int256 = convert(_D, int256)
x_j: int256 = convert(_x[1 - i], int256)
gamma2: int256 = unsafe_mul(gamma, gamma)
# savediv by x_j done here:
y: int256 = D**2 / (x_j * N_COINS**2)
# K0_i: int256 = (10**18 * N_COINS) * x_j / D
K0_i: int256 = unsafe_div(10**18 * N_COINS * x_j, D)
assert (K0_i > 10**16 * N_COINS - 1) and (K0_i < 10**20 * N_COINS + 1) # dev: unsafe values x[i]
ann_gamma2: int256 = ANN * gamma2
# a = 10**36 / N_COINS**2
a: int256 = 10**32
# b = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14
b: int256 = (
D*ann_gamma2/400000000/x_j
- convert(unsafe_mul(10**32, 3), int256)
- unsafe_mul(unsafe_mul(2, gamma), 10**14)
)
# c = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4
c: int256 = (
unsafe_mul(10**32, convert(3, int256))
+ unsafe_mul(unsafe_mul(4, gamma), 10**14)
+ unsafe_div(gamma2, 10**4)
+ unsafe_div(unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) * x_j, D)
- unsafe_div(unsafe_mul(4, ann_gamma2), 400000000)
)
# d = -(10**18+gamma)**2 / 10**4
d: int256 = -unsafe_div(unsafe_add(10**18, gamma) **2, 10**4)
# delta0: int256 = 3*a*c/b - b
delta0: int256 = 3 * a * c / b - b # safediv by b
# delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1: int256 = 3 * delta0 + b - 27*a**2/b*d/b
divider: int256 = 1
threshold: int256 = min(min(abs(delta0), abs(delta1)), a)
if threshold > 10**48:
divider = 10**30
elif threshold > 10**46:
divider = 10**28
elif threshold > 10**44:
divider = 10**26
elif threshold > 10**42:
divider = 10**24
elif threshold > 10**40:
divider = 10**22
elif threshold > 10**38:
divider = 10**20
elif threshold > 10**36:
divider = 10**18
elif threshold > 10**34:
divider = 10**16
elif threshold > 10**32:
divider = 10**14
elif threshold > 10**30:
divider = 10**12
elif threshold > 10**28:
divider = 10**10
elif threshold > 10**26:
divider = 10**8
elif threshold > 10**24:
divider = 10**6
elif threshold > 10**20:
divider = 10**2
a = unsafe_div(a, divider)
b = unsafe_div(b, divider)
c = unsafe_div(c, divider)
d = unsafe_div(d, divider)
# delta0 = 3*a*c/b - b: here we can do more unsafe ops now:
delta0 = unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b) - b
# delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1 = 3 * delta0 + b - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b)
# sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0
sqrt_arg: int256 = delta1**2 + unsafe_mul(unsafe_div(4*delta0**2, b), delta0)
sqrt_val: int256 = 0
if sqrt_arg > 0:
sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256)
else:
return [
self._newton_y(_ANN, _gamma, _x, _D, i),
0
]
b_cbrt: int256 = 0
if b > 0:
b_cbrt = convert(self._cbrt(convert(b, uint256)), int256)
else:
b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256)
second_cbrt: int256 = 0
if delta1 > 0:
# second_cbrt = convert(self._cbrt(convert((delta1 + sqrt_val), uint256) / 2), int256)
second_cbrt = convert(self._cbrt(convert(unsafe_add(delta1, sqrt_val), uint256) / 2), int256)
else:
# second_cbrt = -convert(self._cbrt(convert(unsafe_sub(sqrt_val, delta1), uint256) / 2), int256)
second_cbrt = -convert(self._cbrt(unsafe_div(convert(unsafe_sub(sqrt_val, delta1), uint256), 2)), int256)
# C1: int256 = b_cbrt**2/10**18*second_cbrt/10**18
C1: int256 = unsafe_div(unsafe_mul(unsafe_div(b_cbrt**2, 10**18), second_cbrt), 10**18)
# root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here.
root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_mul(10**18, b)/C1*delta0)/unsafe_mul(3, a)
# y_out: uint256[2] = [
# convert(D**2/x_j*root/4/10**18, uint256), # <--- y
# convert(root, uint256) # <----------------------- K0Prev
# ]
y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)]
frac: uint256 = unsafe_div(y_out[0] * 10**18, _D)
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y
return y_out
```
```shell
>>> Views.calc_withdraw_one_coin(100, 0, pool) # withdraw 100 lp tokens in as coin(0) from "pool"
returns dy # amount of tokens received
```
::::
### `calc_token_amount`
::::description[`Views.calc_token_amount(amounts: uint256[N_COINS], deposit: bool, swap: address) -> uint256: view`]
Function to calculate LP tokens to be minted or burned when depositing or removing `amounts` of coins to or from the pool.
| Input | Type | Description |
|----------|---------------------|----------------------------------------------------------------------|
| `amounts`| `uint256[N_COINS]` | Array of amounts of coins being deposited or withdrawn. |
| `deposit`| `bool` | Indicates the action: `True` for deposit, `False` for withdrawal. |
| `swap` | `address` | Address of the pool contract involved in the transaction. |
Returns: `d_token` (`uint256`).
```vyper
@view
@external
def calc_token_amount(
amounts: uint256[N_COINS], deposit: bool, swap: address
) -> uint256:
d_token: uint256 = 0
amountsp: uint256[N_COINS] = empty(uint256[N_COINS])
xp: uint256[N_COINS] = empty(uint256[N_COINS])
d_token, amountsp, xp = self._calc_dtoken_nofee(amounts, deposit, swap)
d_token -= (
Curve(swap).calc_token_fee(amountsp, xp) * d_token / 10**10 + 1
)
return d_token
@internal
@view
def _calc_dtoken_nofee(
amounts: uint256[N_COINS], deposit: bool, swap: address
) -> (uint256, uint256[N_COINS], uint256[N_COINS]):
math: Math = Curve(swap).MATH()
xp: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
price_scale: uint256 = 0
D0: uint256 = 0
token_supply: uint256 = 0
A: uint256 = 0
gamma: uint256 = 0
xp, D0, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap)
amountsp: uint256[N_COINS] = amounts
if deposit:
for k in range(N_COINS):
xp[k] += amounts[k]
else:
for k in range(N_COINS):
xp[k] -= amounts[k]
xp = [
xp[0] * precisions[0],
xp[1] * price_scale * precisions[1] / PRECISION
]
amountsp = [
amountsp[0]* precisions[0],
amountsp[1] * price_scale * precisions[1] / PRECISION
]
D: uint256 = math.newton_D(A, gamma, xp, 0)
d_token: uint256 = token_supply * D / D0
if deposit:
d_token -= token_supply
else:
d_token = token_supply - d_token
return d_token, amountsp, xp
@internal
@view
def _prep_calc(swap: address) -> (
uint256[N_COINS],
uint256,
uint256,
uint256,
uint256,
uint256,
uint256[N_COINS]
):
precisions: uint256[N_COINS] = Curve(swap).precisions()
token_supply: uint256 = Curve(swap).totalSupply()
xp: uint256[N_COINS] = empty(uint256[N_COINS])
for k in range(N_COINS):
xp[k] = Curve(swap).balances(k)
price_scale: uint256 = Curve(swap).price_scale()
A: uint256 = Curve(swap).A()
gamma: uint256 = Curve(swap).gamma()
D: uint256 = self._calc_D_ramp(
A, gamma, xp, precisions, price_scale, swap
)
return xp, D, token_supply, price_scale, A, gamma, precisions
@internal
@view
def _calc_D_ramp(
A: uint256,
gamma: uint256,
xp: uint256[N_COINS],
precisions: uint256[N_COINS],
price_scale: uint256,
swap: address
) -> uint256:
math: Math = Curve(swap).MATH()
D: uint256 = Curve(swap).D()
if Curve(swap).future_A_gamma_time() > block.timestamp:
_xp: uint256[N_COINS] = xp
_xp[0] *= precisions[0]
_xp[1] = _xp[1] * price_scale * precisions[1] / PRECISION
D = math.newton_D(A, gamma, _xp, 0)
return D
```
```vyper
@external
@view
def calc_token_fee(
amounts: uint256[N_COINS], xp: uint256[N_COINS]
) -> uint256:
"""
@notice Returns the fee charged on the given amounts for add_liquidity.
@param amounts The amounts of coins being added to the pool.
@param xp The current balances of the pool multiplied by coin precisions.
@return uint256 Fee charged.
"""
return self._calc_token_fee(amounts, xp)
@view
@internal
def _calc_token_fee(amounts: uint256[N_COINS], xp: uint256[N_COINS]) -> uint256:
# fee = sum(amounts_i - avg(amounts)) * fee' / sum(amounts)
fee: uint256 = unsafe_div(
unsafe_mul(self._fee(xp), N_COINS),
unsafe_mul(4, unsafe_sub(N_COINS, 1))
)
S: uint256 = 0
for _x in amounts:
S += _x
avg: uint256 = unsafe_div(S, N_COINS)
Sdiff: uint256 = 0
for _x in amounts:
if _x > avg:
Sdiff += unsafe_sub(_x, avg)
else:
Sdiff += unsafe_sub(avg, _x)
return fee * Sdiff / S + NOISE_FEE
```
```vyper
@external
@view
def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev: uint256 = 0) -> uint256:
"""
Finding the invariant using Newton method.
ANN is higher by the factor A_MULTIPLIER
ANN is already A * N**N
"""
# Safety checks
assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A
assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
# Initial value of invariant D is that for constant-product invariant
x: uint256[N_COINS] = x_unsorted
if x[0] < x[1]:
x = [x_unsorted[1], x_unsorted[0]]
assert x[0] > 10**9 - 1 and x[0] < 10**15 * 10**18 + 1 # dev: unsafe values x[0]
assert unsafe_div(x[1] * 10**18, x[0]) > 10**14 - 1 # dev: unsafe values x[i] (input)
S: uint256 = unsafe_add(x[0], x[1]) # can unsafe add here because we checked x[0] bounds
D: uint256 = 0
if K0_prev == 0:
D = N_COINS * isqrt(unsafe_mul(x[0], x[1]))
else:
# D = isqrt(x[0] * x[1] * 4 / K0_prev * 10**18)
D = isqrt(unsafe_mul(unsafe_div(unsafe_mul(unsafe_mul(4, x[0]), x[1]), K0_prev), 10**18))
if S < D:
D = S
__g1k0: uint256 = gamma + 10**18
diff: uint256 = 0
for i in range(255):
D_prev: uint256 = D
assert D > 0
# Unsafe division by D and D_prev is now safe
# K0: uint256 = 10**18
# for _x in x:
# K0 = K0 * _x * N_COINS / D
# collapsed for 2 coins
K0: uint256 = unsafe_div(unsafe_div((10**18 * N_COINS**2) * x[0], D) * x[1], D)
_g1k0: uint256 = __g1k0
if _g1k0 > K0:
_g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1) # > 0
else:
_g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1) # > 0
# D / (A * N**N) * _g1k0**2 / gamma**2
mul1: uint256 = unsafe_div(unsafe_div(unsafe_div(10**18 * D, gamma) * _g1k0, gamma) * _g1k0 * A_MULTIPLIER, ANN)
# 2*N*K0 / _g1k0
mul2: uint256 = unsafe_div(((2 * 10**18) * N_COINS) * K0, _g1k0)
# calculate neg_fprime. here K0 > 0 is being validated (safediv).
neg_fprime: uint256 = (S + unsafe_div(S * mul2, 10**18)) + mul1 * N_COINS / K0 - unsafe_div(mul2 * D, 10**18)
# D -= f / fprime; neg_fprime safediv being validated
D_plus: uint256 = D * (neg_fprime + S) / neg_fprime
D_minus: uint256 = unsafe_div(D * D, neg_fprime)
if 10**18 > K0:
D_minus += unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(10**18, K0), K0)
else:
D_minus -= unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(K0, 10**18), K0)
if D_plus > D_minus:
D = unsafe_sub(D_plus, D_minus)
else:
D = unsafe_div(unsafe_sub(D_minus, D_plus), 2)
if D > D_prev:
diff = unsafe_sub(D, D_prev)
else:
diff = unsafe_sub(D_prev, D)
if diff * 10**14 < max(10**16, D): # Could reduce precision for gas efficiency here
for _x in x:
frac: uint256 = _x * 10**18 / D
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe values x[i]
return D
raise "Did not converge"
```
```shell
>>> Views.calc_token_amount([100, 100], True, pool) # depositing 100 of each coin into "pool"
returns d_token # LP tokens to be minted
>>> Views.calc_token_amount([100, 100], False, pool) # withdrawing 100 of each coin from "pool"
returns d_token # LP tokens to be burned
```
::::
### `calc_fee_withdraw_one_coin`
::::description[`Views.calc_fee_withdraw_one_coin(token_amount: uint256, i: uint256, swap: address) -> uint256: view`]
Function to calculate the fee for `withdraw_one_coin`.
| Input | Type | Description |
|---------------|-----------|---------------------------------------------------------------------------|
| `token_amount`| `uint256` | Amount of LP tokens involved in the withdrawal. |
| `i` | `uint256` | Index of the token to be withdrawn (use `pool.coins(i)` to get the coin address at the i-th index). |
| `swap` | `address` | Address of the pool contract from which the withdrawal is being made. |
Returns: Approximate fee (`uint256`).
```vyper
@external
@view
def calc_fee_withdraw_one_coin(
token_amount: uint256, i: uint256, swap: address
) -> uint256:
return self._calc_withdraw_one_coin(token_amount, i, swap)[1]
@internal
@view
def _calc_withdraw_one_coin(
token_amount: uint256,
i: uint256,
swap: address
) -> (uint256, uint256):
token_supply: uint256 = Curve(swap).totalSupply()
assert token_amount <= token_supply # dev: token amount more than supply
assert i < N_COINS # dev: coin out of range
math: Math = Curve(swap).MATH()
xx: uint256[N_COINS] = empty(uint256[N_COINS])
for k in range(N_COINS):
xx[k] = Curve(swap).balances(k)
precisions: uint256[N_COINS] = Curve(swap).precisions()
A: uint256 = Curve(swap).A()
gamma: uint256 = Curve(swap).gamma()
D0: uint256 = 0
p: uint256 = 0
price_scale_i: uint256 = Curve(swap).price_scale() * precisions[1]
xp: uint256[N_COINS] = [
xx[0] * precisions[0],
unsafe_div(xx[1] * price_scale_i, PRECISION)
]
if i == 0:
price_scale_i = PRECISION * precisions[0]
if Curve(swap).future_A_gamma_time() > block.timestamp:
D0 = math.newton_D(A, gamma, xp, 0)
else:
D0 = Curve(swap).D()
D: uint256 = D0
fee: uint256 = self._fee(xp, swap)
dD: uint256 = token_amount * D / token_supply
D_fee: uint256 = fee * dD / (2 * 10**10) + 1
approx_fee: uint256 = N_COINS * D_fee * xx[i] / D
D -= (dD - D_fee)
y_out: uint256[2] = math.get_y(A, gamma, xp, D, i)
dy: uint256 = (xp[i] - y_out[0]) * PRECISION / price_scale_i
xp[i] = y_out[0]
return dy, approx_fee
```
```vyper
@external
@pure
def get_y(
_ANN: uint256,
_gamma: uint256,
_x: uint256[N_COINS],
_D: uint256,
i: uint256
) -> uint256[2]:
# Safety checks
assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1 # dev: unsafe values A
assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1 # dev: unsafe values D
ANN: int256 = convert(_ANN, int256)
gamma: int256 = convert(_gamma, int256)
D: int256 = convert(_D, int256)
x_j: int256 = convert(_x[1 - i], int256)
gamma2: int256 = unsafe_mul(gamma, gamma)
# savediv by x_j done here:
y: int256 = D**2 / (x_j * N_COINS**2)
# K0_i: int256 = (10**18 * N_COINS) * x_j / D
K0_i: int256 = unsafe_div(10**18 * N_COINS * x_j, D)
assert (K0_i > 10**16 * N_COINS - 1) and (K0_i < 10**20 * N_COINS + 1) # dev: unsafe values x[i]
ann_gamma2: int256 = ANN * gamma2
# a = 10**36 / N_COINS**2
a: int256 = 10**32
# b = ANN*D*gamma2/4/10000/x_j/10**4 - 10**32*3 - 2*gamma*10**14
b: int256 = (
D*ann_gamma2/400000000/x_j
- convert(unsafe_mul(10**32, 3), int256)
- unsafe_mul(unsafe_mul(2, gamma), 10**14)
)
# c = 10**32*3 + 4*gamma*10**14 + gamma2/10**4 + 4*ANN*gamma2*x_j/D/10000/4/10**4 - 4*ANN*gamma2/10000/4/10**4
c: int256 = (
unsafe_mul(10**32, convert(3, int256))
+ unsafe_mul(unsafe_mul(4, gamma), 10**14)
+ unsafe_div(gamma2, 10**4)
+ unsafe_div(unsafe_div(unsafe_mul(4, ann_gamma2), 400000000) * x_j, D)
- unsafe_div(unsafe_mul(4, ann_gamma2), 400000000)
)
# d = -(10**18+gamma)**2 / 10**4
d: int256 = -unsafe_div(unsafe_add(10**18, gamma) **2, 10**4)
# delta0: int256 = 3*a*c/b - b
delta0: int256 = 3 * a * c / b - b # safediv by b
# delta1: int256 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1: int256 = 3 * delta0 + b - 27*a**2/b*d/b
divider: int256 = 1
threshold: int256 = min(min(abs(delta0), abs(delta1)), a)
if threshold > 10**48:
divider = 10**30
elif threshold > 10**46:
divider = 10**28
elif threshold > 10**44:
divider = 10**26
elif threshold > 10**42:
divider = 10**24
elif threshold > 10**40:
divider = 10**22
elif threshold > 10**38:
divider = 10**20
elif threshold > 10**36:
divider = 10**18
elif threshold > 10**34:
divider = 10**16
elif threshold > 10**32:
divider = 10**14
elif threshold > 10**30:
divider = 10**12
elif threshold > 10**28:
divider = 10**10
elif threshold > 10**26:
divider = 10**8
elif threshold > 10**24:
divider = 10**6
elif threshold > 10**20:
divider = 10**2
a = unsafe_div(a, divider)
b = unsafe_div(b, divider)
c = unsafe_div(c, divider)
d = unsafe_div(d, divider)
# delta0 = 3*a*c/b - b: here we can do more unsafe ops now:
delta0 = unsafe_div(unsafe_mul(unsafe_mul(3, a), c), b) - b
# delta1 = 9*a*c/b - 2*b - 27*a**2/b*d/b
delta1 = 3 * delta0 + b - unsafe_div(unsafe_mul(unsafe_div(unsafe_mul(27, a**2), b), d), b)
# sqrt_arg: int256 = delta1**2 + 4*delta0**2/b*delta0
sqrt_arg: int256 = delta1**2 + unsafe_mul(unsafe_div(4*delta0**2, b), delta0)
sqrt_val: int256 = 0
if sqrt_arg > 0:
sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256)
else:
return [
self._newton_y(_ANN, _gamma, _x, _D, i),
0
]
b_cbrt: int256 = 0
if b > 0:
b_cbrt = convert(self._cbrt(convert(b, uint256)), int256)
else:
b_cbrt = -convert(self._cbrt(convert(-b, uint256)), int256)
second_cbrt: int256 = 0
if delta1 > 0:
# second_cbrt = convert(self._cbrt(convert((delta1 + sqrt_val), uint256) / 2), int256)
second_cbrt = convert(self._cbrt(convert(unsafe_add(delta1, sqrt_val), uint256) / 2), int256)
else:
# second_cbrt = -convert(self._cbrt(convert(unsafe_sub(sqrt_val, delta1), uint256) / 2), int256)
second_cbrt = -convert(self._cbrt(unsafe_div(convert(unsafe_sub(sqrt_val, delta1), uint256), 2)), int256)
# C1: int256 = b_cbrt**2/10**18*second_cbrt/10**18
C1: int256 = unsafe_div(unsafe_mul(unsafe_div(b_cbrt**2, 10**18), second_cbrt), 10**18)
# root: int256 = (10**18*C1 - 10**18*b - 10**18*b*delta0/C1)/(3*a), keep 2 safe ops here.
root: int256 = (unsafe_mul(10**18, C1) - unsafe_mul(10**18, b) - unsafe_mul(10**18, b)/C1*delta0)/unsafe_mul(3, a)
# y_out: uint256[2] = [
# convert(D**2/x_j*root/4/10**18, uint256), # <--- y
# convert(root, uint256) # <----------------------- K0Prev
# ]
y_out: uint256[2] = [convert(unsafe_div(unsafe_div(unsafe_mul(unsafe_div(D**2, x_j), root), 4), 10**18), uint256), convert(root, uint256)]
frac: uint256 = unsafe_div(y_out[0] * 10**18, _D)
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe value for y
return y_out
@external
@view
def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev: uint256 = 0) -> uint256:
"""
Finding the invariant using Newton method.
ANN is higher by the factor A_MULTIPLIER
ANN is already A * N**N
"""
# Safety checks
assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A
assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
# Initial value of invariant D is that for constant-product invariant
x: uint256[N_COINS] = x_unsorted
if x[0] < x[1]:
x = [x_unsorted[1], x_unsorted[0]]
assert x[0] > 10**9 - 1 and x[0] < 10**15 * 10**18 + 1 # dev: unsafe values x[0]
assert unsafe_div(x[1] * 10**18, x[0]) > 10**14 - 1 # dev: unsafe values x[i] (input)
S: uint256 = unsafe_add(x[0], x[1]) # can unsafe add here because we checked x[0] bounds
D: uint256 = 0
if K0_prev == 0:
D = N_COINS * isqrt(unsafe_mul(x[0], x[1]))
else:
# D = isqrt(x[0] * x[1] * 4 / K0_prev * 10**18)
D = isqrt(unsafe_mul(unsafe_div(unsafe_mul(unsafe_mul(4, x[0]), x[1]), K0_prev), 10**18))
if S < D:
D = S
__g1k0: uint256 = gamma + 10**18
diff: uint256 = 0
for i in range(255):
D_prev: uint256 = D
assert D > 0
# Unsafe division by D and D_prev is now safe
# K0: uint256 = 10**18
# for _x in x:
# K0 = K0 * _x * N_COINS / D
# collapsed for 2 coins
K0: uint256 = unsafe_div(unsafe_div((10**18 * N_COINS**2) * x[0], D) * x[1], D)
_g1k0: uint256 = __g1k0
if _g1k0 > K0:
_g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1) # > 0
else:
_g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1) # > 0
# D / (A * N**N) * _g1k0**2 / gamma**2
mul1: uint256 = unsafe_div(unsafe_div(unsafe_div(10**18 * D, gamma) * _g1k0, gamma) * _g1k0 * A_MULTIPLIER, ANN)
# 2*N*K0 / _g1k0
mul2: uint256 = unsafe_div(((2 * 10**18) * N_COINS) * K0, _g1k0)
# calculate neg_fprime. here K0 > 0 is being validated (safediv).
neg_fprime: uint256 = (S + unsafe_div(S * mul2, 10**18)) + mul1 * N_COINS / K0 - unsafe_div(mul2 * D, 10**18)
# D -= f / fprime; neg_fprime safediv being validated
D_plus: uint256 = D * (neg_fprime + S) / neg_fprime
D_minus: uint256 = unsafe_div(D * D, neg_fprime)
if 10**18 > K0:
D_minus += unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(10**18, K0), K0)
else:
D_minus -= unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(K0, 10**18), K0)
if D_plus > D_minus:
D = unsafe_sub(D_plus, D_minus)
else:
D = unsafe_div(unsafe_sub(D_minus, D_plus), 2)
if D > D_prev:
diff = unsafe_sub(D, D_prev)
else:
diff = unsafe_sub(D_prev, D)
if diff * 10**14 < max(10**16, D): # Could reduce precision for gas efficiency here
for _x in x:
frac: uint256 = _x * 10**18 / D
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe values x[i]
return D
raise "Did not converge"
```
```shell
>>> Views.calc_fee_withdraw_one_coin(100, 0, pool) # withdrawing 100 lp tokens in coin(0)
returns apporx_fee # approx fee
```
::::
### `calc_fee_token_amount`
::::description[`Views.calc_fee_token_amount(amounts: uint256[N_COINS], deposit: bool, swap: address) -> uint256: view`]
Function to calculate the fee for `calc_token_amount`.
| Input | Type | Description |
|-----------|---------------------|----------------------------------------------------------------------|
| `amounts` | `uint256[N_COINS]` | Array of amounts of each coin being deposited or withdrawn. |
| `deposit` | `bool` | Indicates the action: `True` for deposit, `False` for withdrawal. |
| `swap` | `address` | Address of the pool contract involved in the transaction. |
Returns: Approximate fee (`uint256`).
```vyper
@view
@external
def calc_fee_token_amount(
amounts: uint256[N_COINS], deposit: bool, swap: address
) -> uint256:
d_token: uint256 = 0
amountsp: uint256[N_COINS] = empty(uint256[N_COINS])
xp: uint256[N_COINS] = empty(uint256[N_COINS])
d_token, amountsp, xp = self._calc_dtoken_nofee(amounts, deposit, swap)
return Curve(swap).calc_token_fee(amountsp, xp) * d_token / 10**10 + 1
@internal
@view
def _calc_dtoken_nofee(
amounts: uint256[N_COINS], deposit: bool, swap: address
) -> (uint256, uint256[N_COINS], uint256[N_COINS]):
math: Math = Curve(swap).MATH()
xp: uint256[N_COINS] = empty(uint256[N_COINS])
precisions: uint256[N_COINS] = empty(uint256[N_COINS])
price_scale: uint256 = 0
D0: uint256 = 0
token_supply: uint256 = 0
A: uint256 = 0
gamma: uint256 = 0
xp, D0, token_supply, price_scale, A, gamma, precisions = self._prep_calc(swap)
amountsp: uint256[N_COINS] = amounts
if deposit:
for k in range(N_COINS):
xp[k] += amounts[k]
else:
for k in range(N_COINS):
xp[k] -= amounts[k]
xp = [
xp[0] * precisions[0],
xp[1] * price_scale * precisions[1] / PRECISION
]
amountsp = [
amountsp[0]* precisions[0],
amountsp[1] * price_scale * precisions[1] / PRECISION
]
D: uint256 = math.newton_D(A, gamma, xp, 0)
d_token: uint256 = token_supply * D / D0
if deposit:
d_token -= token_supply
else:
d_token = token_supply - d_token
return d_token, amountsp, xp
```
```vyper
@external
@view
def calc_token_fee(
amounts: uint256[N_COINS], xp: uint256[N_COINS]
) -> uint256:
"""
@notice Returns the fee charged on the given amounts for add_liquidity.
@param amounts The amounts of coins being added to the pool.
@param xp The current balances of the pool multiplied by coin precisions.
@return uint256 Fee charged.
"""
return self._calc_token_fee(amounts, xp)
@view
@internal
def _calc_token_fee(amounts: uint256[N_COINS], xp: uint256[N_COINS]) -> uint256:
# fee = sum(amounts_i - avg(amounts)) * fee' / sum(amounts)
fee: uint256 = unsafe_div(
unsafe_mul(self._fee(xp), N_COINS),
unsafe_mul(4, unsafe_sub(N_COINS, 1))
)
S: uint256 = 0
for _x in amounts:
S += _x
avg: uint256 = unsafe_div(S, N_COINS)
Sdiff: uint256 = 0
for _x in amounts:
if _x > avg:
Sdiff += unsafe_sub(_x, avg)
else:
Sdiff += unsafe_sub(avg, _x)
return fee * Sdiff / S + NOISE_FEE
```
```vyper
@external
@view
def newton_D(ANN: uint256, gamma: uint256, x_unsorted: uint256[N_COINS], K0_prev: uint256 = 0) -> uint256:
"""
Finding the invariant using Newton method.
ANN is higher by the factor A_MULTIPLIER
ANN is already A * N**N
"""
# Safety checks
assert ANN > MIN_A - 1 and ANN < MAX_A + 1 # dev: unsafe values A
assert gamma > MIN_GAMMA - 1 and gamma < MAX_GAMMA + 1 # dev: unsafe values gamma
# Initial value of invariant D is that for constant-product invariant
x: uint256[N_COINS] = x_unsorted
if x[0] < x[1]:
x = [x_unsorted[1], x_unsorted[0]]
assert x[0] > 10**9 - 1 and x[0] < 10**15 * 10**18 + 1 # dev: unsafe values x[0]
assert unsafe_div(x[1] * 10**18, x[0]) > 10**14 - 1 # dev: unsafe values x[i] (input)
S: uint256 = unsafe_add(x[0], x[1]) # can unsafe add here because we checked x[0] bounds
D: uint256 = 0
if K0_prev == 0:
D = N_COINS * isqrt(unsafe_mul(x[0], x[1]))
else:
# D = isqrt(x[0] * x[1] * 4 / K0_prev * 10**18)
D = isqrt(unsafe_mul(unsafe_div(unsafe_mul(unsafe_mul(4, x[0]), x[1]), K0_prev), 10**18))
if S < D:
D = S
__g1k0: uint256 = gamma + 10**18
diff: uint256 = 0
for i in range(255):
D_prev: uint256 = D
assert D > 0
# Unsafe division by D and D_prev is now safe
# K0: uint256 = 10**18
# for _x in x:
# K0 = K0 * _x * N_COINS / D
# collapsed for 2 coins
K0: uint256 = unsafe_div(unsafe_div((10**18 * N_COINS**2) * x[0], D) * x[1], D)
_g1k0: uint256 = __g1k0
if _g1k0 > K0:
_g1k0 = unsafe_add(unsafe_sub(_g1k0, K0), 1) # > 0
else:
_g1k0 = unsafe_add(unsafe_sub(K0, _g1k0), 1) # > 0
# D / (A * N**N) * _g1k0**2 / gamma**2
mul1: uint256 = unsafe_div(unsafe_div(unsafe_div(10**18 * D, gamma) * _g1k0, gamma) * _g1k0 * A_MULTIPLIER, ANN)
# 2*N*K0 / _g1k0
mul2: uint256 = unsafe_div(((2 * 10**18) * N_COINS) * K0, _g1k0)
# calculate neg_fprime. here K0 > 0 is being validated (safediv).
neg_fprime: uint256 = (S + unsafe_div(S * mul2, 10**18)) + mul1 * N_COINS / K0 - unsafe_div(mul2 * D, 10**18)
# D -= f / fprime; neg_fprime safediv being validated
D_plus: uint256 = D * (neg_fprime + S) / neg_fprime
D_minus: uint256 = unsafe_div(D * D, neg_fprime)
if 10**18 > K0:
D_minus += unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(10**18, K0), K0)
else:
D_minus -= unsafe_div(unsafe_div(D * unsafe_div(mul1, neg_fprime), 10**18) * unsafe_sub(K0, 10**18), K0)
if D_plus > D_minus:
D = unsafe_sub(D_plus, D_minus)
else:
D = unsafe_div(unsafe_sub(D_minus, D_plus), 2)
if D > D_prev:
diff = unsafe_sub(D, D_prev)
else:
diff = unsafe_sub(D_prev, D)
if diff * 10**14 < max(10**16, D): # Could reduce precision for gas efficiency here
for _x in x:
frac: uint256 = _x * 10**18 / D
assert (frac >= 10**16 - 1) and (frac < 10**20 + 1) # dev: unsafe values x[i]
return D
raise "Did not converge"
```
```shell
>>> Views.calc_fee_token_amount([100, 100], True, pool) # depositing 100 of each coin into "pool"
returns fee
>>> Views.calc_fee_token_amount([100, 100], False, pool) # withdrawing 100 of each coin from "pool"
returns fee
```
::::
---
## BlockOracle
The `BlockOracle` contract is a decentralized block hash oracle which implements a threshold-based, multi-committer consensus mechanism for block hash commitments. Trusted committers submit and validate block hashes; once a threshold of matching commitments is reached, the block hash is confirmed and becomes immutable. The contract supports secure, permissionless block hash storage, committers management, threshold configuration, and integration with external header verifiers for cross-chain and state proof use cases.
:::vyper[`BlockOracle.vy`]
The source code for the `BlockOracle.vy` contract can be found on [GitHub](https://github.com/curvefi/blockhash-oracle/blob/main/contracts/BlockOracle.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.4.3`.
The contract is deployed on all supported chains at `0xb10cface69821Ff7b245Cf5f28f3e714fDbd86b8`.
```json
[{"anonymous":false,"inputs":[{"indexed":true,"name":"committer","type":"address"},{"indexed":true,"name":"block_number","type":"uint256"},{"indexed":false,"name":"block_hash","type":"bytes32"}],"name":"CommitBlock","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"block_number","type":"uint256"},{"indexed":false,"name":"block_hash","type":"bytes32"}],"name":"ApplyBlock","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"block_number","type":"uint256"},{"indexed":false,"name":"block_hash","type":"bytes32"}],"name":"SubmitBlockHeader","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"committer","type":"address"}],"name":"AddCommitter","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"committer","type":"address"}],"name":"RemoveCommitter","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"new_threshold","type":"uint256"}],"name":"SetThreshold","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"old_verifier","type":"address"},{"indexed":true,"name":"new_verifier","type":"address"}],"name":"SetHeaderVerifier","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previous_owner","type":"address"},{"indexed":true,"name":"new_owner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"new_owner","type":"address"}],"name":"transfer_ownership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounce_ownership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_verifier","type":"address"}],"name":"set_header_verifier","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_committer","type":"address"}],"name":"add_committer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_committer","type":"address"},{"name":"_bump_threshold","type":"bool"}],"name":"add_committer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_committer","type":"address"}],"name":"remove_committer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_new_threshold","type":"uint256"}],"name":"set_threshold","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_block_number","type":"uint256"},{"name":"_block_hash","type":"bytes32"}],"name":"admin_apply_block","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_block_number","type":"uint256"},{"name":"_block_hash","type":"bytes32"}],"name":"commit_block","outputs":[{"name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_block_number","type":"uint256"},{"name":"_block_hash","type":"bytes32"},{"name":"_apply","type":"bool"}],"name":"commit_block","outputs":[{"name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_block_number","type":"uint256"},{"name":"_block_hash","type":"bytes32"}],"name":"apply_block","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"name":"block_hash","type":"bytes32"},{"name":"parent_hash","type":"bytes32"},{"name":"state_root","type":"bytes32"},{"name":"receipt_root","type":"bytes32"},{"name":"block_number","type":"uint256"},{"name":"timestamp","type":"uint256"}],"name":"_header_data","type":"tuple"}],"name":"submit_block_header","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"get_all_committers","outputs":[{"name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_block_number","type":"uint256"}],"name":"get_block_hash","outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_block_number","type":"uint256"}],"name":"get_state_root","outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"last_confirmed_block_number","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"header_verifier","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint256"}],"name":"block_header","outputs":[{"components":[{"name":"block_hash","type":"bytes32"},{"name":"parent_hash","type":"bytes32"},{"name":"state_root","type":"bytes32"},{"name":"receipt_root","type":"bytes32"},{"name":"block_number","type":"uint256"},{"name":"timestamp","type":"uint256"}],"name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"last_confirmed_header","outputs":[{"components":[{"name":"block_hash","type":"bytes32"},{"name":"parent_hash","type":"bytes32"},{"name":"state_root","type":"bytes32"},{"name":"receipt_root","type":"bytes32"},{"name":"block_number","type":"uint256"},{"name":"timestamp","type":"uint256"}],"name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint256"}],"name":"committers","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"}],"name":"is_committer","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint256"},{"name":"arg1","type":"bytes32"}],"name":"commitment_count","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"},{"name":"arg1","type":"uint256"}],"name":"committer_votes","outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"threshold","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"outputs":[],"stateMutability":"nonpayable","type":"constructor"}]
```
:::
---
## Committers
Committers are tracked on-chain and are required for consensus operations. Only the contract owner can modify the committer set; all users can query committer status.
## Managing Committers
Owner-only functions for adding or removing committers, as well as retrieving the current committer list. These operations directly affect consensus security, as only committers can submit or update block hash commitments. Exceeding the maximum committer count (32) is prevented at the contract level.
### `add_committer`
::::description[`BlockOracle.add_committer(_committer: address, _bump_threshold: bool = False)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Function to add a committer.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_committer` | `address` | Address of the committer to add |
| `_bump_threshold` | `bool` | If true, automatically increase threshold by 1 |
Emits: `AddCommitter` event.
```vyper
event AddCommitter:
committer: indexed(address)
MAX_COMMITTERS: constant(uint256) = 32
committers: public(DynArray[address, MAX_COMMITTERS]) # List of all committers
is_committer: public(HashMap[address, bool])
threshold: public(uint256)
@external
def add_committer(_committer: address, _bump_threshold: bool = False):
"""
@notice Set trusted address that can commit block data
@param _committer Address of trusted committer
@param _bump_threshold If True, bump threshold to 1 more (useful for initial setup)
"""
ownable._check_owner()
if not self.is_committer[_committer]:
assert len(self.committers) < MAX_COMMITTERS, "Max committers reached"
self.is_committer[_committer] = True
self.committers.append(_committer)
log AddCommitter(committer=_committer)
if _bump_threshold:
self.threshold += 1
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> BlockOracle.add_committer('0x1234567890123456789012345678901234567890', False)
```
::::
### `remove_committer`
::::description[`BlockOracle.remove_committer(_committer: address)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Function to remove a committer.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_committer` | `address` | Address of the committer to remove |
Emits: `RemoveCommitter` event.
```vyper
event RemoveCommitter:
committer: indexed(address)
MAX_COMMITTERS: constant(uint256) = 32
committers: public(DynArray[address, MAX_COMMITTERS]) # List of all committers
is_committer: public(HashMap[address, bool])
@external
def remove_committer(_committer: address):
"""
@notice Remove trusted address that can commit block data
@param _committer Address of trusted committer
"""
ownable._check_owner()
if self.is_committer[_committer]:
self.is_committer[_committer] = False
# Rebuild committers array excluding the removed committer
new_committers: DynArray[address, MAX_COMMITTERS] = []
for committer: address in self.committers:
if committer != _committer:
new_committers.append(committer)
self.committers = new_committers
log RemoveCommitter(committer=_committer)
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> BlockOracle.remove_committer('0x1234567890123456789012345678901234567890')
```
::::
### `get_all_committers`
::::description[`BlockOracle.get_all_committers() -> DynArray[address, MAX_COMMITTERS]: view`]
Getter which returns all registered committers.
Returns: array of all committers (`DynArray[address, MAX_COMMITTERS]`).
```vyper
MAX_COMMITTERS: constant(uint256) = 32
committers: public(DynArray[address, MAX_COMMITTERS]) # List of all committers
@view
@external
def get_all_committers() -> DynArray[address, MAX_COMMITTERS]:
"""
@notice Utility viewer that returns list of all committers
@return Array of all registered committer addresses
"""
return self.committers
```
```shell
>>> BlockOracle.get_all_committers()
['0xFacEFeeD696BFC0ebe7EaD3FFBb9a56290d31752']
```
::::
### `committers`
::::description[`BlockOracle.committers(arg0: uint256) -> address: view`]
Getter for the committers at a specific index.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `arg0` | `uint256` | Index of the committer |
Returns: address of the committer at the specified index (`address`).
```vyper
MAX_COMMITTERS: constant(uint256) = 32
committers: public(DynArray[address, MAX_COMMITTERS]) # List of all committers
```
This example returns the committer at index 0.
```shell
>>> BlockOracle.committers(0)
'0xFacEFeeD696BFC0ebe7EaD3FFBb9a56290d31752'
```
::::
### `is_committer`
::::description[`BlockOracle.is_committer(arg0: address) -> bool: view`]
Getter to check if a certain address is a committer.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `arg0` | `address` | Address to check |
Returns: true if the address is a committer, false otherwise (`bool`).
```vyper
is_committer: public(HashMap[address, bool])
```
```shell
>>> BlockOracle.is_committer('0xFacEFeeD696BFC0ebe7EaD3FFBb9a56290d31752')
True
```
::::
## Threshold Management
Owner-only functions for setting and querying the threshold parameter, which defines the minimum number of matching committer votes required to confirm a block hash. The threshold cannot exceed the number of registered committers.
### `set_threshold`
::::description[`BlockOracle.set_threshold(_new_threshold: uint256)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Function to update the threshold for block applications.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_new_threshold` | `uint256` | New `threshold` value |
Emits: `SetThreshold` event.
```vyper
event SetThreshold:
new_threshold: indexed(uint256)
threshold: public(uint256)
@external
def set_threshold(_new_threshold: uint256):
"""
@notice Update threshold for block application
@param _new_threshold New threshold value
"""
ownable._check_owner()
assert _new_threshold <= len(
self.committers
), "Threshold cannot be greater than number of committers"
self.threshold = _new_threshold
log SetThreshold(new_threshold=_new_threshold)
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> BlockOracle.set_threshold(2)
```
::::
### `threshold`
::::description[`BlockOracle.threshold() -> uint256: view`]
Getter for the threshold of how many matching commitments required to confirm a block.
Returns: number of commitments required (`uint256`).
```vyper
threshold: public(uint256)
```
```shell
>>> BlockOracle.threshold()
1
```
::::
---
## Committing Block Hashes
Only registered committers may call commit functions. The contract maintains a mapping of committer votes and a count of votes per block hash. Commitments are mutable until a block is confirmed; after confirmation, the block hash is immutable.
### `commit_block`
::::description[`BlockOracle.commit_block(_block_number: uint256, _block_hash: bytes32, _apply: bool = True) -> bool`]
Function to commit a block hash and optionally attempt to apply it.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_block_number` | `uint256` | Block number to commit |
| `_block_hash` | `bytes32` | The hash to commit |
| `_apply` | `bool` | `true` -> check if threshold is met and applies the block. |
Returns: true if the block was applied (`bool`).
Emits: `CommitBlock` event.
```vyper
@external
def commit_block(_block_number: uint256, _block_hash: bytes32, _apply: bool = True) -> bool:
"""
@notice Commit a block hash and optionally attempt to apply it
@param _block_number The block number to commit
@param _block_hash The hash to commit
@param _apply If True, checks if threshold is met and applies block
@return True if block was applied
"""
assert self.is_committer[msg.sender], "Not authorized"
assert self.block_hash[_block_number] == empty(bytes32), "Already applied"
assert _block_hash != empty(bytes32), "Invalid block hash"
previous_commitment: bytes32 = self.committer_votes[msg.sender][_block_number]
# Remove previous vote if exists, to avoid duplicate commitments
if previous_commitment != empty(bytes32):
self.commitment_count[_block_number][previous_commitment] -= 1
self.committer_votes[msg.sender][_block_number] = _block_hash
self.commitment_count[_block_number][_block_hash] += 1
log CommitBlock(committer=msg.sender, block_number=_block_number, block_hash=_block_hash)
# Optional attempt to apply block
if _apply and self.commitment_count[_block_number][_block_hash] >= self.threshold:
self._apply_block(_block_number, _block_hash)
return True
return False
```
```shell
>>> BlockOracle.commit_block(22788903, 0xc215221221dd6673ae7ed2e50f47f6d020034657bb4a08010b5677a1f9d06d6d, True)
True
```
::::
### `committer_votes`
::::description[`BlockOracle.committer_votes(arg0: address, arg1: uint256) -> bytes32: view`]
Getter for the committed hash by a specific committer for a specific block number.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `arg0` | `address` | Address of the committer |
| `arg1` | `uint256` | Block number |
Returns: committed hash for the committer and block number, or empty bytes32 if no commitment (`bytes32`).
```vyper
committer_votes: public(
HashMap[address, HashMap[uint256, bytes32]]
) # committer => block_number => committed_hash
```
```shell
>>> BlockOracle.committer_votes('0xFacEFeeD696BFC0ebe7EaD3FFBb9a56290d31752', 22788903)
'0xc215221221dd6673ae7ed2e50f47f6d020034657bb4a08010b5677a1f9d06d6d'
```
::::
### `commitment_count`
::::description[`BlockOracle.commitment_count(arg0: uint256, arg1: bytes32) -> uint256: view`]
Getter for the number of commitments for a specific hash at a specific block number.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `arg0` | `uint256` | Block number |
| `arg1` | `bytes32` | Hash to get count for |
Returns: number of commitments for the hash at the block number (`uint256`).
```vyper
commitment_count: public(
HashMap[uint256, HashMap[bytes32, uint256]]
) # block_number => hash => count
```
```shell
>>> BlockOracle.commitment_count(22788903, 0xc215221221dd6673ae7ed2e50f47f6d020034657bb4a08010b5677a1f9d06d6d)
1
```
::::
---
## Block Application
Block application includes both permissionless (anyone can call) and owner-only (admin) application. Also provides views for querying confirmed block hashes and the most recent confirmed block number. Once applied, block hashes are immutable and serve as the canonical record for the oracle.
### `apply_block`
::::description[`BlockOracle.apply_block(_block_number: uint256, _block_hash: bytes32)`]
Function to apply a block hash if it has sufficient commitments.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_block_number` | `uint256` | Block number to apply |
| `_block_hash` | `bytes32` | The block hash to apply |
Emits: `ApplyBlock` event.
```vyper
event ApplyBlock:
block_number: indexed(uint256)
block_hash: bytes32
block_hash: HashMap[uint256, bytes32] # block_number => hash
last_confirmed_block_number: public(uint256) # number of the last confirmed block hash
@external
def apply_block(_block_number: uint256, _block_hash: bytes32):
"""
@notice Apply a block hash if it has sufficient commitments
@param _block_number The block number to apply
@param _block_hash The block hash to apply
"""
assert self.block_hash[_block_number] == empty(bytes32), "Already applied"
assert (
self.commitment_count[_block_number][_block_hash] >= self.threshold
), "Insufficient commitments"
self._apply_block(_block_number, _block_hash)
@internal
def _apply_block(_block_number: uint256, _block_hash: bytes32):
"""
@notice Confirm a block hash and apply it
@dev All security checks must be performed outside!
@param _block_number The block number to confirm
@param _block_hash The hash to confirm
"""
self.block_hash[_block_number] = _block_hash
if self.last_confirmed_block_number < _block_number:
self.last_confirmed_block_number = _block_number
log ApplyBlock(block_number=_block_number, block_hash=_block_hash)
```
```shell
>>> BlockOracle.apply_block(22788903, 0xc215221221dd6673ae7ed2e50f47f6d020034657bb4a08010b5677a1f9d06d6d)
```
::::
### `admin_apply_block`
::::description[`BlockOracle.admin_apply_block(_block_number: uint256, _block_hash: bytes32)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Function to apply a block hash with admin rights.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_block_number` | `uint256` | Block number to apply |
| `_block_hash` | `bytes32` | Hash to apply |
Emits: `ApplyBlock` event.
```vyper
@external
def admin_apply_block(_block_number: uint256, _block_hash: bytes32):
"""
@notice Apply a block hash with admin rights
@param _block_number The block number to apply
@param _block_hash The hash to apply
@dev Only callable by owner
"""
ownable._check_owner()
self._apply_block(_block_number, _block_hash)
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> BlockOracle.admin_apply_block(22788903, 0xc215221221dd6673ae7ed2e50f47f6d020034657bb4a08010b5677a1f9d06d6d)
```
::::
### `get_block_hash`
::::description[`BlockOracle.get_block_hash(_block_number: uint256) -> bytes32: view`]
Getter for the confirmed block hash for a given block number.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_block_number` | `uint256` | Block number |
Returns: confirmed block hash or empty `bytes32` if not confirmed (`bytes32`).
```vyper
block_hash: HashMap[uint256, bytes32] # block_number => hash
@view
@external
def get_block_hash(_block_number: uint256) -> bytes32:
"""
@notice Get the confirmed block hash for a given block number
@param _block_number The block number to query
@return The confirmed block hash, or empty bytes32 if not confirmed
"""
return self.block_hash[_block_number]
```
```shell
>>> BlockOracle.get_block_hash(22788903)
'0xc215221221dd6673ae7ed2e50f47f6d020034657bb4a08010b5677a1f9d06d6d'
```
::::
### `last_confirmed_block_number`
::::description[`BlockOracle.last_confirmed_block_number() -> uint256: view`]
Getter for the last confirmed block number.
Returns: most recently confirmed block number (`uint256`).
```vyper
last_confirmed_block_number: public(uint256) # number of the last confirmed block hash
```
```shell
>>> BlockOracle.last_confirmed_block_number()
22788903
```
::::
---
## Block Headers
Functions for submitting and retrieving full block headers. Only the designated verifier contract may submit headers, which must match a previously confirmed block hash. Includes views for retrieving the state root, full header data, and the most recently confirmed header. These features enable advanced use cases such as state proofs and cross-chain verification.
### `submit_block_header`
::::description[`BlockOracle.submit_block_header(_header_data: bh_rlp.BlockHeader)`]
:::guard[Guarded Method]
This function can only be called by the designated `header_verifier` contract.
:::
Function to submit a block header.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_header_data` | `bh_rlp.BlockHeader` | Block header to submit |
Emits: `SubmitBlockHeader` event.
```vyper
# Import RLP Block Header Decoder
from modules import BlockHeaderRLPDecoder as bh_rlp
block_hash: HashMap[uint256, bytes32] # block_number => hash
last_confirmed_block_number: public(uint256) # number of the last confirmed block hash
header_verifier: public(address) # address of the header verifier
block_header: public(HashMap[uint256, bh_rlp.BlockHeader]) # block_number => header
last_confirmed_header: public(bh_rlp.BlockHeader) # last confirmed header
@external
def submit_block_header(_header_data: bh_rlp.BlockHeader):
"""
@notice Submit block header. Available only to whitelisted verifier contract.
@param _header_data The block header to submit
"""
assert msg.sender == self.header_verifier, "Not authorized"
# Safety checks
assert _header_data.block_hash != empty(bytes32), "Invalid block hash"
assert self.block_hash[_header_data.block_number] != empty(bytes32), "Blockhash not applied"
assert _header_data.block_hash == self.block_hash[_header_data.block_number], "Blockhash does not match"
assert self.block_header[_header_data.block_number].block_hash == empty(bytes32), "Header already submitted"
# Store decoded header
self.block_header[_header_data.block_number] = _header_data
# Update last confirmed header if new
if _header_data.block_number > self.last_confirmed_header.block_number:
self.last_confirmed_header = _header_data
log SubmitBlockHeader(
block_number=_header_data.block_number,
block_hash=_header_data.block_hash,
)
```
```shell
>>> BlockOracle.submit_block_header(header_data)
```
::::
### `get_state_root`
::::description[`BlockOracle.get_state_root(_block_number: uint256) -> bytes32: view`]
Getter for the state root for a given block number.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_block_number` | `uint256` | Block number |
Returns: state root from the block header, or empty `bytes32` if the header is not submitted (`bytes32`).
```vyper
@view
@external
def get_state_root(_block_number: uint256) -> bytes32:
"""
@notice Get the state root for a given block number
@param _block_number The block number to query
@return The state root from the block header, or empty bytes32 if header not submitted
"""
return self.block_header[_block_number].state_root
```
```shell
>>> BlockOracle.get_state_root(22788903)
'0x1338f27ed74ec2766875e2db65ca08b79fd2ce67a4f800acac4fa264e99a1984'
```
::::
### `block_header`
::::description[`BlockOracle.block_header(arg0: uint256) -> tuple: view`]
Getter for the block header for a specific block number.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `arg0` | `uint256` | Block number |
Returns: block header tuple containing (block_hash, parent_hash, state_root, receipt_root, block_number, timestamp) (`tuple`).
```vyper
block_header: public(HashMap[uint256, bh_rlp.BlockHeader]) # block_number => header
```
```shell
>>> BlockOracle.block_header(22788903)
0xc215221221dd6673ae7ed2e50f47f6d020034657bb4a08010b5677a1f9d06d6d, 0x1338f27ed74ec2766875e2db65ca08b79fd2ce67a4f800acac4fa264e99a1984, 0x168986623a9cca07db8e17253b119dc1e93d68c2838e59c8cb5a10a88f5ad7a7, 0x46a8b05c582e75b876b106621e02542c6a9b3608d10d3e1973f3bf5aae9639a1,22788903,1750944347
```
::::
### `last_confirmed_header`
::::description[`BlockOracle.last_confirmed_header() -> tuple: view`]
Getter for the last confirmed header.
Returns: block header tuple of the most recently confirmed header (`tuple`).
```vyper
last_confirmed_header: public(bh_rlp.BlockHeader) # last confirmed header
```
```shell
>>> BlockOracle.last_confirmed_header()
0xc215221221dd6673ae7ed2e50f47f6d020034657bb4a08010b5677a1f9d06d6d, 0x1338f27ed74ec2766875e2db65ca08b79fd2ce67a4f800acac4fa264e99a1984, 0x168986623a9cca07db8e17253b119dc1e93d68c2838e59c8cb5a10a88f5ad7a7, 0x46a8b05c582e75b876b106621e02542c6a9b3608d10d3e1973f3bf5aae9639a1,22788903,1750944347
```
::::
---
## Verifier
Owner-only functions for setting and querying the verifier contract address. The verifier is responsible for submitting RLP-encoded block headers. Proper verifier management is essential for the integrity of header submissions and downstream state proof operations.
### `set_header_verifier`
::::description[`BlockOracle.set_header_verifier(_verifier: address)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Function to set the block header verifier.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_verifier` | `address` | Block verifier address |
Emits: `SetHeaderVerifier` event.
```vyper
event SetHeaderVerifier:
old_verifier: indexed(address)
new_verifier: indexed(address)
header_verifier: public(address) # address of the header verifier
@external
def set_header_verifier(_verifier: address):
"""
@notice Set the block header verifier
@dev Emits SetHeaderVerifier event
@param _verifier Address of the block header verifier
"""
ownable._check_owner()
old_verifier: address = self.header_verifier
self.header_verifier = _verifier
log SetHeaderVerifier(old_verifier=old_verifier, new_verifier=_verifier)
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> BlockOracle.set_header_verifier('0x1234567890123456789012345678901234567890')
```
::::
### `header_verifier`
::::description[`BlockOracle.header_verifier() -> address: view`]
Getter for the block header verifier.
Returns: verifier (`address`).
```vyper
header_verifier: public(address) # address of the header verifier
```
```shell
>>> BlockOracle.header_verifier()
'0xB10CDEC0DE69c88a47c280a97A5AEcA8b0b83385'
```
::::
---
## Ownership
Standard Ownable interface for querying the current owner and transferring or renouncing ownership. Ownership controls all privileged operations, including committer management, threshold updates, and verifier assignment. Owner of the contract is the DAO.
---
## HeaderVerifier
The `HeaderVerifier` contract decodes RLP-encoded Ethereum block headers and forwards the extracted data to oracle contracts. It uses the `BlockHeaderRLPDecoder` module to parse block headers and extract key information such as block hash, parent hash, state root, receipt root, block number, and timestamp. The contract serves as a bridge between raw block header data and oracle systems, enabling cross-chain verification and data availability without implementing security checks or validation logic.
:::vyper[`HeaderVerifier.vy`]
The source code for the `HeaderVerifier.vy` contract can be found on [GitHub](https://github.com/curvefi/blockhash-oracle/blob/main/contracts/HeaderVerifier.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.4.3`.
The contract is deployed on all supported chains at `0xB10CDEC0DE69c88a47c280a97A5AEcA8b0b83385`.
```json
[{"inputs":[{"name":"encoded_header","type":"bytes"}],"name":"decode_block_header","outputs":[{"components":[{"name":"block_hash","type":"bytes32"},{"name":"parent_hash","type":"bytes32"},{"name":"state_root","type":"bytes32"},{"name":"receipt_root","type":"bytes32"},{"name":"block_number","type":"uint256"},{"name":"timestamp","type":"uint256"}],"name":"","type":"tuple"}],"stateMutability":"pure","type":"function"},{"inputs":[{"name":"_oracle_address","type":"address"},{"name":"_encoded_header","type":"bytes"}],"name":"submit_block_header","outputs":[],"stateMutability":"nonpayable","type":"function"}]
```
:::
### `decode_block_header`
::::description[`HeaderVerifier.decode_block_header(encoded_header: Bytes[BLOCK_HEADER_SIZE]) -> BlockHeader: pure`]
Function to decode RLP encoded block header into a BlockHeader struct.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `encoded_header` | `Bytes[BLOCK_HEADER_SIZE]` | RLP encoded header data |
Returns: a `BlockHeader` struct containing decoded block data (`BlockHeader`).
```vyper
from modules import BlockHeaderRLPDecoder as bh_rlp
exports: (bh_rlp.decode_block_header,)
```
```vyper
# BlockHeaderRLPDecoder.vy
# Source code of this Vyper module can be found here:
# https://github.com/curvefi/blockhash-oracle/blob/main/contracts/modules/BlockHeaderRLPDecoder.vy
# pragma version 0.4.3
"""
@title Block Header RLP Decoder Vyper Module
@author curve.fi
@license Copyright (c) Curve.Fi, 2025 - all rights reserved
@notice Decodes RLP-encoded Ethereum block header and stores key fields
@dev Extracts block number from RLP and uses it as storage key
"""
################################################################
# CONSTANTS #
################################################################
# Block header size upper limit
BLOCK_HEADER_SIZE: constant(uint256) = 1024
# RLP decoding constants
RLP_SHORT_START: constant(uint256) = 128 # 0x80
RLP_LONG_START: constant(uint256) = 183 # 0xb7
RLP_LIST_SHORT_START: constant(uint256) = 192 # 0xc0
RLP_LIST_LONG_START: constant(uint256) = 247 # 0xf7
################################################################
# STRUCTS #
################################################################
struct BlockHeader:
block_hash: bytes32
parent_hash: bytes32
state_root: bytes32
receipt_root: bytes32
block_number: uint256
timestamp: uint256
################################################################
# CONSTRUCTOR #
################################################################
@deploy
def __init__():
pass
################################################################
# EXTERNAL FUNCTIONS #
################################################################
# can be exposed optionally, or used in testing
@pure
@external
def calculate_block_hash(encoded_header: Bytes[BLOCK_HEADER_SIZE]) -> bytes32:
"""
@notice Calculates block hash from RLP encoded header
@param encoded_header RLP encoded header data
@return Block hash
"""
return keccak256(encoded_header)
@pure
@external
def decode_block_header(encoded_header: Bytes[BLOCK_HEADER_SIZE]) -> BlockHeader:
"""
@notice Decodes RLP encoded block header into BlockHeader struct
@param encoded_header RLP encoded header data
@return BlockHeader struct containing decoded block data
"""
return self._decode_block_header(encoded_header)
################################################################
# CORE FUNCTIONS #
################################################################
@pure
@internal
def _decode_block_header(encoded_header: Bytes[BLOCK_HEADER_SIZE]) -> BlockHeader:
"""
@notice Decodes key fields from RLP-encoded Ethereum block header
@dev RLP encoding rules:
- Single byte values (< 0x80) are encoded as themselves
- Short strings (length < 56) start with 0x80 + length
- Long strings (length >= 56) start with 0xb7 + length_of_length, followed by length
- Lists follow similar rules but with 0xc0 and 0xf7 as starting points
Makes use of utility functions to parse the RLP encoded header,
and passes entire header to them which is not optimal in terms of gas, but
makes code more readable.
@param encoded_header RLP encoded block header
@return BlockHeader(block_hash, parent_hash, state_root, block_number, timestamp)
"""
# Placeholder variables
tmp_int: uint256 = 0
tmp_bytes: bytes32 = empty(bytes32)
# Current position in the encoded header
current_pos: uint256 = 0
# 1. Skip RLP list length
current_pos = self._skip_rlp_list_header(encoded_header, current_pos)
# 2. Extract hashes
parent_hash: bytes32 = empty(bytes32)
parent_hash, current_pos = self._read_hash32(encoded_header, current_pos) # parent hash
tmp_bytes, current_pos = self._read_hash32(encoded_header, current_pos) # skip uncle hash
# 3. Skip miner address (20 bytes + 0x94)
assert convert(slice(encoded_header, current_pos, 1), bytes1) == 0x94
current_pos += 21
# 4. Read state root
state_root: bytes32 = empty(bytes32)
state_root, current_pos = self._read_hash32(encoded_header, current_pos)
# 5. Skip transaction root
tmp_bytes, current_pos = self._read_hash32(encoded_header, current_pos) # skip tx root
# 6. Read receipt root
receipt_root: bytes32 = empty(bytes32)
receipt_root, current_pos = self._read_hash32(encoded_header, current_pos)
# 7. Skip logs bloom
current_pos = self._skip_rlp_string(encoded_header, current_pos)
# 8. Skip difficulty
tmp_int, current_pos = self._read_rlp_number(encoded_header, current_pos)
# 9. Read block number
block_number: uint256 = 0
block_number, current_pos = self._read_rlp_number(encoded_header, current_pos)
# 10. Skip gas fields
tmp_int, current_pos = self._read_rlp_number(encoded_header, current_pos) # skip gas limit
tmp_int, current_pos = self._read_rlp_number(encoded_header, current_pos) # skip gas used
# 11. Read timestamp
timestamp: uint256 = 0
timestamp, current_pos = self._read_rlp_number(encoded_header, current_pos)
return BlockHeader(
block_hash=keccak256(encoded_header),
parent_hash=parent_hash,
state_root=state_root,
receipt_root=receipt_root,
block_number=block_number,
timestamp=timestamp,
)
################################################################
# UTILITY FUNCTIONS #
################################################################
@pure
@internal
def _skip_rlp_list_header(encoded: Bytes[BLOCK_HEADER_SIZE], pos: uint256) -> uint256:
"""@dev Returns position after list header"""
first_byte: uint256 = convert(slice(encoded, 0, 1), uint256)
assert first_byte >= RLP_LIST_SHORT_START, "Not a list"
if first_byte <= RLP_LIST_LONG_START:
return pos + 1
else:
return pos + 1 + (first_byte - RLP_LIST_LONG_START)
@pure
@internal
def _skip_rlp_string(encoded: Bytes[BLOCK_HEADER_SIZE], pos: uint256) -> uint256:
"""@dev Skip RLP string field, returns next_pos"""
prefix: uint256 = convert(slice(encoded, pos, 1), uint256)
if prefix < RLP_SHORT_START:
return pos + 1
elif prefix <= RLP_LONG_START:
return pos + 1 + (prefix - RLP_SHORT_START)
else:
# Sanity check: ensure this is a string, not a list
assert prefix < RLP_LIST_SHORT_START, "Expected string, found list prefix"
len_of_len: uint256 = prefix - RLP_LONG_START
data_length: uint256 = convert(
abi_decode(abi_encode(slice(encoded, pos + 1, len_of_len)), (Bytes[32])), uint256
)
return pos + 1 + len_of_len + data_length
@pure
@internal
def _read_hash32(encoded: Bytes[BLOCK_HEADER_SIZE], pos: uint256) -> (bytes32, uint256):
"""@dev Read 32-byte hash field, returns (hash, next_pos)"""
assert convert(slice(encoded, pos, 1), uint256) == 160 # RLP_SHORT_START + 32
return extract32(encoded, pos + 1), pos + 33
@pure
@internal
def _read_rlp_number(encoded: Bytes[BLOCK_HEADER_SIZE], pos: uint256) -> (uint256, uint256):
"""@dev Read RLP-encoded number, returns (value, next_pos)"""
prefix: uint256 = convert(slice(encoded, pos, 1), uint256)
if prefix < RLP_SHORT_START:
return prefix, pos + 1
# Sanity check: ensure this is a short string (not a long string or list)
assert prefix <= RLP_LONG_START, "Invalid RLP number encoding"
length: uint256 = prefix - RLP_SHORT_START
value: uint256 = convert(
abi_decode(abi_encode(slice(encoded, pos + 1, length)), (Bytes[32])), uint256
)
# abi_decode(abi_encode(bytesA), bytesB) is needed to unsafe cast bytesA to bytesB
return value, pos + 1 + length
```
```shell
>>> HeaderVerifier.decode_block_header(rlp_encoded_header)
(0xc215221221dd6673ae7ed2e50f47f6d020034657bb4a08010b5677a1f9d06d6d, 0x..., 0x..., 0x..., 22788903, 1750944347)
```
::::
### `submit_block_header`
::::description[`HeaderVerifier.submit_block_header(_oracle_address: address, _encoded_header: Bytes[bh_rlp.BLOCK_HEADER_SIZE])`]
Function to submit a block header. Decodes the RLP-encoded header and forwards it to the specified oracle contract.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_oracle_address` | `address` | The address of the oracle contract to submit the decoded header to |
| `_encoded_header` | `Bytes[bh_rlp.BLOCK_HEADER_SIZE]` | RLP-encoded block header data |
```vyper
interface IBlockOracle:
def submit_block_header(block_header: bh_rlp.BlockHeader): nonpayable
from modules import BlockHeaderRLPDecoder as bh_rlp
exports: (bh_rlp.decode_block_header,)
@external
def submit_block_header(_oracle_address: address, _encoded_header: Bytes[bh_rlp.BLOCK_HEADER_SIZE]):
"""
@notice Submit a block header. If it's correct and blockhash is applied, store it.
@param _oracle_address The address of the oracle contract
@param _encoded_header The block header to submit
"""
# Decode whatever is submitted
decoded_header: bh_rlp.BlockHeader = bh_rlp._decode_block_header(_encoded_header)
# Submit decoded header to oracle
extcall IBlockOracle(_oracle_address).submit_block_header(decoded_header)
```
```vyper
# BlockOracle.vy
@external
def submit_block_header(_header_data: bh_rlp.BlockHeader):
"""
@notice Submit block header. Available only to whitelisted verifier contract.
@param _header_data The block header to submit
"""
assert msg.sender == self.header_verifier, "Not authorized"
# Safety checks
assert _header_data.block_hash != empty(bytes32), "Invalid block hash"
assert self.block_hash[_header_data.block_number] != empty(bytes32), "Blockhash not applied"
assert _header_data.block_hash == self.block_hash[_header_data.block_number], "Blockhash does not match"
assert self.block_header[_header_data.block_number].block_hash == empty(bytes32), "Header already submitted"
# Store decoded header
self.block_header[_header_data.block_number] = _header_data
# Update last confirmed header if new
if _header_data.block_number > self.last_confirmed_header.block_number:
self.last_confirmed_header = _header_data
log SubmitBlockHeader(
block_number=_header_data.block_number,
block_hash=_header_data.block_hash,
)
```
```shell
>>> HeaderVerifier.submit_block_header('0xb10cface69821Ff7b245Cf5f28f3e714fDbd86b8', rlp_encoded_header)
```
::::
---
## LZBlockRelay
The `LZBlockRelay` contract is a cross-chain block hash relay built on LayerZero's messaging protocol, designed for deployment on multiple EVM-compatible chains alongside the `BlockOracle` and `MainnetBlockView` contracts. Its core function is to securely and efficiently relay recent Ethereum mainnet block hashes to other chains, enabling trust-minimized cross-chain state proofs and interoperability.
Operating in two modes — **read-enabled** (which can request and broadcast block hashes) and **broadcast-only** (which only receives broadcasts) — the contract verifies incoming LayerZero messages, commits block hashes to the local `BlockOracle`, and, when appropriate, rebroadcasts them to additional chains. All LayerZero peer and channel configurations are owner-controlled to ensure only trusted sources are permitted, supporting robust, decentralized, and secure cross-chain communication.
:::vyper[`LZBlockRelay.vy`]
The source code for the `LZBlockRelay.vy` contract can be found on [GitHub](https://github.com/curvefi/blockhash-oracle/blob/main/contracts/messengers/LZBlockRelay.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.4.3`.
The contract is deployed on all supported chains at `0xFacEFeeD696BFC0ebe7EaD3FFBb9a56290d31752`.
```json
[{"anonymous":false,"inputs":[{"indexed":true,"name":"block_number","type":"uint256"},{"indexed":true,"name":"block_hash","type":"bytes32"},{"components":[{"name":"eid","type":"uint32"},{"name":"fee","type":"uint256"}],"indexed":false,"name":"targets","type":"tuple[]"}],"name":"BlockHashBroadcast","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previous_owner","type":"address"},{"indexed":true,"name":"new_owner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"eid","type":"uint32"},{"indexed":false,"name":"peer","type":"bytes32"}],"name":"PeerSet","type":"event"},{"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"new_owner","type":"address"}],"name":"transfer_ownership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounce_ownership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"endpoint","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint32"}],"name":"peers","outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_eid","type":"uint32"},{"name":"_peer","type":"bytes32"}],"name":"setPeer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_delegate","type":"address"}],"name":"setDelegate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"name":"srcEid","type":"uint32"},{"name":"sender","type":"bytes32"},{"name":"nonce","type":"uint64"}],"name":"_origin","type":"tuple"},{"name":"_message","type":"bytes"},{"name":"_sender","type":"address"}],"name":"isComposeMsgSender","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"name":"srcEid","type":"uint32"},{"name":"sender","type":"bytes32"},{"name":"nonce","type":"uint64"}],"name":"_origin","type":"tuple"}],"name":"allowInitializePath","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_srcEid","type":"uint32"},{"name":"_sender","type":"bytes32"}],"name":"nextNonce","outputs":[{"name":"","type":"uint64"}],"stateMutability":"pure","type":"function"},{"inputs":[{"name":"_is_enabled","type":"bool"},{"name":"_read_channel","type":"uint32"},{"name":"_mainnet_eid","type":"uint32"},{"name":"_mainnet_view","type":"address"}],"name":"set_read_config","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_eids","type":"uint32[]"},{"name":"_peers","type":"address[]"}],"name":"set_peers","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_oracle","type":"address"}],"name":"set_block_oracle","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_amount","type":"uint256"}],"name":"withdraw_eth","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"fallback"},{"inputs":[{"name":"_read_gas_limit","type":"uint128"},{"name":"_value","type":"uint128"}],"name":"quote_read_fee","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_target_eids","type":"uint32[]"},{"name":"_lz_receive_gas_limit","type":"uint128"}],"name":"quote_broadcast_fees","outputs":[{"name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_target_eids","type":"uint32[]"},{"name":"_target_fees","type":"uint256[]"},{"name":"_lz_receive_gas_limit","type":"uint128"},{"name":"_read_gas_limit","type":"uint128"}],"name":"request_block_hash","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"name":"_target_eids","type":"uint32[]"},{"name":"_target_fees","type":"uint256[]"},{"name":"_lz_receive_gas_limit","type":"uint128"},{"name":"_read_gas_limit","type":"uint128"},{"name":"_block_number","type":"uint256"}],"name":"request_block_hash","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"name":"_target_eids","type":"uint32[]"},{"name":"_target_fees","type":"uint256[]"},{"name":"_lz_receive_gas_limit","type":"uint128"}],"name":"broadcast_latest_block","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"name":"srcEid","type":"uint32"},{"name":"sender","type":"bytes32"},{"name":"nonce","type":"uint64"}],"name":"_origin","type":"tuple"},{"name":"_guid","type":"bytes32"},{"name":"_message","type":"bytes"},{"name":"_executor","type":"address"},{"name":"_extraData","type":"bytes"}],"name":"lzReceive","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"read_enabled","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"read_channel","outputs":[{"name":"","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mainnet_eid","outputs":[{"name":"","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mainnet_block_view","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"block_oracle","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_endpoint","type":"address"}],"outputs":[],"stateMutability":"nonpayable","type":"constructor"}]
```
:::
---
## Configuration & Ownership
This section covers owner-only functions for configuring LayerZero channels, peers, delegates, and the block oracle. These functions are critical for secure cross-chain operation and must be managed by the contract owner (DAO).
### `set_read_config`
::::description[`LZBlockRelay.set_read_config(_is_enabled: bool, _read_channel: uint32, _mainnet_eid: uint32, _mainnet_view: address)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Function to configure read functionality.
| Input | Type | Description |
|--------------|-----------|--------------------------------------|
| `_is_enabled` | `bool` | Whether this contract can initiate reads |
| `_read_channel` | `uint32` | LZ read channel ID |
| `_mainnet_eid` | `uint32` | Mainnet endpoint ID |
| `_mainnet_view` | `address` | MainnetBlockView contract address |
Emits: `PeerSet` event.
```vyper
read_enabled: public(bool)
read_channel: public(uint32)
mainnet_eid: public(uint32)
mainnet_block_view: public(address)
@external
def set_read_config(
_is_enabled: bool, _read_channel: uint32, _mainnet_eid: uint32, _mainnet_view: address
):
"""
@notice Configure read functionality
@param _is_enabled Whether this contract can initiate reads
@param _read_channel LZ read channel ID
@param _mainnet_eid Mainnet endpoint ID
@param _mainnet_view MainnetBlockView contract address
"""
ownable._check_owner()
assert _read_channel > OApp.READ_CHANNEL_THRESHOLD, "Invalid read channel"
assert (_is_enabled and _mainnet_eid != 0 and _mainnet_view != empty(address)) or (
not _is_enabled and _mainnet_eid == 0 and _mainnet_view == empty(address)
), "Invalid read config"
# Clean up old peer if switching channels while read is enabled
# This prevents leaving stale peer mappings when changing read channels
if self.read_enabled and self.read_channel != _read_channel:
OApp._setPeer(self.read_channel, convert(empty(address), bytes32))
self.read_enabled = _is_enabled
self.read_channel = _read_channel
self.mainnet_eid = _mainnet_eid
self.mainnet_block_view = _mainnet_view
peer: bytes32 = convert(self, bytes32) if _is_enabled else convert(empty(address), bytes32)
OApp._setPeer(_read_channel, peer)
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```vyper
event PeerSet:
eid: uint32
peer: bytes32
# Mapping to store peers associated with corresponding endpoints
peers: public(HashMap[uint32, bytes32])
@internal
def _setPeer(_eid: uint32, _peer: bytes32):
"""
@notice Internal function to set peer address
@param _eid The endpoint ID.
@param _peer The address of the peer to be associated with the corresponding endpoint.
"""
self.peers[_eid] = _peer
log PeerSet(eid=_eid, peer=_peer)
```
```shell
>>> LZBlockRelay.set_read_config(True, 30101, 30101, '0xb10CfacE69cc0B7F1AE0Dc8E6aD186914f6e7EEA')
```
::::
### `set_block_oracle`
::::description[`LZBlockRelay.set_block_oracle(_oracle: address)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Sets the BlockOracle address for this contract.
| Input | Type | Description |
|-----------|---------|----------------------------|
| `_oracle` | `address` | The BlockOracle address to set. |
```vyper
interface IBlockOracle:
def commit_block(block_number: uint256, block_hash: bytes32) -> bool: nonpayable
def last_confirmed_block_number() -> uint256: view
def get_block_hash(block_number: uint256) -> bytes32: view
from snekmate.auth import ownable
@external
def set_block_oracle(_oracle: address):
"""
@notice Set the block oracle address
@param _oracle Block oracle address
"""
ownable._check_owner()
self.block_oracle = IBlockOracle(_oracle)
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> LZBlockRelay.set_block_oracle('0xb10cface69821Ff7b245Cf5f28f3e714fDbd86b8')
```
::::
### `withdraw_eth`
::::description[`LZBlockRelay.withdraw_eth(_amount: uint256)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Withdraws ETH from the contract. ETH can be accumulated from LayerZero refunds.
| Input | Type | Description |
|---------|---------|----------------------------|
| `_amount` | `uint256` | Amount of ETH to withdraw. |
```vyper
from snekmate.auth import ownable
@external
def withdraw_eth(_amount: uint256):
"""
@notice Withdraw ETH from contract
@dev ETH can be accumulated from LZ refunds
@param _amount Amount to withdraw
"""
ownable._check_owner()
assert self.balance >= _amount, "Insufficient balance"
send(msg.sender, _amount)
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> LZBlockRelay.withdraw_eth(1000000000000000000)
```
::::
---
## LayerZero Messaging & Peers
This section documents LayerZero-specific configuration and peer management. These functions are critical for secure cross-chain communication. Only trusted peers should be set to avoid malicious message injection.
### `endpoint`
::::description[`LZBlockRelay.endpoint() -> address: view`]
Getter for the LayerZero endpoint.
Returns: Lz Endpoint (`address`).
```vyper
from ..modules.oapp_vyper.src import OApp # main module
exports: (
OApp.endpoint,
OApp.peers,
OApp.setPeer,
OApp.setDelegate,
OApp.isComposeMsgSender,
OApp.allowInitializePath,
OApp.nextNonce,
)
```
```vyper
# LayerZero EndpointV2 interface
interface ILayerZeroEndpointV2:
def quote(_params: MessagingParams, _sender: address) -> MessagingFee: view
def send(_params: MessagingParams, _refundAddress: address) -> MessagingReceipt: payable
def setDelegate(_delegate: address): nonpayable
def eid() -> uint32: view
def lzToken() -> address: view
# The LayerZero endpoint associated with the given OApp
endpoint: public(immutable(ILayerZeroEndpointV2))
```
endpoint on Arbitrum
```shell
>>> LZBlockRelay.endpoint()
'0x1a44076050125825900e736c501f859c50fE728c'
```
::::
### `peers`
::::description[`LZBlockRelay.peers(_eid: uint32) -> bytes32: view`]
Getter for the peer address (OApp instance) for a given endpoint ID.
| Input | Type | Description |
|-------|--------|------------------|
| `_eid` | `uint32` | The endpoint ID. |
Returns: peer address for the given endpoint ID (`bytes32`).
```vyper
from ..modules.oapp_vyper.src import OApp # main module
exports: (
OApp.endpoint,
OApp.peers,
OApp.setPeer,
OApp.setDelegate,
OApp.isComposeMsgSender,
OApp.allowInitializePath,
OApp.nextNonce,
)
```
```vyper
# Mapping to store peers associated with corresponding endpoints
peers: public(HashMap[uint32, bytes32])
```
```shell
>>> LZBlockRelay.peers(0)
'0x0000000000000000000000000000000000000000000000000000000000000000'
```
::::
### `setPeer`
::::description[`LZBlockRelay.setPeer(_eid: uint32, _peer: bytes32)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Sets the peer address (OApp instance) for a corresponding endpoint. This establishes a trusted cross-chain communication channel.
| Input | Type | Description |
|-------|--------|----------------------------------|
| `_eid` | `uint32` | The endpoint ID. |
| `_peer` | `bytes32`| The peer address (OApp instance) |
Emits: `PeerSet` event.
```vyper
from ..modules.oapp_vyper.src import OApp # main module
exports: (
OApp.endpoint,
OApp.peers,
OApp.setPeer,
OApp.setDelegate,
OApp.isComposeMsgSender,
OApp.allowInitializePath,
OApp.nextNonce,
)
```
```vyper
event PeerSet:
eid: uint32
peer: bytes32
# Mapping to store peers associated with corresponding endpoints
peers: public(HashMap[uint32, bytes32])
@external
def setPeer(_eid: uint32, _peer: bytes32):
"""
@notice Sets the peer address (OApp instance) for a corresponding endpoint.
@param _eid The endpoint ID.
@param _peer The address of the peer to be associated with the corresponding endpoint.
@dev Only the owner/admin of the OApp can call this function.
@dev Indicates that the peer is trusted to send LayerZero messages to this OApp.
@dev Set this to bytes32(0) to remove the peer address.
@dev Peer is a bytes32 to accommodate non-evm chains.
"""
ownable._check_owner()
self._setPeer(_eid, _peer)
@internal
def _setPeer(_eid: uint32, _peer: bytes32):
"""
@notice Internal function to set peer address
@param _eid The endpoint ID.
@param _peer The address of the peer to be associated with the corresponding endpoint.
"""
self.peers[_eid] = _peer
log PeerSet(eid=_eid, peer=_peer)
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> LZBlockRelay.setPeer(30110, 0x000000000000000000000000FacEFeeD696BFC0ebe7EaD3FFBb9a56290d31752)
```
::::
### `set_peers`
::::description[`LZBlockRelay.set_peers(_eids: DynArray[uint32, MAX_N_BROADCAST], _peers: DynArray[address, MAX_N_BROADCAST])`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Function to set peers for a corresponding endpoints. This is a batched version of the `OApp.setPeer` that accepts EVM addresses only.
| Input | Type | Description |
|----------|-----------------------------------|----------------------------------|
| `_eids` | `DynArray[uint32, MAX_N_BROADCAST]` | The endpoint IDs |
| `_peers` | `DynArray[address, MAX_N_BROADCAST]` | Addresses of the peers to associate |
Emits: `PeerSet` event.
```vyper
from ..modules.oapp_vyper.src import OApp # main module
exports: (
OApp.endpoint,
OApp.peers,
OApp.setPeer,
OApp.setDelegate,
OApp.isComposeMsgSender,
OApp.allowInitializePath,
OApp.nextNonce,
)
@external
def set_peers(_eids: DynArray[uint32, MAX_N_BROADCAST], _peers: DynArray[address, MAX_N_BROADCAST]):
"""
@notice Set peers for a corresponding endpoints. Batched version of OApp.setPeer that accept address (EVM only).
@param _eids The endpoint IDs.
@param _peers Addresses of the peers to be associated with the corresponding endpoints.
"""
ownable._check_owner()
assert len(_eids) == len(_peers), "Invalid peer arrays"
for i: uint256 in range(0, len(_eids), bound=MAX_N_BROADCAST):
OApp._setPeer(_eids[i], convert(_peers[i], bytes32))
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```vyper
event PeerSet:
eid: uint32
peer: bytes32
# Mapping to store peers associated with corresponding endpoints
peers: public(HashMap[uint32, bytes32])
@internal
def _setPeer(_eid: uint32, _peer: bytes32):
"""
@notice Internal function to set peer address
@param _eid The endpoint ID.
@param _peer The address of the peer to be associated with the corresponding endpoint.
"""
self.peers[_eid] = _peer
log PeerSet(eid=_eid, peer=_peer)
```
```shell
>>> LZBlockRelay.set_peers([30110, 30111], ['0xFacEFeeD696BFC0ebe7EaD3FFBb9a56290d31752', '0xFacEFeeD696BFC0ebe7EaD3FFBb9a56290d31752'])
```
::::
### `setDelegate`
::::description[`LZBlockRelay.setDelegate(_delegate: address)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Sets the delegate address for the OApp. The delegate can manage LayerZero configurations on behalf of the contract.
| Input | Type | Description |
|------------|-----------|------------------------------------|
| `_delegate` | `address` | The address of the delegate to set. |
```vyper
from ..modules.oapp_vyper.src import OApp # main module
exports: (
OApp.endpoint,
OApp.peers,
OApp.setPeer,
OApp.setDelegate,
OApp.isComposeMsgSender,
OApp.allowInitializePath,
OApp.nextNonce,
)
```
```vyper
# LayerZero EndpointV2 interface
interface ILayerZeroEndpointV2:
def quote(_params: MessagingParams, _sender: address) -> MessagingFee: view
def send(_params: MessagingParams, _refundAddress: address) -> MessagingReceipt: payable
def setDelegate(_delegate: address): nonpayable
def eid() -> uint32: view
def lzToken() -> address: view
@external
def setDelegate(_delegate: address):
"""
@notice Sets the delegate address for the OApp.
@param _delegate The address of the delegate to be set.
@dev Only the owner/admin of the OApp can call this function.
@dev Provides the ability for a delegate to set configs, on behalf of the OApp,
directly on the Endpoint contract.
"""
ownable._check_owner()
extcall endpoint.setDelegate(_delegate)
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> LZBlockRelay.setDelegate('0x1234567890123456789012345678901234567890')
```
::::
### `isComposeMsgSender`
::::description[`LZBlockRelay.isComposeMsgSender(_origin: Origin, _message: Bytes[MAX_MESSAGE_SIZE], _sender: address) -> bool: view`]
Function to check whether an address is an approved composeMsg sender to the Endpoint.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_origin` | `Origin` | Struct containing of srcEid, sender and nonce |
| `_message` | `Bytes[MAX_MESSAGE_SIZE]` | The lzReceive payload |
| `_sender` | `address` | The sender address |
Returns: true or false (`bool`).
```vyper
from ..modules.oapp_vyper.src import OApp # main module
exports: (
OApp.endpoint,
OApp.peers,
OApp.setPeer,
OApp.setDelegate,
OApp.isComposeMsgSender,
OApp.allowInitializePath,
OApp.nextNonce,
)
```
```vyper
struct Origin:
srcEid: uint32
sender: bytes32
nonce: uint64
@external
@view
def isComposeMsgSender(
_origin: Origin, _message: Bytes[MAX_MESSAGE_SIZE], _sender: address
) -> bool:
"""
@notice Indicates whether an address is an approved composeMsg sender to the Endpoint.
@param _origin The origin information containing the source endpoint and sender address.
@param _message The lzReceive payload.
@param _sender The sender address.
@return isSender Is a valid sender.
"""
return _sender == self
```
```shell
>>> LZBlockRelay.isComposeMsgSender(origin, message, '0xFacEFeeD696BFC0ebe7EaD3FFBb9a56290d31752')
True
```
::::
### `allowInitializePath`
::::description[`LZBlockRelay.allowInitializePath(_origin: Origin) -> bool: view`]
Function to check if the path initialization is allowed based on the provided origin.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_origin` | `Origin` | Struct containing of srcEid, sender and nonce |
Returns: true or false (`bool`).
```vyper
from ..modules.oapp_vyper.src import OApp # main module
exports: (
OApp.endpoint,
OApp.peers,
OApp.setPeer,
OApp.setDelegate,
OApp.isComposeMsgSender,
OApp.allowInitializePath,
OApp.nextNonce,
)
```
```vyper
struct Origin:
srcEid: uint32
sender: bytes32
nonce: uint64
@external
@view
def allowInitializePath(_origin: Origin) -> bool:
"""
@notice Checks if the path initialization is allowed based on the provided origin.
@param _origin The origin information containing the source endpoint and sender address.
@return Whether the path has been initialized.
@dev This indicates to the endpoint that the OApp has enabled msgs for this particular path to be received.
@dev This defaults to assuming if a peer has been set, its initialized.
"""
return self.peers[_origin.srcEid] == _origin.sender
```
```shell
>>> LZBlockRelay.allowInitializePath(origin)
True
```
::::
### `nextNonce`
::::description[`LZBlockRelay.nextNonce(_srcEid: uint32, _sender: bytes32) -> uint64: pure`]
:::warning
Vyper-specific: If your app relies on ordered execution, you must change this function. By default this is NOT enabled. ie. nextNonce is hardcoded to return 0.
:::
Function which retrieves the next nonce for a given source endpoint and sender address. The path nonce starts from 1. If 0 is returned it means that there is NO nonce ordered enforcement. Is required by the off-chain executor to determine the OApp expects msg execution is ordered. This is also enforced by the OApp.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_srcEid` | `uint32` | The source endpoint ID. |
| `_sender` | `bytes32` | The sender address. |
Returns: next nonce (`uint64`).
```vyper
from ..modules.oapp_vyper.src import OApp # main module
exports: (
OApp.endpoint,
OApp.peers,
OApp.setPeer,
OApp.setDelegate,
OApp.isComposeMsgSender,
OApp.allowInitializePath,
OApp.nextNonce,
)
```
```vyper
@external
@pure
def nextNonce(_srcEid: uint32, _sender: bytes32) -> uint64:
"""
@notice Retrieves the next nonce for a given source endpoint and sender address.
@dev Vyper-specific: If your app relies on ordered execution, you must change this function.
@param _srcEid The source endpoint ID.
@param _sender The sender address.
@return nonce The next nonce.
@dev The path nonce starts from 1. If 0 is returned it means that there is NO nonce ordered enforcement.
@dev Is required by the off-chain executor to determine the OApp expects msg execution is ordered.
@dev This is also enforced by the OApp.
@dev By default this is NOT enabled. ie. nextNonce is hardcoded to return 0.
"""
return 0
```
```shell
>>> LZBlockRelay.nextNonce(30110, 0x000000000000000000000000FacEFeeD696BFC0ebe7EaD3FFBb9a56290d31752)
0
```
::::
---
## Block Hash Operations
This section covers the core cross-chain and block hash relay logic. These functions are responsible for requesting, broadcasting, and receiving block hashes.
:::info
Currently, only block hashes received via trusted LayerZero channels are committed to the oracle. Later on, more channels can be added.
:::
### `request_block_hash`
::::description[`LZBlockRelay.request_block_hash(_target_eids: DynArray[uint32, MAX_N_BROADCAST], _target_fees: DynArray[uint256, MAX_N_BROADCAST], _lz_receive_gas_limit: uint128, _read_gas_limit: uint128, _block_number: uint256 = 0)`]
Function to request a block hash from mainnet and broadcast it to specified targets. User must ensure `msg.value` is sufficient. The caller covers read fee (`quote_read_fee`) and broadcast fee (`quote_broadcast_fees`).
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_target_eids` | `DynArray[uint32, MAX_N_BROADCAST]` | List of chain IDs to broadcast to |
| `_target_fees` | `DynArray[uint256, MAX_N_BROADCAST]` | List of fees per chain (must match _target_eids length) |
| `_lz_receive_gas_limit` | `uint128` | Gas limit for lzReceive (same for all targets) |
| `_read_gas_limit` | `uint128` | Gas limit for read operation |
| `_block_number` | `uint256` | Optional block number (0 means latest) |
```vyper
@external
@payable
def request_block_hash(
_target_eids: DynArray[uint32, MAX_N_BROADCAST],
_target_fees: DynArray[uint256, MAX_N_BROADCAST],
_lz_receive_gas_limit: uint128,
_read_gas_limit: uint128,
_block_number: uint256 = 0,
):
"""
@notice Request block hash from mainnet and broadcast to specified targets
@param _target_eids List of chain IDs to broadcast to
@param _target_fees List of fees per chain (must match _target_eids length)
@param _lz_receive_gas_limit Gas limit for lzReceive (same for all targets)
@param _read_gas_limit Gas limit for read operation
@param _block_number Optional block number (0 means latest)
@dev User must ensure msg.value is sufficient:
- must cover read fee (quote_read_fee)
- must cover broadcast fees (quote_broadcast_fees)
"""
assert self.read_enabled, "Read not enabled"
assert len(_target_eids) == len(_target_fees), "Length mismatch"
self._request_block_hash(
_block_number,
_target_eids,
_target_fees,
_lz_receive_gas_limit,
_read_gas_limit,
)
@internal
@payable
def _request_block_hash(
_block_number: uint256,
_target_eids: DynArray[uint32, MAX_N_BROADCAST],
_target_fees: DynArray[uint256, MAX_N_BROADCAST],
_lz_receive_gas_limit: uint128,
_read_gas_limit: uint128,
):
"""
@notice Internal function to request block hash from mainnet and broadcast to specified targets
@param _block_number Block number to request
@param _target_eids Target EIDs to broadcast to
@param _target_fees Target fees to pay per broadcast
@param _lz_receive_gas_limit Gas limit for lzReceive
@param _read_gas_limit Gas limit for read operation
"""
# Store target EIDs and fees for lzReceive
cached_targets: DynArray[BroadcastTarget, MAX_N_BROADCAST] = []
sum_target_fees: uint256 = 0
for i: uint256 in range(0, len(_target_eids), bound=MAX_N_BROADCAST):
cached_targets.append(BroadcastTarget(eid=_target_eids[i], fee=_target_fees[i]))
sum_target_fees += _target_fees[i]
assert sum_target_fees <= msg.value, "Insufficient value" # dev: check is here because we sum here
message: Bytes[OApp.MAX_MESSAGE_SIZE] = self._prepare_read_request(_block_number)
# Create options using OptionsBuilder module
options: Bytes[OptionsBuilder.MAX_OPTIONS_TOTAL_SIZE] = OptionsBuilder.newOptions()
options = OptionsBuilder.addExecutorLzReadOption(
options, _read_gas_limit, READ_RETURN_SIZE, convert(sum_target_fees, uint128)
)
# Send message
fees: OApp.MessagingFee = OApp.MessagingFee(nativeFee=msg.value, lzTokenFee=0)
# Fees = read fee + broadcast fees (value of read return message)
receipt: OApp.MessagingReceipt = OApp._lzSend(
self.read_channel, message, options, fees, msg.sender # dev: refund excess fee to sender
)
# Store targets for lzReceive using receipt.guid as key
self.broadcast_data[receipt.guid] = BroadcastData(
targets=cached_targets,
gas_limit=_lz_receive_gas_limit,
)
```
```shell
>>> LZBlockRelay.request_block_hash([30110], [1000000000000000], 200000, 200000, 0)
```
::::
### `broadcast_latest_block`
::::description[`LZBlockRelay.broadcast_latest_block(_target_eids: DynArray[uint32, MAX_N_BROADCAST], _target_fees: DynArray[uint256, MAX_N_BROADCAST], _lz_receive_gas_limit: uint128)`]
:::info
Only broadcast what was received via lzRead to prevent potentially malicious hashes from other sources
:::
Function to broadcast the latest confirmed block hash to specified chains.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_target_eids` | `DynArray[uint32, MAX_N_BROADCAST]` | List of chain IDs to broadcast to |
| `_target_fees` | `DynArray[uint256, MAX_N_BROADCAST]` | List of fees per chain (must match _target_eids length) |
| `_lz_receive_gas_limit` | `uint128` | Gas limit for lzReceive (same for all targets) |
Emits: `BlockHashBroadcast` event.
```vyper
@external
@payable
def broadcast_latest_block(
_target_eids: DynArray[uint32, MAX_N_BROADCAST],
_target_fees: DynArray[uint256, MAX_N_BROADCAST],
_lz_receive_gas_limit: uint128,
):
"""
@notice Broadcast latest confirmed block hash to specified chains
@param _target_eids List of chain IDs to broadcast to
@param _target_fees List of fees per chain (must match _target_eids length)
@param _lz_receive_gas_limit Gas limit for lzReceive (same for all targets)
@dev Only broadcast what was received via lzRead to prevent potentially malicious hashes from other sources
"""
assert self.read_enabled, "Can only broadcast from read-enabled chains"
assert self.block_oracle != empty(IBlockOracle), "Oracle not configured"
assert len(_target_eids) == len(_target_fees), "Length mismatch"
# Get latest block from oracle
block_number: uint256 = staticcall self.block_oracle.last_confirmed_block_number()
block_hash: bytes32 = staticcall self.block_oracle.get_block_hash(block_number)
assert block_hash != empty(bytes32), "No confirmed blocks"
# Only broadcast if this block was received via lzRead
assert self.received_blocks[block_number] == block_hash, "Unknown source"
# Prepare broadcast targets
broadcast_targets: DynArray[BroadcastTarget, MAX_N_BROADCAST] = []
for i: uint256 in range(0, len(_target_eids), bound=MAX_N_BROADCAST):
broadcast_targets.append(BroadcastTarget(eid=_target_eids[i], fee=_target_fees[i]))
self._broadcast_block(
block_number,
block_hash,
BroadcastData(targets=broadcast_targets, gas_limit=_lz_receive_gas_limit),
msg.sender,
)
```
```shell
>>> LZBlockRelay.broadcast_latest_block([30110], [1000000000000000], 200000)
```
::::
### `lzReceive`
::::description[`LZBlockRelay.lzReceive(_origin: OApp.Origin, _guid: bytes32, _message: Bytes[OApp.MAX_MESSAGE_SIZE], _executor: address, _extraData: Bytes[OApp.MAX_EXTRA_DATA_SIZE])`]
Handles incoming LayerZero messages, including block hash read responses from mainnet and block hash broadcasts from other chains. Verifies the message source, commits the block hash to the local BlockOracle, and, if appropriate, rebroadcasts the hash to additional chains. Only block hashes received via trusted LayerZero channels are committed.
This function may emit events such as block hash commit or broadcast events, depending on the message type and contract state.
| Input | Type | Description |
|-------------|-----------------------------------|-----------------------------------------------------------------------------|
| `_origin` | `OApp.Origin` | Struct containing the source endpoint ID (`srcEid`), sender address, and nonce. Used to verify the message source. |
| `_guid` | `bytes32` | Global unique identifier for the message, used for tracking and rebroadcast logic. |
| `_message` | `Bytes[OApp.MAX_MESSAGE_SIZE]` | Encoded message payload containing the block number and block hash. |
| `_executor` | `address` | Address of the executor for the message. |
| `_extraData`| `Bytes[OApp.MAX_EXTRA_DATA_SIZE]` | Additional data passed by the executor, used for advanced LayerZero features.|
```vyper
@payable
@external
def lzReceive(
_origin: OApp.Origin,
_guid: bytes32,
_message: Bytes[OApp.MAX_MESSAGE_SIZE],
_executor: address,
_extraData: Bytes[OApp.MAX_EXTRA_DATA_SIZE],
):
"""
@notice Handle messages: read responses, and regular messages
@dev Two types of messages:
1. Read responses (from read channel)
2. Regular messages (block hash broadcasts from other chains)
@param _origin Origin information containing srcEid, sender, and nonce
@param _guid Global unique identifier for the message
@param _message The encoded message payload containing block number and hash
@param _executor Address of the executor for the message
@param _extraData Additional data passed by the executor
"""
# Verify message source
OApp._lzReceive(_origin, _guid, _message, _executor, _extraData)
if _origin.srcEid == self.read_channel:
# Only handle read response if read is enabled
assert self.read_enabled, "Read not enabled"
# Decode block hash and number from response
block_number: uint256 = 0
block_hash: bytes32 = empty(bytes32)
block_number, block_hash = abi_decode(_message, (uint256, bytes32))
if block_hash == empty(bytes32):
return # Invalid response
# Store received block hash
self.received_blocks[block_number] = block_hash
# Commit block hash to oracle
self._commit_block(block_number, block_hash)
broadcast_data: BroadcastData = self.broadcast_data[_guid]
if len(broadcast_data.targets) > 0:
# Verify that attached value covers requested broadcast fees
total_fee: uint256 = 0
for target: BroadcastTarget in broadcast_data.targets:
total_fee += target.fee
assert msg.value >= total_fee, "Insufficient msg.value"
# Perform broadcast
self._broadcast_block(
block_number,
block_hash,
broadcast_data,
self, # dev: refund excess fee to self
)
else:
# Regular message - decode and commit block hash
block_number: uint256 = 0
block_hash: bytes32 = empty(bytes32)
block_number, block_hash = abi_decode(_message, (uint256, bytes32))
self._commit_block(block_number, block_hash)
```
```vyper
struct Origin:
srcEid: uint32
sender: bytes32
nonce: uint64
@internal
def _lzReceive(
_origin: Origin,
_guid: bytes32,
_message: Bytes[MAX_MESSAGE_SIZE],
_executor: address,
_extraData: Bytes[MAX_EXTRA_DATA_SIZE],
):
"""
@dev Entry point for receiving messages or packets from the endpoint.
@param _origin The origin information containing the source endpoint and sender address.
@param _guid The unique identifier for the received LayerZero message.
@param _message The payload of the received message.
@param _executor The address of the executor for the received message.
@param _extraData Additional arbitrary data provided by the corresponding executor.
"""
# Verify that the sender is the endpoint
assert msg.sender == endpoint.address, "OApp: only endpoint"
# Verify that the message comes from a trusted peer
assert self._getPeerOrRevert(_origin.srcEid) == _origin.sender, "OApp: invalid sender"
```
```shell
# Called by the LayerZero endpoint, not directly by users
>>> LZBlockRelay.lzReceive(origin, guid, message, executor, extra_data)
```
::::
---
## Fee Quoting
### `quote_read_fee`
::::description[`LZBlockRelay.quote_read_fee(_read_gas_limit: uint128, _value: uint128) -> uint256: view`]
Quotes the fee required for reading a block hash from mainnet via LayerZero. Only callable if read is enabled.
| Input | Type | Description |
|------------------|-----------|---------------------------------------------|
| `_read_gas_limit`| `uint128` | Gas to be provided in return message |
| `_value` | `uint128` | Value to be provided in return message |
Returns: Fee in native tokens required for the read operation (`uint256`).
```vyper
@external
@view
def quote_read_fee(
_read_gas_limit: uint128,
_value: uint128,
) -> uint256:
"""
@notice Quote fee for reading block hash from mainnet
@param _read_gas_limit Gas to be provided in return message
@param _value Value to be provided in return message
@return Fee in native tokens required for the read operation
"""
assert self.read_enabled, "Read not enabled - call set_read_config"
message: Bytes[OApp.MAX_MESSAGE_SIZE] = self._prepare_read_request(0) # dev: 0 for latest block
# Create options using OptionsBuilder module
options: Bytes[OptionsBuilder.MAX_OPTIONS_TOTAL_SIZE] = OptionsBuilder.newOptions()
options = OptionsBuilder.addExecutorLzReadOption(
options, _read_gas_limit, READ_RETURN_SIZE, _value
)
return OApp._quote(
self.read_channel,
message,
options,
False,
).nativeFee
```
```vyper
@internal
@pure
def newOptions() -> Bytes[MAX_OPTIONS_TOTAL_SIZE]:
"""
@notice Creates a new options container with type 3.
@return options The newly created options container.
"""
options: Bytes[MAX_OPTIONS_TOTAL_SIZE] = concat(convert(TYPE_3, bytes2), b"")
return options
@internal
@pure
def addExecutorOption(
_options: Bytes[MAX_OPTIONS_TOTAL_SIZE],
_optionType: uint8,
_option: Bytes[MAX_OPTION_SINGLE_SIZE],
) -> Bytes[MAX_OPTIONS_TOTAL_SIZE]:
"""
@dev Adds an executor option to the existing options.
@param _options The existing options container.
@param _optionType The type of the executor option.
@param _option The encoded data for the executor option.
@return options The updated options container.
"""
assert convert(slice(_options, 0, 2), uint16) == TYPE_3, "OApp: invalid option type"
# Account for header bytes: 1 worker + 2 size + 1 type = 4 bytes
assert (len(_options) + len(_option) + 4 <= MAX_OPTIONS_TOTAL_SIZE), "OApp: options size exceeded"
return concat(
convert(_options, Bytes[MAX_OPTIONS_TOTAL_SIZE - MAX_OPTION_SINGLE_SIZE - 4]), # downcast Bytes size, -4 for header
convert(EXECUTOR_WORKER_ID, bytes1),
convert(convert(len(_option) + 1, uint16), bytes2), # +1 for optionType
convert(_optionType, bytes1),
_option,
)
```
```vyper
interface ILayerZeroEndpointV2:
def quote(_params: MessagingParams, _sender: address) -> MessagingFee: view
def send(_params: MessagingParams, _refundAddress: address) -> MessagingReceipt: payable
def setDelegate(_delegate: address): nonpayable
def eid() -> uint32: view
def lzToken() -> address: view
# Mapping to store peers associated with corresponding endpoints
peers: public(HashMap[uint32, bytes32])
@internal
@view
def _quote(
_dstEid: uint32,
_message: Bytes[MAX_MESSAGE_SIZE],
_options: Bytes[MAX_OPTIONS_TOTAL_SIZE],
_payInLzToken: bool,
) -> MessagingFee:
"""
@dev Internal function to interact with the LayerZero EndpointV2.quote() for fee calculation.
@param _dstEid The destination endpoint ID.
@param _message The message payload.
@param _options Additional options for the message.
@param _payInLzToken Flag indicating whether to pay the fee in LZ tokens.
@return fee The calculated MessagingFee for the message.
- nativeFee: The native fee for the message.
- lzTokenFee: The LZ token fee for the message.
"""
return staticcall endpoint.quote(
MessagingParams(
dstEid=_dstEid,
receiver=self._getPeerOrRevert(_dstEid),
message=_message,
options=_options,
payInLzToken=_payInLzToken,
),
self,
)
@view
@internal
def _getPeerOrRevert(_eid: uint32) -> bytes32:
"""
@notice Internal function to get the peer address associated with a specific endpoint;
reverts if NOT set.
@param _eid The endpoint ID.
@return peer The address of the peer associated with the specified endpoint.
"""
peer: bytes32 = self.peers[_eid]
assert peer != empty(bytes32), "OApp: no peer"
return peer
```
```shell
>>> LZBlockRelay.quote_read_fee(200000, 0)
1000000000000000
```
::::
### `quote_broadcast_fees`
::::description[`LZBlockRelay.quote_broadcast_fees(_target_eids: DynArray[uint32, MAX_N_BROADCAST], _lz_receive_gas_limit: uint128) -> DynArray[uint256, MAX_N_BROADCAST]: view`]
Estimates the LayerZero fee required to broadcast a block hash to each specified target chain. Useful for integrators to determine the cost of broadcasting to multiple chains before submitting a transaction. Only targets with a configured peer will return a nonzero fee.
| Input | Type | Description |
|------|------|-------------|
| `_target_eids` | `DynArray[uint32, MAX_N_BROADCAST]` | List of target chain endpoint IDs to quote broadcast fees for. |
| `_lz_receive_gas_limit` | `uint128` | Gas limit to be provided for the lzReceive call on each target. |
Returns: an array of fees in native tokens, one per target chain, with zero for any target not configured (`DynArray[uint256, MAX_N_BROADCAST]`).
```vyper
@external
@view
def quote_broadcast_fees(
_target_eids: DynArray[uint32, MAX_N_BROADCAST],
_lz_receive_gas_limit: uint128,
) -> DynArray[uint256, MAX_N_BROADCAST]:
"""
@notice Quote fees for broadcasting block hash to specified targets
@param _target_eids List of chain IDs to broadcast to
@param _lz_receive_gas_limit Gas limit for lzReceive
@return Array of fees per target chain (0 if target not configured)
"""
# Prepare dummy broadcast message (uint256 number, bytes32 hash)
message: Bytes[OApp.MAX_MESSAGE_SIZE] = abi_encode(empty(uint256), empty(bytes32))
# Prepare array of fees per chain
fees: DynArray[uint256, MAX_N_BROADCAST] = []
# Prepare options (same for all targets)
options: Bytes[OptionsBuilder.MAX_OPTIONS_TOTAL_SIZE] = OptionsBuilder.newOptions()
options = OptionsBuilder.addExecutorLzReceiveOption(options, _lz_receive_gas_limit, 0)
# Cycle through targets
for eid: uint32 in _target_eids:
target: bytes32 = OApp.peers[eid] # Use peers directly
if target == empty(bytes32):
fees.append(0)
continue
# Get fee for target EID and append to array
fees.append(OApp._quote(eid, message, options, False).nativeFee)
return fees
```
```vyper
from . import VyperConstants as constants
MAX_OPTIONS_TOTAL_SIZE: constant(uint256) = constants.MAX_OPTIONS_TOTAL_SIZE
MAX_OPTION_SINGLE_SIZE: constant(uint256) = constants.MAX_OPTION_SINGLE_SIZE
@internal
@pure
def newOptions() -> Bytes[MAX_OPTIONS_TOTAL_SIZE]:
"""
@notice Creates a new options container with type 3.
@return options The newly created options container.
"""
options: Bytes[MAX_OPTIONS_TOTAL_SIZE] = concat(convert(TYPE_3, bytes2), b"")
return options
@internal
@pure
def addExecutorOption(
_options: Bytes[MAX_OPTIONS_TOTAL_SIZE],
_optionType: uint8,
_option: Bytes[MAX_OPTION_SINGLE_SIZE],
) -> Bytes[MAX_OPTIONS_TOTAL_SIZE]:
"""
@dev Adds an executor option to the existing options.
@param _options The existing options container.
@param _optionType The type of the executor option.
@param _option The encoded data for the executor option.
@return options The updated options container.
"""
assert convert(slice(_options, 0, 2), uint16) == TYPE_3, "OApp: invalid option type"
# Account for header bytes: 1 worker + 2 size + 1 type = 4 bytes
assert (len(_options) + len(_option) + 4 <= MAX_OPTIONS_TOTAL_SIZE), "OApp: options size exceeded"
return concat(
convert(_options, Bytes[MAX_OPTIONS_TOTAL_SIZE - MAX_OPTION_SINGLE_SIZE - 4]), # downcast Bytes size, -4 for header
convert(EXECUTOR_WORKER_ID, bytes1),
convert(convert(len(_option) + 1, uint16), bytes2), # +1 for optionType
convert(_optionType, bytes1),
_option,
)
```
```vyper
interface ILayerZeroEndpointV2:
def quote(_params: MessagingParams, _sender: address) -> MessagingFee: view
def send(_params: MessagingParams, _refundAddress: address) -> MessagingReceipt: payable
def setDelegate(_delegate: address): nonpayable
def eid() -> uint32: view
def lzToken() -> address: view
# Mapping to store peers associated with corresponding endpoints
peers: public(HashMap[uint32, bytes32])
@internal
@view
def _quote(
_dstEid: uint32,
_message: Bytes[MAX_MESSAGE_SIZE],
_options: Bytes[MAX_OPTIONS_TOTAL_SIZE],
_payInLzToken: bool,
) -> MessagingFee:
"""
@dev Internal function to interact with the LayerZero EndpointV2.quote() for fee calculation.
@param _dstEid The destination endpoint ID.
@param _message The message payload.
@param _options Additional options for the message.
@param _payInLzToken Flag indicating whether to pay the fee in LZ tokens.
@return fee The calculated MessagingFee for the message.
- nativeFee: The native fee for the message.
- lzTokenFee: The LZ token fee for the message.
"""
return staticcall endpoint.quote(
MessagingParams(
dstEid=_dstEid,
receiver=self._getPeerOrRevert(_dstEid),
message=_message,
options=_options,
payInLzToken=_payInLzToken,
),
self,
)
@view
@internal
def _getPeerOrRevert(_eid: uint32) -> bytes32:
"""
@notice Internal function to get the peer address associated with a specific endpoint;
reverts if NOT set.
@param _eid The endpoint ID.
@return peer The address of the peer associated with the specified endpoint.
"""
peer: bytes32 = self.peers[_eid]
assert peer != empty(bytes32), "OApp: no peer"
return peer
```
```shell
>>> LZBlockRelay.quote_broadcast_fees([30110, 30111], 200000)
[1000000000000000, 1000000000000000]
```
::::
---
## State & Utility Views
### `read_enabled`
::::description[`LZBlockRelay.read_enabled() -> bool: view`]
Getter whether the contract is configured to initiate block hash reads from mainnet. This is true if the contract is operating in read-enabled mode.
Returns: `True` if read functionality is enabled (`bool`).
```vyper
read_enabled: public(bool)
```
```shell
>>> LZBlockRelay.read_enabled()
True
```
::::
### `read_channel`
::::description[`LZBlockRelay.read_channel() -> uint32: view`]
Getter for the LayerZero endpoint ID for the configured read channel. This is the channel used for mainnet block hash reads.
Returns: read channel endpoint ID (`uint32`).
```vyper
read_channel: public(uint32)
```
```shell
>>> LZBlockRelay.read_channel()
30101
```
::::
### `mainnet_eid`
::::description[`LZBlockRelay.mainnet_eid() -> uint32: view`]
Getter for the mainnet eid.
Returns: mainnet eid (`uint32`).
```vyper
mainnet_eid: public(uint32)
```
```shell
>>> LZBlockRelay.mainnet_eid()
30101
```
::::
### `mainnet_block_view`
::::description[`LZBlockRelay.mainnet_block_view() -> address: view`]
Getter for the `MainnetBlockViewer` contract.
Returns: MainnetBlockView contract address (`address`).
```vyper
mainnet_block_view: public(address)
```
```shell
>>> LZBlockRelay.mainnet_block_view()
'0xb10CfacE69cc0B7F1AE0Dc8E6aD186914f6e7EEA'
```
::::
### `block_oracle`
::::description[`LZBlockRelay.block_oracle() -> address: view`]
Getter for the `BlockOracle` contract.
Returns: BlockOracle contract address (`address`).
```vyper
block_oracle: public(IBlockOracle)
```
```shell
>>> LZBlockRelay.block_oracle()
'0xb10cface69821Ff7b245Cf5f28f3e714fDbd86b8'
```
::::
---
## Ownership
Standard Ownable interface for querying the current owner and transferring or renouncing ownership. Ownership controls all privileged operations, including configuration and peer management. Owner of the contract is the DAO.
More here: https://github.com/pcaversaccio/snekmate/blob/main/src/snekmate/auth/ownable.vy
---
## MainnetBlockView
A viewer contract deployed to Ethereum mainnet that provides access to block numbers and hashes. This contract is useful for cross-chain applications that need to verify block data from Ethereum mainnet. To prevent reorg-related issues, it only returns hashes for blocks that are at least 65 blocks old.
This contract is called off-chain via LayerZero's `lzRead` functionality.
:::vyper[`MainnetBlockView.vy`]
The source code for the `MainnetBlockView.vy` contract can be found on [GitHub](https://github.com/curvefi/blockhash-oracle/blob/main/contracts/MainnetBlockView.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.4.3`.
The contract is deployed on :logos-ethereum: Ethereum at [`0xb10CfacE69cc0B7F1AE0Dc8E6aD186914f6e7EEA`](https://etherscan.io/address/0xb10CfacE69cc0B7F1AE0Dc8E6aD186914f6e7EEA).
```json
[{"inputs":[],"name":"get_blockhash","outputs":[{"name":"","type":"uint256"},{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_block_number","type":"uint256"}],"name":"get_blockhash","outputs":[{"name":"","type":"uint256"},{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_block_number","type":"uint256"},{"name":"_avoid_failure","type":"bool"}],"name":"get_blockhash","outputs":[{"name":"","type":"uint256"},{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"}]
```
:::
---
### `get_blockhash`
::::description[`MainnetBlockView.get_blockhash(_block_number: uint256 = block.number - 65, _avoid_failure: bool = False) -> (uint256, bytes32): view`]
Retrieves the block hash for a given block number. The valid range for historical block hashes is between the last 64 and the last 8192 blocks.
Block Range Constraints:
- **Too recent**: Blocks within the last 64 blocks (to mitigate Ethereum Mainnet reorg risk)
- **Too old:** Blocks older than 8192 blocks (EVM limit post EIP-2935)
- **Valid range:** Between `block.number - 8192` and `block.number - 64`
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `_block_number` | `uint256` | Block number to get the hash for. Defaults to `block.number - 65` |
| `_avoid_failure` | `bool` | If `True`, returns `(0, 0x0)` on failure instead of reverting. Useful for cross-chain calls. |
Returns: a tuple containing the block number and block hash (`(uint256, bytes32)`).
```vyper
from snekmate.utils import block_hash as snekmate_block_hash
@view
@external
def get_blockhash(
_block_number: uint256 = block.number - 65, _avoid_failure: bool = False
) -> (uint256, bytes32):
"""
@notice Get block hash for a given block number.
@dev The valid range for historical block hashes is between the last 64
and the last 8192 blocks.
@param _block_number Block number to get hash for, defaults to block.number - 65.
@param _avoid_failure If True, returns (0, 0x0) on failure instead of reverting.
@return Tuple of (actual block number, block hash).
"""
# Use a local variable for the requested block number.
requested_block_number: uint256 = _block_number
# If the default value was passed as 0 (e.g., from a cross-chain call
# that doesn't know the current block number), set a safe default.
if requested_block_number == 0:
requested_block_number = block.number - 65
# Check for invalid conditions first to exit early.
# The requested block must be at least 64 blocks old for reorg protection
# and not more than 8192 blocks old, which is the EVM's limit post EIP-2935.
is_too_recent: bool = requested_block_number >= block.number - 64
is_too_old: bool = requested_block_number <= block.number - 8192
if is_too_recent or is_too_old:
if _avoid_failure:
# For sensitive callers (like LayerZero), return a zeroed response
# instead of reverting the transaction.
return 0, empty(bytes32)
else:
# Revert with a descriptive custom error.
raise ("Block is too recent or too old")
# If all checks pass, retrieve and return the blockhash.
return requested_block_number, snekmate_block_hash._block_hash(requested_block_number)
```
```vyper
# Source code of this Vyper module can be found here:
# https://github.com/pcaversaccio/snekmate/blob/main/src/snekmate/utils/block_hash.vy
# pragma version ~=0.4.3
# pragma nonreentrancy off
"""
@title Utility Functions to Access Historical Block Hashes
@custom:contract-name block_hash
@license GNU Affero General Public License v3.0 only
@author pcaversaccio
@notice These functions can be used to access the historical block
hashes beyond the default 256-block limit. We use the EIP-2935
(https://eips.ethereum.org/EIPS/eip-2935) history contract,
which maintains a ring buffer of the last 8,191 block hashes
stored in state. For the blocks within the last 256 blocks,
we use the native `BLOCKHASH` opcode. For blocks between 257
and 8,191 blocks ago, the function `_block_hash` queries via
the specified `get` (https://eips.ethereum.org/EIPS/eip-2935#get)
method the EIP-2935 history contract. For blocks older than
8,191 or future blocks (including the current one), we return
zero, matching the `BLOCKHASH` behaviour.
Please note that after EIP-2935 is activated, it takes 8,191
blocks to fully populate the history. Before that, only block
hashes from the fork block onward are available.
"""
# @dev The `HISTORY_STORAGE_ADDRESS` contract address.
# @notice See the EIP-2935 specifications here: https://eips.ethereum.org/EIPS/eip-2935#specification.
_HISTORY_STORAGE_ADDRESS: constant(address) = 0x0000F90827F1C53a10cb7A02335B175320002935
# @dev The `keccak256` hash of the runtime bytecode of the
# history contract deployed at `HISTORY_STORAGE_ADDRESS`.
_HISTORY_STORAGE_RUNTIME_BYTECODE_HASH: constant(bytes32) = (
0x6e49e66782037c0555897870e29fa5e552daf4719552131a0abce779daec0a5d
)
@deploy
@payable
def __init__():
"""
@dev To omit the opcodes for checking the `msg.value`
in the creation-time EVM bytecode, the constructor
is declared as `payable`.
"""
pass
@internal
@view
def _block_hash(block_number: uint256) -> bytes32:
"""
@dev Returns the block hash for block number `block_number`.
@notice For blocks older than 8,191 or future blocks (including
the current one), returns zero, matching the `BLOCKHASH`
behaviour. Furthermore, this function does verify if the
history contract is deployed. If the history contract is
undeployed, the function will fallback to the `BLOCKHASH`
behaviour.
@param block_number The 32-byte block number.
@return bytes32 The 32-byte block hash for block number `block_number`.
"""
# For future blocks (including the current one), we already return
# an empty `bytes32` value here in order not to iterate through the
# remaining code.
if block_number >= block.number:
return empty(bytes32)
delta: uint256 = unsafe_sub(block.number, block_number)
if delta <= 256:
return blockhash(block_number)
elif delta > 8191 or _HISTORY_STORAGE_ADDRESS.codehash != _HISTORY_STORAGE_RUNTIME_BYTECODE_HASH:
# The Vyper built-in function `blockhash` reverts if the block number
# is more than `256` blocks behind the current block. We explicitly
# handle this case (i.e. `delta > 8191`) to ensure the function returns
# an empty `bytes32` value rather than reverting (i.e. exactly matching
# the `BLOCKHASH` opcode behaviour).
return empty(bytes32)
else:
return self._get_history_storage(block_number)
@internal
@view
def _get_history_storage(block_number: uint256) -> bytes32:
"""
@dev Returns the block hash for block number `block_number` by
calling the `HISTORY_STORAGE_ADDRESS` contract address.
@notice Please note that for any request outside the range of
`[block.number - 8191, block.number - 1]`, this function
reverts (see https://eips.ethereum.org/EIPS/eip-2935#get).
Furthermore, this function does not verify if the history
contract is deployed. If the history contract is undeployed,
the function will return an empty `bytes32` value.
@param block_number The 32-byte block number.
@return bytes32 The 32-byte block hash for block number `block_number`.
"""
return convert(
raw_call(
_HISTORY_STORAGE_ADDRESS,
abi_encode(block_number),
max_outsize=32,
is_static_call=True,
),
bytes32,
)
```
Get the default block hash (65 blocks ago):
Get a specific block hash:
```shell
>>> MainnetBlockView.get_blockhash(22787600)
(22787600, 0x4bdaa00a7e9b85a9ab25565ef2d2de8817cbba08dd0c6880ecee4ac4674e1378)
```
Use avoid_failure parameter:
```shell
>>> MainnetBlockView.get_blockhash(22787809, True) # Too recent
(0, 0x0000000000000000000000000000000000000000000000000000000000000000)
```
Error when block is too recent (without avoid_failure):
```shell
>>> MainnetBlockView.get_blockhash(1, False) # Will revert
Error: Returned error: execution reverted: Block is too recent or too old
```
::::
---
## Curve Block Oracle Overview
:::github[GitHub]
Source code for the storage proof architecture is available here: [GitHub: `storage-proofs`](https://github.com/curvefi/storage-proofs)
The [Curve Block Oracle Dashboard](https://curvefi.github.io/blockhash-oracle/) offers a live view of relayed blocks and headers across supported networks.
A real-time [Storage Proof Monitoring Dashboard](https://curvefi.github.io/storage-proofs/) provides live insights into storage proofs and system status.
:::
The **Curve Block Oracle** is a **decentralized**, **cross-chain** infrastructure designed to securely relay and verify Ethereum mainnet block hashes and state roots on other blockchains. This system enables **trust-minimized interoperability**, cross-chain oracles, and secure data streaming for applications such as price feeds, state proofs, and cross-chain governance. By providing a **canonical reference to Ethereum state**, the oracle allows DeFi protocols and dApps to safely use Ethereum-based data and logic across multiple networks.

## Why Block Oracles?
Curve DAO and core projects like crvUSD reside on Ethereum, with governance and canonical data stored on mainnet. To support new deployments and cross-chain integrations, it is essential to verify Ethereum state (such as block hashes and storage slots) on other networks.
The simplest and most robust way to support multiple transport layers is to send **block hashes** or **state roots** from Ethereum and use **storage proofs** on other chains. This approach abstracts away the messaging protocol—dApps simply verify data against the known Ethereum root.
## Storage Proofs
Each block has a **block header**, which includes the **state root**—a root hash of the entire Merkle Patricia Trie containing accounts and storage. Given a block hash, you can verify the state root.
To prove a storage slot, you must verify the full path from the root down to the leaf node—or prove that it does not exist.

## System Architecture
The Curve Block Oracle system consists of several smart contracts and off-chain actors:
- [`BlockOracle`](./block-oracle.md): A decentralized contract that stores block hashes using a **threshold-based, multi-committer consensus**. Only after a threshold of trusted committers submit matching block hashes is a block considered confirmed and immutable.
- [`LZBlockRelay`](./lz-block-relay.md): A cross-chain relay contract built on LayerZero, responsible for securely transmitting block hashes and state roots to other chains. It supports both read-enabled and broadcast-only modes.
- [`MainnetBlockView`](./mainnet-block-view.md): A lightweight contract on Ethereum mainnet that provides access to recent block hashes, ensuring only finalized blocks are relayed.
- [`HeaderVerifier`](./header-verifier.md): Decodes RLP-encoded Ethereum block headers and forwards the extracted data to the BlockOracle for verification and storage.
- **Offchain Prover**: An off-chain actor that fetches data from Ethereum, generates storage proofs, and submits them (with proofs) to the verifier contracts on target chains. The prover does not need to be trusted, as the safety of the whole system relies on the fact that it is not feasible to push an update with a forged proof. The prover must be online to provide the proof in a timely manner; if the prover is offline, the system might not be able to provide a correct (or accurate) price for scrvUSD.
## How It Works
1. **Block Hash Commitment**: Trusted committers submit block hashes to the BlockOracle. Once a threshold is reached, the block hash is confirmed.
2. **Cross-Chain Relay**: The LZBlockRelay contract uses LayerZero to relay confirmed block hashes to other chains, where they can be used for state proofs and oracles.
3. **State Proof Verification**: On the target chain, a verifier contract checks the provided storage proof against the known Ethereum state root, ensuring data integrity.
4. **Application Integration**: dApps (such as stableswap-ng pools or cross-chain price oracles) can now trustlessly verify Ethereum state and use it for pricing, governance, or other logic.
## System Specification
Depending on the type of chain, the proof and its verification process will differ:
- **OP Stack-based chains**: The verifier expects a block hash (to be matched with the one available in a precompile) and a state proof of the memory slots relevant to the growth rate computation.
- **Taiko Stack-based chains**: The verifier expects the block number and a state proof of the memory slots relevant to the growth rate computation.
- **Other chains**: The prover provides the same data as for the OP Stack, and relevant data to verify the proof will be bridged from Optimism using LayerZero.
### Example Flow (OP Stack-based chain)
```mermaid
flowchart TD
A[Prover] --> |Generates from L1 state| E[State Proof]
E[State Proof] --> B[Verifier Contract]
B -->|Push update price if proof is correct| C[Price Oracle Contract]
C -->|Provides scrvUSD price| D[stableswap-ng Pool]
subgraph L2 Chain
E2[Precompile] --> |Used to obtain| E1
E1[L1 Blockhash] --> B
end
```
## Prover's Trust Assumptions
A key feature of the Curve Block Oracle system is that the off-chain prover does **not need to be trusted for the system's safety**. The design ensures that it is not feasible to push an update with a forged proof, so even if the prover is malicious, it cannot compromise the correctness of the data. However, the **liveness** of the system does depend on the prover: if the prover is offline or fails to provide timely proofs, the system may not be able to update or provide accurate prices (such as for scrvUSD) on the target chain. In this way, the system is robust against malicious actors but still requires at least one honest and available prover to function smoothly.
## Security Model
Security in the Curve Block Oracle is achieved through a combination of **threshold consensus** and **permissioned committers**. No single committer can control the oracle; instead, a configurable threshold of trusted parties must agree on each block hash before it is accepted. Only addresses added by the DAO (as owner) are allowed to submit block hashes, and the set of committers can be updated as needed to maintain decentralization and resilience. For cross-chain communication, only messages from trusted LayerZero peers are accepted, with all configuration managed by the contract owner. Finally, all data is verified using **Ethereum's Merkle Patricia Trie proofs**, so correctness does not depend on trusting the prover or any single relayer.
---
## LLAMMA
LLAMMA (Lending Liquidating Automated Market Maker Algorithm) is the **market-making contract that rebalances the collateral of a loan**. It is an algorithm implemented into a smart contract which is **responsible for liquidating and de-liquidating collateral based on market conditions** through arbitrage traders. Each individual market has its own AMM **containing the collateral and borrowable asset**. E.g. the AMM of the [ETH<>crvUSD](https://etherscan.io/address/0x1681195c176239ac5e72d9aebacf5b2492e0c4ee) contains of `ETH` and `crvUSD`.
:::vyper[`AMM.vy`]
Each market deploys its own AMM from a blueprint contract. Source code is available on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/AMM.vy). Relevant deployments can be found [here](../deployments.md).
:::
:::info[Contract Versions]
The AMM (LLAMMA) contract is shared across all Controller versions. Unlike the Controller, the AMM API has remained stable — the same contract is used by both crvUSD mint markets and Llamalend markets. For version details, see the [crvUSD Overview](./overview.md#controller--amm-versions).
:::
:::info Getting familiar with LLAMMA
Before interacting with the `LLAMMA` contract, it is highly advised to read the following section to gain a broader understanding of the system: [LLAMMA Explainer](llamma-explainer.md).
:::
| Glossary | Description |
| -------------------- | ---------------------------------------------------------------------------- |
| `ticks`, `bands` | Price ranges where liquidity is deposited. |
| `x` | Coin which is being borrowed, typically a stablecoin. |
| `y` | Collateral coin. |
| `band_width_factor` | Parameter which controls the width of each band. Sometimes denoted `A` |
| `rate` | Interest rate. |
| `rate_mul` | Rate multiplier, 1 + integral(rate * dt). |
| `active_band` | Current band. Other bands are either in one or the other coin, but not both. |
| `min_band` | Bands below this are definitely empty. |
| `max_band` | Bands above this are definitely empty. |
| `bands_x[n]`, `bands_y[n]` | Amounts of coin x or y deposited in band n. |
| `user_shares[user,n] / total_shares[n]` | Fraction of the n'th band owned by a user. |
| `p_oracle` | External oracle price (can be from another AMM). |
| `p (as in get_p)` | Current price of AMM. It depends not only on the balances (x,y) in the band and active_band, but also on p_oracle. |
| `p_current_up`, `p_current_down` | The value of p at constant p_oracle when y=0 or x=0 respectively for the band n. |
| `p_oracle_up`, `p_oracle_down` | Edges of the band when p=p_oracle (steady state), happen when x=0 or y=0 respectively, for band n. |
---
## Depositing and Withdrawing Collateral
Whenever a user performs a collateral-specific action such as creating a new loan or adding more collateral, the collateral asset is deposited or withdrawn from the AMM. These functions are only callable by the admin of the AMM, which is the Controller.
*There are two functions to facilitate the depositing or withdrawing of collateral:*
- Collateral is put into bands by calling `deposit_range()`: This function is called by the `Controller` when one of the following functions is called: `_create_loan`, `_add_collateral_borrow`, `repay`, and `repay_extended`.
- Collateral is removed by calling `withdraw()`: This function is called by the `Controller` when one of the following functions is called: `_liquidate`, `_add_collateral_borrow`, `repay`, and `repay_extended`.
### `deposit_range`
::::description[`AMM.deposit_range(user: address, amount: uint256, n1: int256, n2: int256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the `Controller`.
:::
Function to deposit collateral `amount` for `user` in the range between the upper band `n1` and the lower band `n2`. Values for `n1` and `n2` are already determined in the `Controller` contract using the internal `_calculate_debt_n1` method.
| Input | Type | Description |
| -------- | --------- | --------------------------------- |
| `user` | `address` | User address. |
| `amount` | `uint256` | Amount of collateral to deposit. |
| `n1` | `int256` | Lower band in the deposit range. |
| `n2` | `int256` | Upper band in the deposit range. |
Emits: `Deposit`
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
event Deposit:
provider: indexed(address)
amount: uint256
n1: int256
n2: int256
@external
@nonreentrant('lock')
def deposit_range(user: address, amount: uint256, n1: int256, n2: int256):
"""
@notice Deposit for a user in a range of bands. Only admin contract (Controller) can do it
@param user User address
@param amount Amount of collateral to deposit
@param n1 Lower band in the deposit range
@param n2 Upper band in the deposit range
"""
assert msg.sender == self.admin
user_shares: DynArray[uint256, MAX_TICKS_UINT] = []
collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = []
n0: int256 = self.active_band
# We assume that n1,n2 area already sorted (and they are in Controller)
assert n2 < 2**127
assert n1 > -2**127
n_bands: uint256 = unsafe_add(convert(unsafe_sub(n2, n1), uint256), 1)
assert n_bands <= MAX_TICKS_UINT
y_per_band: uint256 = unsafe_div(amount * COLLATERAL_PRECISION, n_bands)
assert y_per_band > 100, "Amount too low"
assert self.user_shares[user].ticks[0] == 0 # dev: User must have no liquidity
self.user_shares[user].ns = unsafe_add(n1, unsafe_mul(n2, 2**128))
lm: LMGauge = self.liquidity_mining_callback
# Autoskip bands if we can
for i in range(MAX_SKIP_TICKS + 1):
if n1 > n0:
if i != 0:
self.active_band = n0
break
assert self.bands_x[n0] == 0 and i < MAX_SKIP_TICKS, "Deposit below current band"
n0 -= 1
for i in range(MAX_TICKS):
band: int256 = unsafe_add(n1, i)
if band > n2:
break
assert self.bands_x[band] == 0, "Band not empty"
y: uint256 = y_per_band
if i == 0:
y = amount * COLLATERAL_PRECISION - y * unsafe_sub(n_bands, 1)
total_y: uint256 = self.bands_y[band]
# Total / user share
s: uint256 = self.total_shares[band]
ds: uint256 = unsafe_div((s + DEAD_SHARES) * y, total_y + 1)
assert ds > 0, "Amount too low"
user_shares.append(ds)
s += ds
assert s <= 2**128 - 1
self.total_shares[band] = s
total_y += y
self.bands_y[band] = total_y
if lm.address != empty(address):
# If initial s == 0 - s becomes equal to y which is > 100 => nonzero
collateral_shares.append(unsafe_div(total_y * 10**18, s))
self.min_band = min(self.min_band, n1)
self.max_band = max(self.max_band, n2)
self.save_user_shares(user, user_shares)
log Deposit(user, amount, n1, n2)
if lm.address != empty(address):
lm.callback_collateral_shares(n1, collateral_shares)
lm.callback_user_shares(user, n1, user_shares)
```
The following source code includes all changes up to commit hash [58289a4](https://github.com/curvefi/curve-stablecoin/tree/58289a4283d7cc3c53aba2d3801dcac5ef124957); any changes made after this commit are not included.
```vyper
@internal
@view
def _calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256) -> int256:
"""
@notice Calculate the upper band number for the deposit to sit in to support
the given debt. Reverts if requested debt is too high.
@param collateral Amount of collateral (at its native precision)
@param debt Amount of requested debt
@param N Number of bands to deposit into
@return Upper band n1 (n1 <= n2) to deposit into. Signed integer
"""
assert debt > 0, "No loan"
n0: int256 = AMM.active_band()
p_base: uint256 = AMM.p_oracle_up(n0)
# x_effective = y / N * p_oracle_up(n1) * sqrt((A - 1) / A) * sum_{0..N-1}(((A-1) / A)**k)
# === d_y_effective * p_oracle_up(n1) * sum(...) === y_effective * p_oracle_up(n1)
# d_y_effective = y / N / sqrt(A / (A - 1))
y_effective: uint256 = self.get_y_effective(collateral * COLLATERAL_PRECISION, N, self.loan_discount)
# p_oracle_up(n1) = base_price * ((A - 1) / A)**n1
# We borrow up until min band touches p_oracle,
# or it touches non-empty bands which cannot be skipped.
# We calculate required n1 for given (collateral, debt),
# and if n1 corresponds to price_oracle being too high, or unreachable band
# - we revert.
# n1 is band number based on adiabatic trading, e.g. when p_oracle ~ p
y_effective = y_effective * p_base / (debt + 1) # Now it's a ratio
# n1 = floor(log2(y_effective) / self.logAratio)
# EVM semantics is not doing floor unlike Python, so we do this
assert y_effective > 0, "Amount too low"
n1: int256 = self.log2(y_effective) # <- switch to faster ln() XXX?
if n1 < 0:
n1 -= LOG2_A_RATIO - 1 # This is to deal with vyper's rounding of negative numbers
n1 /= LOG2_A_RATIO
n1 = min(n1, 1024 - convert(N, int256)) + n0
if n1 <= n0:
assert AMM.can_skip_bands(n1 - 1), "Debt too high"
# Let's not rely on active_band corresponding to price_oracle:
# this will be not correct if we are in the area of empty bands
assert AMM.p_oracle_up(n1) < AMM.price_oracle(), "Debt too high"
return n1
```
::::
### `withdraw`
::::description[`AMM.withdraw(user: address, frac: uint256) -> uint256[2]`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the `Controller`.
:::
Function to withdraw liquidity from bands for `user`.
Returns: amount of x (borrow token) and y (collateral token) withdrawn (`uint256[2]`).
Emits: `Withdraw`
| Input | Type | Description |
| ---------- | ---------- | ----------- |
| `user` | `address` | User address. |
| `frac` | `uint256` | Fraction to withdraw (1e18 = 100%). |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
event Withdraw:
provider: indexed(address)
amount_borrowed: uint256
amount_collateral: uint256
@external
@nonreentrant('lock')
def withdraw(user: address, frac: uint256) -> uint256[2]:
"""
@notice Withdraw liquidity for the user. Only admin contract can do it
@param user User who owns liquidity
@param frac Fraction to withdraw (1e18 being 100%)
@return Amount of [stablecoins, collateral] withdrawn
"""
assert msg.sender == self.admin
assert frac <= 10**18
lm: LMGauge = self.liquidity_mining_callback
ns: int256[2] = self._read_user_tick_numbers(user)
n: int256 = ns[0]
user_shares: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns)
assert user_shares[0] > 0, "No deposits"
total_x: uint256 = 0
total_y: uint256 = 0
min_band: int256 = self.min_band
old_min_band: int256 = min_band
old_max_band: int256 = self.max_band
max_band: int256 = n - 1
for i in range(MAX_TICKS):
x: uint256 = self.bands_x[n]
y: uint256 = self.bands_y[n]
ds: uint256 = unsafe_div(frac * user_shares[i], 10**18)
user_shares[i] = unsafe_sub(user_shares[i], ds) # Can ONLY zero out when frac == 10**18
s: uint256 = self.total_shares[n]
new_shares: uint256 = s - ds
self.total_shares[n] = new_shares
s += DEAD_SHARES # after this s is guaranteed to be bigger than 0
dx: uint256 = unsafe_div((x + 1) * ds, s)
dy: uint256 = unsafe_div((y + 1) * ds, s)
x -= dx
y -= dy
# If withdrawal is the last one - transfer dust to admin fees
if new_shares == 0:
if x > 0:
self.admin_fees_x += unsafe_div(x, BORROWED_PRECISION)
if y > 0:
self.admin_fees_y += unsafe_div(y, COLLATERAL_PRECISION)
x = 0
y = 0
if n == min_band:
if x == 0:
if y == 0:
min_band += 1
if x > 0 or y > 0:
max_band = n
self.bands_x[n] = x
self.bands_y[n] = y
total_x += dx
total_y += dy
if n == ns[1]:
break
else:
n = unsafe_add(n, 1)
# Empty the ticks
if frac == 10**18:
self.user_shares[user].ticks[0] = 0
else:
self.save_user_shares(user, user_shares)
if old_min_band != min_band:
self.min_band = min_band
if old_max_band <= ns[1]:
self.max_band = max_band
total_x = unsafe_div(total_x, BORROWED_PRECISION)
total_y = unsafe_div(total_y, COLLATERAL_PRECISION)
log Withdraw(user, total_x, total_y)
if lm.address != empty(address):
lm.callback_collateral_shares(0, []) # collateral/shares ratio is unchanged
lm.callback_user_shares(user, ns[0], user_shares)
return [total_x, total_y]
```
::::
---
## Exchanging Tokens
The LLAMMA can be used to exchange tokens, just like any other AMM. This is crucial as arbitrage opportunities are created by the LLAMMA, which can be exploited by buying and selling tokens in the AMM.
*There are two functions to exchange tokens:*
- `exchange`: Allows users to swap a certain amount of input token `i` for output token `j`.
- `exchange_dy`: Allows users to swap input token `i` for a desired amount of output token `j`.
Besides these two exchange functions, there are plenty of "helper functions" which are definitely of good use for searchers and arbitrageurs.
:::colab[Google Colab Notebook]
A Google Colab notebook that showcases the use of `exchange` and `exchange_dy` can be found here: [ Google Colab Notebook](https://colab.research.google.com/drive/1jT8eMgsFNdYIN2EPBeRtJ-SqaJdzYmIe?usp=sharing).
:::
### `exchange`
::::description[`AMM.exchange(i: uint256, j: uint256, in_amount: uint256, min_amount: uint256, _for: address = msg.sender) -> uint256[2]`]
Function to exchange `in_amount` of token `i` for a minimum amount of `min_amount` of token `j`. If the exchange results in less than `min_amount` of tokens, the function call reverts.
| Input | Type | Description |
| ------------ | --------- | --------------------------------------------------- |
| `i` | `uint256` | Input coin index. |
| `j` | `uint256` | Output coin index. |
| `in_amount` | `uint256` | Amount of input coin to swap. |
| `min_amount` | `uint256` | Minimum amount of output coin to get. |
| `_for` | `address` | Address to send coins to. Defaults to `msg.sender`. |
Returns: amount of coins swapped in and out (`uint256`).
Emits: `TokenExchange`
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
event TokenExchange:
buyer: indexed(address)
sold_id: uint256
tokens_sold: uint256
bought_id: uint256
tokens_bought: uint256
@external
@nonreentrant('lock')
def exchange(i: uint256, j: uint256, in_amount: uint256, min_amount: uint256, _for: address = msg.sender) -> uint256[2]:
"""
@notice Exchanges two coins, callable by anyone
@param i Input coin index
@param j Output coin index
@param in_amount Amount of input coin to swap
@param min_amount Minimal amount to get as output
@param _for Address to send coins to
@return Amount of coins given in/out
"""
return self._exchange(i, j, in_amount, min_amount, _for, True)
@internal
def _exchange(i: uint256, j: uint256, amount: uint256, minmax_amount: uint256, _for: address, use_in_amount: bool) -> uint256[2]:
"""
@notice Exchanges two coins, callable by anyone
@param i Input coin index
@param j Output coin index
@param amount Amount of input/output coin to swap
@param minmax_amount Minimal/maximum amount to get as output/input
@param _for Address to send coins to
@param use_in_amount Whether input or output amount is specified
@return Amount of coins given in and out
"""
assert (i == 0 and j == 1) or (i == 1 and j == 0), "Wrong index"
p_o: uint256[2] = self._price_oracle_w() # Let's update the oracle even if we exchange 0
if amount == 0:
return [0, 0]
lm: LMGauge = self.liquidity_mining_callback
collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = []
in_coin: ERC20 = BORROWED_TOKEN
out_coin: ERC20 = COLLATERAL_TOKEN
in_precision: uint256 = BORROWED_PRECISION
out_precision: uint256 = COLLATERAL_PRECISION
if i == 1:
in_precision = out_precision
in_coin = out_coin
out_precision = BORROWED_PRECISION
out_coin = BORROWED_TOKEN
out: DetailedTrade = empty(DetailedTrade)
if use_in_amount:
out = self.calc_swap_out(i == 0, amount * in_precision, p_o, in_precision, out_precision)
else:
amount_to_swap: uint256 = max_value(uint256)
if amount < amount_to_swap:
amount_to_swap = amount * out_precision
out = self.calc_swap_in(i == 0, amount_to_swap, p_o, in_precision, out_precision)
in_amount_done: uint256 = unsafe_div(out.in_amount, in_precision)
out_amount_done: uint256 = unsafe_div(out.out_amount, out_precision)
if use_in_amount:
assert out_amount_done >= minmax_amount, "Slippage"
else:
assert in_amount_done <= minmax_amount and (out_amount_done == amount or amount == max_value(uint256)), "Slippage"
if out_amount_done == 0 or in_amount_done == 0:
return [0, 0]
out.admin_fee = unsafe_div(out.admin_fee, in_precision)
if i == 0:
self.admin_fees_x += out.admin_fee
else:
self.admin_fees_y += out.admin_fee
n: int256 = min(out.n1, out.n2)
n_start: int256 = n
n_diff: int256 = abs(unsafe_sub(out.n2, out.n1))
for k in range(MAX_TICKS):
x: uint256 = 0
y: uint256 = 0
if i == 0:
x = out.ticks_in[k]
if n == out.n2:
y = out.last_tick_j
else:
y = out.ticks_in[unsafe_sub(n_diff, k)]
if n == out.n2:
x = out.last_tick_j
self.bands_x[n] = x
self.bands_y[n] = y
if lm.address != empty(address):
s: uint256 = 0
if y > 0:
s = unsafe_div(y * 10**18, self.total_shares[n])
collateral_shares.append(s)
if k == n_diff:
break
n = unsafe_add(n, 1)
self.active_band = out.n2
log TokenExchange(_for, i, in_amount_done, j, out_amount_done)
if lm.address != empty(address):
lm.callback_collateral_shares(n_start, collateral_shares)
assert in_coin.transferFrom(msg.sender, self, in_amount_done, default_return_value=True)
assert out_coin.transfer(_for, out_amount_done, default_return_value=True)
return [in_amount_done, out_amount_done]
@internal
@view
def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: uint256, out_precision: uint256) -> DetailedTrade:
"""
@notice Calculate the amount which can be obtained as a result of exchange.
If couldn't exchange all - will also update the amount which was actually used.
Also returns other parameters related to state after swap.
This function is core to the AMM functionality.
@param pump Indicates whether the trade buys or sells collateral
@param in_amount Amount of token going in
@param p_o Current oracle price and ratio (p_o, dynamic_fee)
@return Amounts spent and given out, initial and final bands of the AMM, new
amounts of coins in bands in the AMM, as well as admin fee charged,
all in one data structure
"""
# pump = True: borrowable (USD) in, collateral (ETH) out; going up
# pump = False: collateral (ETH) in, borrowable (USD) out; going down
min_band: int256 = self.min_band
max_band: int256 = self.max_band
out: DetailedTrade = empty(DetailedTrade)
out.n2 = self.active_band
p_o_up: uint256 = self._p_oracle_up(out.n2)
x: uint256 = self.bands_x[out.n2]
y: uint256 = self.bands_y[out.n2]
in_amount_left: uint256 = in_amount
fee: uint256 = max(self.fee, p_o[1])
admin_fee: uint256 = self.admin_fee
j: uint256 = MAX_TICKS_UINT
for i in range(MAX_TICKS + MAX_SKIP_TICKS):
y0: uint256 = 0
f: uint256 = 0
g: uint256 = 0
Inv: uint256 = 0
dynamic_fee: uint256 = fee
if x > 0 or y > 0:
if j == MAX_TICKS_UINT:
out.n1 = out.n2
j = 0
y0 = self._get_y0(x, y, p_o[0], p_o_up) # <- also checks p_o
f = unsafe_div(A * y0 * p_o[0] / p_o_up * p_o[0], 10**18)
g = unsafe_div(Aminus1 * y0 * p_o_up, p_o[0])
Inv = (f + x) * (g + y)
dynamic_fee = max(self.get_dynamic_fee(p_o[0], p_o_up), fee)
antifee: uint256 = unsafe_div(
(10**18)**2,
unsafe_sub(10**18, min(dynamic_fee, 10**18 - 1))
)
if j != MAX_TICKS_UINT:
# Initialize
_tick: uint256 = y
if pump:
_tick = x
out.ticks_in.append(_tick)
# Need this to break if price is too far
p_ratio: uint256 = unsafe_div(p_o_up * 10**18, p_o[0])
if pump:
if y != 0:
if g != 0:
x_dest: uint256 = (unsafe_div(Inv, g) - f) - x
dx: uint256 = unsafe_div(x_dest * antifee, 10**18)
if dx >= in_amount_left:
# This is the last band
x_dest = unsafe_div(in_amount_left * 10**18, antifee) # LESS than in_amount_left
out.last_tick_j = min(Inv / (f + (x + x_dest)) - g + 1, y) # Should be always >= 0
x_dest = unsafe_div(unsafe_sub(in_amount_left, x_dest) * admin_fee, 10**18) # abs admin fee now
x += in_amount_left # x is precise after this
# Round down the output
out.out_amount += y - out.last_tick_j
out.ticks_in[j] = x - x_dest
out.in_amount = in_amount
out.admin_fee = unsafe_add(out.admin_fee, x_dest)
break
else:
# We go into the next band
dx = max(dx, 1) # Prevents from leaving dust in the band
x_dest = unsafe_div(unsafe_sub(dx, x_dest) * admin_fee, 10**18) # abs admin fee now
in_amount_left -= dx
out.ticks_in[j] = x + dx - x_dest
out.in_amount += dx
out.out_amount += y
out.admin_fee = unsafe_add(out.admin_fee, x_dest)
if i != MAX_TICKS + MAX_SKIP_TICKS - 1:
if out.n2 == max_band:
break
if j == MAX_TICKS_UINT - 1:
break
if p_ratio < unsafe_div(10**36, MAX_ORACLE_DN_POW):
# Don't allow to be away by more than ~50 ticks
break
out.n2 += 1
p_o_up = unsafe_div(p_o_up * Aminus1, A)
x = 0
y = self.bands_y[out.n2]
else: # dump
if x != 0:
if f != 0:
y_dest: uint256 = (unsafe_div(Inv, f) - g) - y
dy: uint256 = unsafe_div(y_dest * antifee, 10**18)
if dy >= in_amount_left:
# This is the last band
y_dest = unsafe_div(in_amount_left * 10**18, antifee)
out.last_tick_j = min(Inv / (g + (y + y_dest)) - f + 1, x)
y_dest = unsafe_div(unsafe_sub(in_amount_left, y_dest) * admin_fee, 10**18) # abs admin fee now
y += in_amount_left
out.out_amount += x - out.last_tick_j
out.ticks_in[j] = y - y_dest
out.in_amount = in_amount
out.admin_fee = unsafe_add(out.admin_fee, y_dest)
break
else:
# We go into the next band
dy = max(dy, 1) # Prevents from leaving dust in the band
y_dest = unsafe_div(unsafe_sub(dy, y_dest) * admin_fee, 10**18) # abs admin fee now
in_amount_left -= dy
out.ticks_in[j] = y + dy - y_dest
out.in_amount += dy
out.out_amount += x
out.admin_fee = unsafe_add(out.admin_fee, y_dest)
if i != MAX_TICKS + MAX_SKIP_TICKS - 1:
if out.n2 == min_band:
break
if j == MAX_TICKS_UINT - 1:
break
if p_ratio > MAX_ORACLE_DN_POW:
# Don't allow to be away by more than ~50 ticks
break
out.n2 -= 1
p_o_up = unsafe_div(p_o_up * A, Aminus1)
x = self.bands_x[out.n2]
y = 0
if j != MAX_TICKS_UINT:
j = unsafe_add(j, 1)
# Round up what goes in and down what goes out
# ceil(in_amount_used/BORROWED_PRECISION) * BORROWED_PRECISION
out.in_amount = unsafe_mul(unsafe_div(unsafe_add(out.in_amount, unsafe_sub(in_precision, 1)), in_precision), in_precision)
out.out_amount = unsafe_mul(unsafe_div(out.out_amount, out_precision), out_precision)
return out
@internal
@view
def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: uint256, out_precision: uint256) -> DetailedTrade:
"""
@notice Calculate the input amount required to receive the desired output amount.
If couldn't exchange all - will also update the amount which was actually received.
Also returns other parameters related to state after swap.
@param pump Indicates whether the trade buys or sells collateral
@param out_amount Desired amount of token going out
@param p_o Current oracle price and antisandwich fee (p_o, dynamic_fee)
@return Amounts required and given out, initial and final bands of the AMM, new
amounts of coins in bands in the AMM, as well as admin fee charged,
all in one data structure
"""
# pump = True: borrowable (USD) in, collateral (ETH) out; going up
# pump = False: collateral (ETH) in, borrowable (USD) out; going down
min_band: int256 = self.min_band
max_band: int256 = self.max_band
out: DetailedTrade = empty(DetailedTrade)
out.n2 = self.active_band
p_o_up: uint256 = self._p_oracle_up(out.n2)
x: uint256 = self.bands_x[out.n2]
y: uint256 = self.bands_y[out.n2]
out_amount_left: uint256 = out_amount
fee: uint256 = max(self.fee, p_o[1])
admin_fee: uint256 = self.admin_fee
j: uint256 = MAX_TICKS_UINT
for i in range(MAX_TICKS + MAX_SKIP_TICKS):
y0: uint256 = 0
f: uint256 = 0
g: uint256 = 0
Inv: uint256 = 0
dynamic_fee: uint256 = fee
if x > 0 or y > 0:
if j == MAX_TICKS_UINT:
out.n1 = out.n2
j = 0
y0 = self._get_y0(x, y, p_o[0], p_o_up) # <- also checks p_o
f = unsafe_div(A * y0 * p_o[0] / p_o_up * p_o[0], 10**18)
g = unsafe_div(Aminus1 * y0 * p_o_up, p_o[0])
Inv = (f + x) * (g + y)
dynamic_fee = max(self.get_dynamic_fee(p_o[0], p_o_up), fee)
antifee: uint256 = unsafe_div(
(10**18)**2,
unsafe_sub(10**18, min(dynamic_fee, 10**18 - 1))
)
if j != MAX_TICKS_UINT:
# Initialize
_tick: uint256 = y
if pump:
_tick = x
out.ticks_in.append(_tick)
# Need this to break if price is too far
p_ratio: uint256 = unsafe_div(p_o_up * 10**18, p_o[0])
if pump:
if y != 0:
if g != 0:
if y >= out_amount_left:
# This is the last band
out.last_tick_j = unsafe_sub(y, out_amount_left)
x_dest: uint256 = Inv / (g + out.last_tick_j) - f - x
dx: uint256 = unsafe_div(x_dest * antifee, 10**18) # MORE than x_dest
out.out_amount = out_amount # We successfully found liquidity for all the out_amount
out.in_amount += dx
x_dest = unsafe_div(unsafe_sub(dx, x_dest) * admin_fee, 10**18) # abs admin fee now
out.ticks_in[j] = x + dx - x_dest
out.admin_fee = unsafe_add(out.admin_fee, x_dest)
break
else:
# We go into the next band
x_dest: uint256 = (unsafe_div(Inv, g) - f) - x
dx: uint256 = max(unsafe_div(x_dest * antifee, 10**18), 1)
out_amount_left -= y
out.in_amount += dx
out.out_amount += y
x_dest = unsafe_div(unsafe_sub(dx, x_dest) * admin_fee, 10**18) # abs admin fee now
out.ticks_in[j] = x + dx - x_dest
out.admin_fee = unsafe_add(out.admin_fee, x_dest)
if i != MAX_TICKS + MAX_SKIP_TICKS - 1:
if out.n2 == max_band:
break
if j == MAX_TICKS_UINT - 1:
break
if p_ratio < unsafe_div(10**36, MAX_ORACLE_DN_POW):
# Don't allow to be away by more than ~50 ticks
break
out.n2 += 1
p_o_up = unsafe_div(p_o_up * Aminus1, A)
x = 0
y = self.bands_y[out.n2]
else: # dump
if x != 0:
if f != 0:
if x >= out_amount_left:
# This is the last band
out.last_tick_j = unsafe_sub(x, out_amount_left)
y_dest: uint256 = Inv / (f + out.last_tick_j) - g - y
dy: uint256 = unsafe_div(y_dest * antifee, 10**18) # MORE than y_dest
out.out_amount = out_amount
out.in_amount += dy
y_dest = unsafe_div(unsafe_sub(dy, y_dest) * admin_fee, 10**18) # abs admin fee now
out.ticks_in[j] = y + dy - y_dest
out.admin_fee = unsafe_add(out.admin_fee, y_dest)
break
else:
# We go into the next band
y_dest: uint256 = (unsafe_div(Inv, f) - g) - y
dy: uint256 = max(unsafe_div(y_dest * antifee, 10**18), 1)
out_amount_left -= x
out.in_amount += dy
out.out_amount += x
y_dest = unsafe_div(unsafe_sub(dy, y_dest) * admin_fee, 10**18) # abs admin fee now
out.ticks_in[j] = y + dy - y_dest
out.admin_fee = unsafe_add(out.admin_fee, y_dest)
if i != MAX_TICKS + MAX_SKIP_TICKS - 1:
if out.n2 == min_band:
break
if j == MAX_TICKS_UINT - 1:
break
if p_ratio > MAX_ORACLE_DN_POW:
# Don't allow to be away by more than ~50 ticks
break
out.n2 -= 1
p_o_up = unsafe_div(p_o_up * A, Aminus1)
x = self.bands_x[out.n2]
y = 0
if j != MAX_TICKS_UINT:
j = unsafe_add(j, 1)
# Round up what goes in and down what goes out
# ceil(in_amount_used/BORROWED_PRECISION) * BORROWED_PRECISION
out.in_amount = unsafe_mul(unsafe_div(unsafe_add(out.in_amount, unsafe_sub(in_precision, 1)), in_precision), in_precision)
out.out_amount = unsafe_mul(unsafe_div(out.out_amount, out_precision), out_precision)
return out
```
::::
### `exchange_dy`
::::description[`AMM.exchange_dy(i: uint256, j: uint256, out_amount: uint256, max_amount: uint256, _for: address = msg.sender) -> uint256[2]`]
Function to exchange a maximum amount of `max_amount` of input token `i` for a total of `out_amount` of output token `j`. If `max_amount` is not enough to cover the purchase of `out_amount` of tokens, the function will revert.
| Input | Type | Description |
| ------------ | --------- | ---------------------------------------------------- |
| `i` | `uint256` | Input coin index. |
| `j` | `uint256` | Output coin index. |
| `out_amount` | `uint256` | Desired amout of output tokens to receive. |
| `max_amount` | `uint256` | Maximum amount of input token to use. |
| `_for` | `address` | Address to send coins to (defaults to `msg.sender`). |
Returns: amount of coins swapped in and out (`uint256`).
Emits: `TokenExchange`
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
event TokenExchange:
buyer: indexed(address)
sold_id: uint256
tokens_sold: uint256
bought_id: uint256
tokens_bought: uint256
@external
@nonreentrant('lock')
def exchange_dy(i: uint256, j: uint256, out_amount: uint256, max_amount: uint256, _for: address = msg.sender) -> uint256[2]:
"""
@notice Exchanges two coins, callable by anyone
@param i Input coin index
@param j Output coin index
@param out_amount Desired amount of output coin to receive
@param max_amount Maximum amount to spend (revert if more)
@param _for Address to send coins to
@return Amount of coins given in/out
"""
return self._exchange(i, j, out_amount, max_amount, _for, False)
@internal
def _exchange(i: uint256, j: uint256, amount: uint256, minmax_amount: uint256, _for: address, use_in_amount: bool) -> uint256[2]:
"""
@notice Exchanges two coins, callable by anyone
@param i Input coin index
@param j Output coin index
@param amount Amount of input/output coin to swap
@param minmax_amount Minimal/maximum amount to get as output/input
@param _for Address to send coins to
@param use_in_amount Whether input or output amount is specified
@return Amount of coins given in and out
"""
assert (i == 0 and j == 1) or (i == 1 and j == 0), "Wrong index"
p_o: uint256[2] = self._price_oracle_w() # Let's update the oracle even if we exchange 0
if amount == 0:
return [0, 0]
lm: LMGauge = self.liquidity_mining_callback
collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = []
in_coin: ERC20 = BORROWED_TOKEN
out_coin: ERC20 = COLLATERAL_TOKEN
in_precision: uint256 = BORROWED_PRECISION
out_precision: uint256 = COLLATERAL_PRECISION
if i == 1:
in_precision = out_precision
in_coin = out_coin
out_precision = BORROWED_PRECISION
out_coin = BORROWED_TOKEN
out: DetailedTrade = empty(DetailedTrade)
if use_in_amount:
out = self.calc_swap_out(i == 0, amount * in_precision, p_o, in_precision, out_precision)
else:
amount_to_swap: uint256 = max_value(uint256)
if amount < amount_to_swap:
amount_to_swap = amount * out_precision
out = self.calc_swap_in(i == 0, amount_to_swap, p_o, in_precision, out_precision)
in_amount_done: uint256 = unsafe_div(out.in_amount, in_precision)
out_amount_done: uint256 = unsafe_div(out.out_amount, out_precision)
if use_in_amount:
assert out_amount_done >= minmax_amount, "Slippage"
else:
assert in_amount_done <= minmax_amount and (out_amount_done == amount or amount == max_value(uint256)), "Slippage"
if out_amount_done == 0 or in_amount_done == 0:
return [0, 0]
out.admin_fee = unsafe_div(out.admin_fee, in_precision)
if i == 0:
self.admin_fees_x += out.admin_fee
else:
self.admin_fees_y += out.admin_fee
n: int256 = min(out.n1, out.n2)
n_start: int256 = n
n_diff: int256 = abs(unsafe_sub(out.n2, out.n1))
for k in range(MAX_TICKS):
x: uint256 = 0
y: uint256 = 0
if i == 0:
x = out.ticks_in[k]
if n == out.n2:
y = out.last_tick_j
else:
y = out.ticks_in[unsafe_sub(n_diff, k)]
if n == out.n2:
x = out.last_tick_j
self.bands_x[n] = x
self.bands_y[n] = y
if lm.address != empty(address):
s: uint256 = 0
if y > 0:
s = unsafe_div(y * 10**18, self.total_shares[n])
collateral_shares.append(s)
if k == n_diff:
break
n = unsafe_add(n, 1)
self.active_band = out.n2
log TokenExchange(_for, i, in_amount_done, j, out_amount_done)
if lm.address != empty(address):
lm.callback_collateral_shares(n_start, collateral_shares)
assert in_coin.transferFrom(msg.sender, self, in_amount_done, default_return_value=True)
assert out_coin.transfer(_for, out_amount_done, default_return_value=True)
return [in_amount_done, out_amount_done]
@internal
@view
def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: uint256, out_precision: uint256) -> DetailedTrade:
"""
@notice Calculate the amount which can be obtained as a result of exchange.
If couldn't exchange all - will also update the amount which was actually used.
Also returns other parameters related to state after swap.
This function is core to the AMM functionality.
@param pump Indicates whether the trade buys or sells collateral
@param in_amount Amount of token going in
@param p_o Current oracle price and ratio (p_o, dynamic_fee)
@return Amounts spent and given out, initial and final bands of the AMM, new
amounts of coins in bands in the AMM, as well as admin fee charged,
all in one data structure
"""
# pump = True: borrowable (USD) in, collateral (ETH) out; going up
# pump = False: collateral (ETH) in, borrowable (USD) out; going down
min_band: int256 = self.min_band
max_band: int256 = self.max_band
out: DetailedTrade = empty(DetailedTrade)
out.n2 = self.active_band
p_o_up: uint256 = self._p_oracle_up(out.n2)
x: uint256 = self.bands_x[out.n2]
y: uint256 = self.bands_y[out.n2]
in_amount_left: uint256 = in_amount
fee: uint256 = max(self.fee, p_o[1])
admin_fee: uint256 = self.admin_fee
j: uint256 = MAX_TICKS_UINT
for i in range(MAX_TICKS + MAX_SKIP_TICKS):
y0: uint256 = 0
f: uint256 = 0
g: uint256 = 0
Inv: uint256 = 0
dynamic_fee: uint256 = fee
if x > 0 or y > 0:
if j == MAX_TICKS_UINT:
out.n1 = out.n2
j = 0
y0 = self._get_y0(x, y, p_o[0], p_o_up) # <- also checks p_o
f = unsafe_div(A * y0 * p_o[0] / p_o_up * p_o[0], 10**18)
g = unsafe_div(Aminus1 * y0 * p_o_up, p_o[0])
Inv = (f + x) * (g + y)
dynamic_fee = max(self.get_dynamic_fee(p_o[0], p_o_up), fee)
antifee: uint256 = unsafe_div(
(10**18)**2,
unsafe_sub(10**18, min(dynamic_fee, 10**18 - 1))
)
if j != MAX_TICKS_UINT:
# Initialize
_tick: uint256 = y
if pump:
_tick = x
out.ticks_in.append(_tick)
# Need this to break if price is too far
p_ratio: uint256 = unsafe_div(p_o_up * 10**18, p_o[0])
if pump:
if y != 0:
if g != 0:
x_dest: uint256 = (unsafe_div(Inv, g) - f) - x
dx: uint256 = unsafe_div(x_dest * antifee, 10**18)
if dx >= in_amount_left:
# This is the last band
x_dest = unsafe_div(in_amount_left * 10**18, antifee) # LESS than in_amount_left
out.last_tick_j = min(Inv / (f + (x + x_dest)) - g + 1, y) # Should be always >= 0
x_dest = unsafe_div(unsafe_sub(in_amount_left, x_dest) * admin_fee, 10**18) # abs admin fee now
x += in_amount_left # x is precise after this
# Round down the output
out.out_amount += y - out.last_tick_j
out.ticks_in[j] = x - x_dest
out.in_amount = in_amount
out.admin_fee = unsafe_add(out.admin_fee, x_dest)
break
else:
# We go into the next band
dx = max(dx, 1) # Prevents from leaving dust in the band
x_dest = unsafe_div(unsafe_sub(dx, x_dest) * admin_fee, 10**18) # abs admin fee now
in_amount_left -= dx
out.ticks_in[j] = x + dx - x_dest
out.in_amount += dx
out.out_amount += y
out.admin_fee = unsafe_add(out.admin_fee, x_dest)
if i != MAX_TICKS + MAX_SKIP_TICKS - 1:
if out.n2 == max_band:
break
if j == MAX_TICKS_UINT - 1:
break
if p_ratio < unsafe_div(10**36, MAX_ORACLE_DN_POW):
# Don't allow to be away by more than ~50 ticks
break
out.n2 += 1
p_o_up = unsafe_div(p_o_up * Aminus1, A)
x = 0
y = self.bands_y[out.n2]
else: # dump
if x != 0:
if f != 0:
y_dest: uint256 = (unsafe_div(Inv, f) - g) - y
dy: uint256 = unsafe_div(y_dest * antifee, 10**18)
if dy >= in_amount_left:
# This is the last band
y_dest = unsafe_div(in_amount_left * 10**18, antifee)
out.last_tick_j = min(Inv / (g + (y + y_dest)) - f + 1, x)
y_dest = unsafe_div(unsafe_sub(in_amount_left, y_dest) * admin_fee, 10**18) # abs admin fee now
y += in_amount_left
out.out_amount += x - out.last_tick_j
out.ticks_in[j] = y - y_dest
out.in_amount = in_amount
out.admin_fee = unsafe_add(out.admin_fee, y_dest)
break
else:
# We go into the next band
dy = max(dy, 1) # Prevents from leaving dust in the band
y_dest = unsafe_div(unsafe_sub(dy, y_dest) * admin_fee, 10**18) # abs admin fee now
in_amount_left -= dy
out.ticks_in[j] = y + dy - y_dest
out.in_amount += dy
out.out_amount += x
out.admin_fee = unsafe_add(out.admin_fee, y_dest)
if i != MAX_TICKS + MAX_SKIP_TICKS - 1:
if out.n2 == min_band:
break
if j == MAX_TICKS_UINT - 1:
break
if p_ratio > MAX_ORACLE_DN_POW:
# Don't allow to be away by more than ~50 ticks
break
out.n2 -= 1
p_o_up = unsafe_div(p_o_up * A, Aminus1)
x = self.bands_x[out.n2]
y = 0
if j != MAX_TICKS_UINT:
j = unsafe_add(j, 1)
# Round up what goes in and down what goes out
# ceil(in_amount_used/BORROWED_PRECISION) * BORROWED_PRECISION
out.in_amount = unsafe_mul(unsafe_div(unsafe_add(out.in_amount, unsafe_sub(in_precision, 1)), in_precision), in_precision)
out.out_amount = unsafe_mul(unsafe_div(out.out_amount, out_precision), out_precision)
return out
@internal
@view
def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: uint256, out_precision: uint256) -> DetailedTrade:
"""
@notice Calculate the input amount required to receive the desired output amount.
If couldn't exchange all - will also update the amount which was actually received.
Also returns other parameters related to state after swap.
@param pump Indicates whether the trade buys or sells collateral
@param out_amount Desired amount of token going out
@param p_o Current oracle price and antisandwich fee (p_o, dynamic_fee)
@return Amounts required and given out, initial and final bands of the AMM, new
amounts of coins in bands in the AMM, as well as admin fee charged,
all in one data structure
"""
# pump = True: borrowable (USD) in, collateral (ETH) out; going up
# pump = False: collateral (ETH) in, borrowable (USD) out; going down
min_band: int256 = self.min_band
max_band: int256 = self.max_band
out: DetailedTrade = empty(DetailedTrade)
out.n2 = self.active_band
p_o_up: uint256 = self._p_oracle_up(out.n2)
x: uint256 = self.bands_x[out.n2]
y: uint256 = self.bands_y[out.n2]
out_amount_left: uint256 = out_amount
fee: uint256 = max(self.fee, p_o[1])
admin_fee: uint256 = self.admin_fee
j: uint256 = MAX_TICKS_UINT
for i in range(MAX_TICKS + MAX_SKIP_TICKS):
y0: uint256 = 0
f: uint256 = 0
g: uint256 = 0
Inv: uint256 = 0
dynamic_fee: uint256 = fee
if x > 0 or y > 0:
if j == MAX_TICKS_UINT:
out.n1 = out.n2
j = 0
y0 = self._get_y0(x, y, p_o[0], p_o_up) # <- also checks p_o
f = unsafe_div(A * y0 * p_o[0] / p_o_up * p_o[0], 10**18)
g = unsafe_div(Aminus1 * y0 * p_o_up, p_o[0])
Inv = (f + x) * (g + y)
dynamic_fee = max(self.get_dynamic_fee(p_o[0], p_o_up), fee)
antifee: uint256 = unsafe_div(
(10**18)**2,
unsafe_sub(10**18, min(dynamic_fee, 10**18 - 1))
)
if j != MAX_TICKS_UINT:
# Initialize
_tick: uint256 = y
if pump:
_tick = x
out.ticks_in.append(_tick)
# Need this to break if price is too far
p_ratio: uint256 = unsafe_div(p_o_up * 10**18, p_o[0])
if pump:
if y != 0:
if g != 0:
if y >= out_amount_left:
# This is the last band
out.last_tick_j = unsafe_sub(y, out_amount_left)
x_dest: uint256 = Inv / (g + out.last_tick_j) - f - x
dx: uint256 = unsafe_div(x_dest * antifee, 10**18) # MORE than x_dest
out.out_amount = out_amount # We successfully found liquidity for all the out_amount
out.in_amount += dx
x_dest = unsafe_div(unsafe_sub(dx, x_dest) * admin_fee, 10**18) # abs admin fee now
out.ticks_in[j] = x + dx - x_dest
out.admin_fee = unsafe_add(out.admin_fee, x_dest)
break
else:
# We go into the next band
x_dest: uint256 = (unsafe_div(Inv, g) - f) - x
dx: uint256 = max(unsafe_div(x_dest * antifee, 10**18), 1)
out_amount_left -= y
out.in_amount += dx
out.out_amount += y
x_dest = unsafe_div(unsafe_sub(dx, x_dest) * admin_fee, 10**18) # abs admin fee now
out.ticks_in[j] = x + dx - x_dest
out.admin_fee = unsafe_add(out.admin_fee, x_dest)
if i != MAX_TICKS + MAX_SKIP_TICKS - 1:
if out.n2 == max_band:
break
if j == MAX_TICKS_UINT - 1:
break
if p_ratio < unsafe_div(10**36, MAX_ORACLE_DN_POW):
# Don't allow to be away by more than ~50 ticks
break
out.n2 += 1
p_o_up = unsafe_div(p_o_up * Aminus1, A)
x = 0
y = self.bands_y[out.n2]
else: # dump
if x != 0:
if f != 0:
if x >= out_amount_left:
# This is the last band
out.last_tick_j = unsafe_sub(x, out_amount_left)
y_dest: uint256 = Inv / (f + out.last_tick_j) - g - y
dy: uint256 = unsafe_div(y_dest * antifee, 10**18) # MORE than y_dest
out.out_amount = out_amount
out.in_amount += dy
y_dest = unsafe_div(unsafe_sub(dy, y_dest) * admin_fee, 10**18) # abs admin fee now
out.ticks_in[j] = y + dy - y_dest
out.admin_fee = unsafe_add(out.admin_fee, y_dest)
break
else:
# We go into the next band
y_dest: uint256 = (unsafe_div(Inv, f) - g) - y
dy: uint256 = max(unsafe_div(y_dest * antifee, 10**18), 1)
out_amount_left -= x
out.in_amount += dy
out.out_amount += x
y_dest = unsafe_div(unsafe_sub(dy, y_dest) * admin_fee, 10**18) # abs admin fee now
out.ticks_in[j] = y + dy - y_dest
out.admin_fee = unsafe_add(out.admin_fee, y_dest)
if i != MAX_TICKS + MAX_SKIP_TICKS - 1:
if out.n2 == min_band:
break
if j == MAX_TICKS_UINT - 1:
break
if p_ratio > MAX_ORACLE_DN_POW:
# Don't allow to be away by more than ~50 ticks
break
out.n2 -= 1
p_o_up = unsafe_div(p_o_up * A, Aminus1)
x = self.bands_x[out.n2]
y = 0
if j != MAX_TICKS_UINT:
j = unsafe_add(j, 1)
# Round up what goes in and down what goes out
# ceil(in_amount_used/BORROWED_PRECISION) * BORROWED_PRECISION
out.in_amount = unsafe_mul(unsafe_div(unsafe_add(out.in_amount, unsafe_sub(in_precision, 1)), in_precision), in_precision)
out.out_amount = unsafe_mul(unsafe_div(out.out_amount, out_precision), out_precision)
return out
```
::::
### `get_dy`
::::description[`AMM.get_dy(i: uint256, j: uint256, in_amount: uint256) -> uint256: view`]
Function to calculate the amount of output tokens `j` to receive when exchanging for `in_amount` of input token `i`.
Returns: out amount (`uint256`).
| Input | Type | Description |
| ----------- | --------- | ------------------------------ |
| `i` | `uint256` | Input coin index. |
| `j` | `uint256` | Output coin index. |
| `in_amount` | `uint256` | Amount of input coin to swap. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
struct DetailedTrade:
in_amount: uint256
out_amount: uint256
n1: int256
n2: int256
ticks_in: DynArray[uint256, MAX_TICKS_UINT]
last_tick_j: uint256
admin_fee: uint256
@external
@view
@nonreentrant('lock')
def get_dy(i: uint256, j: uint256, in_amount: uint256) -> uint256:
"""
@notice Method to use to calculate out amount
@param i Input coin index
@param j Output coin index
@param in_amount Amount of input coin to swap
@return Amount of coin j to give out
"""
return self._get_dxdy(i, j, in_amount, True).out_amount
@internal
@view
def _get_dxdy(i: uint256, j: uint256, amount: uint256, is_in: bool) -> DetailedTrade:
"""
@notice Method to use to calculate out amount and spent in amount
@param i Input coin index
@param j Output coin index
@param amount Amount of input or output coin to swap
@param is_in Whether IN our OUT amount is known
@return DetailedTrade with all swap results
"""
# i = 0: borrowable (USD) in, collateral (ETH) out; going up
# i = 1: collateral (ETH) in, borrowable (USD) out; going down
assert (i == 0 and j == 1) or (i == 1 and j == 0), "Wrong index"
out: DetailedTrade = empty(DetailedTrade)
if amount == 0:
return out
in_precision: uint256 = COLLATERAL_PRECISION
out_precision: uint256 = BORROWED_PRECISION
if i == 0:
in_precision = BORROWED_PRECISION
out_precision = COLLATERAL_PRECISION
p_o: uint256[2] = self._price_oracle_ro()
if is_in:
out = self.calc_swap_out(i == 0, amount * in_precision, p_o, in_precision, out_precision)
else:
out = self.calc_swap_in(i == 0, amount * out_precision, p_o, in_precision, out_precision)
out.in_amount = unsafe_div(out.in_amount, in_precision)
out.out_amount = unsafe_div(out.out_amount, out_precision)
return out
```
```shell
>>> AMM.get_dy(0, 1, 10**21) -> swapping 1,000 crvUSD (`i`) for ETH (`j`).
0.305354737739302832
>>> AMM.get_dy(1, 0, 10**18) -> swapping 1 ETH (`j`) for crvUSD (`i`).
3150.900144377803783784
```
::::
### `get_dx`
::::description[`AMM.get_dx(i: uint256, j: uint256, out_amount: uint256) -> uint256: view`]
Function to calculate the `in_amount` of token `i` required to receive `out_amount` of token `j`.
Returns: in amount (`uint256`).
| Input | Type | Description |
| ----------- | --------- | ----------------------------------------- |
| `i` | `uint256` | Input coin index. |
| `j` | `uint256` | Output coin index. |
| `out_amount`| `uint256` | Desired amount of output coin to receive. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
struct DetailedTrade:
in_amount: uint256
out_amount: uint256
n1: int256
n2: int256
ticks_in: DynArray[uint256, MAX_TICKS_UINT]
last_tick_j: uint256
admin_fee: uint256
@external
@view
@nonreentrant('lock')
def get_dx(i: uint256, j: uint256, out_amount: uint256) -> uint256:
"""
@notice Method to use to calculate in amount required to receive the desired out_amount
@param i Input coin index
@param j Output coin index
@param out_amount Desired amount of output coin to receive
@return Amount of coin i to spend
"""
# i = 0: borrowable (USD) in, collateral (ETH) out; going up
# i = 1: collateral (ETH) in, borrowable (USD) out; going down
trade: DetailedTrade = self._get_dxdy(i, j, out_amount, False)
assert trade.out_amount == out_amount
return trade.in_amount
@internal
@view
def _get_dxdy(i: uint256, j: uint256, amount: uint256, is_in: bool) -> DetailedTrade:
"""
@notice Method to use to calculate out amount and spent in amount
@param i Input coin index
@param j Output coin index
@param amount Amount of input or output coin to swap
@param is_in Whether IN our OUT amount is known
@return DetailedTrade with all swap results
"""
# i = 0: borrowable (USD) in, collateral (ETH) out; going up
# i = 1: collateral (ETH) in, borrowable (USD) out; going down
assert (i == 0 and j == 1) or (i == 1 and j == 0), "Wrong index"
out: DetailedTrade = empty(DetailedTrade)
if amount == 0:
return out
in_precision: uint256 = COLLATERAL_PRECISION
out_precision: uint256 = BORROWED_PRECISION
if i == 0:
in_precision = BORROWED_PRECISION
out_precision = COLLATERAL_PRECISION
p_o: uint256[2] = self._price_oracle_ro()
if is_in:
out = self.calc_swap_out(i == 0, amount * in_precision, p_o, in_precision, out_precision)
else:
out = self.calc_swap_in(i == 0, amount * out_precision, p_o, in_precision, out_precision)
out.in_amount = unsafe_div(out.in_amount, in_precision)
out.out_amount = unsafe_div(out.out_amount, out_precision)
return out
```
The function essentially returns how much input tokens (`crvUSD`) are needed to receive `10**18` output tokens (`tBTC`).
```shell
>>> AMM.get_dx(0, 1, 10**18)
3276112209984625364927
>>> AMM.get_dx(1, 0, 10**21)
317266677056025978
```
::::
### `get_dydx`
::::description[`AMM.get_dydx(i: uint256, j: uint256, out_amount: uint256) -> (uint256, uint256): view`]
Function to calculate both the input amount required and the output amount received when swapping tokens `i` for a specified `out_amount` of token `j`. This function performs similar calculations to `get_dx` but additionally returns the amount of output tokens received.
Function to calculate the `in_amount` required and `out_amount` received.
Returns: out and in amount (`(uint256, uint256)`).
| Input | Type | Description |
| ----------- | --------- | ----------------------------------------- |
| `i` | `uint256` | Input coin index. |
| `j` | `uint256` | Output coin index. |
| `out_amount`| `uint256` | Desired amount of output coin to receive. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
struct DetailedTrade:
in_amount: uint256
out_amount: uint256
n1: int256
n2: int256
ticks_in: DynArray[uint256, MAX_TICKS_UINT]
last_tick_j: uint256
admin_fee: uint256
@external
@view
@nonreentrant('lock')
def get_dydx(i: uint256, j: uint256, out_amount: uint256) -> (uint256, uint256):
"""
@notice Method to use to calculate in amount required and out amount received
@param i Input coin index
@param j Output coin index
@param out_amount Desired amount of output coin to receive
@return A tuple with out_amount received and in_amount returned
"""
# i = 0: borrowable (USD) in, collateral (ETH) out; going up
# i = 1: collateral (ETH) in, borrowable (USD) out; going down
out: DetailedTrade = self._get_dxdy(i, j, out_amount, False)
return (out.out_amount, out.in_amount)
@internal
@view
def _get_dxdy(i: uint256, j: uint256, amount: uint256, is_in: bool) -> DetailedTrade:
"""
@notice Method to use to calculate out amount and spent in amount
@param i Input coin index
@param j Output coin index
@param amount Amount of input or output coin to swap
@param is_in Whether IN our OUT amount is known
@return DetailedTrade with all swap results
"""
# i = 0: borrowable (USD) in, collateral (ETH) out; going up
# i = 1: collateral (ETH) in, borrowable (USD) out; going down
assert (i == 0 and j == 1) or (i == 1 and j == 0), "Wrong index"
out: DetailedTrade = empty(DetailedTrade)
if amount == 0:
return out
in_precision: uint256 = COLLATERAL_PRECISION
out_precision: uint256 = BORROWED_PRECISION
if i == 0:
in_precision = BORROWED_PRECISION
out_precision = COLLATERAL_PRECISION
p_o: uint256[2] = self._price_oracle_ro()
if is_in:
out = self.calc_swap_out(i == 0, amount * in_precision, p_o, in_precision, out_precision)
else:
out = self.calc_swap_in(i == 0, amount * out_precision, p_o, in_precision, out_precision)
out.in_amount = unsafe_div(out.in_amount, in_precision)
out.out_amount = unsafe_div(out.out_amount, out_precision)
return out
```
```shell
>>> AMM.get_dydx(0, 1, 10**18)
(1000000000000000000, 3275951499856300880467)
>>> AMM.get_dydx(1, 0, 10**21)
(1000000000000000000000, 317280848541649185)
```
::::
### `get_dxdy`
::::description[`AMM.get_dxdy(i: uint256, j: uint256, in_amount: uint256) -> (uint256, uint256): view`]
Function to calculate both the input and output amounts when swapping `in_amount` of token `i` for token `j`. This function performs similar calculations to `get_dy` but additionally returns the amount of input tokens used in the swap.
Returns: in and out amount (`uint256`).
| Input | Type | Description |
| ----------- | --------- | ----------------------------- |
| `i` | `uint256` | Input coin index. |
| `j` | `uint256` | Output coin index. |
| `in_amount` | `uint256` | Amount of input coin to swap. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
struct DetailedTrade:
in_amount: uint256
out_amount: uint256
n1: int256
n2: int256
ticks_in: DynArray[uint256, MAX_TICKS_UINT]
last_tick_j: uint256
admin_fee: uint256
@external
@view
@nonreentrant('lock')
def get_dxdy(i: uint256, j: uint256, in_amount: uint256) -> (uint256, uint256):
"""
@notice Method to use to calculate out amount and spent in amount
@param i Input coin index
@param j Output coin index
@param in_amount Amount of input coin to swap
@return A tuple with in_amount used and out_amount returned
"""
out: DetailedTrade = self._get_dxdy(i, j, in_amount, True)
return (out.in_amount, out.out_amount)
@internal
@view
def _get_dxdy(i: uint256, j: uint256, amount: uint256, is_in: bool) -> DetailedTrade:
"""
@notice Method to use to calculate out amount and spent in amount
@param i Input coin index
@param j Output coin index
@param amount Amount of input or output coin to swap
@param is_in Whether IN our OUT amount is known
@return DetailedTrade with all swap results
"""
# i = 0: borrowable (USD) in, collateral (ETH) out; going up
# i = 1: collateral (ETH) in, borrowable (USD) out; going down
assert (i == 0 and j == 1) or (i == 1 and j == 0), "Wrong index"
out: DetailedTrade = empty(DetailedTrade)
if amount == 0:
return out
in_precision: uint256 = COLLATERAL_PRECISION
out_precision: uint256 = BORROWED_PRECISION
if i == 0:
in_precision = BORROWED_PRECISION
out_precision = COLLATERAL_PRECISION
p_o: uint256[2] = self._price_oracle_ro()
if is_in:
out = self.calc_swap_out(i == 0, amount * in_precision, p_o, in_precision, out_precision)
else:
out = self.calc_swap_in(i == 0, amount * out_precision, p_o, in_precision, out_precision)
out.in_amount = unsafe_div(out.in_amount, in_precision)
out.out_amount = unsafe_div(out.out_amount, out_precision)
return out
```
```shell
>>> AMM.get_dxdy(0, 1, 10**21)
(1000000000000000000000, 305269619091735494)
>>> AMM.get_dxdy(1, 0, 10**18)
(1000000000000000000, 3151594754708902902339)
```
::::
### `get_amount_for_price`
::::description[`AMM.get_amount_for_price(p: uint256) -> (uint256, bool)`]
Function to calculate the necessary amount of tokens to be exchanged to achieve the final price `p` in the AMM.
Returns: The amount to exchange (`uint256`) and a boolean (`bool`). `True` means exchanging the borrowed token for the collateral token to reach the final price `p` (pumping the collateral price). `False` means the opposite (dumping the collateral price).
| Input | Type | Description |
| ----- | --------- | ----------------------- |
| `p` | `uint256` | Final price of the AMM. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
@external
@view
@nonreentrant('lock')
def get_amount_for_price(p: uint256) -> (uint256, bool):
"""
@notice Amount necessary to be exchanged to have the AMM at the final price `p`
@return (amount, is_pump)
"""
min_band: int256 = self.min_band
max_band: int256 = self.max_band
n: int256 = self.active_band
p_o: uint256[2] = self._price_oracle_ro()
p_o_up: uint256 = self._p_oracle_up(n)
p_down: uint256 = unsafe_div(unsafe_div(p_o[0]**2, p_o_up) * p_o[0], p_o_up) # p_current_down
p_up: uint256 = unsafe_div(p_down * A2, Aminus12) # p_crurrent_up
amount: uint256 = 0
y0: uint256 = 0
f: uint256 = 0
g: uint256 = 0
Inv: uint256 = 0
j: uint256 = MAX_TICKS_UINT
pump: bool = True
for i in range(MAX_TICKS + MAX_SKIP_TICKS):
assert p_o_up > 0
x: uint256 = self.bands_x[n]
y: uint256 = self.bands_y[n]
if i == 0:
if p < self._get_p(n, x, y):
pump = False
not_empty: bool = x > 0 or y > 0
if not_empty:
y0 = self._get_y0(x, y, p_o[0], p_o_up)
f = unsafe_div(unsafe_div(A * y0 * p_o[0], p_o_up) * p_o[0], 10**18)
g = unsafe_div(Aminus1 * y0 * p_o_up, p_o[0])
Inv = (f + x) * (g + y)
if j == MAX_TICKS_UINT:
j = 0
if p <= p_up:
if p >= p_down:
if not_empty:
ynew: uint256 = unsafe_sub(max(self.sqrt_int(Inv * 10**18 / p), g), g)
xnew: uint256 = unsafe_sub(max(Inv / (g + ynew), f), f)
if pump:
amount += unsafe_sub(max(xnew, x), x)
else:
amount += unsafe_sub(max(ynew, y), y)
break
# Need this to break if price is too far
p_ratio: uint256 = unsafe_div(p_o_up * 10**18, p_o[0])
if pump:
if not_empty:
amount += (Inv / g - f) - x
if n == max_band:
break
if j == MAX_TICKS_UINT - 1:
break
if p_ratio < unsafe_div(10**36, MAX_ORACLE_DN_POW):
# Don't allow to be away by more than ~50 ticks
break
n += 1
p_down = p_up
p_up = unsafe_div(p_up * A2, Aminus12)
p_o_up = unsafe_div(p_o_up * Aminus1, A)
else:
if not_empty:
amount += (Inv / f - g) - y
if n == min_band:
break
if j == MAX_TICKS_UINT - 1:
break
if p_ratio > MAX_ORACLE_DN_POW:
# Don't allow to be away by more than ~50 ticks
break
n -= 1
p_up = p_down
p_down = unsafe_div(p_down * Aminus12, A2)
p_o_up = unsafe_div(p_o_up * A, Aminus1)
if j != MAX_TICKS_UINT:
j = unsafe_add(j, 1)
amount = amount * 10**18 / unsafe_sub(10**18, max(self.fee, p_o[1]))
if amount == 0:
return 0, pump
# Precision and round up
if pump:
amount = unsafe_add(unsafe_div(unsafe_sub(amount, 1), BORROWED_PRECISION), 1)
else:
amount = unsafe_add(unsafe_div(unsafe_sub(amount, 1), COLLATERAL_PRECISION), 1)
return amount, pump
```
- `bool = true`: One needs to exchange borrowed for collateral tokens (to get the price of the collateral **UP**).
- `bool = false`: One needs to exchange collateral for borrowed tokens (to get the price of the collateral **DOWN**).
```shell
>>> Controller.amm_price()
3213458506041024105600
>>> AMM.get_amount_for_price(3213458506041024105600)
(0, True) # no need for any swaps as the price is already where we want it
>>> AMM.get_amount_for_price(3250000000000000000000)
(80060004111772800648528, true) # need to swap around 80k crvUSD for ETH to raise the price of ETH up to 3250 within the AMM
>>> AMM.get_amount_for_price(3150000000000000000000)
(42058023845976978330, false) # need to swap around 42 ETH for crvUSD to dump the price of ETH down to 3150 within the AMM
```
::::
---
## Bands
*"Each band works like Uniswap V3, concentrating liquidity between two prices, and being all in the collateral at the lower price and all in crvUSD at the higher price. However since the entire interval of prices are aggressively placed with respect to the market (higher than oracle price when it's moving up and lower when it's moving down), each band gets arbed to hold all of either collateral or stablecoin in the opposite manner than expected when LP-ing with Uniswap V3."*[^2]
[^2]: https://github.com/chanhosuh/curvefi-math/blob/master/LLAMMA.ipynb
Bands in LLAMMA function similarly to UniswapV3, concentrating liquidity between two prices. Essentially, a band is a range of prices into which liquidity is deposited. LLAMMA consists of multiple bands, and when creating a loan, liquidity is equally distributed across the number of bands (`N`) chosen when opening the loan using the [`deposit_range`](amm.md#deposit_range) function. The minimum number of bands is 4, and the maximum is 50.
Each individual band has an upper ([`p_oracle_up`](#p_oracle_up)) and lower ([`p_oracle_down`](#p_oracle_down)) price bound. These prices are not actual AMM prices, but rather thresholds for the bands. A single band is part of the entire liquidation range. When considering all bands combined, where a user has deposited collateral, we essentially get the entire liquidation range. The entire liquidation range is composed of the "smaller" liquidation ranges of the individual bands. For example, a loan with bands spanning from $1000 to $600 would have a total liquidation range from $1000 to $600.
:::warning
The following sections assume that arbitrage traders are performing their role and arbitraging the bands. In theory, prices can move through bands without any action if arbitrage traders do not capitalize on the opportunity for free money. We assume that arbitrage traders are taking these opportunities and arbitraging the bands accordingly.
:::
*There are three possible scenarios for bands regarding their content of assets. The asset composition of the individual bands is dependant on the collateral price bzw. the "liquidation status" of the loan:*
1. **Band contains both collateral and borrowable token:**This indicates that the band is currently in continuous liquidation mode (either being soft-liquidated because the collateral price is decreasing or de-liquidated because the collateral price is increasing). The band in which the collateral price is currently located is defined as the [`active_band`](amm.md#active_band).
2. **Band contains only the collateral token:**This band has not been soft-liquidated. The collateral price is higher than the upper price of the band and is therefore outside the band. The liquidity in this band is untouched. These are the bands above the [`active_band`](amm.md#active_band). If the active band is 0, all bands greater than 0 consist solely of the collateral token.
3. **Band contains only the borrowable token:**This band has already been soft-liquidated, meaning the collateral price is below the band, and arbitrage trades have already exchanged all the ETH for crvUSD in the band. These are the bands below the [`active_band`](amm.md#active_band). If the active band is 0, all bands less than 0 consist solely of the borrowable token.
*A full set up bands can look the following:*
*In the code, `x` represents the borrowable token, and `y` the collateral token.*
### `A`
::::description[`AMM.A() -> uint256: view`]
Getter for A (band width factor). This parameter defines the density of the liquidty and band size. The higher `A`, the smaller are the upper and lower prices of the bands an therefor the more leveraged the AMM within each band. The relative band size is $\frac{1}{A}$.
Returns: band width factor (`uint256`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
A: public(immutable(uint256))
@external
def __init__(
_borrowed_token: address,
_borrowed_precision: uint256,
_collateral_token: address,
_collateral_precision: uint256,
_A: uint256,
_sqrt_band_ratio: uint256,
_log_A_ratio: int256,
_base_price: uint256,
fee: uint256,
admin_fee: uint256,
_price_oracle_contract: address,
):
"""
@notice LLAMMA constructor
@param _borrowed_token Token which is being borrowed
@param _collateral_token Token used as collateral
@param _collateral_precision Precision of collateral: we pass it because we want the blueprint to fit into bytecode
@param _A "Amplification coefficient" which also defines density of liquidity and band size. Relative band size is 1/_A
@param _sqrt_band_ratio Precomputed int(sqrt(A / (A - 1)) * 1e18)
@param _log_A_ratio Precomputed int(ln(A / (A - 1)) * 1e18)
@param _base_price Typically the initial crypto price at which AMM is deployed. Will correspond to band 0
@param fee Relative fee of the AMM: int(fee * 1e18)
@param admin_fee Admin fee: how much of fee goes to admin. 50% === int(0.5 * 1e18)
@param _price_oracle_contract External price oracle which has price() and price_w() methods
which both return current price of collateral multiplied by 1e18
"""
...
A = _A
...
```
```shell
>>> AMM.A()
100
```
::::
### `active_band`
::::description[`AMM.active_band() -> int256: view`]
Getter for the currently active band, which is the band where `get_p` (collateral price in the AMM) currently is in. Other bands are either fully in the collateral or borrowable token, but not in both.
- bands > `active_band`: These bands are fully in the borrowable token.
- bands < `active_band`: These bands are fully in the collateral token.
Returns: active band (`int256`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
active_band: public(int256)
```
```shell
>>> AMM.active_band()
-40
```
::::
### `min_band`
::::description[`AMM.min_band() -> int256: view`]
Getter for the minimum band. This is essentially the lowest band where liquidity was deposited into. All bands below are definitely empty.
Returns: minimum band (`int256`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
min_band: public(int256)
```
```shell
>>> AMM.min_band()
-70
```
::::
### `max_band`
::::description[`AMM.max_band() -> int256: view`]
Getter for the maximum band. thi is the highest band where liquidity was deposited into. All bands above are definitely empty.
Returns: maximum band (`int256`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
max_band: public(int256)
```
```shell
>>> AMM.max_band()
1043
```
::::
### `has_liquidity`
::::description[`AMM.has_liquidity(user_: address) -> bool`]
Function to check if `user` has any liquidity in the AMM.
Returns: true or false (`bool`).
| Input | Type | Description |
| ------ | --------- | ------------- |
| `user` | `address` | User Address. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
user_shares: HashMap[address, UserTicks]
struct UserTicks:
ns: int256 # packs n1 and n2, each is int128
ticks: uint256[MAX_TICKS/2] # Share fractions packed 2 per slot
@external
@view
@nonreentrant('lock')
def has_liquidity(user: address) -> bool:
"""
@notice Check if `user` has any liquidity in the AMM
"""
return self.user_shares[user].ticks[0] != 0
```
```shell
>>> AMM.has_liquidity('0x5A684c08261380B91D8976eDB0cabf87744650a5')
'True'
```
::::
### `bands_x`
::::description[`AMM.bands_x(arg0: int256) -> uint256: view`]
Getter for the amount of the borrowable token deposited in a specific band.
Returns: amount of borrowable token in the band (`uint256`).
| Input | Type | Description |
| ------ | --------- | ------------------- |
| `arg0` | `int256` | Number of the band. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
bands_x: public(HashMap[int256, uint256])
```
In this example, the `active_band` is `-40`, which consists both of the colltaral and borrowable token. All bands above do not hold any balances of the borrowable token as those bands fully consist of the collateral token. But all bands below are fully in the borrowable token.
```shell
>>> AMM.bands_x(-39)
0
>>> AMM.bands_x(-40)
45065024748584993052511
>>> AMM.bands_x(-41)
128175190583901226590255
```
::::
### `bands_y`
::::description[`AMM.bands_y(arg0: int256) -> uint256: view`]
Getter for the amount of collateral token deposited in band number `arg0`.
Returns: amount of collateral token in the band (`uint256`).
| Input | Type | Description |
| ------ | --------- | ------------------- |
| `arg0` | `int256` | Number of the band. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
bands_y: public(HashMap[int256, uint256])
```
In this example, the `active_band` is `-40`, which consists both of the colltaral and borrowable token. All bands above do not hold any balances of the borrowable token as those bands fully consist of the collateral token. But all bands below are fully in the borrowable token.
```shell
>>> AMM.bands_x(-39)
53056498461522064143
>>> AMM.bands_x(-40)
29290524643091268376
>>> AMM.bands_x(-41)
0
```
::::
### `get_xy`
::::description[`AMM.get_xy(user: address) -> DynArray[uint256, MAX_TICKS_UINT][2]`]
Function to get the collateral and borrowable token balance of a user across all bands.
Returns: balance of borrowed and collateral token (`uint256`).
| Input | Type | Description |
| ------ | --------- | ------------- |
| `user` | `address` | User address. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
@external
@view
@nonreentrant('lock')
def get_xy(user: address) -> DynArray[uint256, MAX_TICKS_UINT][2]:
"""
@notice A low-gas function to measure amounts of stablecoins and collateral by bands which user currently owns
@param user User address
@return Amounts of (stablecoin, collateral) by bands in a tuple
"""
return self._get_xy(user, False)
@internal
@view
def _get_xy(user: address, is_sum: bool) -> DynArray[uint256, MAX_TICKS_UINT][2]:
"""
@notice A low-gas function to measure amounts of stablecoins and collateral which user currently owns
@param user User address
@param is_sum Return sum or amounts by bands
@return Amounts of (stablecoin, collateral) in a tuple
"""
xs: DynArray[uint256, MAX_TICKS_UINT] = []
ys: DynArray[uint256, MAX_TICKS_UINT] = []
if is_sum:
xs.append(0)
ys.append(0)
ns: int256[2] = self._read_user_tick_numbers(user)
ticks: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns)
if ticks[0] != 0:
for i in range(MAX_TICKS):
total_shares: uint256 = self.total_shares[ns[0]] + DEAD_SHARES
ds: uint256 = ticks[i]
dx: uint256 = unsafe_div((self.bands_x[ns[0]] + 1) * ds, total_shares)
dy: uint256 = unsafe_div((self.bands_y[ns[0]] + 1) * ds, total_shares)
if is_sum:
xs[0] += dx
ys[0] += dy
else:
xs.append(unsafe_div(dx, BORROWED_PRECISION))
ys.append(unsafe_div(dy, COLLATERAL_PRECISION))
if ns[0] == ns[1]:
break
ns[0] = unsafe_add(ns[0], 1)
if is_sum:
xs[0] = unsafe_div(xs[0], BORROWED_PRECISION)
ys[0] = unsafe_div(ys[0], COLLATERAL_PRECISION)
return [xs, ys]
```
This user uses 4 bands for their loan. The function returns the collateral composition of all bands. In this case, the 4 bands do not hold any borrow token (the first four returned values), but they hold the collateral token (the last four return values). This signals, that user is not is soft-liquidation, as all his bands are still fully allocated in the collateral tokens.
**Fictive example:**E.g. if the first band of the loan would have been liquidated and the second band is currently undergoing liquidation, the returned values could look like the second example below. The first band would be fully in the borrow token (because the band as already been soft-liquidated), the second band would be in both, the borrow and collateral token (because the band is currently being liquidated) and the remaining two bands are still fully composited of the collteral token (because these bands have not been liquidated).
```shell
>>> AMM.get_xy('0x5A684c08261380B91D8976eDB0cabf87744650a5')
[0, 0, 0, 0][524583942253332472, 525000000000000000, 525000000000000000, 525000000000000000]
# see fivtive example above
[1573751826760000000000, 7573751826760000000000, 0, 0] [0, 265000000000000000, 525000000000000000, 525000000000000000]
```
::::
### `get_sum_xy`
::::description[`AMM.get_sum_xy(user: address) -> uint256[2]: view`]
Function to measure the amount of borrow and collateral token a user currently owns inside the AMM. This function does not include the borrowed tokens from the market in any way but rather reflects the current collateral composition summed up across the entire AMM.
Returns: total borrow token and collateral token (`uint256[2]`).
| Input | Type | Description |
| ------- | -------- | ------------ |
| `user` | `address`| User address. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
@external
@view
@nonreentrant('lock')
def get_sum_xy(user: address) -> uint256[2]:
"""
@notice A low-gas function to measure amounts of stablecoins and collateral which user currently owns
@param user User address
@return Amounts of (stablecoin, collateral) in a tuple
"""
xy: DynArray[uint256, MAX_TICKS_UINT][2] = self._get_xy(user, True)
return [xy[0][0], xy[1][0]]
@internal
@view
def _get_xy(user: address, is_sum: bool) -> DynArray[uint256, MAX_TICKS_UINT][2]:
"""
@notice A low-gas function to measure amounts of stablecoins and collateral which user currently owns
@param user User address
@param is_sum Return sum or amounts by bands
@return Amounts of (stablecoin, collateral) in a tuple
"""
xs: DynArray[uint256, MAX_TICKS_UINT] = []
ys: DynArray[uint256, MAX_TICKS_UINT] = []
if is_sum:
xs.append(0)
ys.append(0)
ns: int256[2] = self._read_user_tick_numbers(user)
ticks: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns)
if ticks[0] != 0:
for i in range(MAX_TICKS):
total_shares: uint256 = self.total_shares[ns[0]] + DEAD_SHARES
ds: uint256 = ticks[i]
dx: uint256 = unsafe_div((self.bands_x[ns[0]] + 1) * ds, total_shares)
dy: uint256 = unsafe_div((self.bands_y[ns[0]] + 1) * ds, total_shares)
if is_sum:
xs[0] += dx
ys[0] += dy
else:
xs.append(unsafe_div(dx, BORROWED_PRECISION))
ys.append(unsafe_div(dy, COLLATERAL_PRECISION))
if ns[0] == ns[1]:
break
ns[0] = unsafe_add(ns[0], 1)
if is_sum:
xs[0] = unsafe_div(xs[0], BORROWED_PRECISION)
ys[0] = unsafe_div(ys[0], COLLATERAL_PRECISION)
return [xs, ys]
```
This function returns the total balance of the borrow and collateral token across all bands. This essentially equals to the summed up returned values of the `get_xy` method (see above).
```shell
>>> AMM.get_sum_xy('0x5A684c08261380B91D8976eDB0cabf87744650a5')
[0, 2099583942253332472]
```
::::
### `read_user_tick_numbers`
::::description[`AMM.read_user_tick_numbers(user: address) -> int256[2]: view`]
Getter for the band (tick) numbers of a user's loan.
Returns: highest and lowest band (`int256`).
| Input | Type | Description |
| ------ | --------- | ------------- |
| `user` | `address` | User address. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
user_shares: HashMap[address, UserTicks]
@external
@view
@nonreentrant('lock')
def read_user_tick_numbers(user: address) -> int256[2]:
"""
@notice Unpacks and reads user tick numbers
@param user User address
@return Lowest and highest band the user deposited into
"""
return self._read_user_tick_numbers(user)
@internal
@view
def _read_user_tick_numbers(user: address) -> int256[2]:
"""
@notice Unpacks and reads user tick numbers
@param user User address
@return Lowest and highest band the user deposited into
"""
ns: int256 = self.user_shares[user].ns
n2: int256 = unsafe_div(ns, 2**128)
n1: int256 = ns % 2**128
if n1 >= 2**127:
n1 = unsafe_sub(n1, 2**128)
n2 = unsafe_add(n2, 1)
return [n1, n2]
```
```shell
>>> AMM.read_user_tick_numbers('0x5A684c08261380B91D8976eDB0cabf87744650a5')
[-32, -29]
```
::::
### `get_y_up`
::::description[`AMM.get_y_up(user: address) -> uint256`]
Function to measure the amount of `y` (collateral token) in band n for `user` if its adiabatically traded near `p_oracle` on the way up.
Returns: amount of collateral (`uint256`).
| Input | Type | Description |
| ------ | --------- | ------------- |
| `user` | `address` | User address. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
@external
@view
@nonreentrant('lock')
def get_y_up(user: address) -> uint256:
"""
@notice Measure the amount of y (collateral) in the band n if we adiabatically trade near p_oracle on the way up
@param user User the amount is calculated for
@return Amount of coins
"""
return self.get_xy_up(user, True)
@internal
@view
def get_xy_up(user: address, use_y: bool) -> uint256:
"""
@notice Measure the amount of y (collateral) in the band n if we adiabatically trade near p_oracle on the way up,
or the amount of x (stablecoin) if we trade adiabatically down
@param user User the amount is calculated for
@param use_y Calculate amount of collateral if True and of stablecoin if False
@return Amount of coins
"""
ns: int256[2] = self._read_user_tick_numbers(user)
ticks: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns)
if ticks[0] == 0: # Even dynamic array will have 0th element set here
return 0
p_o: uint256 = self._price_oracle_ro()[0]
assert p_o != 0
n: int256 = ns[0] - 1
n_active: int256 = self.active_band
p_o_down: uint256 = self._p_oracle_up(ns[0])
XY: uint256 = 0
for i in range(MAX_TICKS):
n += 1
if n > ns[1]:
break
x: uint256 = 0
y: uint256 = 0
if n >= n_active:
y = self.bands_y[n]
if n <= n_active:
x = self.bands_x[n]
# p_o_up: uint256 = self._p_oracle_up(n)
p_o_up: uint256 = p_o_down
# p_o_down = self._p_oracle_up(n + 1)
p_o_down = unsafe_div(p_o_down * Aminus1, A)
if x == 0:
if y == 0:
continue
total_share: uint256 = self.total_shares[n]
user_share: uint256 = ticks[i]
if total_share == 0:
continue
if user_share == 0:
continue
total_share += DEAD_SHARES
# Also ideally we'd want to add +1 to all quantities when calculating with shares
# but we choose to save bytespace and slightly under-estimate the result of this call
# which is also more conservative
# Also this will revert if p_o_down is 0, and p_o_down is 0 if p_o_up is 0
p_current_mid: uint256 = unsafe_div(p_o**2 / p_o_down * p_o, p_o_up)
# if p_o > p_o_up - we "trade" everything to y and then convert to the result
# if p_o < p_o_down - "trade" to x, then convert to result
# otherwise we are in-band, so we do the more complex logic to trade
# to p_o rather than to the edge of the band
# trade to the edge of the band == getting to the band edge while p_o=const
# Cases when special conversion is not needed (to save on computations)
if x == 0 or y == 0:
if p_o > p_o_up: # p_o < p_current_down
# all to y at constant p_o, then to target currency adiabatically
y_equiv: uint256 = y
if y == 0:
y_equiv = x * 10**18 / p_current_mid
if use_y:
XY += unsafe_div(y_equiv * user_share, total_share)
else:
XY += unsafe_div(unsafe_div(y_equiv * p_o_up, SQRT_BAND_RATIO) * user_share, total_share)
continue
elif p_o < p_o_down: # p_o > p_current_up
# all to x at constant p_o, then to target currency adiabatically
x_equiv: uint256 = x
if x == 0:
x_equiv = unsafe_div(y * p_current_mid, 10**18)
if use_y:
XY += unsafe_div(unsafe_div(x_equiv * SQRT_BAND_RATIO, p_o_up) * user_share, total_share)
else:
XY += unsafe_div(x_equiv * user_share, total_share)
continue
# If we are here - we need to "trade" to somewhere mid-band
# So we need more heavy math
y0: uint256 = self._get_y0(x, y, p_o, p_o_up)
f: uint256 = unsafe_div(unsafe_div(A * y0 * p_o, p_o_up) * p_o, 10**18)
g: uint256 = unsafe_div(Aminus1 * y0 * p_o_up, p_o)
# (f + x)(g + y) = const = p_top * A**2 * y0**2 = I
Inv: uint256 = (f + x) * (g + y)
# p = (f + x) / (g + y) => p * (g + y)**2 = I or (f + x)**2 / p = I
# First, "trade" in this band to p_oracle
x_o: uint256 = 0
y_o: uint256 = 0
if p_o > p_o_up: # p_o < p_current_down, all to y
# x_o = 0
y_o = unsafe_sub(max(Inv / f, g), g)
if use_y:
XY += unsafe_div(y_o * user_share, total_share)
else:
XY += unsafe_div(unsafe_div(y_o * p_o_up, SQRT_BAND_RATIO) * user_share, total_share)
elif p_o < p_o_down: # p_o > p_current_up, all to x
# y_o = 0
x_o = unsafe_sub(max(Inv / g, f), f)
if use_y:
XY += unsafe_div(unsafe_div(x_o * SQRT_BAND_RATIO, p_o_up) * user_share, total_share)
else:
XY += unsafe_div(x_o * user_share, total_share)
else:
# Equivalent from Chainsecurity (which also has less numerical errors):
y_o = unsafe_div(A * y0 * unsafe_sub(p_o, p_o_down), p_o)
# x_o = unsafe_div(A * y0 * p_o, p_o_up) * unsafe_sub(p_o_up, p_o)
# Old math
# y_o = unsafe_sub(max(self.sqrt_int(unsafe_div(Inv * 10**18, p_o)), g), g)
x_o = unsafe_sub(max(Inv / (g + y_o), f), f)
# Now adiabatic conversion from definitely in-band
if use_y:
XY += unsafe_div((y_o + x_o * 10**18 / self.sqrt_int(p_o_up * p_o)) * user_share, total_share)
else:
XY += unsafe_div((x_o + unsafe_div(y_o * self.sqrt_int(p_o_down * p_o), 10**18)) * user_share, total_share)
if use_y:
return unsafe_div(XY, COLLATERAL_PRECISION)
else:
return unsafe_div(XY, BORROWED_PRECISION)
```
```shell
>>> AMM.get_y_up('0x5A684c08261380B91D8976eDB0cabf87744650a5')
2099583942253332471
```
::::
### `get_x_down`
::::description[`AMM.get_x_down(user: address) -> uint256: view`]
Function to measure the amount of x (borrowable token) in band n for `user` if its adiabatically traded down.
Returns: amount of collateral (`uint256`).
| Input | Type | Description |
| ------ | --------- | ------------- |
| `user` | `address` | User address. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
@external
@view
@nonreentrant('lock')
def get_x_down(user: address) -> uint256:
"""
@notice Measure the amount of x (stablecoin) if we trade adiabatically down
@param user User the amount is calculated for
@return Amount of coins
"""
return self.get_xy_up(user, False)
@internal
@view
def get_xy_up(user: address, use_y: bool) -> uint256:
"""
@notice Measure the amount of y (collateral) in the band n if we adiabatically trade near p_oracle on the way up,
or the amount of x (stablecoin) if we trade adiabatically down
@param user User the amount is calculated for
@param use_y Calculate amount of collateral if True and of stablecoin if False
@return Amount of coins
"""
ns: int256[2] = self._read_user_tick_numbers(user)
ticks: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns)
if ticks[0] == 0: # Even dynamic array will have 0th element set here
return 0
p_o: uint256 = self._price_oracle_ro()[0]
assert p_o != 0
n: int256 = ns[0] - 1
n_active: int256 = self.active_band
p_o_down: uint256 = self._p_oracle_up(ns[0])
XY: uint256 = 0
for i in range(MAX_TICKS):
n += 1
if n > ns[1]:
break
x: uint256 = 0
y: uint256 = 0
if n >= n_active:
y = self.bands_y[n]
if n <= n_active:
x = self.bands_x[n]
# p_o_up: uint256 = self._p_oracle_up(n)
p_o_up: uint256 = p_o_down
# p_o_down = self._p_oracle_up(n + 1)
p_o_down = unsafe_div(p_o_down * Aminus1, A)
if x == 0:
if y == 0:
continue
total_share: uint256 = self.total_shares[n]
user_share: uint256 = ticks[i]
if total_share == 0:
continue
if user_share == 0:
continue
total_share += DEAD_SHARES
# Also ideally we'd want to add +1 to all quantities when calculating with shares
# but we choose to save bytespace and slightly under-estimate the result of this call
# which is also more conservative
# Also this will revert if p_o_down is 0, and p_o_down is 0 if p_o_up is 0
p_current_mid: uint256 = unsafe_div(p_o**2 / p_o_down * p_o, p_o_up)
# if p_o > p_o_up - we "trade" everything to y and then convert to the result
# if p_o < p_o_down - "trade" to x, then convert to result
# otherwise we are in-band, so we do the more complex logic to trade
# to p_o rather than to the edge of the band
# trade to the edge of the band == getting to the band edge while p_o=const
# Cases when special conversion is not needed (to save on computations)
if x == 0 or y == 0:
if p_o > p_o_up: # p_o < p_current_down
# all to y at constant p_o, then to target currency adiabatically
y_equiv: uint256 = y
if y == 0:
y_equiv = x * 10**18 / p_current_mid
if use_y:
XY += unsafe_div(y_equiv * user_share, total_share)
else:
XY += unsafe_div(unsafe_div(y_equiv * p_o_up, SQRT_BAND_RATIO) * user_share, total_share)
continue
elif p_o < p_o_down: # p_o > p_current_up
# all to x at constant p_o, then to target currency adiabatically
x_equiv: uint256 = x
if x == 0:
x_equiv = unsafe_div(y * p_current_mid, 10**18)
if use_y:
XY += unsafe_div(unsafe_div(x_equiv * SQRT_BAND_RATIO, p_o_up) * user_share, total_share)
else:
XY += unsafe_div(x_equiv * user_share, total_share)
continue
# If we are here - we need to "trade" to somewhere mid-band
# So we need more heavy math
y0: uint256 = self._get_y0(x, y, p_o, p_o_up)
f: uint256 = unsafe_div(unsafe_div(A * y0 * p_o, p_o_up) * p_o, 10**18)
g: uint256 = unsafe_div(Aminus1 * y0 * p_o_up, p_o)
# (f + x)(g + y) = const = p_top * A**2 * y0**2 = I
Inv: uint256 = (f + x) * (g + y)
# p = (f + x) / (g + y) => p * (g + y)**2 = I or (f + x)**2 / p = I
# First, "trade" in this band to p_oracle
x_o: uint256 = 0
y_o: uint256 = 0
if p_o > p_o_up: # p_o < p_current_down, all to y
# x_o = 0
y_o = unsafe_sub(max(Inv / f, g), g)
if use_y:
XY += unsafe_div(y_o * user_share, total_share)
else:
XY += unsafe_div(unsafe_div(y_o * p_o_up, SQRT_BAND_RATIO) * user_share, total_share)
elif p_o < p_o_down: # p_o > p_current_up, all to x
# y_o = 0
x_o = unsafe_sub(max(Inv / g, f), f)
if use_y:
XY += unsafe_div(unsafe_div(x_o * SQRT_BAND_RATIO, p_o_up) * user_share, total_share)
else:
XY += unsafe_div(x_o * user_share, total_share)
else:
# Equivalent from Chainsecurity (which also has less numerical errors):
y_o = unsafe_div(A * y0 * unsafe_sub(p_o, p_o_down), p_o)
# x_o = unsafe_div(A * y0 * p_o, p_o_up) * unsafe_sub(p_o_up, p_o)
# Old math
# y_o = unsafe_sub(max(self.sqrt_int(unsafe_div(Inv * 10**18, p_o)), g), g)
x_o = unsafe_sub(max(Inv / (g + y_o), f), f)
# Now adiabatic conversion from definitely in-band
if use_y:
XY += unsafe_div((y_o + x_o * 10**18 / self.sqrt_int(p_o_up * p_o)) * user_share, total_share)
else:
XY += unsafe_div((x_o + unsafe_div(y_o * self.sqrt_int(p_o_down * p_o), 10**18)) * user_share, total_share)
if use_y:
return unsafe_div(XY, COLLATERAL_PRECISION)
else:
return unsafe_div(XY, BORROWED_PRECISION)
```
```shell
>>> AMM.get_x_down('0x5A684c08261380B91D8976eDB0cabf87744650a5')
5911655561808789389866
```
::::
### `can_skip_bands`
::::description[`AMM.can_skip_bands(n_end: int256) -> bool`]
Function to check if there is no liquidity between `active_band` and `n_end`.
Returns: true or false (`bool`).
| Input | Type | Description |
| ------- | -------- | ---------------------------------- |
| `n_end` | `int256` | Band number to check until. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
@external
@view
@nonreentrant('lock')
def can_skip_bands(n_end: int256) -> bool:
"""
@notice Check that we have no liquidity between active_band and `n_end`
"""
n: int256 = self.active_band
for i in range(MAX_SKIP_TICKS):
if n_end > n:
if self.bands_y[n] != 0:
return False
n = unsafe_add(n, 1)
else:
if self.bands_x[n] != 0:
return False
n = unsafe_sub(n, 1)
if n == n_end: # not including n_end
break
return True
# Actually skipping bands:
# * change self.active_band to the new n
# * change self.p_base_mul
# to do n2-n1 times (if n2 > n1):
# out.base_mul = unsafe_div(out.base_mul * Aminus1, A)
```
```shell
>>> AMM.can_skip_bands(-50)
'False'
```
::::
---
## AMM and Oracle Prices
:::info[A user's loan is only in soft-liquidation when the price oracle is within the bands of the deposited collateral]
A position enters soft-liquidation mode only when the price oracle falls within a band where the user has deposited collateral. For example, if a user has collateral deposited between bands 10 and 0, they will not enter soft-liquidation as long as the oracle price stays outside these bands. In this scenario, the only "loss" the user faces is the variable interest rate of the market. Additionally, there is a rather rare possibility that a user's loan was fully soft-liquidated, resulting in all their collateral being converted to the borrowable asset. In such a case, the user would be out of soft-liquidation because the price oracle is below the lowest band.
:::
*The AMM relies on two different prices:*
Soft- and de-liquidation of a loan only occurrs when the collateral price is within a band the user deposited liquidity into. The AMM creates an arbitrage opportunity by utilizing the following two prices:
- **`price_oracle`**: The collateral price fetched from a price oracle contract.
- **`get_p`**: The price of colalteral in the AMM itself.
When `price_oracle` equals `get_p`, the external oracle price and the AMM price are identical, indicating no need for arbitrage. When the external oracle price diverges, the AMM price `get_p` is adjusted to be more sensitive than the regular `price_oracle`, creating arbitrage opportunities. Essentially, arbitrage traders are incentivized to maintain `get_p = price_oracle` within the AMM.
### `get_p`
::::description[`AMM.get_p() -> uint256`]
Function to get the current collateral price within the AMM. `get_p` in always in the active band (`acitve_band`).
Returns: collateral price within the AMM (`uint256`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
@external
@view
@nonreentrant('lock')
def get_p() -> uint256:
"""
@notice Get current AMM price in active_band
@return Current price at 1e18 base
"""
n: int256 = self.active_band
return self._get_p(n, self.bands_x[n], self.bands_y[n])
@internal
@view
def _get_p(n: int256, x: uint256, y: uint256) -> uint256:
"""
@notice Get current AMM price in band
@param n Band number
@param x Amount of stablecoin in band
@param y Amount of collateral in band
@return Current price at 1e18 base
"""
p_o_up: uint256 = self._p_oracle_up(n)
p_o: uint256 = self._price_oracle_ro()[0]
assert p_o_up != 0
# Special cases
if x == 0:
if y == 0: # x and y are 0
# Return mid-band
return unsafe_div((unsafe_div(unsafe_div(p_o**2, p_o_up) * p_o, p_o_up) * A), Aminus1)
# if x == 0: # Lowest point of this band -> p_current_down
return unsafe_div(unsafe_div(p_o**2, p_o_up) * p_o, p_o_up)
if y == 0: # Highest point of this band -> p_current_up
p_o_up = unsafe_div(p_o_up * Aminus1, A) # now this is _actually_ p_o_down
return unsafe_div(p_o**2 / p_o_up * p_o, p_o_up)
y0: uint256 = self._get_y0(x, y, p_o, p_o_up)
# ^ that call also checks that p_o != 0
# (f(y0) + x) / (g(y0) + y)
f: uint256 = unsafe_div(A * y0 * p_o, p_o_up) * p_o
g: uint256 = unsafe_div(Aminus1 * y0 * p_o_up, p_o)
return (f + x * 10**18) / (g + y)
```
```shell
>>> AMM.get_p()
3215032751001233561432
```
::::
### `price_oracle`
::::description[`AMM.price_oracle() -> uint256: view`]
Getter for the collateral price according to an external price oracle contract. The address of the price oracle contract is stored in the `price_oracle_contract` variable.
Returns: collateral price according to external price oracle (`uint256`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
@external
@view
def price_oracle() -> uint256:
"""
@notice Value returned by the external price oracle contract
"""
return self._price_oracle_ro()[0]
@internal
@view
def _price_oracle_ro() -> uint256[2]:
return self.limit_p_o(price_oracle_contract.price())
@internal
@view
def limit_p_o(p: uint256) -> uint256[2]:
"""
@notice Limits oracle price to avoid losses at abrupt changes, as well as calculates a dynamic fee.
If we consider oracle_change such as:
ratio = p_new / p_old
(let's take for simplicity p_new < p_old, otherwise we compute p_old / p_new)
Then if the minimal AMM fee will be:
fee = (1 - ratio**3),
AMM will not have a loss associated with the price change.
However, over time fee should still go down (over PREV_P_O_DELAY), and also ratio should be limited
because we don't want the fee to become too large (say, 50%) which is achieved by limiting the instantaneous
change in oracle price.
@return (limited_price_oracle, dynamic_fee)
"""
p_new: uint256 = p
dt: uint256 = unsafe_sub(PREV_P_O_DELAY, min(PREV_P_O_DELAY, block.timestamp - self.prev_p_o_time))
ratio: uint256 = 0
# ratio = 1 - (p_o_min / p_o_max)**3
if dt > 0:
old_p_o: uint256 = self.old_p_o
old_ratio: uint256 = self.old_dfee
# ratio = p_o_min / p_o_max
if p > old_p_o:
ratio = unsafe_div(old_p_o * 10**18, p)
if ratio < 10**36 / MAX_P_O_CHG:
p_new = unsafe_div(old_p_o * MAX_P_O_CHG, 10**18)
ratio = 10**36 / MAX_P_O_CHG
else:
ratio = unsafe_div(p * 10**18, old_p_o)
if ratio < 10**36 / MAX_P_O_CHG:
p_new = unsafe_div(old_p_o * 10**18, MAX_P_O_CHG)
ratio = 10**36 / MAX_P_O_CHG
# ratio is lower than 1e18
# Also guaranteed to be limited, therefore can have all ops unsafe
ratio = min(
unsafe_div(
unsafe_mul(
unsafe_sub(unsafe_add(10**18, old_ratio), unsafe_div(pow_mod256(ratio, 3), 10**36)), # (f' + (1 - r**3))
dt), # * dt / T
PREV_P_O_DELAY),
10**18 - 1)
return [p_new, ratio]
```
```shell
>>> AMM.price_oracle()
3140087429510122285500
```
::::
### `get_base_price`
::::description[`AMM.get_base_price() -> uint256`]
Function to get the base price of the AMM which corresponds to band 0. The base price grows over time to account for the interest rate: `BASE_PRICE` (= the 'real' base price when the contract was deployed) is multiplied by `_rate_mul` to do so.
Returns: base price (`uint256`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
BASE_PRICE: immutable(uint256)
rate: public(uint256)
rate_time: uint256
rate_mul: uint256
@external
@view
def get_base_price() -> uint256:
"""
@notice Price which corresponds to band 0.
Base price grows with time to account for interest rate (which is 0 by default)
"""
return self._base_price()
@internal
@view
def _base_price() -> uint256:
"""
@notice Price which corresponds to band 0.
Base price grows with time to account for interest rate (which is 0 by default)
"""
return unsafe_div(BASE_PRICE * self._rate_mul(), 10**18)
@internal
@view
def _rate_mul() -> uint256:
"""
@notice Rate multiplier which is 1.0 + integral(rate, dt)
@return Rate multiplier in units where 1.0 == 1e18
"""
return unsafe_div(self.rate_mul * (10**18 + self.rate * (block.timestamp - self.rate_time)), 10**18)
```
```shell
>>> AMM.get_base_price()
2082598343438884801420
```
::::
### `p_current_up`
::::description[`AMM.p_current_up(n: int256) -> uint256`]
Getter for the highest possible price of the band at the current oracle price.
Returns: highest possible price (`uint256`).
| Input | Type | Description |
| ----- | ------- | ------------- |
| `n` | `int256`| Band Number. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
@external
@view
def p_current_up(n: int256) -> uint256:
"""
@notice Highest possible price of the band at current oracle price
@param n Band number (can be negative)
@return Price at 1e18 base
"""
return self._p_current_band(n + 1)
@internal
@view
def _p_current_band(n: int256) -> uint256:
"""
@notice Lowest possible price of the band at current oracle price
@param n Band number (can be negative)
@return Price at 1e18 base
"""
# k = (self.A - 1) / self.A # equal to (p_down / p_up)
# p_base = self.p_base * k **n = p_oracle_up(n)
p_base: uint256 = self._p_oracle_up(n)
# return self.p_oracle**3 / p_base**2
p_oracle: uint256 = self._price_oracle_ro()[0]
return unsafe_div(p_oracle**2 / p_base * p_oracle, p_base)
@internal
@view
def _p_oracle_up(n: int256) -> uint256:
"""
@notice Upper oracle price for the band to have liquidity when p = p_oracle
@param n Band number (can be negative)
@return Price at 1e18 base
"""
# p_oracle_up(n) = p_base * ((A - 1) / A) **n
# p_oracle_down(n) = p_base * ((A - 1) / A) **(n + 1) = p_oracle_up(n+1)
# return unsafe_div(self._base_price() * self.exp_int(-n * LOG_A_RATIO), 10**18)
power: int256 = -n * LOG_A_RATIO
# ((A - 1) / A) **n = exp(-n * A / (A - 1)) = exp(-n * LOG_A_RATIO)
## Exp implementation based on solmate's
assert power > -42139678854452767551
assert power < 135305999368893231589
x: int256 = unsafe_div(unsafe_mul(power, 2**96), 10**18)
k: int256 = unsafe_div(
unsafe_add(
unsafe_div(unsafe_mul(x, 2**96), 54916777467707473351141471128),
2**95),
2**96)
x = unsafe_sub(x, unsafe_mul(k, 54916777467707473351141471128))
y: int256 = unsafe_add(x, 1346386616545796478920950773328)
y = unsafe_add(unsafe_div(unsafe_mul(y, x), 2**96), 57155421227552351082224309758442)
p: int256 = unsafe_sub(unsafe_add(y, x), 94201549194550492254356042504812)
p = unsafe_add(unsafe_div(unsafe_mul(p, y), 2**96), 28719021644029726153956944680412240)
p = unsafe_add(unsafe_mul(p, x), (4385272521454847904659076985693276 * 2**96))
q: int256 = x - 2855989394907223263936484059900
q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 50020603652535783019961831881945)
q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 533845033583426703283633433725380)
q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 3604857256930695427073651918091429)
q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 14423608567350463180887372962807573)
q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 26449188498355588339934803723976023)
exp_result: uint256 = shift(
unsafe_mul(convert(unsafe_div(p, q), uint256), 3822833074963236453042738258902158003155416615667),
unsafe_sub(k, 195))
## End exp
return unsafe_div(self._base_price() * exp_result, 10**18)
```
```shell
>>> AMM.p_current_up(-40)
3260783573764672399539
```
::::
### `p_current_down`
::::description[`AMM.p_current_down(n: int256) -> uint256`]
Getter for the lowest possible price of the band at the current oracle price.
Returns: lowest price (`uint256`) of band `n`.
| Input | Type | Description |
| ----- | ------- | ------------- |
| `n` | `int256`| Band Number. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
@external
@view
def p_current_down(n: int256) -> uint256:
"""
@notice Lowest possible price of the band at current oracle price
@param n Band number (can be negative)
@return Price at 1e18 base
"""
return self._p_current_band(n)
@internal
@view
def _p_current_band(n: int256) -> uint256:
"""
@notice Lowest possible price of the band at current oracle price
@param n Band number (can be negative)
@return Price at 1e18 base
"""
# k = (self.A - 1) / self.A # equal to (p_down / p_up)
# p_base = self.p_base * k **n = p_oracle_up(n)
p_base: uint256 = self._p_oracle_up(n)
# return self.p_oracle**3 / p_base**2
p_oracle: uint256 = self._price_oracle_ro()[0]
return unsafe_div(p_oracle**2 / p_base * p_oracle, p_base)
```
```shell
>>> AMM.p_current_down(-40)
3260768088630089780416
```
::::
### `p_oracle_up`
::::description[`AMM.p_oracle_up(n: int256) -> uint256`]
Getter for the upper price bound of an individual band when `get_p` = `price_oracle`.
Returns: upper band price (`uint256`).
| Input | Type | Description |
| ----- | ------- | ------------ |
| `n` | `int256`| Band Number. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
@external
@view
def p_oracle_up(n: int256) -> uint256:
"""
@notice Highest oracle price for the band to have liquidity when p = p_oracle
@param n Band number (can be negative)
@return Price at 1e18 base
"""
return self._p_oracle_up(n)
@internal
@view
def _p_oracle_up(n: int256) -> uint256:
"""
@notice Upper oracle price for the band to have liquidity when p = p_oracle
@param n Band number (can be negative)
@return Price at 1e18 base
"""
# p_oracle_up(n) = p_base * ((A - 1) / A) **n
# p_oracle_down(n) = p_base * ((A - 1) / A) **(n + 1) = p_oracle_up(n+1)
# return unsafe_div(self._base_price() * self.exp_int(-n * LOG_A_RATIO), 10**18)
power: int256 = -n * LOG_A_RATIO
# ((A - 1) / A) **n = exp(-n * ln(A / (A - 1))) = exp(-n * LOG_A_RATIO)
## Exp implementation based on solmate's
assert power > -41446531673892821376
assert power < 135305999368893231589
x: int256 = unsafe_div(unsafe_mul(power, 2**96), 10**18)
k: int256 = unsafe_div(
unsafe_add(
unsafe_div(unsafe_mul(x, 2**96), 54916777467707473351141471128),
2**95),
2**96)
x = unsafe_sub(x, unsafe_mul(k, 54916777467707473351141471128))
y: int256 = unsafe_add(x, 1346386616545796478920950773328)
y = unsafe_add(unsafe_div(unsafe_mul(y, x), 2**96), 57155421227552351082224309758442)
p: int256 = unsafe_sub(unsafe_add(y, x), 94201549194550492254356042504812)
p = unsafe_add(unsafe_div(unsafe_mul(p, y), 2**96), 28719021644029726153956944680412240)
p = unsafe_add(unsafe_mul(p, x), (4385272521454847904659076985693276 * 2**96))
q: int256 = x - 2855989394907223263936484059900
q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 50020603652535783019961831881945)
q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 533845033583426703283633433725380)
q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 3604857256930695427073651918091429)
q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 14423608567350463180887372962807573)
q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 26449188498355588339934803723976023)
exp_result: uint256 = shift(
unsafe_mul(convert(unsafe_div(p, q), uint256), 3822833074963236453042738258902158003155416615667),
unsafe_sub(k, 195))
## End exp
assert exp_result > 1000 # dev: limit precision of the multiplier
return unsafe_div(self._base_price() * exp_result, 10**18)
```
```shell
>>> AMM.p_oracle_down(-40)
3113143435284666584035
```
::::
### `p_oracle_down`
::::description[`AMM.p_oracle_down(n: int256) -> uint256`]
Getter for the lower price bound of an individual band when `get_p` = `price_oracle`. This lower bound is calculated in the same way as `p_oracle_up` but for the next band, essentially taking the upper price of band `n + 1`. It calculates \( n + 1 \) because the lower bound of the current band is defined by the upper bound of the next band.
Returns: lower price bound (`uint256`).
| Input | Type | Description |
| ----- | ------- | ------------ |
| `n` | `int256`| Band Number. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
@external
@view
def p_oracle_down(n: int256) -> uint256:
"""
@notice Lowest oracle price for the band to have liquidity when p = p_oracle
@param n Band number (can be negative)
@return Price at 1e18 base
"""
return self._p_oracle_up(n + 1)
@internal
@view
def _p_oracle_up(n: int256) -> uint256:
"""
@notice Upper oracle price for the band to have liquidity when p = p_oracle
@param n Band number (can be negative)
@return Price at 1e18 base
"""
# p_oracle_up(n) = p_base * ((A - 1) / A) **n
# p_oracle_down(n) = p_base * ((A - 1) / A) **(n + 1) = p_oracle_up(n+1)
# return unsafe_div(self._base_price() * self.exp_int(-n * LOG_A_RATIO), 10**18)
power: int256 = -n * LOG_A_RATIO
# ((A - 1) / A) **n = exp(-n * A / (A - 1)) = exp(-n * LOG_A_RATIO)
## Exp implementation based on solmate's
assert power > -42139678854452767551
assert power < 135305999368893231589
x: int256 = unsafe_div(unsafe_mul(power, 2**96), 10**18)
k: int256 = unsafe_div(
unsafe_add(
unsafe_div(unsafe_mul(x, 2**96), 54916777467707473351141471128),
2**95),
2**96)
x = unsafe_sub(x, unsafe_mul(k, 54916777467707473351141471128))
y: int256 = unsafe_add(x, 1346386616545796478920950773328)
y = unsafe_add(unsafe_div(unsafe_mul(y, x), 2**96), 57155421227552351082224309758442)
p: int256 = unsafe_sub(unsafe_add(y, x), 94201549194550492254356042504812)
p = unsafe_add(unsafe_div(unsafe_mul(p, y), 2**96), 28719021644029726153956944680412240)
p = unsafe_add(unsafe_mul(p, x), (4385272521454847904659076985693276 * 2**96))
q: int256 = x - 2855989394907223263936484059900
q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 50020603652535783019961831881945)
q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 533845033583426703283633433725380)
q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 3604857256930695427073651918091429)
q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 14423608567350463180887372962807573)
q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 26449188498355588339934803723976023)
exp_result: uint256 = shift(
unsafe_mul(convert(unsafe_div(p, q), uint256), 3822833074963236453042738258902158003155416615667),
unsafe_sub(k, 195))
## End exp
return unsafe_div(self._base_price() * exp_result, 10**18)
```
```shell
>>> AMM.p_oracle_down(-40)
3082012397252988332539
```
::::
### `price_oracle_contract`
::::description[`AMM.price_oracle_contract() -> uint256: view`]
Getter for the price oracle contract which provides the external `price_oracle`.
Returns: oracle contract (`address`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
price_oracle_contract: public(PriceOracle)
@external
def __init__(
_borrowed_token: address,
_borrowed_precision: uint256,
_collateral_token: address,
_collateral_precision: uint256,
_A: uint256,
_sqrt_band_ratio: uint256,
_log_A_ratio: int256,
_base_price: uint256,
fee: uint256,
admin_fee: uint256,
_price_oracle_contract: address,
):
"""
@notice LLAMMA constructor
@param _borrowed_token Token which is being borrowed
@param _collateral_token Token used as collateral
@param _collateral_precision Precision of collateral: we pass it because we want the blueprint to fit into bytecode
@param _A "Amplification coefficient" which also defines density of liquidity and band size. Relative band size is 1/_A
@param _sqrt_band_ratio Precomputed int(sqrt(A / (A - 1)) * 1e18)
@param _log_A_ratio Precomputed int(ln(A / (A - 1)) * 1e18)
@param _base_price Typically the initial crypto price at which AMM is deployed. Will correspond to band 0
@param fee Relative fee of the AMM: int(fee * 1e18)
@param admin_fee Admin fee: how much of fee goes to admin. 50% === int(0.5 * 1e18)
@param _price_oracle_contract External price oracle which has price() and price_w() methods
which both return current price of collateral multiplied by 1e18
"""
...
self.price_oracle_contract = PriceOracle(_price_oracle_contract)
...
```
```shell
>>> AMM.price_oracle_contract()
'0x966cBDeceFB60A289b0460F7638f4A75F432cA06'
```
::::
---
## Fees and Interest Rates
**There are three different types of "fees" within the AMM:**
- **`fee`**: This is charged on token exchanges within the AMM.
- **`admin_fee`**: This determines the percentage of the total fees that are distributed to veCRV holders.
- **`rate`**: This represents the borrow rate a user pays on their loan.
The interest rate (`rate`) is updated whenever the `_save_rate()` method within the `Controller.vy` contract is called. This method is triggered under several circumstances:
- When a loan is created (`_create_loan`).
- When collateral is added or removed, or more debt is borrowed (`_add_collateral_borrow`).
- When debt is repaid (`repay` or `repay_extended`).
- When a hard liquidation is performed (`_liquidate`).
- When fees are collected (`collect_fees`).
### `fee`
::::description[`AMM.fee() -> uint256: view`]
Getter for the fee for exchanging tokens in the AMM. This fee is static and can only be changed by the DAO using the [`set_fee`](#set_fee) function. The fee is denominated to a base of $10^{18}$.
Returns: fee (`uint256`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
fee: public(uint256)
@external
def __init__(
_borrowed_token: address,
_borrowed_precision: uint256,
_collateral_token: address,
_collateral_precision: uint256,
_A: uint256,
_sqrt_band_ratio: uint256,
_log_A_ratio: int256,
_base_price: uint256,
fee: uint256,
admin_fee: uint256,
_price_oracle_contract: address,
):
"""
@notice LLAMMA constructor
@param _borrowed_token Token which is being borrowed
@param _collateral_token Token used as collateral
@param _collateral_precision Precision of collateral: we pass it because we want the blueprint to fit into bytecode
@param _A "Amplification coefficient" which also defines density of liquidity and band size. Relative band size is 1/_A
@param _sqrt_band_ratio Precomputed int(sqrt(A / (A - 1)) * 1e18)
@param _log_A_ratio Precomputed int(ln(A / (A - 1)) * 1e18)
@param _base_price Typically the initial crypto price at which AMM is deployed. Will correspond to band 0
@param fee Relative fee of the AMM: int(fee * 1e18)
@param admin_fee Admin fee: how much of fee goes to admin. 50% === int(0.5 * 1e18)
@param _price_oracle_contract External price oracle which has price() and price_w() methods
which both return current price of collateral multiplied by 1e18
"""
...
self.fee = fee
...
```
The fee value is denominated to a base of $10^{18}$. Therefore, `19000000000000000` corresponds to a fee of `1.9%`.
```shell
>>> AMM.fee()
19000000000000000
```
::::
### `set_fee`
::::description[`AMM.set_fee(fee: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the `Controller` contract.
:::
Function to set a new AMM exchange fee.
Emits: `SetFee`
| Input | Type | Description |
| ----- | --------- | ------------------ |
| `fee` | `uint256` | Fee (1e18 == 100%). |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
event SetFee:
fee: uint256
fee: public(uint256)
@external
@nonreentrant('lock')
def set_fee(fee: uint256):
"""
@notice Set AMM fee
@param fee Fee where 1e18 == 100%
"""
assert msg.sender == self.admin
self.fee = fee
log SetFee(fee)
```
```shell
>>> soon
```
::::
### `admin_fee`
::::description[`AMM.admin_fee() -> uint256: view`]
Getter for the admin fee of the AMM. This value represents the portion of the `fee` that is awarded to veCRV holders. Currently, the admin fees of the AMMs are set to 1 (1 / 1e18), making them virtually nonexistent. The reason for setting such a small value is to increase resistance to oracle manipulation. Essentially, taking no admin fee ensures that the accumulated fees are distributed among liquidity providers in the AMM (those who provide collateral), which helps offset the losses incurred through soft or de-liquidation and interest rates. Admin fees can be changed by the DAO via the `set_admin_fee` function.
Returns: admin fee (`uint256`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
admin_fee: public(uint256)
@external
def __init__(
_borrowed_token: address,
_borrowed_precision: uint256,
_collateral_token: address,
_collateral_precision: uint256,
_A: uint256,
_sqrt_band_ratio: uint256,
_log_A_ratio: int256,
_base_price: uint256,
fee: uint256,
admin_fee: uint256,
_price_oracle_contract: address,
):
"""
@notice LLAMMA constructor
@param _borrowed_token Token which is being borrowed
@param _collateral_token Token used as collateral
@param _collateral_precision Precision of collateral: we pass it because we want the blueprint to fit into bytecode
@param _A "Amplification coefficient" which also defines density of liquidity and band size. Relative band size is 1/_A
@param _sqrt_band_ratio Precomputed int(sqrt(A / (A - 1)) * 1e18)
@param _log_A_ratio Precomputed int(ln(A / (A - 1)) * 1e18)
@param _base_price Typically the initial crypto price at which AMM is deployed. Will correspond to band 0
@param fee Relative fee of the AMM: int(fee * 1e18)
@param admin_fee Admin fee: how much of fee goes to admin. 50% === int(0.5 * 1e18)
@param _price_oracle_contract External price oracle which has price() and price_w() methods
which both return current price of collateral multiplied by 1e18
"""
...
self.admin_fee = admin_fee
...
```
```shell
>>> AMM.admin_fee()
1
```
::::
### `admin_fees_x`
::::description[`AMM.admin_fees_x() -> uint256: view`]
Getter for the accured admin fees in form of the borrowed token since the last fee collection.
Returns: accured fees (`uint256`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
admin_fees_x: public(uint256)
```
```shell
>>> AMM.admin_fees_x()
632
```
::::
### `admin_fees_y`
::::description[`AMM.admin_fees_y() -> uint256: view`]
Getter for the accured admin fees in form of the collateral token since the last fee collection.
Returns: accured fees (`uint256`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
admin_fees_y: public(uint256)
```
```shell
>>> AMM.admin_fees_y()
0
```
::::
### `set_admin_fee`
::::description[`AMM.set_admin_fee(fee: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the `Controller` contract.
:::
Function to set a new admin fee value.
Emits: `SetAdminFee`
| Input | Type | Description |
| ----- | --------- | --------------------- |
| `fee` | `uint256` | Admin Fee (1e18 == 100%). |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
event SetAdminFee:
fee: uint256
admin_fee: public(uint256)
@external
@nonreentrant('lock')
def set_admin_fee(fee: uint256):
"""
@notice Set admin fee - fraction of the AMM fee to go to admin
@param fee Admin fee where 1e18 == 100%
"""
assert msg.sender == self.admin
self.admin_fee = fee
log SetAdminFee(fee)
```
```shell
>>> soon
```
::::
### `reset_admin_fees`
::::description[`AMM.reset_admin_fees()`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the Controller.
:::
Function to reset the accumulated admin fees (`admin_fees_x` and `admin_fees_y`) to zero. This function is automatically called when `collect_fees()` via the `Controller` contract is called.
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
@external
@nonreentrant('lock')
def reset_admin_fees():
"""
@notice Zero out AMM fees collected
"""
assert msg.sender == self.admin
self.admin_fees_x = 0
self.admin_fees_y = 0
```
The following source code includes all changes up to commit hash [58289a4](https://github.com/curvefi/curve-stablecoin/tree/58289a4283d7cc3c53aba2d3801dcac5ef124957); any changes made after this commit are not included.
```vyper
@external
@nonreentrant('lock')
def collect_fees() -> uint256:
"""
@notice Collect the fees charged as interest
"""
_to: address = FACTORY.fee_receiver()
# AMM-based fees
borrowed_fees: uint256 = AMM.admin_fees_x()
collateral_fees: uint256 = AMM.admin_fees_y()
if borrowed_fees > 0:
STABLECOIN.transferFrom(AMM.address, _to, borrowed_fees)
if collateral_fees > 0:
assert COLLATERAL_TOKEN.transferFrom(AMM.address, _to, collateral_fees, default_return_value=True)
AMM.reset_admin_fees()
# Borrowing-based fees
rate_mul: uint256 = self._rate_mul_w()
loan: Loan = self._total_debt
loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul
loan.rate_mul = rate_mul
self._total_debt = loan
# Amount which would have been redeemed if all the debt was repaid now
to_be_redeemed: uint256 = loan.initial_debt + self.redeemed
# Amount which was minted when borrowing + all previously claimed admin fees
minted: uint256 = self.minted
# Difference between to_be_redeemed and minted amount is exactly due to interest charged
if to_be_redeemed > minted:
self.minted = to_be_redeemed
to_be_redeemed = unsafe_sub(to_be_redeemed, minted) # Now this is the fees to charge
STABLECOIN.transfer(_to, to_be_redeemed)
log CollectFees(to_be_redeemed, loan.initial_debt)
return to_be_redeemed
else:
log CollectFees(0, loan.initial_debt)
return 0
```
```shell
>>> AMM.admin_fees_x()
327
>>> AMM.admin_fees_y()
0
>>> Controller.collect_fees() # this function calls `reset_admin_fees`
>>> AMM.admin_fees_x()
0
>>> AMM.admin_fees_y()
0
```
::::
### `rate`
::::description[`AMM.rate() -> uint256: view`]
Getter for the current interest rate per second. This rate is determined by the monetary policy contract and can depend on various factors within the contract.
Returns: interest rate (`uint256`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
rate: public(uint256)
```
This is the interest rate per second. The formula for calculating the annualized rate is the following:
```math
rate_{\text{annualized}} = \left(1 + \frac{\text{rate}}{10^{18}}\right)^{86400 \times 365} - 1
```
```shell
>>> AMM.rate()
5358112633 # annualized: 0.18789942609 ≈ 18.79%
```
::::
### `get_rate_mul`
::::description[`AMM.get_rate_mul() -> uint256: view`]
Getter for the interest rate multiplier, which is $1.0 + \int \text{rate}(t) \, dt$.
Returns: interest rate multiplier (`uint256`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
rate: public(uint256)
rate_time: uint256
rate_mul: uint256
@external
@view
def get_rate_mul() -> uint256:
"""
@notice Rate multiplier which is 1.0 + integral(rate, dt)
@return Rate multiplier in units where 1.0 == 1e18
"""
return self._rate_mul()
@internal
@view
def _rate_mul() -> uint256:
"""
@notice Rate multiplier which is 1.0 + integral(rate, dt)
@return Rate multiplier in units where 1.0 == 1e18
"""
return unsafe_div(self.rate_mul * (10**18 + self.rate * (block.timestamp - self.rate_time)), 10**18)
```
```shell
>>> AMM.get_rate_mul()
1100902413540693190
```
::::
### `set_rate`
::::description[`AMM.set_rate(rate: uint256) -> uint256:`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the `Controller` contract.
:::
Function to set the interest rate. The rate is always updated whenever the internal `_save_rate` function within the Controller contract is called (e.g., when a new loan is created or assets are repaid). The new rate is calculated in `get_rate_mul()`.
Returns: rate multiplier (`uint256`).
Emits: `SetRate`
| Input | Type | Description |
| ------ | --------- | ------------ |
| `rate` | `uint256` | New rate. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
event SetRate:
rate: uint256
rate_mul: uint256
time: uint256
@external
@nonreentrant('lock')
def set_rate(rate: uint256) -> uint256:
"""
@notice Set interest rate. That affects the dependence of AMM base price over time
@param rate New rate in units of int(fraction * 1e18) per second
@return rate_mul multiplier (e.g. 1.0 + integral(rate, dt))
"""
assert msg.sender == self.admin
rate_mul: uint256 = self._rate_mul()
self.rate_mul = rate_mul
self.rate_time = block.timestamp
self.rate = rate
log SetRate(rate, rate_mul, block.timestamp)
return rate_mul
@internal
@view
def _rate_mul() -> uint256:
"""
@notice Rate multiplier which is 1.0 + integral(rate, dt)
@return Rate multiplier in units where 1.0 == 1e18
"""
return unsafe_div(self.rate_mul * (10**18 + self.rate * (block.timestamp - self.rate_time)), 10**18)
```
```shell
>>> soon
```
::::
---
## Admin Ownership
The `admin` of each AMM is usually set to the corresponding `Controller` contract of the according market. This variable can only be set once and not updated agian as the `set_admin` function checks the follwoing: `assert self.admin == empty(address)`.
The admin can only be set once, which is done when deploying the AMM. Therefore, the `admin` cannot be changed.
### `admin`
::::description[`AMM.admin() -> address: view`]
Getter for the admin of the contract, which is the corresponding Controller.
Returns: admin (`address`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
admin: public(address)
```
```shell
>>> AMM.admin()
'0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635'
```
::::
### `set_admin`
::::description[`AMM.set_admin(_admin: address):`]
:::guard[Guarded Method]
This function is only callable when `admin` is set to `ZERO_ADDRESS`. This condition was met at deployment, but after setting the admin for the first time, it cannot be changed. Admin for the AMM is always the corresponding Controller.
:::
Function to set the admin of the AMM. Maximum approval is given to the Controller in order for it to effectively call functions such as `deposit_range` and `withdraw`. This is achieved through an extra `approve_max` function, because it consumes less byte space compared to calling it directly.
| Input | Type | Description |
| -------- | --------- | ---------------- |
| `_admin` | `address` | Admin address. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
@external
def set_admin(_admin: address):
"""
@notice Set admin of the AMM. Typically it's a controller (unless it's tests)
@param _admin Admin address
"""
assert self.admin == empty(address)
self.admin = _admin
self.approve_max(BORROWED_TOKEN, _admin)
self.approve_max(COLLATERAL_TOKEN, _admin)
@internal
def approve_max(token: ERC20, _admin: address):
"""
Approve max in a separate function because it uses less bytespace than
calling directly, and gas doesn't matter in set_admin
"""
assert token.approve(_admin, max_value(uint256), default_return_value=True)
```
```shell
>>> soon
```
::::
---
## Contract Info Methods
### `coins`
::::description[`AMM.coins(i: uint256) -> address`]
Getter for the coins in the AMM, with `i = 0` as the borrowed token and `i = 1` as the collateral token.
Returns: coin (`address`).
| Input | Type | Description |
| ----- | --------- | ------------- |
| `i` | `uint256` | Coin Index. |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
BORROWED_TOKEN: immutable(ERC20) # x
COLLATERAL_TOKEN: immutable(ERC20) # y
@external
@pure
def coins(i: uint256) -> address:
return [BORROWED_TOKEN.address, COLLATERAL_TOKEN.address][i]
```
```shell
>>> AMM.coins(0)
'0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'
>>> AMM.coins(1)
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
```
::::
### `liquidity_mining_callback`
::::description[`AMM.liquidity_mining_callback() -> address: view`]
Getter for the liquidity mining callback address.
Returns: liquidity mining callback contract (`address`).
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
liquidity_mining_callback: public(LMGauge)
```
```shell
>>> AMM.liquidity_mining_callback()
0x0000000000000000000000000000000000000000
```
::::
### `set_callback`
::::description[`AMM.set_callback(liquidity_mining_callback: LMGauge):`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the Controller.
:::
Function to set the liquidity mining callback.
| Input | Type | Description |
| ----------- | -------| ----|
| `liquidity_mining_callback` | `LMGauge` | Liquidity Mining Gauge Address |
The following source code includes all changes up to commit hash [afc2608](https://github.com/curvefi/curve-stablecoin/tree/afc26087ab558d33a94d037c88579d9dfc52396f); any changes made after this commit are not included.
```vyper
interface LMGauge:
def callback_collateral_shares(n: int256, collateral_per_share: DynArray[uint256, MAX_TICKS_UINT]): nonpayable
def callback_user_shares(user: address, n: int256, user_shares: DynArray[uint256, MAX_TICKS_UINT]): nonpayable
liquidity_mining_callback: public(LMGauge)
# nonreentrant decorator is in Controller which is admin
@external
def set_callback(liquidity_mining_callback: LMGauge):
"""
@notice Set a gauge address with callbacks for liquidity mining for collateral
@param liquidity_mining_callback Gauge address
"""
assert msg.sender == self.admin
self.liquidity_mining_callback = liquidity_mining_callback
```
```shell
>>> soon
```
::::
---
## Controller
The Controller contract acts as a on-chain interface for **creating loans and further managing existing positions**. It holds all user debt information. External liquidations are also done through it.
**Each market has its own Controller**, automatically deployed from a blueprint contract, as soon as a new market is added via the `add_market` function or, for lending markets, via the `create` or `create_from_pool` function within the respective Factory.
:::vyper[`Controller.vy`]
Each market deploys its own Controller from a blueprint contract. Source code is available on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/Controller.vy). Relevant deployments can be found [here](../deployments.md).
:::
:::info[Contract Versions]
This page documents **Controller V3** (Vyper 0.3.10), the current blueprint used for new crvUSD mint markets and all Llamalend markets. Where the API differs from earlier versions, both implementations are shown in tabs.
| Version | Vyper | Markets |
|---------|-------|---------|
| V1 | 0.3.7 | sfrxETH (v1), wstETH, WBTC, WETH |
| V2 | 0.3.9 | sfrxETH (v2), tBTC |
| **V3** | **0.3.10** | **weETH, cbBTC, LBTC + all Llamalend** |
For full version details, see the [crvUSD Overview](./overview.md#controller--amm-versions).
:::
---
*Controller contracts are currently used for the following two cases:*
Minting crvUSD is only possible with whitelisted collateral approved by the DAO. Users provide collateral, which is deposited into LLAMMA according to the number of bands chosen. crvUSD is **backed by the assets provided as collateral**. The system does not actually mint crvUSD — tokens are "pre-minted" to the Controller from which they can be borrowed.
In lending markets, any token composition is possible as long as one of the assets is crvUSD. Unlike the minting system, borrowed assets are **not minted but provided by lenders** who deposit into an [ERC-4626 Vault](../lending/contracts/vault.md), where they earn interest.
---
## Creating and Repaying Loans
New loans are created via the **`create_loan`** function. When creating a loan the user needs to specify the **amount of collateral**, **debt** and the **number of bands** to deposit the collateral into.
The maximum amount of borrowable debt is determined by the number of bands, the amount of collateral, and the oracle price.
The loan-to-value (LTV) ratio depends on the number of bands `N` and the parameter `A`. The higher the number of bands, the lower the LTV. More on bands [here](../crvusd/amm.md#bands).
$$LTV = 100\% - \text{loan\_discount} - 100 \cdot \frac{N}{2 \cdot A}$$
:::info[V3 Only — Extra Health Buffer]
Controller V3 allows users to set [`extra_health`](#extra_health) via [`set_extra_health`](#set_extra_health) before creating a loan. This adds a health buffer when entering soft liquidation. Available in weETH, cbBTC, LBTC markets and all Llamalend markets.
:::
:::colab[Google Colab Notebook]
A simple notebook showcasing how to create a loan using `create_loan` and then how to read loan information can be found here: [https://colab.research.google.com/drive/1MTtpbdeTDVB3LxzGhFc4vwLsDM_xJWKz?usp=sharing](https://colab.research.google.com/drive/1MTtpbdeTDVB3LxzGhFc4vwLsDM_xJWKz?usp=sharing)
:::
### `create_loan`
::::description[`Controller.create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = msg.sender)`]
Function to create a new loan, requiring specification of the amount of `collateral` to be deposited into `N` bands and the amount of `debt` to be borrowed. The lower bands choosen, the higher the loss when the position is in soft-liquiation. Should there already be an existing loan, the function will revert. Before creating a loan, there is the option to set [`extra_health`](#extra_health) using [`set_extra_health`](#set_extra_health) which leads to a higher health when entering soft liquidation.
| Input | Type | Description |
| ------------ | --------- | ------------------------------------- |
| `collateral` | `uint256` | Amount of collateral token to put up as collateral (at its native precision) |
| `debt` | `uint256` | Amount of debt to take on |
| `N` | `uint256` | Number of bands to deposit into; must range between `MIN_TICKS` and `MAX_TICKS` |
| `_for` | `address` | Address to create the loan for (requires [`approval`](#approve)); **V3 only** |
Emits: `UserState`, `Borrow`, `Deposit` and `Transfer`
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Borrow:
user: indexed(address)
collateral_increase: uint256
loan_increase: uint256
MAX_TICKS: constant(int256) = 50
MIN_TICKS: constant(int256) = 4
approval: public(HashMap[address, HashMap[address, bool]])
@external
@nonreentrant('lock')
def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = msg.sender):
"""
@notice Create loan
@param collateral Amount of collateral to use
@param debt Stablecoin debt to take
@param N Number of bands to deposit into (to do autoliquidation-deliquidation),
can be from MIN_TICKS to MAX_TICKS
@param _for Address to create the loan for
"""
if _for != tx.origin:
# We can create a loan for tx.origin (for example when wrapping ETH with EOA),
# however need to approve in other cases
assert self._check_approval(_for)
self._create_loan(collateral, debt, N, True, _for)
@internal
@view
def _check_approval(_for: address) -> bool:
return msg.sender == _for or self.approval[_for][msg.sender]
@internal
def _create_loan(collateral: uint256, debt: uint256, N: uint256, transfer_coins: bool, _for: address):
assert self.loan[_for].initial_debt == 0, "Loan already created"
assert N > MIN_TICKS-1, "Need more ticks"
assert N < MAX_TICKS+1, "Need less ticks"
n1: int256 = self._calculate_debt_n1(collateral, debt, N, _for)
n2: int256 = n1 + convert(unsafe_sub(N, 1), int256)
rate_mul: uint256 = AMM.get_rate_mul()
self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul})
liquidation_discount: uint256 = self.liquidation_discount
self.liquidation_discounts[_for] = liquidation_discount
n_loans: uint256 = self.n_loans
self.loans[n_loans] = _for
self.loan_ix[_for] = n_loans
self.n_loans = unsafe_add(n_loans, 1)
self._total_debt.initial_debt = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + debt
self._total_debt.rate_mul = rate_mul
AMM.deposit_range(_for, collateral, n1, n2)
self.minted += debt
if transfer_coins:
self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral)
self.transfer(BORROWED_TOKEN, _for, debt)
self._save_rate()
log UserState(_for, collateral, debt, n1, n2, liquidation_discount)
log Borrow(_for, collateral, debt)
```
```vyper
event Deposit:
provider: indexed(address)
amount: uint256
n1: int256
n2: int256
@external
@nonreentrant('lock')
def deposit_range(user: address, amount: uint256, n1: int256, n2: int256):
"""
@notice Deposit for a user in a range of bands. Only admin contract (Controller) can do it
@param user User address
@param amount Amount of collateral to deposit
@param n1 Lower band in the deposit range
@param n2 Upper band in the deposit range
"""
assert msg.sender == self.admin
user_shares: DynArray[uint256, MAX_TICKS_UINT] = []
collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = []
n0: int256 = self.active_band
# We assume that n1,n2 area already sorted (and they are in Controller)
assert n2 < 2**127
assert n1 > -2**127
n_bands: uint256 = unsafe_add(convert(unsafe_sub(n2, n1), uint256), 1)
assert n_bands <= MAX_TICKS_UINT
y_per_band: uint256 = unsafe_div(amount * COLLATERAL_PRECISION, n_bands)
assert y_per_band > 100, "Amount too low"
assert self.user_shares[user].ticks[0] == 0 # dev: User must have no liquidity
self.user_shares[user].ns = unsafe_add(n1, unsafe_mul(n2, 2**128))
lm: LMGauge = self.liquidity_mining_callback
# Autoskip bands if we can
for i in range(MAX_SKIP_TICKS + 1):
if n1 > n0:
if i != 0:
self.active_band = n0
break
assert self.bands_x[n0] == 0 and i < MAX_SKIP_TICKS, "Deposit below current band"
n0 -= 1
for i in range(MAX_TICKS):
band: int256 = unsafe_add(n1, i)
if band > n2:
break
assert self.bands_x[band] == 0, "Band not empty"
y: uint256 = y_per_band
if i == 0:
y = amount * COLLATERAL_PRECISION - y * unsafe_sub(n_bands, 1)
total_y: uint256 = self.bands_y[band]
# Total / user share
s: uint256 = self.total_shares[band]
ds: uint256 = unsafe_div((s + DEAD_SHARES) * y, total_y + 1)
assert ds > 0, "Amount too low"
user_shares.append(ds)
s += ds
assert s <= 2**128 - 1
self.total_shares[band] = s
total_y += y
self.bands_y[band] = total_y
if lm.address != empty(address):
# If initial s == 0 - s becomes equal to y which is > 100 => nonzero
collateral_shares.append(unsafe_div(total_y * 10**18, s))
self.min_band = min(self.min_band, n1)
self.max_band = max(self.max_band, n2)
self.save_user_shares(user, user_shares)
log Deposit(user, amount, n1, n2)
if lm.address != empty(address):
lm.callback_collateral_shares(n1, collateral_shares)
lm.callback_user_shares(user, n1, user_shares)
```
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Borrow:
user: indexed(address)
collateral_increase: uint256
loan_increase: uint256
MAX_TICKS: constant(int256) = 50
MIN_TICKS: constant(int256) = 4
@external
@nonreentrant('lock')
def create_loan(collateral: uint256, debt: uint256, N: uint256):
"""
@notice Create loan
@param collateral Amount of collateral to use
@param debt Stablecoin debt to take
@param N Number of bands to deposit into (to do autoliquidation-deliquidation),
can be from MIN_TICKS to MAX_TICKS
"""
self._create_loan(collateral, debt, N, True)
@internal
def _create_loan(collateral: uint256, debt: uint256, N: uint256, transfer_coins: bool):
assert self.loan[msg.sender].initial_debt == 0, "Loan already created"
assert N > MIN_TICKS-1, "Need more ticks"
assert N < MAX_TICKS+1, "Need less ticks"
n1: int256 = self._calculate_debt_n1(collateral, debt, N)
n2: int256 = n1 + convert(N - 1, int256)
rate_mul: uint256 = AMM.get_rate_mul()
self.loan[msg.sender] = Loan({initial_debt: debt, rate_mul: rate_mul})
liquidation_discount: uint256 = self.liquidation_discount
self.liquidation_discounts[msg.sender] = liquidation_discount
n_loans: uint256 = self.n_loans
self.loans[n_loans] = msg.sender
self.loan_ix[msg.sender] = n_loans
self.n_loans = unsafe_add(n_loans, 1)
self._total_debt.initial_debt = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + debt
self._total_debt.rate_mul = rate_mul
AMM.deposit_range(msg.sender, collateral, n1, n2)
self.minted += debt
if transfer_coins:
self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral)
self.transfer(BORROWED_TOKEN, msg.sender, debt)
self._save_rate()
log UserState(msg.sender, collateral, debt, n1, n2, liquidation_discount)
log Borrow(msg.sender, collateral, debt)
```
```vyper
event Deposit:
provider: indexed(address)
amount: uint256
n1: int256
n2: int256
@external
@nonreentrant('lock')
def deposit_range(user: address, amount: uint256, n1: int256, n2: int256):
"""
@notice Deposit for a user in a range of bands. Only admin contract (Controller) can do it
@param user User address
@param amount Amount of collateral to deposit
@param n1 Lower band in the deposit range
@param n2 Upper band in the deposit range
"""
assert msg.sender == self.admin
user_shares: DynArray[uint256, MAX_TICKS_UINT] = []
collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = []
n0: int256 = self.active_band
# We assume that n1,n2 area already sorted (and they are in Controller)
assert n2 < 2**127
assert n1 > -2**127
n_bands: uint256 = unsafe_add(convert(unsafe_sub(n2, n1), uint256), 1)
assert n_bands <= MAX_TICKS_UINT
y_per_band: uint256 = unsafe_div(amount * COLLATERAL_PRECISION, n_bands)
assert y_per_band > 100, "Amount too low"
assert self.user_shares[user].ticks[0] == 0 # dev: User must have no liquidity
self.user_shares[user].ns = unsafe_add(n1, unsafe_mul(n2, 2**128))
lm: LMGauge = self.liquidity_mining_callback
# Autoskip bands if we can
for i in range(MAX_SKIP_TICKS + 1):
if n1 > n0:
if i != 0:
self.active_band = n0
break
assert self.bands_x[n0] == 0 and i < MAX_SKIP_TICKS, "Deposit below current band"
n0 -= 1
for i in range(MAX_TICKS):
band: int256 = unsafe_add(n1, i)
if band > n2:
break
assert self.bands_x[band] == 0, "Band not empty"
y: uint256 = y_per_band
if i == 0:
y = amount * COLLATERAL_PRECISION - y * unsafe_sub(n_bands, 1)
total_y: uint256 = self.bands_y[band]
# Total / user share
s: uint256 = self.total_shares[band]
ds: uint256 = unsafe_div((s + DEAD_SHARES) * y, total_y + 1)
assert ds > 0, "Amount too low"
user_shares.append(ds)
s += ds
assert s <= 2**128 - 1
self.total_shares[band] = s
total_y += y
self.bands_y[band] = total_y
if lm.address != empty(address):
# If initial s == 0 - s becomes equal to y which is > 100 => nonzero
collateral_shares.append(unsafe_div(total_y * 10**18, s))
self.min_band = min(self.min_band, n1)
self.max_band = max(self.max_band, n2)
self.save_user_shares(user, user_shares)
log Deposit(user, amount, n1, n2)
if lm.address != empty(address):
lm.callback_collateral_shares(n1, collateral_shares)
lm.callback_user_shares(user, n1, user_shares)
```
```shell
>>> Controller.create_loan(10**18, 10**21, 10)
>>> Controller.debt(trader)
1000000000000000000000
>>> Controller.user_state(trader)
[1000000000000000000, 0, 1000000000000000000000, 10]
# [collateral, stablecoin, debt, bands]
```
::::
### `create_loan_extended`
::::description[`Controller.create_loan_extended(collateral: uint256, debt: uint256, N: uint256, callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b"")`]
Function to create a new loan using callbacks. This function passes the stablecoin to a callback first, enabling the construction of leverage. Earlier implementations of the contract did not have `callback_bytes` argument. This was added to enable [leveraging/de-leveraging using the 1inch router](./leverage/leverage-zap-1inch.md#building-leverage). Before creating a loan, there is the option to set [`extra_health`](#extra_health) using [`set_extra_health`](#set_extra_health) which leads to a higher health when entering soft liquidation.
| Input | Type | Description |
| --------------- | --------------------- | ----------------------------------------------------- |
| `collateral` | `uint256` | Amount of collateral token to put up as collateral (at its native precision). |
| `debt` | `uint256` | Amount of debt to take |
| `N` | `uint256` | Number of bands to deposit into |
| `callbacker` | `address` | Address of the callback contract |
| `callback_args` | `DynArray[uint256,5]` | Extra arguments for the callback (up to 5), such as `min_amount`, etc. See [`LeverageZap1inch.vy`](./leverage/leverage-zap-1inch.md) for more information |
| `callback_bytes` | `Bytes[10**4]` | Callback bytes passed to the LeverageZap. Defaults to `b""` |
| `_for` | `address` | Address to create the loan for (requires [`approval`](#approve)); **V3 only** |
Emits: `UserState`, `Borrow`, `Deposit`, and `Transfer`
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Borrow:
user: indexed(address)
collateral_increase: uint256
loan_increase: uint256
MAX_TICKS: constant(int256) = 50
MIN_TICKS: constant(int256) = 4
CALLBACK_DEPOSIT: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_REPAY: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_LIQUIDATE: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_DEPOSIT_WITH_BYTES: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
# CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4) <-- BUG! The reason is 0 at the beginning of method_id
CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = 0x008ae188
CALLBACK_LIQUIDATE_WITH_BYTES: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
approval: public(HashMap[address, HashMap[address, bool]])
@external
@nonreentrant('lock')
def create_loan_extended(collateral: uint256, debt: uint256, N: uint256, callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b"", _for: address = msg.sender):
"""
@notice Create loan but pass stablecoin to a callback first so that it can build leverage
@param collateral Amount of collateral to use
@param debt Stablecoin debt to take
@param N Number of bands to deposit into (to do autoliquidation-deliquidation),
can be from MIN_TICKS to MAX_TICKS
@param callbacker Address of the callback contract
@param callback_args Extra arguments for the callback (up to 5) such as min_amount etc
@param _for Address to create the loan for
"""
if _for != tx.origin:
assert self._check_approval(_for)
# Before callback
self.transfer(BORROWED_TOKEN, callbacker, debt)
# For compatibility
callback_sig: bytes4 = CALLBACK_DEPOSIT_WITH_BYTES
if callback_bytes == b"":
callback_sig = CALLBACK_DEPOSIT
# Callback
# If there is any unused debt, callbacker can send it to the user
more_collateral: uint256 = self.execute_callback(
callbacker, callback_sig, _for, 0, collateral, debt, callback_args, callback_bytes).collateral
# After callback
self._create_loan(collateral + more_collateral, debt, N, False, _for)
self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral)
self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral)
@internal
@view
def _check_approval(_for: address) -> bool:
return msg.sender == _for or self.approval[_for][msg.sender]
@internal
def _create_loan(collateral: uint256, debt: uint256, N: uint256, transfer_coins: bool, _for: address):
assert self.loan[_for].initial_debt == 0, "Loan already created"
assert N > MIN_TICKS-1, "Need more ticks"
assert N < MAX_TICKS+1, "Need less ticks"
n1: int256 = self._calculate_debt_n1(collateral, debt, N, _for)
n2: int256 = n1 + convert(unsafe_sub(N, 1), int256)
rate_mul: uint256 = AMM.get_rate_mul()
self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul})
liquidation_discount: uint256 = self.liquidation_discount
self.liquidation_discounts[_for] = liquidation_discount
n_loans: uint256 = self.n_loans
self.loans[n_loans] = _for
self.loan_ix[_for] = n_loans
self.n_loans = unsafe_add(n_loans, 1)
self._total_debt.initial_debt = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + debt
self._total_debt.rate_mul = rate_mul
AMM.deposit_range(_for, collateral, n1, n2)
self.minted += debt
if transfer_coins:
self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral)
self.transfer(BORROWED_TOKEN, _for, debt)
self._save_rate()
log UserState(_for, collateral, debt, n1, n2, liquidation_discount)
log Borrow(_for, collateral, debt)
@internal
def execute_callback(callbacker: address, callback_sig: bytes4,
user: address, stablecoins: uint256, collateral: uint256, debt: uint256,
callback_args: DynArray[uint256, 5], callback_bytes: Bytes[10**4]) -> CallbackData:
assert callbacker != COLLATERAL_TOKEN.address
assert callbacker != BORROWED_TOKEN.address
data: CallbackData = empty(CallbackData)
data.active_band = AMM.active_band()
band_x: uint256 = AMM.bands_x(data.active_band)
band_y: uint256 = AMM.bands_y(data.active_band)
# Callback
response: Bytes[64] = raw_call(
callbacker,
concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)),
max_outsize=64
)
data.stablecoins = convert(slice(response, 0, 32), uint256)
data.collateral = convert(slice(response, 32, 32), uint256)
# Checks after callback
assert data.active_band == AMM.active_band()
assert band_x == AMM.bands_x(data.active_band)
assert band_y == AMM.bands_y(data.active_band)
```
```vyper
event Deposit:
provider: indexed(address)
amount: uint256
n1: int256
n2: int256
@external
@nonreentrant('lock')
def deposit_range(user: address, amount: uint256, n1: int256, n2: int256):
"""
@notice Deposit for a user in a range of bands. Only admin contract (Controller) can do it
@param user User address
@param amount Amount of collateral to deposit
@param n1 Lower band in the deposit range
@param n2 Upper band in the deposit range
"""
assert msg.sender == self.admin
user_shares: DynArray[uint256, MAX_TICKS_UINT] = []
collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = []
n0: int256 = self.active_band
# We assume that n1,n2 area already sorted (and they are in Controller)
assert n2 < 2**127
assert n1 > -2**127
n_bands: uint256 = unsafe_add(convert(unsafe_sub(n2, n1), uint256), 1)
assert n_bands <= MAX_TICKS_UINT
y_per_band: uint256 = unsafe_div(amount * COLLATERAL_PRECISION, n_bands)
assert y_per_band > 100, "Amount too low"
assert self.user_shares[user].ticks[0] == 0 # dev: User must have no liquidity
self.user_shares[user].ns = unsafe_add(n1, unsafe_mul(n2, 2**128))
lm: LMGauge = self.liquidity_mining_callback
# Autoskip bands if we can
for i in range(MAX_SKIP_TICKS + 1):
if n1 > n0:
if i != 0:
self.active_band = n0
break
assert self.bands_x[n0] == 0 and i < MAX_SKIP_TICKS, "Deposit below current band"
n0 -= 1
for i in range(MAX_TICKS):
band: int256 = unsafe_add(n1, i)
if band > n2:
break
assert self.bands_x[band] == 0, "Band not empty"
y: uint256 = y_per_band
if i == 0:
y = amount * COLLATERAL_PRECISION - y * unsafe_sub(n_bands, 1)
total_y: uint256 = self.bands_y[band]
# Total / user share
s: uint256 = self.total_shares[band]
ds: uint256 = unsafe_div((s + DEAD_SHARES) * y, total_y + 1)
assert ds > 0, "Amount too low"
user_shares.append(ds)
s += ds
assert s <= 2**128 - 1
self.total_shares[band] = s
total_y += y
self.bands_y[band] = total_y
if lm.address != empty(address):
# If initial s == 0 - s becomes equal to y which is > 100 => nonzero
collateral_shares.append(unsafe_div(total_y * 10**18, s))
self.min_band = min(self.min_band, n1)
self.max_band = max(self.max_band, n2)
self.save_user_shares(user, user_shares)
log Deposit(user, amount, n1, n2)
if lm.address != empty(address):
lm.callback_collateral_shares(n1, collateral_shares)
lm.callback_user_shares(user, n1, user_shares)
```
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Borrow:
user: indexed(address)
collateral_increase: uint256
loan_increase: uint256
MAX_TICKS: constant(int256) = 50
MIN_TICKS: constant(int256) = 4
CALLBACK_DEPOSIT: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_REPAY: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_LIQUIDATE: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_DEPOSIT_WITH_BYTES: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
# CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4) <-- BUG! The reason is 0 at the beginning of method_id
CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = 0x008ae188
CALLBACK_LIQUIDATE_WITH_BYTES: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
@external
@nonreentrant('lock')
def create_loan_extended(collateral: uint256, debt: uint256, N: uint256, callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b""):
"""
@notice Create loan but pass stablecoin to a callback first so that it can build leverage
@param collateral Amount of collateral to use
@param debt Stablecoin debt to take
@param N Number of bands to deposit into (to do autoliquidation-deliquidation),
can be from MIN_TICKS to MAX_TICKS
@param callbacker Address of the callback contract
@param callback_args Extra arguments for the callback (up to 5) such as min_amount etc
"""
# Before callback
self.transfer(BORROWED_TOKEN, callbacker, debt)
# For compatibility
callback_sig: bytes4 = CALLBACK_DEPOSIT_WITH_BYTES
if callback_bytes == b"":
callback_sig = CALLBACK_DEPOSIT
# Callback
# If there is any unused debt, callbacker can send it to the user
more_collateral: uint256 = self.execute_callback(
callbacker, callback_sig, msg.sender, 0, collateral, debt, callback_args, callback_bytes).collateral
# After callback
self._create_loan(collateral + more_collateral, debt, N, False)
self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral)
self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral)
@internal
def execute_callback(callbacker: address, callback_sig: bytes4,
user: address, stablecoins: uint256, collateral: uint256, debt: uint256,
callback_args: DynArray[uint256, 5], callback_bytes: Bytes[10**4]) -> CallbackData:
assert callbacker != COLLATERAL_TOKEN.address
data: CallbackData = empty(CallbackData)
data.active_band = AMM.active_band()
band_x: uint256 = AMM.bands_x(data.active_band)
band_y: uint256 = AMM.bands_y(data.active_band)
# Callback
response: Bytes[64] = raw_call(
callbacker,
concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)),
max_outsize=64
)
data.stablecoins = convert(slice(response, 0, 32), uint256)
data.collateral = convert(slice(response, 32, 32), uint256)
# Checks after callback
assert data.active_band == AMM.active_band()
assert band_x == AMM.bands_x(data.active_band)
assert band_y == AMM.bands_y(data.active_band)
return data
```
```vyper
event Deposit:
provider: indexed(address)
amount: uint256
n1: int256
n2: int256
@external
@nonreentrant('lock')
def deposit_range(user: address, amount: uint256, n1: int256, n2: int256):
"""
@notice Deposit for a user in a range of bands. Only admin contract (Controller) can do it
@param user User address
@param amount Amount of collateral to deposit
@param n1 Lower band in the deposit range
@param n2 Upper band in the deposit range
"""
assert msg.sender == self.admin
user_shares: DynArray[uint256, MAX_TICKS_UINT] = []
collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = []
n0: int256 = self.active_band
# We assume that n1,n2 area already sorted (and they are in Controller)
assert n2 < 2**127
assert n1 > -2**127
n_bands: uint256 = unsafe_add(convert(unsafe_sub(n2, n1), uint256), 1)
assert n_bands <= MAX_TICKS_UINT
y_per_band: uint256 = unsafe_div(amount * COLLATERAL_PRECISION, n_bands)
assert y_per_band > 100, "Amount too low"
assert self.user_shares[user].ticks[0] == 0 # dev: User must have no liquidity
self.user_shares[user].ns = unsafe_add(n1, unsafe_mul(n2, 2**128))
lm: LMGauge = self.liquidity_mining_callback
# Autoskip bands if we can
for i in range(MAX_SKIP_TICKS + 1):
if n1 > n0:
if i != 0:
self.active_band = n0
break
assert self.bands_x[n0] == 0 and i < MAX_SKIP_TICKS, "Deposit below current band"
n0 -= 1
for i in range(MAX_TICKS):
band: int256 = unsafe_add(n1, i)
if band > n2:
break
assert self.bands_x[band] == 0, "Band not empty"
y: uint256 = y_per_band
if i == 0:
y = amount * COLLATERAL_PRECISION - y * unsafe_sub(n_bands, 1)
total_y: uint256 = self.bands_y[band]
# Total / user share
s: uint256 = self.total_shares[band]
ds: uint256 = unsafe_div((s + DEAD_SHARES) * y, total_y + 1)
assert ds > 0, "Amount too low"
user_shares.append(ds)
s += ds
assert s <= 2**128 - 1
self.total_shares[band] = s
total_y += y
self.bands_y[band] = total_y
if lm.address != empty(address):
# If initial s == 0 - s becomes equal to y which is > 100 => nonzero
collateral_shares.append(unsafe_div(total_y * 10**18, s))
self.min_band = min(self.min_band, n1)
self.max_band = max(self.max_band, n2)
self.save_user_shares(user, user_shares)
log Deposit(user, amount, n1, n2)
if lm.address != empty(address):
lm.callback_collateral_shares(n1, collateral_shares)
lm.callback_user_shares(user, n1, user_shares)
```
```shell
>>> Controller.create_loan_extended(collateral: uint256, debt: uint256, N: uint256, callbacker: address, callback_args: DynArray[uint256,5])
```
::::
### `extra_health`
::::description[`Controller.extra_health(arg0: address) -> uint256: view`]
:::info[V3 Only]
Introduced in Controller V3 (Vyper 0.3.10). Available in weETH, cbBTC, LBTC markets and all Llamalend markets.
:::
Getter for the extra health value for `arg0`. When setting extra health before creating a loan, a "health buffer" is added, which results in entering soft liquidation with more health. The `health` value when entering SL can be checked by using the [`health`](#health) function with using `bool = False`.
```vyper
extra_health: public(HashMap[address, uint256])
```
```shell
>>> Controller.extra_health('user1')
0
>>> Controller.health('trader1', False)
39438860614534486 # 3.9438860614534486% health when entering SL
>>> Controller.extra_health('user2')
10000000000000000 # 1% extra health
>>> Controller.health('user2')
49438860614534486 # 4.9438860614534486% health when entering SL
```
::::
### `set_extra_health`
::::description[`Controller.set_extra_health(_value: uint256)`]
:::info[V3 Only]
Introduced in Controller V3 (Vyper 0.3.10). Available in weETH, cbBTC, LBTC markets and all Llamalend markets.
:::
Function to set `_value` as extra health for a user. Doing so will add a buffer to the loan a user creats which allows users to enter soft-liquidation with a higher health.
Emits: `SetExtraHealth`
```vyper
event SetExtraHealth:
user: indexed(address)
health: uint256
extra_health: public(HashMap[address, uint256])
@external
def set_extra_health(_value: uint256):
"""
@notice Add a little bit more to loan_discount to start SL with health higher than usual
@param _value 1e18-based addition to loan_discount
"""
self.extra_health[msg.sender] = _value
log SetExtraHealth(msg.sender, _value)
```
```shell
>>> Controller.set_extra_health(10000000000000000) # 1% extra health
```
::::
### `approval`
::::description[`Controller.approval(arg0: address, arg1: address) -> bool: view`]
:::info[V3 Only]
Introduced in Controller V3 (Vyper 0.3.10). Available in weETH, cbBTC, LBTC markets and all Llamalend markets.
:::
Getter to check the approval status. Approval in this case is either `True` or `False`. It does not approve any specific values. Approval can be set using the [`approve`](#approve) function.
Returns: `True` or `False`.
| Input | Type | Description |
| ------ | --------- | ------------------------------------------ |
| `arg0` | `address` | Address for which a certain action is made |
| `arg1` | `address` | Address which does certain actions |
```vyper
approval: public(HashMap[address, HashMap[address, bool]])
```
```shell
>>> Controller.approval('user1', 'user2')
False
```
::::
### `approve`
::::description[`Controller.approve(_spender: address, _allow: bool)`]
:::info[V3 Only]
Introduced in Controller V3 (Vyper 0.3.10). Available in weETH, cbBTC, LBTC markets and all Llamalend markets.
:::
Emits: `Approval`
| Input | Type | Description |
| ---------- | --------- | ----------------------------------- |
| `_spender` | `address` | Address to whitelist for the action |
| `_allow` | `bool` | Allowance status: `True` or `False` |
```vyper
event Approval:
owner: indexed(address)
spender: indexed(address)
allow: bool
approval: public(HashMap[address, HashMap[address, bool]])
@external
def approve(_spender: address, _allow: bool):
"""
@notice Allow another address to borrow and repay for the user
@param _spender Address to whitelist for the action
@param _allow Whether to turn the approval on or off (no amounts)
"""
self.approval[msg.sender][_spender] = _allow
log Approval(msg.sender, _spender, _allow)
@internal
@view
def _check_approval(_for: address) -> bool:
return msg.sender == _for or self.approval[_for][msg.sender]
```
Example to approve `user1` to do certain actions for `user2`.
```shell
>>> Controller.approve('user1', True)
```
::::
### `max_borrowable`
::::description[`Controller.max_borrowable(collateral: uint256, N: uint256, current_debt: uint256 = 0, user: address = empty(address)) -> uint256`]
Function to calculate the maximum amount of crvUSD that can be borrowed against `collateral` using `N` bands. If the max borrowable amount exceeds the crvUSD balance of the controller, which essentially is what's left to be borrowed, it returns the amount that remains available for borrowing.
Returns: maximum borrowable amount (`uint256`).
| Input | Type | Description |
| ----------- | --------- | -------------------- |
| `collateral`| `uint256` | Collateral amount (at its native precision) |
| `N` | `uint256` | Number of bands |
| `current_debt` | `uint256` | Current debt (if any) |
| `user` | `empty(address)` | User to calculate the value for; this input is only necessary for nonzero `extra_health` |
```vyper
MAX_TICKS: constant(int256) = 50
MAX_TICKS_UINT: constant(uint256) = 50
MIN_TICKS: constant(int256) = 4
MIN_TICKS_UINT: constant(uint256) = 4
liquidation_discounts: public(HashMap[address, uint256])
extra_health: public(HashMap[address, uint256])
@external
@view
@nonreentrant('lock')
def max_borrowable(collateral: uint256, N: uint256, current_debt: uint256 = 0, user: address = empty(address)) -> uint256:
"""
@notice Calculation of maximum which can be borrowed (details in comments)
@param collateral Collateral amount against which to borrow
@param N number of bands to have the deposit into
@param current_debt Current debt of the user (if any)
@param user User to calculate the value for (only necessary for nonzero extra_health)
@return Maximum amount of stablecoin to borrow
"""
# Calculation of maximum which can be borrowed.
# It corresponds to a minimum between the amount corresponding to price_oracle
# and the one given by the min reachable band.
#
# Given by p_oracle (perhaps needs to be multiplied by (A - 1) / A to account for mid-band effects)
# x_max ~= y_effective * p_oracle
#
# Given by band number:
# if n1 is the lowest empty band in the AMM
# xmax ~= y_effective * amm.p_oracle_up(n1)
#
# When n1 -= 1:
# p_oracle_up *= A / (A - 1)
# if N < MIN_TICKS or N > MAX_TICKS:
assert N >= MIN_TICKS_UINT and N <= MAX_TICKS_UINT
y_effective: uint256 = self.get_y_effective(collateral * COLLATERAL_PRECISION, N,
self.loan_discount + self.extra_health[user])
x: uint256 = unsafe_sub(max(unsafe_div(y_effective * self.max_p_base(), 10**18), 1), 1)
x = unsafe_div(x * (10**18 - 10**14), unsafe_mul(10**18, BORROWED_PRECISION)) # Make it a bit smaller
return min(x, BORROWED_TOKEN.balanceOf(self) + current_debt) # Cannot borrow beyond the amount of coins Controller has
@internal
@pure
def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint256:
"""
@notice Intermediary method which calculates y_effective defined as x_effective / p_base,
however discounted by loan_discount.
x_effective is an amount which can be obtained from collateral when liquidating
@param collateral Amount of collateral to get the value for
@param N Number of bands the deposit is made into
@param discount Loan discount at 1e18 base (e.g. 1e18 == 100%)
@return y_effective
"""
# x_effective = sum_{i=0..N-1}(y / N * p(n_{n1+i})) =
# = y / N * p_oracle_up(n1) * sqrt((A - 1) / A) * sum_{0..N-1}(((A-1) / A)**k)
# === d_y_effective * p_oracle_up(n1) * sum(...) === y_effective * p_oracle_up(n1)
# d_y_effective = y / N / sqrt(A / (A - 1))
# d_y_effective: uint256 = collateral * unsafe_sub(10**18, discount) / (SQRT_BAND_RATIO * N)
# Make some extra discount to always deposit lower when we have DEAD_SHARES rounding
d_y_effective: uint256 = unsafe_div(
collateral * unsafe_sub(
10**18, min(discount + unsafe_div((DEAD_SHARES * 10**18), max(unsafe_div(collateral, N), DEAD_SHARES)), 10**18)
),
unsafe_mul(SQRT_BAND_RATIO, N))
y_effective: uint256 = d_y_effective
for i in range(1, MAX_TICKS_UINT):
if i == N:
break
d_y_effective = unsafe_div(d_y_effective * Aminus1, A)
y_effective = unsafe_add(y_effective, d_y_effective)
return y_effective
```
```vyper
@external
@view
@nonreentrant('lock')
def max_borrowable(collateral: uint256, N: uint256, current_debt: uint256 = 0) -> uint256:
"""
@notice Calculation of maximum which can be borrowed (details in comments)
@param collateral Collateral amount against which to borrow
@param N number of bands to have the deposit into
@param current_debt Current debt of the user (if any)
@return Maximum amount of stablecoin to borrow
"""
# Calculation of maximum which can be borrowed.
# It corresponds to a minimum between the amount corresponding to price_oracle
# and the one given by the min reachable band.
#
# Given by p_oracle (perhaps needs to be multiplied by (A - 1) / A to account for mid-band effects)
# x_max ~= y_effective * p_oracle
#
# Given by band number:
# if n1 is the lowest empty band in the AMM
# xmax ~= y_effective * amm.p_oracle_up(n1)
#
# When n1 -= 1:
# p_oracle_up *= A / (A - 1)
y_effective: uint256 = self.get_y_effective(collateral * COLLATERAL_PRECISION, N, self.loan_discount)
x: uint256 = unsafe_sub(max(unsafe_div(y_effective * self.max_p_base(), 10**18), 1), 1)
x = unsafe_div(x * (10**18 - 10**14), unsafe_mul(10**18, BORROWED_PRECISION)) # Make it a bit smaller
return min(x, BORROWED_TOKEN.balanceOf(self) + current_debt) # Cannot borrow beyond the amount of coins Controller has
@internal
@pure
def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint256:
"""
@notice Intermediary method which calculates y_effective defined as x_effective / p_base,
however discounted by loan_discount.
x_effective is an amount which can be obtained from collateral when liquidating
@param collateral Amount of collateral to get the value for
@param N Number of bands the deposit is made into
@param discount Loan discount at 1e18 base (e.g. 1e18 == 100%)
@return y_effective
"""
# x_effective = sum_{i=0..N-1}(y / N * p(n_{n1+i})) =
# = y / N * p_oracle_up(n1) * sqrt((A - 1) / A) * sum_{0..N-1}(((A-1) / A)**k)
# === d_y_effective * p_oracle_up(n1) * sum(...) === y_effective * p_oracle_up(n1)
# d_y_effective = y / N / sqrt(A / (A - 1))
# d_y_effective: uint256 = collateral * unsafe_sub(10**18, discount) / (SQRT_BAND_RATIO * N)
# Make some extra discount to always deposit lower when we have DEAD_SHARES rounding
d_y_effective: uint256 = collateral * unsafe_sub(
10**18, min(discount + unsafe_div((DEAD_SHARES * 10**18), max(unsafe_div(collateral, N), DEAD_SHARES)), 10**18)
) / unsafe_mul(SQRT_BAND_RATIO, N)
y_effective: uint256 = d_y_effective
for i in range(1, MAX_TICKS_UINT):
if i == N:
break
d_y_effective = unsafe_div(d_y_effective * Aminus1, A)
y_effective = unsafe_add(y_effective, d_y_effective)
return y_effective
```
This example shows the maximum borrowable debt when using a defined amount of collateral and a specified number of bands. For instance, in the first case, using 1 BTC as collateral with 5 bands, a user can borrow up to approximately 37,965 crvUSD.
```shell
>>> Controller.max_borrowable(10**18, 5)
37965133715410776274198
>>> Controller.max_borrowable(10**18, 50)
30597863183498027832984
```
::::
### `min_collateral`
::::description[`Controller.min_collateral(debt: uint256, N: uint256, user: address = empty(address)) -> uint256`]
Function to calculate the minimum amount of collateral that is necessary to support `debt` using `N` bands.
Returns: minimal collateral required to support the give amount of debt (`uint256`).
| Input | Type | Description |
| ------ | --------- | -------------------- |
| `debt` | `uint256` | Debt to support |
| `N` | `uint256` | Number of bands used |
| `user` | `empty(address)` | User to calculate the value for; this input is only necessary for nonzero `extra_health` |
```vyper
@external
@view
@nonreentrant('lock')
def min_collateral(debt: uint256, N: uint256, user: address = empty(address)) -> uint256:
"""
@notice Minimal amount of collateral required to support debt
@param debt The debt to support
@param N Number of bands to deposit into
@param user User to calculate the value for (only necessary for nonzero extra_health)
@return Minimal collateral required
"""
# Add N**2 to account for precision loss in multiple bands, e.g. N / (y/N) = N**2 / y
assert N <= MAX_TICKS_UINT
return unsafe_div(
unsafe_div(
debt * unsafe_mul(10**18, BORROWED_PRECISION) / self.max_p_base() * 10**18 / self.get_y_effective(10**18, N, self.loan_discount + self.extra_health[user]) + unsafe_add(unsafe_mul(N, unsafe_add(N, 2 * DEAD_SHARES)), unsafe_sub(COLLATERAL_PRECISION, 1)),
COLLATERAL_PRECISION
) * 10**18,
10**18 - 10**14)
@internal
@pure
def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint256:
"""
@notice Intermediary method which calculates y_effective defined as x_effective / p_base,
however discounted by loan_discount.
x_effective is an amount which can be obtained from collateral when liquidating
@param collateral Amount of collateral to get the value for
@param N Number of bands the deposit is made into
@param discount Loan discount at 1e18 base (e.g. 1e18 == 100%)
@return y_effective
"""
# x_effective = sum_{i=0..N-1}(y / N * p(n_{n1+i})) =
# = y / N * p_oracle_up(n1) * sqrt((A - 1) / A) * sum_{0..N-1}(((A-1) / A)**k)
# === d_y_effective * p_oracle_up(n1) * sum(...) === y_effective * p_oracle_up(n1)
# d_y_effective = y / N / sqrt(A / (A - 1))
# d_y_effective: uint256 = collateral * unsafe_sub(10**18, discount) / (SQRT_BAND_RATIO * N)
# Make some extra discount to always deposit lower when we have DEAD_SHARES rounding
d_y_effective: uint256 = unsafe_div(
collateral * unsafe_sub(
10**18, min(discount + unsafe_div((DEAD_SHARES * 10**18), max(unsafe_div(collateral, N), DEAD_SHARES)), 10**18)
),
unsafe_mul(SQRT_BAND_RATIO, N))
y_effective: uint256 = d_y_effective
for i in range(1, MAX_TICKS_UINT):
if i == N:
break
d_y_effective = unsafe_div(d_y_effective * Aminus1, A)
y_effective = unsafe_add(y_effective, d_y_effective)
return y_effective
```
```vyper
@external
@view
@nonreentrant('lock')
def min_collateral(debt: uint256, N: uint256) -> uint256:
"""
@notice Minimal amount of collateral required to support debt
@param debt The debt to support
@param N Number of bands to deposit into
@return Minimal collateral required
"""
# Add N**2 to account for precision loss in multiple bands, e.g. N / (y/N) = N**2 / y
return unsafe_div(
unsafe_div(
debt * unsafe_mul(10**18, BORROWED_PRECISION) / self.max_p_base() * 10**18 / self.get_y_effective(10**18, N, self.loan_discount) + N * (N + 2 * DEAD_SHARES) + unsafe_sub(COLLATERAL_PRECISION, 1),
COLLATERAL_PRECISION
) * 10**18,
10**18 - 10**14)
@internal
@pure
def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint256:
"""
@notice Intermediary method which calculates y_effective defined as x_effective / p_base,
however discounted by loan_discount.
x_effective is an amount which can be obtained from collateral when liquidating
@param collateral Amount of collateral to get the value for
@param N Number of bands the deposit is made into
@param discount Loan discount at 1e18 base (e.g. 1e18 == 100%)
@return y_effective
"""
# x_effective = sum_{i=0..N-1}(y / N * p(n_{n1+i})) =
# = y / N * p_oracle_up(n1) * sqrt((A - 1) / A) * sum_{0..N-1}(((A-1) / A)**k)
# === d_y_effective * p_oracle_up(n1) * sum(...) === y_effective * p_oracle_up(n1)
# d_y_effective = y / N / sqrt(A / (A - 1))
# d_y_effective: uint256 = collateral * unsafe_sub(10**18, discount) / (SQRT_BAND_RATIO * N)
# Make some extra discount to always deposit lower when we have DEAD_SHARES rounding
d_y_effective: uint256 = collateral * unsafe_sub(
10**18, min(discount + unsafe_div((DEAD_SHARES * 10**18), max(unsafe_div(collateral, N), DEAD_SHARES)), 10**18)
) / unsafe_mul(SQRT_BAND_RATIO, N)
y_effective: uint256 = d_y_effective
for i in range(1, MAX_TICKS_UINT):
if i == N:
break
d_y_effective = unsafe_div(d_y_effective * Aminus1, A)
y_effective = unsafe_add(y_effective, d_y_effective)
return y_effective
```
This example shows the amount of collateral needed to support debt using different numbers of bands. The collateral in this example is BTC. For instance, in the first case, to support 10,000 crvUSD as debt using 5 bands, approximately 0.26 BTC is needed as collateral.
```shell
>>> Controller.min_collateral(10**22, 5)
263399572749066565
>>> Controller.min_collateral(10**22, 50)
326820207673727834
```
::::
### `calculate_debt_n1`
::::description[`Controller.calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: address = empty(address)) -> int256`]
Getter method to calculate the upper band number for the deposited collateral to sit in to support the given debt. This call reverts if the requested debt is too high.
Returns: upper band n1 (`int256`) to deposit the collateral into.
| Input | Type | Description |
| ------------ | --------- | ---------------------------------------------- |
| `collateral` | `uint256` | Amount of collateral (at its native precision) |
| `debt` | `uint256` | Amount of requested debt |
| `N` | `uint256` | Number of bands to deposit into |
| `user` | `empty(address)` | User to calculate the value for; this input is only necessary for nonzero `extra_health` |
```vyper
@external
@view
@nonreentrant('lock')
def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: address = empty(address)) -> int256:
"""
@notice Calculate the upper band number for the deposit to sit in to support
the given debt. Reverts if requested debt is too high.
@param collateral Amount of collateral (at its native precision)
@param debt Amount of requested debt
@param N Number of bands to deposit into
@param user User to calculate n1 for (only necessary for nonzero extra_health)
@return Upper band n1 (n1 <= n2) to deposit into. Signed integer
"""
return self._calculate_debt_n1(collateral, debt, N, user)
@internal
@view
def _calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: address) -> int256:
"""
@notice Calculate the upper band number for the deposit to sit in to support
the given debt. Reverts if requested debt is too high.
@param collateral Amount of collateral (at its native precision)
@param debt Amount of requested debt
@param N Number of bands to deposit into
@return Upper band n1 (n1 <= n2) to deposit into. Signed integer
"""
assert debt > 0, "No loan"
n0: int256 = AMM.active_band()
p_base: uint256 = AMM.p_oracle_up(n0)
# x_effective = y / N * p_oracle_up(n1) * sqrt((A - 1) / A) * sum_{0..N-1}(((A-1) / A)**k)
# === d_y_effective * p_oracle_up(n1) * sum(...) === y_effective * p_oracle_up(n1)
# d_y_effective = y / N / sqrt(A / (A - 1))
y_effective: uint256 = self.get_y_effective(collateral * COLLATERAL_PRECISION, N, self.loan_discount + self.extra_health[user])
# p_oracle_up(n1) = base_price * ((A - 1) / A)**n1
# We borrow up until min band touches p_oracle,
# or it touches non-empty bands which cannot be skipped.
# We calculate required n1 for given (collateral, debt),
# and if n1 corresponds to price_oracle being too high, or unreachable band
# - we revert.
# n1 is band number based on adiabatic trading, e.g. when p_oracle ~ p
y_effective = unsafe_div(y_effective * p_base, debt * BORROWED_PRECISION + 1) # Now it's a ratio
# n1 = floor(log(y_effective) / self.logAratio)
# EVM semantics is not doing floor unlike Python, so we do this
assert y_effective > 0, "Amount too low"
n1: int256 = self.wad_ln(y_effective)
if n1 < 0:
n1 -= unsafe_sub(LOGN_A_RATIO, 1) # This is to deal with vyper's rounding of negative numbers
n1 = unsafe_div(n1, LOGN_A_RATIO)
n1 = min(n1, 1024 - convert(N, int256)) + n0
if n1 <= n0:
assert AMM.can_skip_bands(n1 - 1), "Debt too high"
# Let's not rely on active_band corresponding to price_oracle:
# this will be not correct if we are in the area of empty bands
assert AMM.p_oracle_up(n1) < AMM.price_oracle(), "Debt too high"
return n1
```
```vyper
@external
@view
@nonreentrant('lock')
def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256) -> int256:
"""
@notice Calculate the upper band number for the deposit to sit in to support
the given debt. Reverts if requested debt is too high.
@param collateral Amount of collateral (at its native precision)
@param debt Amount of requested debt
@param N Number of bands to deposit into
@return Upper band n1 (n1 <= n2) to deposit into. Signed integer
"""
return self._calculate_debt_n1(collateral, debt, N)
@internal
@view
def _calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256) -> int256:
"""
@notice Calculate the upper band number for the deposit to sit in to support
the given debt. Reverts if requested debt is too high.
@param collateral Amount of collateral (at its native precision)
@param debt Amount of requested debt
@param N Number of bands to deposit into
@return Upper band n1 (n1 <= n2) to deposit into. Signed integer
"""
assert debt > 0, "No loan"
n0: int256 = AMM.active_band()
p_base: uint256 = AMM.p_oracle_up(n0)
# x_effective = y / N * p_oracle_up(n1) * sqrt((A - 1) / A) * sum_{0..N-1}(((A-1) / A)**k)
# === d_y_effective * p_oracle_up(n1) * sum(...) === y_effective * p_oracle_up(n1)
# d_y_effective = y / N / sqrt(A / (A - 1))
y_effective: uint256 = self.get_y_effective(collateral * COLLATERAL_PRECISION, N, self.loan_discount)
# p_oracle_up(n1) = base_price * ((A - 1) / A)**n1
# We borrow up until min band touches p_oracle,
# or it touches non-empty bands which cannot be skipped.
# We calculate required n1 for given (collateral, debt),
# and if n1 corresponds to price_oracle being too high, or unreachable band
# - we revert.
# n1 is band number based on adiabatic trading, e.g. when p_oracle ~ p
y_effective = unsafe_div(y_effective * p_base, debt * BORROWED_PRECISION + 1) # Now it's a ratio
# n1 = floor(log(y_effective) / self.logAratio)
# EVM semantics is not doing floor unlike Python, so we do this
assert y_effective > 0, "Amount too low"
n1: int256 = self.wad_ln(y_effective)
if n1 < 0:
n1 -= unsafe_sub(LOGN_A_RATIO, 1) # This is to deal with vyper's rounding of negative numbers
n1 = unsafe_div(n1, LOGN_A_RATIO)
n1 = min(n1, 1024 - convert(N, int256)) + n0
if n1 <= n0:
assert AMM.can_skip_bands(n1 - 1), "Debt too high"
# Let's not rely on active_band corresponding to price_oracle:
# this will be not correct if we are in the area of empty bands
assert AMM.p_oracle_up(n1) < AMM.price_oracle(), "Debt too high"
return n1
```
This example shows the upper band into which the collateral is deposited.
```shell
>>> Controller.calculate_debt_n1(10**18, 10**22, 5)
85
>>> Controller.calculate_debt_n1(10**18, 10**22, 25)
76
```
::::
### `repay`
::::description[`Controller.repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 = 2**255-1)`]
Function to partially or fully repay `_d_debt` amount of debt. If `_d_debt` exceeds the total debt amount of the user, a full repayment will be done.
| Input | Type | Description |
| ------------------ | --------- | --------------------------------------------------------------- |
| `_d_debt` | `uint256` | Amount of debt to repay |
| `_for` | `address` | Address to repay the debt for; defaults to `msg.sender` |
| `max_active_band` | `int256` | Highest active band. Used to prevent front-running the repay; defaults to `2**255-1` |
| `use_eth` | `bool` | Use wrapping/unwrapping if collateral is ETH |
Emits: `UserState` and `Repay`
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Repay:
user: indexed(address)
collateral_decrease: uint256
loan_decrease: uint256
@external
@nonreentrant('lock')
def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 = 2**255-1):
"""
@notice Repay debt (partially or fully)
@param _d_debt The amount of debt to repay. If higher than the current debt - will do full repayment
@param _for The user to repay the debt for
@param max_active_band Don't allow active band to be higher than this (to prevent front-running the repay)
@param _for Address to repay for
"""
if _d_debt == 0:
return
# Or repay all for MAX_UINT256
# Withdraw if debt become 0
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(_for)
assert debt > 0, "Loan doesn't exist"
d_debt: uint256 = min(debt, _d_debt)
debt = unsafe_sub(debt, d_debt)
approval: bool = self._check_approval(_for)
if debt == 0:
# Allow to withdraw all assets even when underwater
xy: uint256[2] = AMM.withdraw(_for, 10**18)
if xy[0] > 0:
# Only allow full repayment when underwater for the sender to do
assert approval
self.transferFrom(BORROWED_TOKEN, AMM.address, _for, xy[0])
if xy[1] > 0:
self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, xy[1])
log UserState(_for, 0, 0, 0, 0, 0)
log Repay(_for, xy[1], d_debt)
self._remove_from_list(_for)
else:
active_band: int256 = AMM.active_band_with_skip()
assert active_band <= max_active_band
ns: int256[2] = AMM.read_user_tick_numbers(_for)
size: int256 = unsafe_sub(ns[1], ns[0])
liquidation_discount: uint256 = self.liquidation_discounts[_for]
if ns[0] > active_band:
# Not in liquidation - can move bands
xy: uint256[2] = AMM.withdraw(_for, 10**18)
n1: int256 = self._calculate_debt_n1(xy[1], debt, convert(unsafe_add(size, 1), uint256), _for)
n2: int256 = n1 + size
AMM.deposit_range(_for, xy[1], n1, n2)
if approval:
# Update liquidation discount only if we are that same user. No rugs
liquidation_discount = self.liquidation_discount
self.liquidation_discounts[_for] = liquidation_discount
log UserState(_for, xy[1], debt, n1, n2, liquidation_discount)
log Repay(_for, 0, d_debt)
else:
# Underwater - cannot move band but can avoid a bad liquidation
log UserState(_for, max_value(uint256), debt, ns[0], ns[1], liquidation_discount)
log Repay(_for, 0, d_debt)
if not approval:
# Doesn't allow non-sender to repay in a way which ends with unhealthy state
# full = False to make this condition non-manipulatable (and also cheaper on gas)
assert self._health(_for, debt, False, liquidation_discount) > 0
# If we withdrew already - will burn less!
self.transferFrom(BORROWED_TOKEN, msg.sender, self, d_debt) # fail: insufficient funds
self.redeemed += d_debt
self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul})
total_debt: uint256 = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul
self._total_debt.initial_debt = unsafe_sub(max(total_debt, d_debt), d_debt)
self._total_debt.rate_mul = rate_mul
self._save_rate()
```
```vyper
event Withdraw:
provider: indexed(address)
amount_borrowed: uint256
amount_collateral: uint256
@external
@nonreentrant('lock')
def withdraw(user: address, frac: uint256) -> uint256[2]:
"""
@notice Withdraw liquidity for the user. Only admin contract can do it
@param user User who owns liquidity
@param frac Fraction to withdraw (1e18 being 100%)
@return Amount of [stablecoins, collateral] withdrawn
"""
assert msg.sender == self.admin
assert frac <= 10**18
lm: LMGauge = self.liquidity_mining_callback
ns: int256[2] = self._read_user_tick_numbers(user)
n: int256 = ns[0]
user_shares: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns)
assert user_shares[0] > 0, "No deposits"
total_x: uint256 = 0
total_y: uint256 = 0
min_band: int256 = self.min_band
old_min_band: int256 = min_band
old_max_band: int256 = self.max_band
max_band: int256 = n - 1
for i in range(MAX_TICKS):
x: uint256 = self.bands_x[n]
y: uint256 = self.bands_y[n]
ds: uint256 = unsafe_div(frac * user_shares[i], 10**18)
user_shares[i] = unsafe_sub(user_shares[i], ds) # Can ONLY zero out when frac == 10**18
s: uint256 = self.total_shares[n]
new_shares: uint256 = s - ds
self.total_shares[n] = new_shares
s += DEAD_SHARES # after this s is guaranteed to be bigger than 0
dx: uint256 = unsafe_div((x + 1) * ds, s)
dy: uint256 = unsafe_div((y + 1) * ds, s)
x -= dx
y -= dy
# If withdrawal is the last one - transfer dust to admin fees
if new_shares == 0:
if x > 0:
self.admin_fees_x += unsafe_div(x, BORROWED_PRECISION)
if y > 0:
self.admin_fees_y += unsafe_div(y, COLLATERAL_PRECISION)
x = 0
y = 0
if n == min_band:
if x == 0:
if y == 0:
min_band += 1
if x > 0 or y > 0:
max_band = n
self.bands_x[n] = x
self.bands_y[n] = y
total_x += dx
total_y += dy
if n == ns[1]:
break
else:
n = unsafe_add(n, 1)
# Empty the ticks
if frac == 10**18:
self.user_shares[user].ticks[0] = 0
else:
self.save_user_shares(user, user_shares)
if old_min_band != min_band:
self.min_band = min_band
if old_max_band <= ns[1]:
self.max_band = max_band
total_x = unsafe_div(total_x, BORROWED_PRECISION)
total_y = unsafe_div(total_y, COLLATERAL_PRECISION)
log Withdraw(user, total_x, total_y)
if lm.address != empty(address):
lm.callback_collateral_shares(0, []) # collateral/shares ratio is unchanged
lm.callback_user_shares(user, ns[0], user_shares)
return [total_x, total_y]
```
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Repay:
user: indexed(address)
collateral_decrease: uint256
loan_decrease: uint256
@external
@nonreentrant('lock')
def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 = 2**255-1, use_eth: bool = True):
"""
@notice Repay debt (partially or fully)
@param _d_debt The amount of debt to repay. If higher than the current debt - will do full repayment
@param _for The user to repay the debt for
@param max_active_band Don't allow active band to be higher than this (to prevent front-running the repay)
@param use_eth Use wrapping/unwrapping if collateral is ETH
"""
if _d_debt == 0:
return
# Or repay all for MAX_UINT256
# Withdraw if debt become 0
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(_for)
assert debt > 0, "Loan doesn't exist"
d_debt: uint256 = min(debt, _d_debt)
debt = unsafe_sub(debt, d_debt)
if debt == 0:
# Allow to withdraw all assets even when underwater
xy: uint256[2] = AMM.withdraw(_for, 10**18)
if xy[0] > 0:
# Only allow full repayment when underwater for the sender to do
assert _for == msg.sender
self.transferFrom(BORROWED_TOKEN, AMM.address, _for, xy[0])
if xy[1] > 0:
self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, xy[1])
log UserState(_for, 0, 0, 0, 0, 0)
log Repay(_for, xy[1], d_debt)
self._remove_from_list(_for)
else:
active_band: int256 = AMM.active_band_with_skip()
assert active_band <= max_active_band
ns: int256[2] = AMM.read_user_tick_numbers(_for)
size: uint256 = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)
liquidation_discount: uint256 = self.liquidation_discounts[_for]
if ns[0] > active_band:
# Not in liquidation - can move bands
xy: uint256[2] = AMM.withdraw(_for, 10**18)
n1: int256 = self._calculate_debt_n1(xy[1], debt, size)
n2: int256 = n1 + unsafe_sub(ns[1], ns[0])
AMM.deposit_range(_for, xy[1], n1, n2)
if _for == msg.sender:
# Update liquidation discount only if we are that same user. No rugs
liquidation_discount = self.liquidation_discount
self.liquidation_discounts[_for] = liquidation_discount
log UserState(_for, xy[1], debt, n1, n2, liquidation_discount)
log Repay(_for, 0, d_debt)
else:
# Underwater - cannot move band but can avoid a bad liquidation
log UserState(_for, max_value(uint256), debt, ns[0], ns[1], liquidation_discount)
log Repay(_for, 0, d_debt)
if _for != msg.sender:
# Doesn't allow non-sender to repay in a way which ends with unhealthy state
# full = False to make this condition non-manipulatable (and also cheaper on gas)
assert self._health(_for, debt, False, liquidation_discount) > 0
# If we withdrew already - will burn less!
self.transferFrom(BORROWED_TOKEN, msg.sender, self, d_debt) # fail: insufficient funds
self.redeemed += d_debt
self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul})
total_debt: uint256 = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul
self._total_debt.initial_debt = unsafe_sub(max(total_debt, d_debt), d_debt)
self._total_debt.rate_mul = rate_mul
self._save_rate()
```
```vyper
event Withdraw:
provider: indexed(address)
amount_borrowed: uint256
amount_collateral: uint256
@external
@nonreentrant('lock')
def withdraw(user: address, frac: uint256) -> uint256[2]:
"""
@notice Withdraw liquidity for the user. Only admin contract can do it
@param user User who owns liquidity
@param frac Fraction to withdraw (1e18 being 100%)
@return Amount of [stablecoins, collateral] withdrawn
"""
assert msg.sender == self.admin
assert frac <= 10**18
lm: LMGauge = self.liquidity_mining_callback
ns: int256[2] = self._read_user_tick_numbers(user)
n: int256 = ns[0]
user_shares: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns)
assert user_shares[0] > 0, "No deposits"
total_x: uint256 = 0
total_y: uint256 = 0
min_band: int256 = self.min_band
old_min_band: int256 = min_band
old_max_band: int256 = self.max_band
max_band: int256 = n - 1
for i in range(MAX_TICKS):
x: uint256 = self.bands_x[n]
y: uint256 = self.bands_y[n]
ds: uint256 = unsafe_div(frac * user_shares[i], 10**18)
user_shares[i] = unsafe_sub(user_shares[i], ds) # Can ONLY zero out when frac == 10**18
s: uint256 = self.total_shares[n]
new_shares: uint256 = s - ds
self.total_shares[n] = new_shares
s += DEAD_SHARES # after this s is guaranteed to be bigger than 0
dx: uint256 = unsafe_div((x + 1) * ds, s)
dy: uint256 = unsafe_div((y + 1) * ds, s)
x -= dx
y -= dy
# If withdrawal is the last one - transfer dust to admin fees
if new_shares == 0:
if x > 0:
self.admin_fees_x += unsafe_div(x, BORROWED_PRECISION)
if y > 0:
self.admin_fees_y += unsafe_div(y, COLLATERAL_PRECISION)
x = 0
y = 0
if n == min_band:
if x == 0:
if y == 0:
min_band += 1
if x > 0 or y > 0:
max_band = n
self.bands_x[n] = x
self.bands_y[n] = y
total_x += dx
total_y += dy
if n == ns[1]:
break
else:
n = unsafe_add(n, 1)
# Empty the ticks
if frac == 10**18:
self.user_shares[user].ticks[0] = 0
else:
self.save_user_shares(user, user_shares)
if old_min_band != min_band:
self.min_band = min_band
if old_max_band <= ns[1]:
self.max_band = max_band
total_x = unsafe_div(total_x, BORROWED_PRECISION)
total_y = unsafe_div(total_y, COLLATERAL_PRECISION)
log Withdraw(user, total_x, total_y)
if lm.address != empty(address):
lm.callback_collateral_shares(0, []) # collateral/shares ratio is unchanged
lm.callback_user_shares(user, ns[0], user_shares)
return [total_x, total_y]
```
```shell
>>> soon
```
::::
### `repay_extended`
::::description[`Controller.repay_extended(callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b"")`]
Extended function to repay a loan but obtain a stablecoin for that from a callback (to deleverage). Earlier implementations of the contract did not have `callback_bytes` argument. This was added to enable [leveraging/de-leveraging using the 1inch router](./leverage/leverage-zap-1inch.md#unwinding-leverage).
| Input | Type | Description |
| ---------------- | --------------------- | ---------------------------------------------------- |
| `callbacker` | `address` | Address of the callback contract |
| `callback_args` | `DynArray[uint256,5]` | Extra arguments for the callback (up to 5), such as `min_amount` |
| `callback_bytes` | `Bytes[10**4]` | Callback bytes passed to the LeverageZap. Defaults to `b""` |
| `_for` | `address` | Address to repay debt for (requires approval); **V3 only** |
Emits: `UserState` and `Repay`
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Repay:
user: indexed(address)
collateral_decrease: uint256
loan_decrease: uint256
CALLBACK_DEPOSIT: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_REPAY: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_LIQUIDATE: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_DEPOSIT_WITH_BYTES: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
# CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4) <-- BUG! The reason is 0 at the beginning of method_id
CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = 0x008ae188
CALLBACK_LIQUIDATE_WITH_BYTES: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
@external
@nonreentrant('lock')
def repay_extended(callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b"", _for: address = msg.sender):
"""
@notice Repay loan but get a stablecoin for that from callback (to deleverage)
@param callbacker Address of the callback contract
@param callback_args Extra arguments for the callback (up to 5) such as min_amount etc
@param _for Address to repay for
"""
assert self._check_approval(_for)
# Before callback
ns: int256[2] = AMM.read_user_tick_numbers(_for)
xy: uint256[2] = AMM.withdraw(_for, 10**18)
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(_for)
self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1])
# For compatibility
callback_sig: bytes4 = CALLBACK_REPAY_WITH_BYTES
if callback_bytes == b"":
callback_sig = CALLBACK_REPAY
cb: CallbackData = self.execute_callback(
callbacker, callback_sig, _for, xy[0], xy[1], debt, callback_args, callback_bytes)
# After callback
total_stablecoins: uint256 = cb.stablecoins + xy[0]
assert total_stablecoins > 0 # dev: no coins to repay
# d_debt: uint256 = min(debt, total_stablecoins)
d_debt: uint256 = 0
# If we have more stablecoins than the debt - full repayment and closing the position
if total_stablecoins >= debt:
d_debt = debt
debt = 0
self._remove_from_list(_for)
# Transfer debt to self, everything else to _for
self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins)
self.transferFrom(BORROWED_TOKEN, AMM.address, self, xy[0])
if total_stablecoins > d_debt:
self.transfer(BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt))
self.transferFrom(COLLATERAL_TOKEN, callbacker, _for, cb.collateral)
log UserState(_for, 0, 0, 0, 0, 0)
# Else - partial repayment -> deleverage, but only if we are not underwater
else:
size: int256 = unsafe_sub(ns[1], ns[0])
assert ns[0] > cb.active_band
d_debt = cb.stablecoins # cb.stablecoins <= total_stablecoins < debt
debt = unsafe_sub(debt, cb.stablecoins)
# Not in liquidation - can move bands
n1: int256 = self._calculate_debt_n1(cb.collateral, debt, convert(unsafe_add(size, 1), uint256), _for)
n2: int256 = n1 + size
AMM.deposit_range(_for, cb.collateral, n1, n2)
liquidation_discount: uint256 = self.liquidation_discount
self.liquidation_discounts[_for] = liquidation_discount
self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, cb.collateral)
# Stablecoin is all spent to repay debt -> all goes to self
self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins)
# We are above active band, so xy[0] is 0 anyway
log UserState(_for, cb.collateral, debt, n1, n2, liquidation_discount)
xy[1] -= cb.collateral
# No need to check _health() because it's the _for
# Common calls which we will do regardless of whether it's a full repay or not
log Repay(_for, xy[1], d_debt)
self.redeemed += d_debt
self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul})
total_debt: uint256 = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul
self._total_debt.initial_debt = unsafe_sub(max(total_debt, d_debt), d_debt)
self._total_debt.rate_mul = rate_mul
self._save_rate()
```
```vyper
event Withdraw:
provider: indexed(address)
amount_borrowed: uint256
amount_collateral: uint256
@external
@nonreentrant('lock')
def withdraw(user: address, frac: uint256) -> uint256[2]:
"""
@notice Withdraw liquidity for the user. Only admin contract can do it
@param user User who owns liquidity
@param frac Fraction to withdraw (1e18 being 100%)
@return Amount of [stablecoins, collateral] withdrawn
"""
assert msg.sender == self.admin
assert frac <= 10**18
lm: LMGauge = self.liquidity_mining_callback
ns: int256[2] = self._read_user_tick_numbers(user)
n: int256 = ns[0]
user_shares: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns)
assert user_shares[0] > 0, "No deposits"
total_x: uint256 = 0
total_y: uint256 = 0
min_band: int256 = self.min_band
old_min_band: int256 = min_band
old_max_band: int256 = self.max_band
max_band: int256 = n - 1
for i in range(MAX_TICKS):
x: uint256 = self.bands_x[n]
y: uint256 = self.bands_y[n]
ds: uint256 = unsafe_div(frac * user_shares[i], 10**18)
user_shares[i] = unsafe_sub(user_shares[i], ds) # Can ONLY zero out when frac == 10**18
s: uint256 = self.total_shares[n]
new_shares: uint256 = s - ds
self.total_shares[n] = new_shares
s += DEAD_SHARES # after this s is guaranteed to be bigger than 0
dx: uint256 = unsafe_div((x + 1) * ds, s)
dy: uint256 = unsafe_div((y + 1) * ds, s)
x -= dx
y -= dy
# If withdrawal is the last one - transfer dust to admin fees
if new_shares == 0:
if x > 0:
self.admin_fees_x += unsafe_div(x, BORROWED_PRECISION)
if y > 0:
self.admin_fees_y += unsafe_div(y, COLLATERAL_PRECISION)
x = 0
y = 0
if n == min_band:
if x == 0:
if y == 0:
min_band += 1
if x > 0 or y > 0:
max_band = n
self.bands_x[n] = x
self.bands_y[n] = y
total_x += dx
total_y += dy
if n == ns[1]:
break
else:
n = unsafe_add(n, 1)
# Empty the ticks
if frac == 10**18:
self.user_shares[user].ticks[0] = 0
else:
self.save_user_shares(user, user_shares)
if old_min_band != min_band:
self.min_band = min_band
if old_max_band <= ns[1]:
self.max_band = max_band
total_x = unsafe_div(total_x, BORROWED_PRECISION)
total_y = unsafe_div(total_y, COLLATERAL_PRECISION)
log Withdraw(user, total_x, total_y)
if lm.address != empty(address):
lm.callback_collateral_shares(0, []) # collateral/shares ratio is unchanged
lm.callback_user_shares(user, ns[0], user_shares)
return [total_x, total_y]
```
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Repay:
user: indexed(address)
collateral_decrease: uint256
loan_decrease: uint256
CALLBACK_DEPOSIT: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_REPAY: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_LIQUIDATE: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_DEPOSIT_WITH_BYTES: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
# CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4) <-- BUG! The reason is 0 at the beginning of method_id
CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = 0x008ae188
CALLBACK_LIQUIDATE_WITH_BYTES: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
@external
@nonreentrant('lock')
def repay_extended(callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b""):
"""
@notice Repay loan but get a stablecoin for that from callback (to deleverage)
@param callbacker Address of the callback contract
@param callback_args Extra arguments for the callback (up to 5) such as min_amount etc
"""
# Before callback
ns: int256[2] = AMM.read_user_tick_numbers(msg.sender)
xy: uint256[2] = AMM.withdraw(msg.sender, 10**18)
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(msg.sender)
self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1])
# For compatibility
callback_sig: bytes4 = CALLBACK_REPAY_WITH_BYTES
if callback_bytes == b"":
callback_sig = CALLBACK_REPAY
cb: CallbackData = self.execute_callback(
callbacker, callback_sig, msg.sender, xy[0], xy[1], debt, callback_args, callback_bytes)
# After callback
total_stablecoins: uint256 = cb.stablecoins + xy[0]
assert total_stablecoins > 0 # dev: no coins to repay
# d_debt: uint256 = min(debt, total_stablecoins)
d_debt: uint256 = 0
# If we have more stablecoins than the debt - full repayment and closing the position
if total_stablecoins >= debt:
d_debt = debt
debt = 0
self._remove_from_list(msg.sender)
# Transfer debt to self, everything else to sender
self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins)
self.transferFrom(BORROWED_TOKEN, AMM.address, self, xy[0])
if total_stablecoins > d_debt:
self.transfer(BORROWED_TOKEN, msg.sender, unsafe_sub(total_stablecoins, d_debt))
self.transferFrom(COLLATERAL_TOKEN, callbacker, msg.sender, cb.collateral)
log UserState(msg.sender, 0, 0, 0, 0, 0)
# Else - partial repayment -> deleverage, but only if we are not underwater
else:
size: uint256 = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)
assert ns[0] > cb.active_band
d_debt = cb.stablecoins # cb.stablecoins <= total_stablecoins < debt
debt = unsafe_sub(debt, cb.stablecoins)
# Not in liquidation - can move bands
n1: int256 = self._calculate_debt_n1(cb.collateral, debt, size)
n2: int256 = n1 + unsafe_sub(ns[1], ns[0])
AMM.deposit_range(msg.sender, cb.collateral, n1, n2)
liquidation_discount: uint256 = self.liquidation_discount
self.liquidation_discounts[msg.sender] = liquidation_discount
self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, cb.collateral)
# Stablecoin is all spent to repay debt -> all goes to self
self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins)
# We are above active band, so xy[0] is 0 anyway
log UserState(msg.sender, cb.collateral, debt, n1, n2, liquidation_discount)
xy[1] -= cb.collateral
# No need to check _health() because it's the sender
# Common calls which we will do regardless of whether it's a full repay or not
log Repay(msg.sender, xy[1], d_debt)
self.redeemed += d_debt
self.loan[msg.sender] = Loan({initial_debt: debt, rate_mul: rate_mul})
total_debt: uint256 = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul
self._total_debt.initial_debt = unsafe_sub(max(total_debt, d_debt), d_debt)
self._total_debt.rate_mul = rate_mul
self._save_rate()
```
```vyper
event Withdraw:
provider: indexed(address)
amount_borrowed: uint256
amount_collateral: uint256
@external
@nonreentrant('lock')
def withdraw(user: address, frac: uint256) -> uint256[2]:
"""
@notice Withdraw liquidity for the user. Only admin contract can do it
@param user User who owns liquidity
@param frac Fraction to withdraw (1e18 being 100%)
@return Amount of [stablecoins, collateral] withdrawn
"""
assert msg.sender == self.admin
assert frac <= 10**18
lm: LMGauge = self.liquidity_mining_callback
ns: int256[2] = self._read_user_tick_numbers(user)
n: int256 = ns[0]
user_shares: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns)
assert user_shares[0] > 0, "No deposits"
total_x: uint256 = 0
total_y: uint256 = 0
min_band: int256 = self.min_band
old_min_band: int256 = min_band
old_max_band: int256 = self.max_band
max_band: int256 = n - 1
for i in range(MAX_TICKS):
x: uint256 = self.bands_x[n]
y: uint256 = self.bands_y[n]
ds: uint256 = unsafe_div(frac * user_shares[i], 10**18)
user_shares[i] = unsafe_sub(user_shares[i], ds) # Can ONLY zero out when frac == 10**18
s: uint256 = self.total_shares[n]
new_shares: uint256 = s - ds
self.total_shares[n] = new_shares
s += DEAD_SHARES # after this s is guaranteed to be bigger than 0
dx: uint256 = unsafe_div((x + 1) * ds, s)
dy: uint256 = unsafe_div((y + 1) * ds, s)
x -= dx
y -= dy
# If withdrawal is the last one - transfer dust to admin fees
if new_shares == 0:
if x > 0:
self.admin_fees_x += unsafe_div(x, BORROWED_PRECISION)
if y > 0:
self.admin_fees_y += unsafe_div(y, COLLATERAL_PRECISION)
x = 0
y = 0
if n == min_band:
if x == 0:
if y == 0:
min_band += 1
if x > 0 or y > 0:
max_band = n
self.bands_x[n] = x
self.bands_y[n] = y
total_x += dx
total_y += dy
if n == ns[1]:
break
else:
n = unsafe_add(n, 1)
# Empty the ticks
if frac == 10**18:
self.user_shares[user].ticks[0] = 0
else:
self.save_user_shares(user, user_shares)
if old_min_band != min_band:
self.min_band = min_band
if old_max_band <= ns[1]:
self.max_band = max_band
total_x = unsafe_div(total_x, BORROWED_PRECISION)
total_y = unsafe_div(total_y, COLLATERAL_PRECISION)
log Withdraw(user, total_x, total_y)
if lm.address != empty(address):
lm.callback_collateral_shares(0, []) # collateral/shares ratio is unchanged
lm.callback_user_shares(user, ns[0], user_shares)
return [total_x, total_y]
```
```shell
>>> soon
```
::::
---
## Adjusting Existing Loans
An already existing loan can be managed in different ways:
- `add_collateral`: Adding more collateral.
- `remove_collateral`: Removing collateral.
- `borrow_more`: Borrowing more assets.
- `liquidate`: Partially or fully liquidating a position.
### `add_collateral`
::::description[`Controller.add_collateral(collateral: uint256, _for: address = msg.sender)`]
Function to add extra collateral to an existing loan. Reverts when trying to add `0` collateral tokens.
| Input | Type | Description |
| ------------ | --------- | ------------------------------ |
| `collateral` | `uint256` | Amount of collateral to add |
| `_for` | `address` | Address to add collateral for; defaults to `msg.sender` |
Emits: `UserState` and `Borrow`
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Borrow:
user: indexed(address)
collateral_increase: uint256
loan_increase: uint256
@external
@nonreentrant('lock')
def add_collateral(collateral: uint256, _for: address = msg.sender):
"""
@notice Add extra collateral to avoid bad liqidations
@param collateral Amount of collateral to add
@param _for Address to add collateral for
"""
if collateral == 0:
return
self._add_collateral_borrow(collateral, 0, _for, False)
self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral)
self._save_rate()
@internal
def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address, remove_collateral: bool):
"""
@notice Internal method to borrow and add or remove collateral
@param d_collateral Amount of collateral to add
@param d_debt Amount of debt increase
@param _for Address to transfer tokens to
@param remove_collateral Remove collateral instead of adding
"""
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(_for)
assert debt > 0, "Loan doesn't exist"
debt += d_debt
ns: int256[2] = AMM.read_user_tick_numbers(_for)
size: uint256 = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)
xy: uint256[2] = AMM.withdraw(_for, 10**18)
assert xy[0] == 0, "Already in underwater mode"
if remove_collateral:
xy[1] -= d_collateral
else:
xy[1] += d_collateral
n1: int256 = self._calculate_debt_n1(xy[1], debt, size, _for)
n2: int256 = n1 + unsafe_sub(ns[1], ns[0])
AMM.deposit_range(_for, xy[1], n1, n2)
self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul})
liquidation_discount: uint256 = 0
if _for == msg.sender:
liquidation_discount = self.liquidation_discount
self.liquidation_discounts[_for] = liquidation_discount
else:
liquidation_discount = self.liquidation_discounts[_for]
if d_debt != 0:
self._total_debt.initial_debt = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + d_debt
self._total_debt.rate_mul = rate_mul
if remove_collateral:
log RemoveCollateral(_for, d_collateral)
else:
log Borrow(_for, d_collateral, d_debt)
log UserState(_for, xy[1], debt, n1, n2, liquidation_discount)
```
```vyper
event Deposit:
provider: indexed(address)
amount: uint256
n1: int256
n2: int256
@external
@nonreentrant('lock')
def deposit_range(user: address, amount: uint256, n1: int256, n2: int256):
"""
@notice Deposit for a user in a range of bands. Only admin contract (Controller) can do it
@param user User address
@param amount Amount of collateral to deposit
@param n1 Lower band in the deposit range
@param n2 Upper band in the deposit range
"""
assert msg.sender == self.admin
user_shares: DynArray[uint256, MAX_TICKS_UINT] = []
collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = []
n0: int256 = self.active_band
# We assume that n1,n2 area already sorted (and they are in Controller)
assert n2 < 2**127
assert n1 > -2**127
n_bands: uint256 = unsafe_add(convert(unsafe_sub(n2, n1), uint256), 1)
assert n_bands <= MAX_TICKS_UINT
y_per_band: uint256 = unsafe_div(amount * COLLATERAL_PRECISION, n_bands)
assert y_per_band > 100, "Amount too low"
assert self.user_shares[user].ticks[0] == 0 # dev: User must have no liquidity
self.user_shares[user].ns = unsafe_add(n1, unsafe_mul(n2, 2**128))
lm: LMGauge = self.liquidity_mining_callback
# Autoskip bands if we can
for i in range(MAX_SKIP_TICKS + 1):
if n1 > n0:
if i != 0:
self.active_band = n0
break
assert self.bands_x[n0] == 0 and i < MAX_SKIP_TICKS, "Deposit below current band"
n0 -= 1
for i in range(MAX_TICKS):
band: int256 = unsafe_add(n1, i)
if band > n2:
break
assert self.bands_x[band] == 0, "Band not empty"
y: uint256 = y_per_band
if i == 0:
y = amount * COLLATERAL_PRECISION - y * unsafe_sub(n_bands, 1)
total_y: uint256 = self.bands_y[band]
# Total / user share
s: uint256 = self.total_shares[band]
ds: uint256 = unsafe_div((s + DEAD_SHARES) * y, total_y + 1)
assert ds > 0, "Amount too low"
user_shares.append(ds)
s += ds
assert s <= 2**128 - 1
self.total_shares[band] = s
total_y += y
self.bands_y[band] = total_y
if lm.address != empty(address):
# If initial s == 0 - s becomes equal to y which is > 100 => nonzero
collateral_shares.append(unsafe_div(total_y * 10**18, s))
self.min_band = min(self.min_band, n1)
self.max_band = max(self.max_band, n2)
self.save_user_shares(user, user_shares)
log Deposit(user, amount, n1, n2)
if lm.address != empty(address):
lm.callback_collateral_shares(n1, collateral_shares)
lm.callback_user_shares(user, n1, user_shares)
```
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Borrow:
user: indexed(address)
collateral_increase: uint256
loan_increase: uint256
@external
@nonreentrant('lock')
def add_collateral(collateral: uint256, _for: address = msg.sender):
"""
@notice Add extra collateral to avoid bad liqidations
@param collateral Amount of collateral to add
@param _for Address to add collateral for
"""
if collateral == 0:
return
self._add_collateral_borrow(collateral, 0, _for, False)
self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral)
self._save_rate()
@internal
def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address, remove_collateral: bool):
"""
@notice Internal method to borrow and add or remove collateral
@param d_collateral Amount of collateral to add
@param d_debt Amount of debt increase
@param _for Address to transfer tokens to
@param remove_collateral Remove collateral instead of adding
"""
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(_for)
assert debt > 0, "Loan doesn't exist"
debt += d_debt
ns: int256[2] = AMM.read_user_tick_numbers(_for)
size: uint256 = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)
xy: uint256[2] = AMM.withdraw(_for, 10**18)
assert xy[0] == 0, "Already in underwater mode"
if remove_collateral:
xy[1] -= d_collateral
else:
xy[1] += d_collateral
n1: int256 = self._calculate_debt_n1(xy[1], debt, size)
n2: int256 = n1 + unsafe_sub(ns[1], ns[0])
AMM.deposit_range(_for, xy[1], n1, n2)
self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul})
liquidation_discount: uint256 = 0
if _for == msg.sender:
liquidation_discount = self.liquidation_discount
self.liquidation_discounts[_for] = liquidation_discount
else:
liquidation_discount = self.liquidation_discounts[_for]
if d_debt != 0:
self._total_debt.initial_debt = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + d_debt
self._total_debt.rate_mul = rate_mul
if remove_collateral:
log RemoveCollateral(_for, d_collateral)
else:
log Borrow(_for, d_collateral, d_debt)
log UserState(_for, xy[1], debt, n1, n2, liquidation_discount)
```
```vyper
event Deposit:
provider: indexed(address)
amount: uint256
n1: int256
n2: int256
@external
@nonreentrant('lock')
def deposit_range(user: address, amount: uint256, n1: int256, n2: int256):
"""
@notice Deposit for a user in a range of bands. Only admin contract (Controller) can do it
@param user User address
@param amount Amount of collateral to deposit
@param n1 Lower band in the deposit range
@param n2 Upper band in the deposit range
"""
assert msg.sender == self.admin
user_shares: DynArray[uint256, MAX_TICKS_UINT] = []
collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = []
n0: int256 = self.active_band
# We assume that n1,n2 area already sorted (and they are in Controller)
assert n2 < 2**127
assert n1 > -2**127
n_bands: uint256 = unsafe_add(convert(unsafe_sub(n2, n1), uint256), 1)
assert n_bands <= MAX_TICKS_UINT
y_per_band: uint256 = unsafe_div(amount * COLLATERAL_PRECISION, n_bands)
assert y_per_band > 100, "Amount too low"
assert self.user_shares[user].ticks[0] == 0 # dev: User must have no liquidity
self.user_shares[user].ns = unsafe_add(n1, unsafe_mul(n2, 2**128))
lm: LMGauge = self.liquidity_mining_callback
# Autoskip bands if we can
for i in range(MAX_SKIP_TICKS + 1):
if n1 > n0:
if i != 0:
self.active_band = n0
break
assert self.bands_x[n0] == 0 and i < MAX_SKIP_TICKS, "Deposit below current band"
n0 -= 1
for i in range(MAX_TICKS):
band: int256 = unsafe_add(n1, i)
if band > n2:
break
assert self.bands_x[band] == 0, "Band not empty"
y: uint256 = y_per_band
if i == 0:
y = amount * COLLATERAL_PRECISION - y * unsafe_sub(n_bands, 1)
total_y: uint256 = self.bands_y[band]
# Total / user share
s: uint256 = self.total_shares[band]
ds: uint256 = unsafe_div((s + DEAD_SHARES) * y, total_y + 1)
assert ds > 0, "Amount too low"
user_shares.append(ds)
s += ds
assert s <= 2**128 - 1
self.total_shares[band] = s
total_y += y
self.bands_y[band] = total_y
if lm.address != empty(address):
# If initial s == 0 - s becomes equal to y which is > 100 => nonzero
collateral_shares.append(unsafe_div(total_y * 10**18, s))
self.min_band = min(self.min_band, n1)
self.max_band = max(self.max_band, n2)
self.save_user_shares(user, user_shares)
log Deposit(user, amount, n1, n2)
if lm.address != empty(address):
lm.callback_collateral_shares(n1, collateral_shares)
lm.callback_user_shares(user, n1, user_shares)
```
```shell
>>> Controller.add_collateral(10**18, trader)
>>> Controller.user_state(trader)
[2000000000000000000, 0, 1000000892890902175729, 10]
# [collateral, stablecoin, debt, bands]
```
::::
### `remove_collateral`
::::description[`Controller.remove_collateral(collateral: uint256, _for: address = msg.sender)`]
Function to remove collateral from an existing loan.
| Input | Type | Description |
| ------------ | --------- | ---------------------------------------------------------- |
| `collateral` | `uint256` | Amount of collateral to remove |
| `_for` | `address` | Address to remove collateral for |
Emits: `UserState` and `RemoveCollateral`
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event RemoveCollateral:
user: indexed(address)
collateral_decrease: uint256
approval: public(HashMap[address, HashMap[address, bool]])
@external
@nonreentrant('lock')
def remove_collateral(collateral: uint256, _for: address = msg.sender):
"""
@notice Remove some collateral without repaying the debt
@param collateral Amount of collateral to remove
@param _for Address to remove collateral for
"""
if collateral == 0:
return
assert self._check_approval(_for)
self._add_collateral_borrow(collateral, 0, _for, True)
self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, collateral)
self._save_rate()
@internal
@view
def _check_approval(_for: address) -> bool:
return msg.sender == _for or self.approval[_for][msg.sender]
@internal
def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address, remove_collateral: bool):
"""
@notice Internal method to borrow and add or remove collateral
@param d_collateral Amount of collateral to add
@param d_debt Amount of debt increase
@param _for Address to transfer tokens to
@param remove_collateral Remove collateral instead of adding
"""
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(_for)
assert debt > 0, "Loan doesn't exist"
debt += d_debt
ns: int256[2] = AMM.read_user_tick_numbers(_for)
size: uint256 = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)
xy: uint256[2] = AMM.withdraw(_for, 10**18)
assert xy[0] == 0, "Already in underwater mode"
if remove_collateral:
xy[1] -= d_collateral
else:
xy[1] += d_collateral
n1: int256 = self._calculate_debt_n1(xy[1], debt, size, _for)
n2: int256 = n1 + unsafe_sub(ns[1], ns[0])
AMM.deposit_range(_for, xy[1], n1, n2)
self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul})
liquidation_discount: uint256 = 0
if _for == msg.sender:
liquidation_discount = self.liquidation_discount
self.liquidation_discounts[_for] = liquidation_discount
else:
liquidation_discount = self.liquidation_discounts[_for]
if d_debt != 0:
self._total_debt.initial_debt = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + d_debt
self._total_debt.rate_mul = rate_mul
if remove_collateral:
log RemoveCollateral(_for, d_collateral)
else:
log Borrow(_for, d_collateral, d_debt)
log UserState(_for, xy[1], debt, n1, n2, liquidation_discount)
```
```vyper
event Withdraw:
provider: indexed(address)
amount_borrowed: uint256
amount_collateral: uint256
@external
@nonreentrant('lock')
def withdraw(user: address, frac: uint256) -> uint256[2]:
"""
@notice Withdraw liquidity for the user. Only admin contract can do it
@param user User who owns liquidity
@param frac Fraction to withdraw (1e18 being 100%)
@return Amount of [stablecoins, collateral] withdrawn
"""
assert msg.sender == self.admin
assert frac <= 10**18
lm: LMGauge = self.liquidity_mining_callback
ns: int256[2] = self._read_user_tick_numbers(user)
n: int256 = ns[0]
user_shares: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns)
assert user_shares[0] > 0, "No deposits"
total_x: uint256 = 0
total_y: uint256 = 0
min_band: int256 = self.min_band
old_min_band: int256 = min_band
old_max_band: int256 = self.max_band
max_band: int256 = n - 1
for i in range(MAX_TICKS):
x: uint256 = self.bands_x[n]
y: uint256 = self.bands_y[n]
ds: uint256 = unsafe_div(frac * user_shares[i], 10**18)
user_shares[i] = unsafe_sub(user_shares[i], ds) # Can ONLY zero out when frac == 10**18
s: uint256 = self.total_shares[n]
new_shares: uint256 = s - ds
self.total_shares[n] = new_shares
s += DEAD_SHARES # after this s is guaranteed to be bigger than 0
dx: uint256 = unsafe_div((x + 1) * ds, s)
dy: uint256 = unsafe_div((y + 1) * ds, s)
x -= dx
y -= dy
# If withdrawal is the last one - transfer dust to admin fees
if new_shares == 0:
if x > 0:
self.admin_fees_x += unsafe_div(x, BORROWED_PRECISION)
if y > 0:
self.admin_fees_y += unsafe_div(y, COLLATERAL_PRECISION)
x = 0
y = 0
if n == min_band:
if x == 0:
if y == 0:
min_band += 1
if x > 0 or y > 0:
max_band = n
self.bands_x[n] = x
self.bands_y[n] = y
total_x += dx
total_y += dy
if n == ns[1]:
break
else:
n = unsafe_add(n, 1)
# Empty the ticks
if frac == 10**18:
self.user_shares[user].ticks[0] = 0
else:
self.save_user_shares(user, user_shares)
if old_min_band != min_band:
self.min_band = min_band
if old_max_band <= ns[1]:
self.max_band = max_band
total_x = unsafe_div(total_x, BORROWED_PRECISION)
total_y = unsafe_div(total_y, COLLATERAL_PRECISION)
log Withdraw(user, total_x, total_y)
if lm.address != empty(address):
lm.callback_collateral_shares(0, []) # collateral/shares ratio is unchanged
lm.callback_user_shares(user, ns[0], user_shares)
return [total_x, total_y]
```
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event RemoveCollateral:
user: indexed(address)
collateral_decrease: uint256
@external
@nonreentrant('lock')
def remove_collateral(collateral: uint256, use_eth: bool = True):
"""
@notice Remove some collateral without repaying the debt
@param collateral Amount of collateral to remove
@param use_eth Use wrapping/unwrapping if collateral is ETH
"""
if collateral == 0:
return
self._add_collateral_borrow(collateral, 0, msg.sender, True)
self.transferFrom(COLLATERAL_TOKEN, AMM.address, msg.sender, collateral)
self._save_rate()
@internal
def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address, remove_collateral: bool):
"""
@notice Internal method to borrow and add or remove collateral
@param d_collateral Amount of collateral to add
@param d_debt Amount of debt increase
@param _for Address to transfer tokens to
@param remove_collateral Remove collateral instead of adding
"""
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(_for)
assert debt > 0, "Loan doesn't exist"
debt += d_debt
ns: int256[2] = AMM.read_user_tick_numbers(_for)
size: uint256 = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)
xy: uint256[2] = AMM.withdraw(_for, 10**18)
assert xy[0] == 0, "Already in underwater mode"
if remove_collateral:
xy[1] -= d_collateral
else:
xy[1] += d_collateral
n1: int256 = self._calculate_debt_n1(xy[1], debt, size)
n2: int256 = n1 + unsafe_sub(ns[1], ns[0])
AMM.deposit_range(_for, xy[1], n1, n2)
self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul})
liquidation_discount: uint256 = 0
if _for == msg.sender:
liquidation_discount = self.liquidation_discount
self.liquidation_discounts[_for] = liquidation_discount
else:
liquidation_discount = self.liquidation_discounts[_for]
if d_debt != 0:
self._total_debt.initial_debt = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + d_debt
self._total_debt.rate_mul = rate_mul
if remove_collateral:
log RemoveCollateral(_for, d_collateral)
else:
log Borrow(_for, d_collateral, d_debt)
log UserState(_for, xy[1], debt, n1, n2, liquidation_discount)
```
```vyper
event Withdraw:
provider: indexed(address)
amount_borrowed: uint256
amount_collateral: uint256
@external
@nonreentrant('lock')
def withdraw(user: address, frac: uint256) -> uint256[2]:
"""
@notice Withdraw liquidity for the user. Only admin contract can do it
@param user User who owns liquidity
@param frac Fraction to withdraw (1e18 being 100%)
@return Amount of [stablecoins, collateral] withdrawn
"""
assert msg.sender == self.admin
assert frac <= 10**18
lm: LMGauge = self.liquidity_mining_callback
ns: int256[2] = self._read_user_tick_numbers(user)
n: int256 = ns[0]
user_shares: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns)
assert user_shares[0] > 0, "No deposits"
total_x: uint256 = 0
total_y: uint256 = 0
min_band: int256 = self.min_band
old_min_band: int256 = min_band
old_max_band: int256 = self.max_band
max_band: int256 = n - 1
for i in range(MAX_TICKS):
x: uint256 = self.bands_x[n]
y: uint256 = self.bands_y[n]
ds: uint256 = unsafe_div(frac * user_shares[i], 10**18)
user_shares[i] = unsafe_sub(user_shares[i], ds) # Can ONLY zero out when frac == 10**18
s: uint256 = self.total_shares[n]
new_shares: uint256 = s - ds
self.total_shares[n] = new_shares
s += DEAD_SHARES # after this s is guaranteed to be bigger than 0
dx: uint256 = unsafe_div((x + 1) * ds, s)
dy: uint256 = unsafe_div((y + 1) * ds, s)
x -= dx
y -= dy
# If withdrawal is the last one - transfer dust to admin fees
if new_shares == 0:
if x > 0:
self.admin_fees_x += unsafe_div(x, BORROWED_PRECISION)
if y > 0:
self.admin_fees_y += unsafe_div(y, COLLATERAL_PRECISION)
x = 0
y = 0
if n == min_band:
if x == 0:
if y == 0:
min_band += 1
if x > 0 or y > 0:
max_band = n
self.bands_x[n] = x
self.bands_y[n] = y
total_x += dx
total_y += dy
if n == ns[1]:
break
else:
n = unsafe_add(n, 1)
# Empty the ticks
if frac == 10**18:
self.user_shares[user].ticks[0] = 0
else:
self.save_user_shares(user, user_shares)
if old_min_band != min_band:
self.min_band = min_band
if old_max_band <= ns[1]:
self.max_band = max_band
total_x = unsafe_div(total_x, BORROWED_PRECISION)
total_y = unsafe_div(total_y, COLLATERAL_PRECISION)
log Withdraw(user, total_x, total_y)
if lm.address != empty(address):
lm.callback_collateral_shares(0, []) # collateral/shares ratio is unchanged
lm.callback_user_shares(user, ns[0], user_shares)
return [total_x, total_y]
```
```shell
>>> Controller.remove_collateral(10**18, False)
>>> Controller.user_state(trader)
[1000000000000000000, 0, 1000001403805330760116, 10]
# [collateral, stablecoin, debt, bands]
```
::::
### `borrow_more`
::::description[`Controller.borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender)`]
Function to borrow more assets while adding more collateral (not necessary).
| Input | Type | Description |
| ------------ | --------- | -------------------------------- |
| `collateral` | `uint256` | Amount of collateral to add |
| `debt` | `uint256` | Amount of debt to take |
| `_for` | `address` | Address to borrow for (requires approval); **V3 only** |
Emits: `UserState` and `Borrow`
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Borrow:
user: indexed(address)
collateral_increase: uint256
loan_increase: uint256
approval: public(HashMap[address, HashMap[address, bool]])
@external
@nonreentrant('lock')
def borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender):
"""
@notice Borrow more stablecoins while adding more collateral (not necessary)
@param collateral Amount of collateral to add
@param debt Amount of stablecoin debt to take
@param _for Address to borrow for
"""
if debt == 0:
return
assert self._check_approval(_for)
self._add_collateral_borrow(collateral, debt, _for, False)
self.minted += debt
self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral)
self.transfer(BORROWED_TOKEN, _for, debt)
self._save_rate()
@internal
@view
def _check_approval(_for: address) -> bool:
return msg.sender == _for or self.approval[_for][msg.sender]
@internal
def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address, remove_collateral: bool):
"""
@notice Internal method to borrow and add or remove collateral
@param d_collateral Amount of collateral to add
@param d_debt Amount of debt increase
@param _for Address to transfer tokens to
@param remove_collateral Remove collateral instead of adding
"""
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(_for)
assert debt > 0, "Loan doesn't exist"
debt += d_debt
ns: int256[2] = AMM.read_user_tick_numbers(_for)
size: uint256 = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)
xy: uint256[2] = AMM.withdraw(_for, 10**18)
assert xy[0] == 0, "Already in underwater mode"
if remove_collateral:
xy[1] -= d_collateral
else:
xy[1] += d_collateral
n1: int256 = self._calculate_debt_n1(xy[1], debt, size, _for)
n2: int256 = n1 + unsafe_sub(ns[1], ns[0])
AMM.deposit_range(_for, xy[1], n1, n2)
self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul})
liquidation_discount: uint256 = 0
if _for == msg.sender:
liquidation_discount = self.liquidation_discount
self.liquidation_discounts[_for] = liquidation_discount
else:
liquidation_discount = self.liquidation_discounts[_for]
if d_debt != 0:
self._total_debt.initial_debt = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + d_debt
self._total_debt.rate_mul = rate_mul
if remove_collateral:
log RemoveCollateral(_for, d_collateral)
else:
log Borrow(_for, d_collateral, d_debt)
log UserState(_for, xy[1], debt, n1, n2, liquidation_discount)
```
```vyper
event Deposit:
provider: indexed(address)
amount: uint256
n1: int256
n2: int256
@external
@nonreentrant('lock')
def deposit_range(user: address, amount: uint256, n1: int256, n2: int256):
"""
@notice Deposit for a user in a range of bands. Only admin contract (Controller) can do it
@param user User address
@param amount Amount of collateral to deposit
@param n1 Lower band in the deposit range
@param n2 Upper band in the deposit range
"""
assert msg.sender == self.admin
user_shares: DynArray[uint256, MAX_TICKS_UINT] = []
collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = []
n0: int256 = self.active_band
# We assume that n1,n2 area already sorted (and they are in Controller)
assert n2 < 2**127
assert n1 > -2**127
n_bands: uint256 = unsafe_add(convert(unsafe_sub(n2, n1), uint256), 1)
assert n_bands <= MAX_TICKS_UINT
y_per_band: uint256 = unsafe_div(amount * COLLATERAL_PRECISION, n_bands)
assert y_per_band > 100, "Amount too low"
assert self.user_shares[user].ticks[0] == 0 # dev: User must have no liquidity
self.user_shares[user].ns = unsafe_add(n1, unsafe_mul(n2, 2**128))
lm: LMGauge = self.liquidity_mining_callback
# Autoskip bands if we can
for i in range(MAX_SKIP_TICKS + 1):
if n1 > n0:
if i != 0:
self.active_band = n0
break
assert self.bands_x[n0] == 0 and i < MAX_SKIP_TICKS, "Deposit below current band"
n0 -= 1
for i in range(MAX_TICKS):
band: int256 = unsafe_add(n1, i)
if band > n2:
break
assert self.bands_x[band] == 0, "Band not empty"
y: uint256 = y_per_band
if i == 0:
y = amount * COLLATERAL_PRECISION - y * unsafe_sub(n_bands, 1)
total_y: uint256 = self.bands_y[band]
# Total / user share
s: uint256 = self.total_shares[band]
ds: uint256 = unsafe_div((s + DEAD_SHARES) * y, total_y + 1)
assert ds > 0, "Amount too low"
user_shares.append(ds)
s += ds
assert s <= 2**128 - 1
self.total_shares[band] = s
total_y += y
self.bands_y[band] = total_y
if lm.address != empty(address):
# If initial s == 0 - s becomes equal to y which is > 100 => nonzero
collateral_shares.append(unsafe_div(total_y * 10**18, s))
self.min_band = min(self.min_band, n1)
self.max_band = max(self.max_band, n2)
self.save_user_shares(user, user_shares)
log Deposit(user, amount, n1, n2)
if lm.address != empty(address):
lm.callback_collateral_shares(n1, collateral_shares)
lm.callback_user_shares(user, n1, user_shares)
```
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Borrow:
user: indexed(address)
collateral_increase: uint256
loan_increase: uint256
@external
@nonreentrant('lock')
def borrow_more(collateral: uint256, debt: uint256):
"""
@notice Borrow more stablecoins while adding more collateral (not necessary)
@param collateral Amount of collateral to add
@param debt Amount of stablecoin debt to take
"""
if debt == 0:
return
self._add_collateral_borrow(collateral, debt, msg.sender, False)
self.minted += debt
self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral)
self.transfer(BORROWED_TOKEN, msg.sender, debt)
self._save_rate()
@internal
def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address, remove_collateral: bool):
"""
@notice Internal method to borrow and add or remove collateral
@param d_collateral Amount of collateral to add
@param d_debt Amount of debt increase
@param _for Address to transfer tokens to
@param remove_collateral Remove collateral instead of adding
"""
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(_for)
assert debt > 0, "Loan doesn't exist"
debt += d_debt
ns: int256[2] = AMM.read_user_tick_numbers(_for)
size: uint256 = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)
xy: uint256[2] = AMM.withdraw(_for, 10**18)
assert xy[0] == 0, "Already in underwater mode"
if remove_collateral:
xy[1] -= d_collateral
else:
xy[1] += d_collateral
n1: int256 = self._calculate_debt_n1(xy[1], debt, size)
n2: int256 = n1 + unsafe_sub(ns[1], ns[0])
AMM.deposit_range(_for, xy[1], n1, n2)
self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul})
liquidation_discount: uint256 = 0
if _for == msg.sender:
liquidation_discount = self.liquidation_discount
self.liquidation_discounts[_for] = liquidation_discount
else:
liquidation_discount = self.liquidation_discounts[_for]
if d_debt != 0:
self._total_debt.initial_debt = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + d_debt
self._total_debt.rate_mul = rate_mul
if remove_collateral:
log RemoveCollateral(_for, d_collateral)
else:
log Borrow(_for, d_collateral, d_debt)
log UserState(_for, xy[1], debt, n1, n2, liquidation_discount)
```
```vyper
event Deposit:
provider: indexed(address)
amount: uint256
n1: int256
n2: int256
@external
@nonreentrant('lock')
def deposit_range(user: address, amount: uint256, n1: int256, n2: int256):
"""
@notice Deposit for a user in a range of bands. Only admin contract (Controller) can do it
@param user User address
@param amount Amount of collateral to deposit
@param n1 Lower band in the deposit range
@param n2 Upper band in the deposit range
"""
assert msg.sender == self.admin
user_shares: DynArray[uint256, MAX_TICKS_UINT] = []
collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = []
n0: int256 = self.active_band
# We assume that n1,n2 area already sorted (and they are in Controller)
assert n2 < 2**127
assert n1 > -2**127
n_bands: uint256 = unsafe_add(convert(unsafe_sub(n2, n1), uint256), 1)
assert n_bands <= MAX_TICKS_UINT
y_per_band: uint256 = unsafe_div(amount * COLLATERAL_PRECISION, n_bands)
assert y_per_band > 100, "Amount too low"
assert self.user_shares[user].ticks[0] == 0 # dev: User must have no liquidity
self.user_shares[user].ns = unsafe_add(n1, unsafe_mul(n2, 2**128))
lm: LMGauge = self.liquidity_mining_callback
# Autoskip bands if we can
for i in range(MAX_SKIP_TICKS + 1):
if n1 > n0:
if i != 0:
self.active_band = n0
break
assert self.bands_x[n0] == 0 and i < MAX_SKIP_TICKS, "Deposit below current band"
n0 -= 1
for i in range(MAX_TICKS):
band: int256 = unsafe_add(n1, i)
if band > n2:
break
assert self.bands_x[band] == 0, "Band not empty"
y: uint256 = y_per_band
if i == 0:
y = amount * COLLATERAL_PRECISION - y * unsafe_sub(n_bands, 1)
total_y: uint256 = self.bands_y[band]
# Total / user share
s: uint256 = self.total_shares[band]
ds: uint256 = unsafe_div((s + DEAD_SHARES) * y, total_y + 1)
assert ds > 0, "Amount too low"
user_shares.append(ds)
s += ds
assert s <= 2**128 - 1
self.total_shares[band] = s
total_y += y
self.bands_y[band] = total_y
if lm.address != empty(address):
# If initial s == 0 - s becomes equal to y which is > 100 => nonzero
collateral_shares.append(unsafe_div(total_y * 10**18, s))
self.min_band = min(self.min_band, n1)
self.max_band = max(self.max_band, n2)
self.save_user_shares(user, user_shares)
log Deposit(user, amount, n1, n2)
if lm.address != empty(address):
lm.callback_collateral_shares(n1, collateral_shares)
lm.callback_user_shares(user, n1, user_shares)
```
```shell
>>> Controller.borrow_more(10**18, 10**22)
>>> Controller.user_state(trader)
[2000000000000000000, 0, 11000001592726154783594, 10]
# [collateral, stablecoin, debt, bands]
```
::::
### `borrow_more_extended`
::::description[`Controller.borrow_more_extended(collateral: uint256, debt: uint256, callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b]
Function to borrow more assets while adding more collateral. This method uses callacks to build up leverage. Earlier implementations of the contract did not have `callback_bytes` argument. This was added to enable [leveraging/de-leveraging using the 1inch router](./leverage/leverage-zap-1inch.md#building-leverage).
| Input | Type | Description |
| ---------------- | --------------------- | -------------------------------- |
| `collateral` | `uint256` | Amount of collateral to add |
| `debt` | `uint256` | Amount of debt to take |
| `callabacker` | `address` | Address of the callaback contract |
| `debt` | `DynArray[uint256,5]` | Amount of debt to take on |
| `callback_args` | `DynArray[uint256,5]` | Extra arguments for the callback (up to 5), such as `min_amount`, etc; see [`LeverageZap1inch.vy`](./leverage/leverage-zap-1inch.md) for more informations |
| `callback_bytes` | `Bytes[10**4]` | Callback bytes passed to the LeverageZap. Defaults to `b""` |
| `_for` | `address` | Address to borrow for (requires approval); **V3 only** |
Emits: `UserState` and `Borrow`
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Borrow:
user: indexed(address)
collateral_increase: uint256
loan_increase: uint256
approval: public(HashMap[address, HashMap[address, bool]])
@external
@nonreentrant('lock')
def borrow_more_extended(collateral: uint256, debt: uint256, callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b"", _for: address = msg.sender):
"""
@notice Borrow more stablecoins while adding more collateral using a callback (to leverage more)
@param collateral Amount of collateral to add
@param debt Amount of stablecoin debt to take
@param callbacker Address of the callback contract
@param callback_args Extra arguments for the callback (up to 5) such as min_amount etc
@param _for Address to borrow for
"""
if debt == 0:
return
assert self._check_approval(_for)
# Before callback
self.transfer(BORROWED_TOKEN, callbacker, debt)
# For compatibility
callback_sig: bytes4 = CALLBACK_DEPOSIT_WITH_BYTES
if callback_bytes == b"":
callback_sig = CALLBACK_DEPOSIT
# Callback
# If there is any unused debt, callbacker can send it to the user
more_collateral: uint256 = self.execute_callback(
callbacker, callback_sig, _for, 0, collateral, debt, callback_args, callback_bytes).collateral
# After callback
self._add_collateral_borrow(collateral + more_collateral, debt, _for, False)
self.minted += debt
self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral)
self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral)
self._save_rate()
@internal
@view
def _check_approval(_for: address) -> bool:
return msg.sender == _for or self.approval[_for][msg.sender]
@internal
def execute_callback(callbacker: address, callback_sig: bytes4,
user: address, stablecoins: uint256, collateral: uint256, debt: uint256,
callback_args: DynArray[uint256, 5], callback_bytes: Bytes[10**4]) -> CallbackData:
assert callbacker != COLLATERAL_TOKEN.address
assert callbacker != BORROWED_TOKEN.address
data: CallbackData = empty(CallbackData)
data.active_band = AMM.active_band()
band_x: uint256 = AMM.bands_x(data.active_band)
band_y: uint256 = AMM.bands_y(data.active_band)
# Callback
response: Bytes[64] = raw_call(
callbacker,
concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)),
max_outsize=64
)
data.stablecoins = convert(slice(response, 0, 32), uint256)
data.collateral = convert(slice(response, 32, 32), uint256)
# Checks after callback
assert data.active_band == AMM.active_band()
assert band_x == AMM.bands_x(data.active_band)
assert band_y == AMM.bands_y(data.active_band)
return data
@internal
def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address, remove_collateral: bool):
"""
@notice Internal method to borrow and add or remove collateral
@param d_collateral Amount of collateral to add
@param d_debt Amount of debt increase
@param _for Address to transfer tokens to
@param remove_collateral Remove collateral instead of adding
"""
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(_for)
assert debt > 0, "Loan doesn't exist"
debt += d_debt
ns: int256[2] = AMM.read_user_tick_numbers(_for)
size: uint256 = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)
xy: uint256[2] = AMM.withdraw(_for, 10**18)
assert xy[0] == 0, "Already in underwater mode"
if remove_collateral:
xy[1] -= d_collateral
else:
xy[1] += d_collateral
n1: int256 = self._calculate_debt_n1(xy[1], debt, size, _for)
n2: int256 = n1 + unsafe_sub(ns[1], ns[0])
AMM.deposit_range(_for, xy[1], n1, n2)
self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul})
liquidation_discount: uint256 = 0
if _for == msg.sender:
liquidation_discount = self.liquidation_discount
self.liquidation_discounts[_for] = liquidation_discount
else:
liquidation_discount = self.liquidation_discounts[_for]
if d_debt != 0:
self._total_debt.initial_debt = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + d_debt
self._total_debt.rate_mul = rate_mul
if remove_collateral:
log RemoveCollateral(_for, d_collateral)
else:
log Borrow(_for, d_collateral, d_debt)
log UserState(_for, xy[1], debt, n1, n2, liquidation_discount)
```
```vyper
event Deposit:
provider: indexed(address)
amount: uint256
n1: int256
n2: int256
@external
@nonreentrant('lock')
def deposit_range(user: address, amount: uint256, n1: int256, n2: int256):
"""
@notice Deposit for a user in a range of bands. Only admin contract (Controller) can do it
@param user User address
@param amount Amount of collateral to deposit
@param n1 Lower band in the deposit range
@param n2 Upper band in the deposit range
"""
assert msg.sender == self.admin
user_shares: DynArray[uint256, MAX_TICKS_UINT] = []
collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = []
n0: int256 = self.active_band
# We assume that n1,n2 area already sorted (and they are in Controller)
assert n2 < 2**127
assert n1 > -2**127
n_bands: uint256 = unsafe_add(convert(unsafe_sub(n2, n1), uint256), 1)
assert n_bands <= MAX_TICKS_UINT
y_per_band: uint256 = unsafe_div(amount * COLLATERAL_PRECISION, n_bands)
assert y_per_band > 100, "Amount too low"
assert self.user_shares[user].ticks[0] == 0 # dev: User must have no liquidity
self.user_shares[user].ns = unsafe_add(n1, unsafe_mul(n2, 2**128))
lm: LMGauge = self.liquidity_mining_callback
# Autoskip bands if we can
for i in range(MAX_SKIP_TICKS + 1):
if n1 > n0:
if i != 0:
self.active_band = n0
break
assert self.bands_x[n0] == 0 and i < MAX_SKIP_TICKS, "Deposit below current band"
n0 -= 1
for i in range(MAX_TICKS):
band: int256 = unsafe_add(n1, i)
if band > n2:
break
assert self.bands_x[band] == 0, "Band not empty"
y: uint256 = y_per_band
if i == 0:
y = amount * COLLATERAL_PRECISION - y * unsafe_sub(n_bands, 1)
total_y: uint256 = self.bands_y[band]
# Total / user share
s: uint256 = self.total_shares[band]
ds: uint256 = unsafe_div((s + DEAD_SHARES) * y, total_y + 1)
assert ds > 0, "Amount too low"
user_shares.append(ds)
s += ds
assert s <= 2**128 - 1
self.total_shares[band] = s
total_y += y
self.bands_y[band] = total_y
if lm.address != empty(address):
# If initial s == 0 - s becomes equal to y which is > 100 => nonzero
collateral_shares.append(unsafe_div(total_y * 10**18, s))
self.min_band = min(self.min_band, n1)
self.max_band = max(self.max_band, n2)
self.save_user_shares(user, user_shares)
log Deposit(user, amount, n1, n2)
if lm.address != empty(address):
lm.callback_collateral_shares(n1, collateral_shares)
lm.callback_user_shares(user, n1, user_shares)
```
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Borrow:
user: indexed(address)
collateral_increase: uint256
loan_increase: uint256
@external
@nonreentrant('lock')
def borrow_more_extended(collateral: uint256, debt: uint256, callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b""):
"""
@notice Borrow more stablecoins while adding more collateral using a callback (to leverage more)
@param collateral Amount of collateral to add
@param debt Amount of stablecoin debt to take
@param callbacker Address of the callback contract
@param callback_args Extra arguments for the callback (up to 5) such as min_amount etc
"""
if debt == 0:
return
# Before callback
self.transfer(BORROWED_TOKEN, callbacker, debt)
# For compatibility
callback_sig: bytes4 = CALLBACK_DEPOSIT_WITH_BYTES
if callback_bytes == b"":
callback_sig = CALLBACK_DEPOSIT
# Callback
# If there is any unused debt, callbacker can send it to the user
more_collateral: uint256 = self.execute_callback(
callbacker, callback_sig, msg.sender, 0, collateral, debt, callback_args, callback_bytes).collateral
# After callback
self._add_collateral_borrow(collateral + more_collateral, debt, msg.sender, False)
self.minted += debt
self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral)
self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral)
self._save_rate()
@internal
def execute_callback(callbacker: address, callback_sig: bytes4,
user: address, stablecoins: uint256, collateral: uint256, debt: uint256,
callback_args: DynArray[uint256, 5], callback_bytes: Bytes[10**4]) -> CallbackData:
assert callbacker != COLLATERAL_TOKEN.address
data: CallbackData = empty(CallbackData)
data.active_band = AMM.active_band()
band_x: uint256 = AMM.bands_x(data.active_band)
band_y: uint256 = AMM.bands_y(data.active_band)
# Callback
response: Bytes[64] = raw_call(
callbacker,
concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)),
max_outsize=64
)
data.stablecoins = convert(slice(response, 0, 32), uint256)
data.collateral = convert(slice(response, 32, 32), uint256)
# Checks after callback
assert data.active_band == AMM.active_band()
assert band_x == AMM.bands_x(data.active_band)
assert band_y == AMM.bands_y(data.active_band)
return data
@internal
def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address, remove_collateral: bool):
"""
@notice Internal method to borrow and add or remove collateral
@param d_collateral Amount of collateral to add
@param d_debt Amount of debt increase
@param _for Address to transfer tokens to
@param remove_collateral Remove collateral instead of adding
"""
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(_for)
assert debt > 0, "Loan doesn't exist"
debt += d_debt
ns: int256[2] = AMM.read_user_tick_numbers(_for)
size: uint256 = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)
xy: uint256[2] = AMM.withdraw(_for, 10**18)
assert xy[0] == 0, "Already in underwater mode"
if remove_collateral:
xy[1] -= d_collateral
else:
xy[1] += d_collateral
n1: int256 = self._calculate_debt_n1(xy[1], debt, size)
n2: int256 = n1 + unsafe_sub(ns[1], ns[0])
AMM.deposit_range(_for, xy[1], n1, n2)
self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul})
liquidation_discount: uint256 = 0
if _for == msg.sender:
liquidation_discount = self.liquidation_discount
self.liquidation_discounts[_for] = liquidation_discount
else:
liquidation_discount = self.liquidation_discounts[_for]
if d_debt != 0:
self._total_debt.initial_debt = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + d_debt
self._total_debt.rate_mul = rate_mul
if remove_collateral:
log RemoveCollateral(_for, d_collateral)
else:
log Borrow(_for, d_collateral, d_debt)
log UserState(_for, xy[1], debt, n1, n2, liquidation_discount)
```
```vyper
event Deposit:
provider: indexed(address)
amount: uint256
n1: int256
n2: int256
@external
@nonreentrant('lock')
def deposit_range(user: address, amount: uint256, n1: int256, n2: int256):
"""
@notice Deposit for a user in a range of bands. Only admin contract (Controller) can do it
@param user User address
@param amount Amount of collateral to deposit
@param n1 Lower band in the deposit range
@param n2 Upper band in the deposit range
"""
assert msg.sender == self.admin
user_shares: DynArray[uint256, MAX_TICKS_UINT] = []
collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = []
n0: int256 = self.active_band
# We assume that n1,n2 area already sorted (and they are in Controller)
assert n2 < 2**127
assert n1 > -2**127
n_bands: uint256 = unsafe_add(convert(unsafe_sub(n2, n1), uint256), 1)
assert n_bands <= MAX_TICKS_UINT
y_per_band: uint256 = unsafe_div(amount * COLLATERAL_PRECISION, n_bands)
assert y_per_band > 100, "Amount too low"
assert self.user_shares[user].ticks[0] == 0 # dev: User must have no liquidity
self.user_shares[user].ns = unsafe_add(n1, unsafe_mul(n2, 2**128))
lm: LMGauge = self.liquidity_mining_callback
# Autoskip bands if we can
for i in range(MAX_SKIP_TICKS + 1):
if n1 > n0:
if i != 0:
self.active_band = n0
break
assert self.bands_x[n0] == 0 and i < MAX_SKIP_TICKS, "Deposit below current band"
n0 -= 1
for i in range(MAX_TICKS):
band: int256 = unsafe_add(n1, i)
if band > n2:
break
assert self.bands_x[band] == 0, "Band not empty"
y: uint256 = y_per_band
if i == 0:
y = amount * COLLATERAL_PRECISION - y * unsafe_sub(n_bands, 1)
total_y: uint256 = self.bands_y[band]
# Total / user share
s: uint256 = self.total_shares[band]
ds: uint256 = unsafe_div((s + DEAD_SHARES) * y, total_y + 1)
assert ds > 0, "Amount too low"
user_shares.append(ds)
s += ds
assert s <= 2**128 - 1
self.total_shares[band] = s
total_y += y
self.bands_y[band] = total_y
if lm.address != empty(address):
# If initial s == 0 - s becomes equal to y which is > 100 => nonzero
collateral_shares.append(unsafe_div(total_y * 10**18, s))
self.min_band = min(self.min_band, n1)
self.max_band = max(self.max_band, n2)
self.save_user_shares(user, user_shares)
log Deposit(user, amount, n1, n2)
if lm.address != empty(address):
lm.callback_collateral_shares(n1, collateral_shares)
lm.callback_user_shares(user, n1, user_shares)
```
::::
### `health_calculator`
::::description[`Controller.health_calculator(user: address, d_collateral: int256, d_debt: int256, full: bool, N: uint256 = 0) -> int256`]
Function to predict the health of `user` after changing collateral by `d_collateral` and/or debt by `d_debt`.
Returns: health (`int256`).
| Input | Type | Description |
| -------------- | --------- | -------------------------------------------- |
| `user` | `address` | Address of the user |
| `d_collateral` | `int256` | Change in collateral amount |
| `d_debt` | `int256` | Change in debt amount |
| `full` | `bool` | Weather to take into account the price difference above the highest user's band |
| `N` | `uint256` | Number of bands in case loan does not exist yet |
```vyper
@external
@view
@nonreentrant('lock')
def health_calculator(user: address, d_collateral: int256, d_debt: int256, full: bool, N: uint256 = 0) -> int256:
"""
@notice Health predictor in case user changes the debt or collateral
@param user Address of the user
@param d_collateral Change in collateral amount (signed)
@param d_debt Change in debt amount (signed)
@param full Whether it's a 'full' health or not
@param N Number of bands in case loan doesn't yet exist
@return Signed health value
"""
ns: int256[2] = AMM.read_user_tick_numbers(user)
debt: int256 = convert(self._debt(user)[0], int256)
n: uint256 = N
ld: int256 = 0
if debt != 0:
ld = convert(self.liquidation_discounts[user], int256)
n = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)
else:
ld = convert(self.liquidation_discount, int256)
ns[0] = max_value(int256) # This will trigger a "re-deposit"
n1: int256 = 0
collateral: int256 = 0
x_eff: int256 = 0
debt += d_debt
assert debt > 0, "Non-positive debt"
active_band: int256 = AMM.active_band_with_skip()
if ns[0] > active_band: # re-deposit
collateral = convert(AMM.get_sum_xy(user)[1], int256) + d_collateral
n1 = self._calculate_debt_n1(convert(collateral, uint256), convert(debt, uint256), n, user)
collateral *= convert(COLLATERAL_PRECISION, int256) # now has 18 decimals
else:
n1 = ns[0]
x_eff = convert(AMM.get_x_down(user) * unsafe_mul(10**18, BORROWED_PRECISION), int256)
debt *= convert(BORROWED_PRECISION, int256)
p0: int256 = convert(AMM.p_oracle_up(n1), int256)
if ns[0] > active_band:
x_eff = convert(self.get_y_effective(convert(collateral, uint256), n, 0), int256) * p0
health: int256 = unsafe_div(x_eff, debt)
health = health - unsafe_div(health * ld, 10**18) - 10**18
if full:
if n1 > active_band: # We are not in liquidation mode
p_diff: int256 = max(p0, convert(AMM.price_oracle(), int256)) - p0
if p_diff > 0:
health += unsafe_div(p_diff * collateral, debt)
return health
```
```vyper
@external
@view
@nonreentrant('lock')
def health_calculator(user: address, d_collateral: int256, d_debt: int256, full: bool, N: uint256 = 0) -> int256:
"""
@notice Health predictor in case user changes the debt or collateral
@param user Address of the user
@param d_collateral Change in collateral amount (signed)
@param d_debt Change in debt amount (signed)
@param full Whether it's a 'full' health or not
@param N Number of bands in case loan doesn't yet exist
@return Signed health value
"""
ns: int256[2] = AMM.read_user_tick_numbers(user)
debt: int256 = convert(self._debt(user)[0], int256)
n: uint256 = N
ld: int256 = 0
if debt != 0:
ld = convert(self.liquidation_discounts[user], int256)
n = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)
else:
ld = convert(self.liquidation_discount, int256)
ns[0] = max_value(int256) # This will trigger a "re-deposit"
n1: int256 = 0
collateral: int256 = 0
x_eff: int256 = 0
debt += d_debt
assert debt > 0, "Non-positive debt"
active_band: int256 = AMM.active_band_with_skip()
if ns[0] > active_band: # re-deposit
collateral = convert(AMM.get_sum_xy(user)[1], int256) + d_collateral
n1 = self._calculate_debt_n1(convert(collateral, uint256), convert(debt, uint256), n)
collateral *= convert(COLLATERAL_PRECISION, int256) # now has 18 decimals
else:
n1 = ns[0]
x_eff = convert(AMM.get_x_down(user) * unsafe_mul(10**18, BORROWED_PRECISION), int256)
debt *= convert(BORROWED_PRECISION, int256)
p0: int256 = convert(AMM.p_oracle_up(n1), int256)
if ns[0] > active_band:
x_eff = convert(self.get_y_effective(convert(collateral, uint256), n, 0), int256) * p0
health: int256 = unsafe_div(x_eff, debt)
health = health - unsafe_div(health * ld, 10**18) - 10**18
if full:
if n1 > active_band: # We are not in liquidation mode
p_diff: int256 = max(p0, convert(AMM.price_oracle(), int256)) - p0
if p_diff > 0:
health += unsafe_div(p_diff * collateral, debt)
return health
```
This example shows the calculated health based on changes in collateral and borrowed assets. For instance, in the first example, the predicted health is calculated by adding 1 WBTC as collateral and taking on an additional 10,000 crvUSD in debt.
```shell
>>> Controller.health_calculator(trader, 10**18, 10**22, True, 0)
5026488624797598934
>>> Controller.health_calculator(trader, 10**18, 10**22, False, 0)
40995665483999083
```
::::
### `liquidate`
::::description[`Controller.liquidate(user: address, min_x: uint256, use_eth: bool = True)`]
Function to perform a bad liquidation (or self-liquidation) of `user` if `health` is not good.
| Input | Type | Description |
| -------- | --------- | -------------------------------------------------------------------- |
| `user` | `address` | Address to be liquidated |
| `min_x` | `uint256` | Minimal amount of asset to receive (to avoid liquidators being sandwiched) |
| `use_eth`| `bool` | Use wrapping/unwrapping if collateral is ETH |
Emits: `UserState`, `Repay`, and `Liquidate`
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Repay:
user: indexed(address)
collateral_decrease: uint256
loan_decrease: uint256
event Liquidate:
liquidator: indexed(address)
user: indexed(address)
collateral_received: uint256
stablecoin_received: uint256
debt: uint256
@external
@nonreentrant('lock')
def liquidate(user: address, min_x: uint256, use_eth: bool = True):
"""
@notice Peform a bad liquidation (or self-liquidation) of user if health is not good
@param min_x Minimal amount of stablecoin to receive (to avoid liquidators being sandwiched)
@param use_eth Use wrapping/unwrapping if collateral is ETH
"""
discount: uint256 = 0
if user != msg.sender:
discount = self.liquidation_discounts[user]
self._liquidate(user, min_x, discount, 10**18, use_eth, empty(address), [])
@internal
def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint256,
callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b""):
"""
@notice Perform a bad liquidation of user if the health is too bad
@param user Address of the user
@param min_x Minimal amount of stablecoin withdrawn (to avoid liquidators being sandwiched)
@param health_limit Minimal health to liquidate at
@param frac Fraction to liquidate; 100% = 10**18
@param callbacker Address of the callback contract
@param callback_args Extra arguments for the callback (up to 5) such as min_amount etc
"""
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(user)
if health_limit != 0:
assert self._health(user, debt, True, health_limit) < 0, "Not enough rekt"
final_debt: uint256 = debt
debt = unsafe_div(debt * frac, 10**18)
assert debt > 0
final_debt = unsafe_sub(final_debt, debt)
# Withdraw sender's stablecoin and collateral to our contract
# When frac is set - we withdraw a bit less for the same debt fraction
# f_remove = ((1 + h/2) / (1 + h) * (1 - frac) + frac) * frac
# where h is health limit.
# This is less than full h discount but more than no discount
xy: uint256[2] = AMM.withdraw(user, self._get_f_remove(frac, health_limit)) # [stable, collateral]
# x increase in same block -> price up -> good
# x decrease in same block -> price down -> bad
assert xy[0] >= min_x, "Slippage"
min_amm_burn: uint256 = min(xy[0], debt)
self.transferFrom(BORROWED_TOKEN, AMM.address, self, min_amm_burn)
if debt > xy[0]:
to_repay: uint256 = unsafe_sub(debt, xy[0])
if callbacker == empty(address):
# Withdraw collateral if no callback is present
self.transferFrom(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1])
# Request what's left from user
self.transferFrom(BORROWED_TOKEN, msg.sender, self, to_repay)
else:
# Move collateral to callbacker, call it and remove everything from it back in
self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1])
# For compatibility
callback_sig: bytes4 = CALLBACK_LIQUIDATE_WITH_BYTES
if callback_bytes == b"":
callback_sig = CALLBACK_LIQUIDATE
# Callback
cb: CallbackData = self.execute_callback(
callbacker, callback_sig, user, xy[0], xy[1], debt, callback_args, callback_bytes)
assert cb.stablecoins >= to_repay, "not enough proceeds"
if cb.stablecoins > to_repay:
self.transferFrom(BORROWED_TOKEN, callbacker, msg.sender, unsafe_sub(cb.stablecoins, to_repay))
self.transferFrom(BORROWED_TOKEN, callbacker, self, to_repay)
self.transferFrom(COLLATERAL_TOKEN, callbacker, msg.sender, cb.collateral)
else:
# Withdraw collateral
self.transferFrom(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1])
# Return what's left to user
if xy[0] > debt:
self.transferFrom(BORROWED_TOKEN, AMM.address, msg.sender, unsafe_sub(xy[0], debt))
self.redeemed += debt
self.loan[user] = Loan({initial_debt: final_debt, rate_mul: rate_mul})
log Repay(user, xy[1], debt)
log Liquidate(msg.sender, user, xy[1], xy[0], debt)
if final_debt == 0:
log UserState(user, 0, 0, 0, 0, 0) # Not logging partial removeal b/c we have not enough info
self._remove_from_list(user)
d: uint256 = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul
self._total_debt.initial_debt = unsafe_sub(max(d, debt), debt)
self._total_debt.rate_mul = rate_mul
self._save_rate()
```
```vyper
event Withdraw:
provider: indexed(address)
amount_borrowed: uint256
amount_collateral: uint256
@external
@nonreentrant('lock')
def withdraw(user: address, frac: uint256) -> uint256[2]:
"""
@notice Withdraw liquidity for the user. Only admin contract can do it
@param user User who owns liquidity
@param frac Fraction to withdraw (1e18 being 100%)
@return Amount of [stablecoins, collateral] withdrawn
"""
assert msg.sender == self.admin
assert frac <= 10**18
lm: LMGauge = self.liquidity_mining_callback
ns: int256[2] = self._read_user_tick_numbers(user)
n: int256 = ns[0]
user_shares: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns)
assert user_shares[0] > 0, "No deposits"
total_x: uint256 = 0
total_y: uint256 = 0
min_band: int256 = self.min_band
old_min_band: int256 = min_band
old_max_band: int256 = self.max_band
max_band: int256 = n - 1
for i in range(MAX_TICKS):
x: uint256 = self.bands_x[n]
y: uint256 = self.bands_y[n]
ds: uint256 = unsafe_div(frac * user_shares[i], 10**18)
user_shares[i] = unsafe_sub(user_shares[i], ds) # Can ONLY zero out when frac == 10**18
s: uint256 = self.total_shares[n]
new_shares: uint256 = s - ds
self.total_shares[n] = new_shares
s += DEAD_SHARES # after this s is guaranteed to be bigger than 0
dx: uint256 = unsafe_div((x + 1) * ds, s)
dy: uint256 = unsafe_div((y + 1) * ds, s)
x -= dx
y -= dy
# If withdrawal is the last one - transfer dust to admin fees
if new_shares == 0:
if x > 0:
self.admin_fees_x += unsafe_div(x, BORROWED_PRECISION)
if y > 0:
self.admin_fees_y += unsafe_div(y, COLLATERAL_PRECISION)
x = 0
y = 0
if n == min_band:
if x == 0:
if y == 0:
min_band += 1
if x > 0 or y > 0:
max_band = n
self.bands_x[n] = x
self.bands_y[n] = y
total_x += dx
total_y += dy
if n == ns[1]:
break
else:
n = unsafe_add(n, 1)
# Empty the ticks
if frac == 10**18:
self.user_shares[user].ticks[0] = 0
else:
self.save_user_shares(user, user_shares)
if old_min_band != min_band:
self.min_band = min_band
if old_max_band <= ns[1]:
self.max_band = max_band
total_x = unsafe_div(total_x, BORROWED_PRECISION)
total_y = unsafe_div(total_y, COLLATERAL_PRECISION)
log Withdraw(user, total_x, total_y)
if lm.address != empty(address):
lm.callback_collateral_shares(0, []) # collateral/shares ratio is unchanged
lm.callback_user_shares(user, ns[0], user_shares)
return [total_x, total_y]
```
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Repay:
user: indexed(address)
collateral_decrease: uint256
loan_decrease: uint256
event Liquidate:
liquidator: indexed(address)
user: indexed(address)
collateral_received: uint256
stablecoin_received: uint256
debt: uint256
@external
@nonreentrant('lock')
def liquidate(user: address, min_x: uint256, use_eth: bool = True):
"""
@notice Peform a bad liquidation (or self-liquidation) of user if health is not good
@param min_x Minimal amount of stablecoin to receive (to avoid liquidators being sandwiched)
@param use_eth Use wrapping/unwrapping if collateral is ETH
"""
discount: uint256 = 0
if user != msg.sender:
discount = self.liquidation_discounts[user]
self._liquidate(user, min_x, discount, 10**18, use_eth, empty(address), [])
@internal
def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint256,
callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b""):
"""
@notice Perform a bad liquidation of user if the health is too bad
@param user Address of the user
@param min_x Minimal amount of stablecoin withdrawn (to avoid liquidators being sandwiched)
@param health_limit Minimal health to liquidate at
@param frac Fraction to liquidate; 100% = 10**18
@param callbacker Address of the callback contract
@param callback_args Extra arguments for the callback (up to 5) such as min_amount etc
"""
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(user)
if health_limit != 0:
assert self._health(user, debt, True, health_limit) < 0, "Not enough rekt"
final_debt: uint256 = debt
debt = unsafe_div(debt * frac, 10**18)
assert debt > 0
final_debt = unsafe_sub(final_debt, debt)
# Withdraw sender's stablecoin and collateral to our contract
# When frac is set - we withdraw a bit less for the same debt fraction
# f_remove = ((1 + h/2) / (1 + h) * (1 - frac) + frac) * frac
# where h is health limit.
# This is less than full h discount but more than no discount
xy: uint256[2] = AMM.withdraw(user, self._get_f_remove(frac, health_limit)) # [stable, collateral]
# x increase in same block -> price up -> good
# x decrease in same block -> price down -> bad
assert xy[0] >= min_x, "Slippage"
min_amm_burn: uint256 = min(xy[0], debt)
self.transferFrom(BORROWED_TOKEN, AMM.address, self, min_amm_burn)
if debt > xy[0]:
to_repay: uint256 = unsafe_sub(debt, xy[0])
if callbacker == empty(address):
# Withdraw collateral if no callback is present
self.transferFrom(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1])
# Request what's left from user
self.transferFrom(BORROWED_TOKEN, msg.sender, self, to_repay)
else:
# Move collateral to callbacker, call it and remove everything from it back in
self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1])
# For compatibility
callback_sig: bytes4 = CALLBACK_LIQUIDATE_WITH_BYTES
if callback_bytes == b"":
callback_sig = CALLBACK_LIQUIDATE
# Callback
cb: CallbackData = self.execute_callback(
callbacker, callback_sig, user, xy[0], xy[1], debt, callback_args, callback_bytes)
assert cb.stablecoins >= to_repay, "not enough proceeds"
if cb.stablecoins > to_repay:
self.transferFrom(BORROWED_TOKEN, callbacker, msg.sender, unsafe_sub(cb.stablecoins, to_repay))
self.transferFrom(BORROWED_TOKEN, callbacker, self, to_repay)
self.transferFrom(COLLATERAL_TOKEN, callbacker, msg.sender, cb.collateral)
else:
# Withdraw collateral
self.transferFrom(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1])
# Return what's left to user
if xy[0] > debt:
self.transferFrom(BORROWED_TOKEN, AMM.address, msg.sender, unsafe_sub(xy[0], debt))
self.redeemed += debt
self.loan[user] = Loan({initial_debt: final_debt, rate_mul: rate_mul})
log Repay(user, xy[1], debt)
log Liquidate(msg.sender, user, xy[1], xy[0], debt)
if final_debt == 0:
log UserState(user, 0, 0, 0, 0, 0) # Not logging partial removeal b/c we have not enough info
self._remove_from_list(user)
d: uint256 = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul
self._total_debt.initial_debt = unsafe_sub(max(d, debt), debt)
self._total_debt.rate_mul = rate_mul
self._save_rate()
```
```vyper
event Withdraw:
provider: indexed(address)
amount_borrowed: uint256
amount_collateral: uint256
@external
@nonreentrant('lock')
def withdraw(user: address, frac: uint256) -> uint256[2]:
"""
@notice Withdraw liquidity for the user. Only admin contract can do it
@param user User who owns liquidity
@param frac Fraction to withdraw (1e18 being 100%)
@return Amount of [stablecoins, collateral] withdrawn
"""
assert msg.sender == self.admin
assert frac <= 10**18
lm: LMGauge = self.liquidity_mining_callback
ns: int256[2] = self._read_user_tick_numbers(user)
n: int256 = ns[0]
user_shares: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns)
assert user_shares[0] > 0, "No deposits"
total_x: uint256 = 0
total_y: uint256 = 0
min_band: int256 = self.min_band
old_min_band: int256 = min_band
old_max_band: int256 = self.max_band
max_band: int256 = n - 1
for i in range(MAX_TICKS):
x: uint256 = self.bands_x[n]
y: uint256 = self.bands_y[n]
ds: uint256 = unsafe_div(frac * user_shares[i], 10**18)
user_shares[i] = unsafe_sub(user_shares[i], ds) # Can ONLY zero out when frac == 10**18
s: uint256 = self.total_shares[n]
new_shares: uint256 = s - ds
self.total_shares[n] = new_shares
s += DEAD_SHARES # after this s is guaranteed to be bigger than 0
dx: uint256 = unsafe_div((x + 1) * ds, s)
dy: uint256 = unsafe_div((y + 1) * ds, s)
x -= dx
y -= dy
# If withdrawal is the last one - transfer dust to admin fees
if new_shares == 0:
if x > 0:
self.admin_fees_x += unsafe_div(x, BORROWED_PRECISION)
if y > 0:
self.admin_fees_y += unsafe_div(y, COLLATERAL_PRECISION)
x = 0
y = 0
if n == min_band:
if x == 0:
if y == 0:
min_band += 1
if x > 0 or y > 0:
max_band = n
self.bands_x[n] = x
self.bands_y[n] = y
total_x += dx
total_y += dy
if n == ns[1]:
break
else:
n = unsafe_add(n, 1)
# Empty the ticks
if frac == 10**18:
self.user_shares[user].ticks[0] = 0
else:
self.save_user_shares(user, user_shares)
if old_min_band != min_band:
self.min_band = min_band
if old_max_band <= ns[1]:
self.max_band = max_band
total_x = unsafe_div(total_x, BORROWED_PRECISION)
total_y = unsafe_div(total_y, COLLATERAL_PRECISION)
log Withdraw(user, total_x, total_y)
if lm.address != empty(address):
lm.callback_collateral_shares(0, []) # collateral/shares ratio is unchanged
lm.callback_user_shares(user, ns[0], user_shares)
return [total_x, total_y]
```
```shell
>>> soon
```
::::
### `liquidate_extended`
::::description[`Controller.liquidate_extended(user: address, min_x: uint256, frac: uint256, callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b]
Extended function to perform a bad liquidation (or self-liquidation) of `user` if `health` is not good using callbacks. Earlier implementations of the contract did not have `callback_bytes` argument. This was added to enable [leveraging/de-leveraging using the 1inch router](./leverage/leverage-zap-1inch.md).
| Input | Type | Description |
| ---------------- | --------------------- | -------------------------------------------------------------------------- |
| `user` | `address` | Address to be liquidated |
| `min_x` | `uint256` | Minimal amount of assets to receive (to avoid liquidators being sandwiched) |
| `frac` | `uint256` | Fraction to liquidate; 100% = 10**18 |
| `use_eth` | `bool` | Use wrapping/unwrapping if collateral is ETH |
| `callbacker` | `address` | Address of the callback contract |
| `callback_args` | `DynArray[uint256,5]` | Extra arguments for the callback (up to 5), such as `min_amount` |
| `callback_bytes` | `Bytes[10**4]` | Callback bytes passed to the LeverageZap. Defaults to `b""` |
Emits: `Repay` and `Liquidate`
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Repay:
user: indexed(address)
collateral_decrease: uint256
loan_decrease: uint256
event Liquidate:
liquidator: indexed(address)
user: indexed(address)
collateral_received: uint256
stablecoin_received: uint256
debt: uint256
@external
@nonreentrant('lock')
def liquidate_extended(user: address, min_x: uint256, frac: uint256,
callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b""):
"""
@notice Peform a bad liquidation (or self-liquidation) of user if health is not good
@param min_x Minimal amount of stablecoin to receive (to avoid liquidators being sandwiched)
@param frac Fraction to liquidate; 100% = 10**18
@param callbacker Address of the callback contract
@param callback_args Extra arguments for the callback (up to 5) such as min_amount etc
"""
discount: uint256 = 0
if user != msg.sender:
discount = self.liquidation_discounts[user]
self._liquidate(user, min_x, discount, min(frac, 10**18), callbacker, callback_args, callback_bytes)
@internal
def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint256,
callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b""):
"""
@notice Perform a bad liquidation of user if the health is too bad
@param user Address of the user
@param min_x Minimal amount of stablecoin withdrawn (to avoid liquidators being sandwiched)
@param health_limit Minimal health to liquidate at
@param frac Fraction to liquidate; 100% = 10**18
@param callbacker Address of the callback contract
@param callback_args Extra arguments for the callback (up to 5) such as min_amount etc
"""
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(user)
if health_limit != 0:
assert self._health(user, debt, True, health_limit) < 0, "Not enough rekt"
final_debt: uint256 = debt
debt = unsafe_div(debt * frac, 10**18)
assert debt > 0
final_debt = unsafe_sub(final_debt, debt)
# Withdraw sender's stablecoin and collateral to our contract
# When frac is set - we withdraw a bit less for the same debt fraction
# f_remove = ((1 + h/2) / (1 + h) * (1 - frac) + frac) * frac
# where h is health limit.
# This is less than full h discount but more than no discount
xy: uint256[2] = AMM.withdraw(user, self._get_f_remove(frac, health_limit)) # [stable, collateral]
# x increase in same block -> price up -> good
# x decrease in same block -> price down -> bad
assert xy[0] >= min_x, "Slippage"
min_amm_burn: uint256 = min(xy[0], debt)
self.transferFrom(BORROWED_TOKEN, AMM.address, self, min_amm_burn)
if debt > xy[0]:
to_repay: uint256 = unsafe_sub(debt, xy[0])
if callbacker == empty(address):
# Withdraw collateral if no callback is present
self.transferFrom(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1])
# Request what's left from user
self.transferFrom(BORROWED_TOKEN, msg.sender, self, to_repay)
else:
# Move collateral to callbacker, call it and remove everything from it back in
self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1])
# For compatibility
callback_sig: bytes4 = CALLBACK_LIQUIDATE_WITH_BYTES
if callback_bytes == b"":
callback_sig = CALLBACK_LIQUIDATE
# Callback
cb: CallbackData = self.execute_callback(
callbacker, callback_sig, user, xy[0], xy[1], debt, callback_args, callback_bytes)
assert cb.stablecoins >= to_repay, "not enough proceeds"
if cb.stablecoins > to_repay:
self.transferFrom(BORROWED_TOKEN, callbacker, msg.sender, unsafe_sub(cb.stablecoins, to_repay))
self.transferFrom(BORROWED_TOKEN, callbacker, self, to_repay)
self.transferFrom(COLLATERAL_TOKEN, callbacker, msg.sender, cb.collateral)
else:
# Withdraw collateral
self.transferFrom(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1])
# Return what's left to user
if xy[0] > debt:
self.transferFrom(BORROWED_TOKEN, AMM.address, msg.sender, unsafe_sub(xy[0], debt))
self.redeemed += debt
self.loan[user] = Loan({initial_debt: final_debt, rate_mul: rate_mul})
log Repay(user, xy[1], debt)
log Liquidate(msg.sender, user, xy[1], xy[0], debt)
if final_debt == 0:
log UserState(user, 0, 0, 0, 0, 0) # Not logging partial removeal b/c we have not enough info
self._remove_from_list(user)
d: uint256 = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul
self._total_debt.initial_debt = unsafe_sub(max(d, debt), debt)
self._total_debt.rate_mul = rate_mul
self._save_rate()
```
```vyper
event Withdraw:
provider: indexed(address)
amount_borrowed: uint256
amount_collateral: uint256
@external
@nonreentrant('lock')
def withdraw(user: address, frac: uint256) -> uint256[2]:
"""
@notice Withdraw liquidity for the user. Only admin contract can do it
@param user User who owns liquidity
@param frac Fraction to withdraw (1e18 being 100%)
@return Amount of [stablecoins, collateral] withdrawn
"""
assert msg.sender == self.admin
assert frac <= 10**18
lm: LMGauge = self.liquidity_mining_callback
ns: int256[2] = self._read_user_tick_numbers(user)
n: int256 = ns[0]
user_shares: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns)
assert user_shares[0] > 0, "No deposits"
total_x: uint256 = 0
total_y: uint256 = 0
min_band: int256 = self.min_band
old_min_band: int256 = min_band
old_max_band: int256 = self.max_band
max_band: int256 = n - 1
for i in range(MAX_TICKS):
x: uint256 = self.bands_x[n]
y: uint256 = self.bands_y[n]
ds: uint256 = unsafe_div(frac * user_shares[i], 10**18)
user_shares[i] = unsafe_sub(user_shares[i], ds) # Can ONLY zero out when frac == 10**18
s: uint256 = self.total_shares[n]
new_shares: uint256 = s - ds
self.total_shares[n] = new_shares
s += DEAD_SHARES # after this s is guaranteed to be bigger than 0
dx: uint256 = unsafe_div((x + 1) * ds, s)
dy: uint256 = unsafe_div((y + 1) * ds, s)
x -= dx
y -= dy
# If withdrawal is the last one - transfer dust to admin fees
if new_shares == 0:
if x > 0:
self.admin_fees_x += unsafe_div(x, BORROWED_PRECISION)
if y > 0:
self.admin_fees_y += unsafe_div(y, COLLATERAL_PRECISION)
x = 0
y = 0
if n == min_band:
if x == 0:
if y == 0:
min_band += 1
if x > 0 or y > 0:
max_band = n
self.bands_x[n] = x
self.bands_y[n] = y
total_x += dx
total_y += dy
if n == ns[1]:
break
else:
n = unsafe_add(n, 1)
# Empty the ticks
if frac == 10**18:
self.user_shares[user].ticks[0] = 0
else:
self.save_user_shares(user, user_shares)
if old_min_band != min_band:
self.min_band = min_band
if old_max_band <= ns[1]:
self.max_band = max_band
total_x = unsafe_div(total_x, BORROWED_PRECISION)
total_y = unsafe_div(total_y, COLLATERAL_PRECISION)
log Withdraw(user, total_x, total_y)
if lm.address != empty(address):
lm.callback_collateral_shares(0, []) # collateral/shares ratio is unchanged
lm.callback_user_shares(user, ns[0], user_shares)
return [total_x, total_y]
```
```vyper
event UserState:
user: indexed(address)
collateral: uint256
debt: uint256
n1: int256
n2: int256
liquidation_discount: uint256
event Repay:
user: indexed(address)
collateral_decrease: uint256
loan_decrease: uint256
event Liquidate:
liquidator: indexed(address)
user: indexed(address)
collateral_received: uint256
stablecoin_received: uint256
debt: uint256
@external
@nonreentrant('lock')
def liquidate_extended(user: address, min_x: uint256, frac: uint256,
callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b""):
"""
@notice Peform a bad liquidation (or self-liquidation) of user if health is not good
@param min_x Minimal amount of stablecoin to receive (to avoid liquidators being sandwiched)
@param frac Fraction to liquidate; 100% = 10**18
@param callbacker Address of the callback contract
@param callback_args Extra arguments for the callback (up to 5) such as min_amount etc
"""
discount: uint256 = 0
if user != msg.sender:
discount = self.liquidation_discounts[user]
self._liquidate(user, min_x, discount, min(frac, 10**18), callbacker, callback_args, callback_bytes)
@internal
def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint256,
callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b""):
"""
@notice Perform a bad liquidation of user if the health is too bad
@param user Address of the user
@param min_x Minimal amount of stablecoin withdrawn (to avoid liquidators being sandwiched)
@param health_limit Minimal health to liquidate at
@param frac Fraction to liquidate; 100% = 10**18
@param callbacker Address of the callback contract
@param callback_args Extra arguments for the callback (up to 5) such as min_amount etc
"""
debt: uint256 = 0
rate_mul: uint256 = 0
debt, rate_mul = self._debt(user)
if health_limit != 0:
assert self._health(user, debt, True, health_limit) < 0, "Not enough rekt"
final_debt: uint256 = debt
debt = unsafe_div(debt * frac, 10**18)
assert debt > 0
final_debt = unsafe_sub(final_debt, debt)
# Withdraw sender's stablecoin and collateral to our contract
# When frac is set - we withdraw a bit less for the same debt fraction
# f_remove = ((1 + h/2) / (1 + h) * (1 - frac) + frac) * frac
# where h is health limit.
# This is less than full h discount but more than no discount
xy: uint256[2] = AMM.withdraw(user, self._get_f_remove(frac, health_limit)) # [stable, collateral]
# x increase in same block -> price up -> good
# x decrease in same block -> price down -> bad
assert xy[0] >= min_x, "Slippage"
min_amm_burn: uint256 = min(xy[0], debt)
self.transferFrom(BORROWED_TOKEN, AMM.address, self, min_amm_burn)
if debt > xy[0]:
to_repay: uint256 = unsafe_sub(debt, xy[0])
if callbacker == empty(address):
# Withdraw collateral if no callback is present
self.transferFrom(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1])
# Request what's left from user
self.transferFrom(BORROWED_TOKEN, msg.sender, self, to_repay)
else:
# Move collateral to callbacker, call it and remove everything from it back in
self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1])
# For compatibility
callback_sig: bytes4 = CALLBACK_LIQUIDATE_WITH_BYTES
if callback_bytes == b"":
callback_sig = CALLBACK_LIQUIDATE
# Callback
cb: CallbackData = self.execute_callback(
callbacker, callback_sig, user, xy[0], xy[1], debt, callback_args, callback_bytes)
assert cb.stablecoins >= to_repay, "not enough proceeds"
if cb.stablecoins > to_repay:
self.transferFrom(BORROWED_TOKEN, callbacker, msg.sender, unsafe_sub(cb.stablecoins, to_repay))
self.transferFrom(BORROWED_TOKEN, callbacker, self, to_repay)
self.transferFrom(COLLATERAL_TOKEN, callbacker, msg.sender, cb.collateral)
else:
# Withdraw collateral
self.transferFrom(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1])
# Return what's left to user
if xy[0] > debt:
self.transferFrom(BORROWED_TOKEN, AMM.address, msg.sender, unsafe_sub(xy[0], debt))
self.redeemed += debt
self.loan[user] = Loan({initial_debt: final_debt, rate_mul: rate_mul})
log Repay(user, xy[1], debt)
log Liquidate(msg.sender, user, xy[1], xy[0], debt)
if final_debt == 0:
log UserState(user, 0, 0, 0, 0, 0) # Not logging partial removeal b/c we have not enough info
self._remove_from_list(user)
d: uint256 = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul
self._total_debt.initial_debt = unsafe_sub(max(d, debt), debt)
self._total_debt.rate_mul = rate_mul
self._save_rate()
```
```vyper
event Withdraw:
provider: indexed(address)
amount_borrowed: uint256
amount_collateral: uint256
@external
@nonreentrant('lock')
def withdraw(user: address, frac: uint256) -> uint256[2]:
"""
@notice Withdraw liquidity for the user. Only admin contract can do it
@param user User who owns liquidity
@param frac Fraction to withdraw (1e18 being 100%)
@return Amount of [stablecoins, collateral] withdrawn
"""
assert msg.sender == self.admin
assert frac <= 10**18
lm: LMGauge = self.liquidity_mining_callback
ns: int256[2] = self._read_user_tick_numbers(user)
n: int256 = ns[0]
user_shares: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns)
assert user_shares[0] > 0, "No deposits"
total_x: uint256 = 0
total_y: uint256 = 0
min_band: int256 = self.min_band
old_min_band: int256 = min_band
old_max_band: int256 = self.max_band
max_band: int256 = n - 1
for i in range(MAX_TICKS):
x: uint256 = self.bands_x[n]
y: uint256 = self.bands_y[n]
ds: uint256 = unsafe_div(frac * user_shares[i], 10**18)
user_shares[i] = unsafe_sub(user_shares[i], ds) # Can ONLY zero out when frac == 10**18
s: uint256 = self.total_shares[n]
new_shares: uint256 = s - ds
self.total_shares[n] = new_shares
s += DEAD_SHARES # after this s is guaranteed to be bigger than 0
dx: uint256 = unsafe_div((x + 1) * ds, s)
dy: uint256 = unsafe_div((y + 1) * ds, s)
x -= dx
y -= dy
# If withdrawal is the last one - transfer dust to admin fees
if new_shares == 0:
if x > 0:
self.admin_fees_x += unsafe_div(x, BORROWED_PRECISION)
if y > 0:
self.admin_fees_y += unsafe_div(y, COLLATERAL_PRECISION)
x = 0
y = 0
if n == min_band:
if x == 0:
if y == 0:
min_band += 1
if x > 0 or y > 0:
max_band = n
self.bands_x[n] = x
self.bands_y[n] = y
total_x += dx
total_y += dy
if n == ns[1]:
break
else:
n = unsafe_add(n, 1)
# Empty the ticks
if frac == 10**18:
self.user_shares[user].ticks[0] = 0
else:
self.save_user_shares(user, user_shares)
if old_min_band != min_band:
self.min_band = min_band
if old_max_band <= ns[1]:
self.max_band = max_band
total_x = unsafe_div(total_x, BORROWED_PRECISION)
total_y = unsafe_div(total_y, COLLATERAL_PRECISION)
log Withdraw(user, total_x, total_y)
if lm.address != empty(address):
lm.callback_collateral_shares(0, []) # collateral/shares ratio is unchanged
lm.callback_user_shares(user, ns[0], user_shares)
return [total_x, total_y]
```
```shell
>>> soon
```
::::
### `tokens_to_liquidate`
::::description[`Controller.tokens_to_liquidate(user: address, frac: uint256 = 10 **18) -> uint256`]
Function to calculate the amount of assets to have in a liquidator's wallet in order to liquidate a user.
Returns: amount of tokens needed (`uint256`).
| Input | Type | Description |
| -------- | --------- | ------------------------------------------- |
| `user` | `address` | Address of the user to liquidate |
| `frac` | `uint256` | Fraction to liquidate; 100% = 10**18 |
```vyper
@view
@external
@nonreentrant('lock')
def tokens_to_liquidate(user: address, frac: uint256 = 10 **18) -> uint256:
"""
@notice Calculate the amount of stablecoins to have in liquidator's wallet to liquidate a user
@param user Address of the user to liquidate
@param frac Fraction to liquidate; 100% = 10**18
@return The amount of stablecoins needed
"""
health_limit: uint256 = 0
if user != msg.sender:
health_limit = self.liquidation_discounts[user]
stablecoins: uint256 = unsafe_div(AMM.get_sum_xy(user)[0] * self._get_f_remove(frac, health_limit), 10 **18)
debt: uint256 = unsafe_div(self._debt(user)[0] * frac, 10 **18)
return unsafe_sub(max(debt, stablecoins), stablecoins)
```
```vyper
@view
@external
@nonreentrant('lock')
def tokens_to_liquidate(user: address, frac: uint256 = 10 **18) -> uint256:
"""
@notice Calculate the amount of stablecoins to have in liquidator's wallet to liquidate a user
@param user Address of the user to liquidate
@param frac Fraction to liquidate; 100% = 10**18
@return The amount of stablecoins needed
"""
health_limit: uint256 = 0
if user != msg.sender:
health_limit = self.liquidation_discounts[user]
stablecoins: uint256 = unsafe_div(AMM.get_sum_xy(user)[0] * self._get_f_remove(frac, health_limit), 10 **18)
debt: uint256 = unsafe_div(self._debt(user)[0] * frac, 10 **18)
return unsafe_sub(max(debt, stablecoins), stablecoins)
```
This example shows the amount of the borrowed asset required to liquidate the user.
```shell
>>> Controller.tokens_to_liquidate(trader)
10000067519253003373620
```
::::
### `users_to_liquidate`
::::description[`Controller.users_to_liquidate(_from: uint256=0, _limit: uint256=0) -> DynArray[Position, 1000]`]
Getter for a dynamic array of users who can be hard-liquidated.
Returns: detailed info about positions of users that can be hard-liquidated (`DynArray[Position, 1000]`).
| Input | Type | Description |
| --------- | -------- | -------------------------------------------------- |
| `_from` | `uint256` | Loan index to start iteration from; defaults to 0 |
| `_limit` | `uint256` | Number of loans to look over; defaults to 0 |
```vyper
@view
@external
@nonreentrant('lock')
def users_to_liquidate(_from: uint256=0, _limit: uint256=0) -> DynArray[Position, 1000]:
"""
@notice Returns a dynamic array of users who can be "hard-liquidated".
This method is designed for convenience of liquidation bots.
@param _from Loan index to start iteration from
@param _limit Number of loans to look over
@return Dynamic array with detailed info about positions of users
"""
n_loans: uint256 = self.n_loans
limit: uint256 = _limit
if _limit == 0:
limit = n_loans
ix: uint256 = _from
out: DynArray[Position, 1000] = []
for i in range(10**6):
if ix >= n_loans or i == limit:
break
user: address = self.loans[ix]
debt: uint256 = self._debt(user)[0]
health: int256 = self._health(user, debt, True, self.liquidation_discounts[user])
if health < 0:
xy: uint256[2] = AMM.get_sum_xy(user)
out.append(Position({
user: user,
x: xy[0],
y: xy[1],
debt: debt,
health: health
}))
ix += 1
return out
```
```vyper
@view
@external
@nonreentrant('lock')
def users_to_liquidate(_from: uint256=0, _limit: uint256=0) -> DynArray[Position, 1000]:
"""
@notice Returns a dynamic array of users who can be "hard-liquidated".
This method is designed for convenience of liquidation bots.
@param _from Loan index to start iteration from
@param _limit Number of loans to look over
@return Dynamic array with detailed info about positions of users
"""
n_loans: uint256 = self.n_loans
limit: uint256 = _limit
if _limit == 0:
limit = n_loans
ix: uint256 = _from
out: DynArray[Position, 1000] = []
for i in range(10**6):
if ix >= n_loans or i == limit:
break
user: address = self.loans[ix]
debt: uint256 = self._debt(user)[0]
health: int256 = self._health(user, debt, True, self.liquidation_discounts[user])
if health < 0:
xy: uint256[2] = AMM.get_sum_xy(user)
out.append(Position({
user: user,
x: xy[0],
y: xy[1],
debt: debt,
health: health
}))
ix += 1
return out
```
This example returns a list of all users with negative health and therefore eligible for hard liquidation. In this case, no positions are eligible.
```shell
>>> Controller.users_to_liquidate(0)
[]
```
::::
---
## Loan Info Methods
*All user information, such as `debt`, `health`, etc., is stored within the Controller contract.*
### `debt`
::::description[`Controller.debt(user: address) -> uint256`]
Getter for the amount of debt for `user`. Constantly increases due to the charged interest rate.
Returns: debt (`uint256`).
| Input | Type | Description |
| ------ | --------- | --------------- |
| `user` | `address` | User Address |
```vyper
struct Loan:
initial_debt: uint256
rate_mul: uint256
@external
@view
@nonreentrant('lock')
def debt(user: address) -> uint256:
"""
@notice Get the value of debt without changing the state
@param user User address
@return Value of debt
"""
return self._debt(user)[0]
@internal
@view
def _debt(user: address) -> (uint256, uint256):
"""
@notice Get the value of debt and rate_mul and update the rate_mul counter
@param user User address
@return (debt, rate_mul)
"""
rate_mul: uint256 = AMM.get_rate_mul()
loan: Loan = self.loan[user]
if loan.initial_debt == 0:
return (0, rate_mul)
else:
# Let user repay 1 smallest decimal more so that the system doesn't lose on precision
# Use ceil div
debt: uint256 = loan.initial_debt * rate_mul
if debt % loan.rate_mul > 0: # if only one loan -> don't have to do it
if self.n_loans > 1:
debt += loan.rate_mul
debt = unsafe_div(debt, loan.rate_mul) # loan.rate_mul is nonzero because we just had % successful
return (debt, rate_mul)
```
```vyper
struct Loan:
initial_debt: uint256
rate_mul: uint256
@external
@view
@nonreentrant('lock')
def debt(user: address) -> uint256:
"""
@notice Get the value of debt without changing the state
@param user User address
@return Value of debt
"""
return self._debt(user)[0]
@internal
@view
def _debt(user: address) -> (uint256, uint256):
"""
@notice Get the value of debt and rate_mul and update the rate_mul counter
@param user User address
@return (debt, rate_mul)
"""
rate_mul: uint256 = AMM.get_rate_mul()
loan: Loan = self.loan[user]
if loan.initial_debt == 0:
return (0, rate_mul)
else:
# Let user repay 1 smallest decimal more so that the system doesn't lose on precision
# Use ceil div
debt: uint256 = loan.initial_debt * rate_mul
if debt % loan.rate_mul > 0: # if only one loan -> don't have to do it
if self.n_loans > 1:
debt += loan.rate_mul
debt /= loan.rate_mul
return (debt, rate_mul)
```
This example shows the debt of a specific user.
```shell
>>> Controller.debt(trader)
11000001592726154783594
```
::::
### `total_debt`
::::description[`Controller.total_debt() -> uint256`]
Getter for the total debt of the Controller.
Returns: total debt (`uint256`).
```vyper
struct Loan:
initial_debt: uint256
rate_mul: uint256
_total_debt: Loan
# No decorator because used in monetary policy
@external
@view
def total_debt() -> uint256:
"""
@notice Total debt of this controller
"""
rate_mul: uint256 = AMM.get_rate_mul()
loan: Loan = self._total_debt
return loan.initial_debt * rate_mul / loan.rate_mul
```
```vyper
struct Loan:
initial_debt: uint256
rate_mul: uint256
_total_debt: Loan
# No decorator because used in monetary policy
@external
@view
def total_debt() -> uint256:
"""
@notice Total debt of this controller
"""
rate_mul: uint256 = AMM.get_rate_mul()
loan: Loan = self._total_debt
return loan.initial_debt * rate_mul / loan.rate_mul
```
This example shows the total debt of the market.
```shell
>>> Controller.total_debt()
4047221089417662821708552
```
::::
### `loan_exists`
::::description[`Controller.loan_exists(user: address) -> bool`]
Function to check if a loan for `user` exists.
Returns: true or false (`bool`).
| Input | Type | Description |
| ------ | --------- | ------------ |
| `user` | `address` | User address |
```vyper
struct Loan:
initial_debt: uint256
rate_mul: uint256
@external
@view
@nonreentrant('lock')
def loan_exists(user: address) -> bool:
"""
@notice Check whether there is a loan of `user` in existence
"""
return self.loan[user].initial_debt > 0
```
```vyper
struct Loan:
initial_debt: uint256
rate_mul: uint256
@external
@view
@nonreentrant('lock')
def loan_exists(user: address) -> bool:
"""
@notice Check whether there is a loan of `user` in existence
"""
return self.loan[user].initial_debt > 0
```
This example shows if a loan exists for a specific user.
```shell
>>> Controller.loan_exists(trader)
'True'
>>> Controller.loan_exists("0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B")
'False'
```
::::
### `user_prices`
::::description[`Controller.user_prices(user: address) -> uint256[2]`]
Getter for the highest price of the upper band and the lowest price of the lower band the user has deposited in the AMM. This is essentially the liquidation price range of the loan.
Returns: upper and lower band price (`uint256`).
| Input | Type | Description |
| ------ | --------- | ------------ |
| `user` | `address` | User address |
```vyper
@view
@external
@nonreentrant('lock')
def user_prices(user: address) -> uint256[2]: # Upper, lower
"""
@notice Lowest price of the lower band and highest price of the upper band the user has deposit in the AMM
@param user User address
@return (upper_price, lower_price)
"""
assert AMM.has_liquidity(user)
ns: int256[2] = AMM.read_user_tick_numbers(user) # ns[1] > ns[0]
return [AMM.p_oracle_up(ns[0]), AMM.p_oracle_down(ns[1])]
```
```vyper
@external
@view
@nonreentrant('lock')
def has_liquidity(user: address) -> bool:
"""
@notice Check if `user` has any liquidity in the AMM
"""
return self.user_shares[user].ticks[0] != 0
@external
@view
@nonreentrant('lock')
def read_user_tick_numbers(user: address) -> int256[2]:
"""
@notice Unpacks and reads user tick numbers
@param user User address
@return Lowest and highest band the user deposited into
"""
return self._read_user_tick_numbers(user)
@internal
@view
def _read_user_tick_numbers(user: address) -> int256[2]:
"""
@notice Unpacks and reads user tick numbers
@param user User address
@return Lowest and highest band the user deposited into
"""
ns: int256 = self.user_shares[user].ns
n2: int256 = unsafe_div(ns, 2**128)
n1: int256 = ns % 2**128
if n1 >= 2**127:
n1 = unsafe_sub(n1, 2**128)
n2 = unsafe_add(n2, 1)
return [n1, n2]
@external
@view
def p_oracle_up(n: int256) -> uint256:
"""
@notice Highest oracle price for the band to have liquidity when p = p_oracle
@param n Band number (can be negative)
@return Price at 1e18 base
"""
return self._p_oracle_up(n)
@external
@view
def p_oracle_down(n: int256) -> uint256:
"""
@notice Lowest oracle price for the band to have liquidity when p = p_oracle
@param n Band number (can be negative)
@return Price at 1e18 base
"""
return self._p_oracle_up(n + 1)
@internal
@view
def _p_oracle_up(n: int256) -> uint256:
"""
@notice Upper oracle price for the band to have liquidity when p = p_oracle
@param n Band number (can be negative)
@return Price at 1e18 base
"""
# p_oracle_up(n) = p_base * ((A - 1) / A) **n
# p_oracle_down(n) = p_base * ((A - 1) / A) **(n + 1) = p_oracle_up(n+1)
# return unsafe_div(self._base_price() * self.exp_int(-n * LOG_A_RATIO), 10**18)
power: int256 = -n * LOG_A_RATIO
# ((A - 1) / A) **n = exp(-n * A / (A - 1)) = exp(-n * LOG_A_RATIO)
## Exp implementation based on solmate's
assert power > -42139678854452767551
assert power < 135305999368893231589
x: int256 = unsafe_div(unsafe_mul(power, 2**96), 10**18)
k: int256 = unsafe_div(
unsafe_add(
unsafe_div(unsafe_mul(x, 2**96), 54916777467707473351141471128),
2**95),
2**96)
x = unsafe_sub(x, unsafe_mul(k, 54916777467707473351141471128))
y: int256 = unsafe_add(x, 1346386616545796478920950773328)
y = unsafe_add(unsafe_div(unsafe_mul(y, x), 2**96), 57155421227552351082224309758442)
p: int256 = unsafe_sub(unsafe_add(y, x), 94201549194550492254356042504812)
p = unsafe_add(unsafe_div(unsafe_mul(p, y), 2**96), 28719021644029726153956944680412240)
p = unsafe_add(unsafe_mul(p, x), (4385272521454847904659076985693276 * 2**96))
q: int256 = x - 2855989394907223263936484059900
q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 50020603652535783019961831881945)
q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 533845033583426703283633433725380)
q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 3604857256930695427073651918091429)
q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 14423608567350463180887372962807573)
q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 26449188498355588339934803723976023)
exp_result: uint256 = shift(
unsafe_mul(convert(unsafe_div(p, q), uint256), 3822833074963236453042738258902158003155416615667),
unsafe_sub(k, 195))
## End exp
return unsafe_div(self._base_price() * exp_result, 10**18)
```
```vyper
@view
@external
@nonreentrant('lock')
def user_prices(user: address) -> uint256[2]: # Upper, lower
"""
@notice Lowest price of the lower band and highest price of the upper band the user has deposit in the AMM
@param user User address
@return (upper_price, lower_price)
"""
assert AMM.has_liquidity(user)
ns: int256[2] = AMM.read_user_tick_numbers(user) # ns[1] > ns[0]
return [AMM.p_oracle_up(ns[0]), AMM.p_oracle_down(ns[1])]
```
```vyper
@external
@view
@nonreentrant('lock')
def has_liquidity(user: address) -> bool:
"""
@notice Check if `user` has any liquidity in the AMM
"""
return self.user_shares[user].ticks[0] != 0
@external
@view
@nonreentrant('lock')
def read_user_tick_numbers(user: address) -> int256[2]:
"""
@notice Unpacks and reads user tick numbers
@param user User address
@return Lowest and highest band the user deposited into
"""
return self._read_user_tick_numbers(user)
@internal
@view
def _read_user_tick_numbers(user: address) -> int256[2]:
"""
@notice Unpacks and reads user tick numbers
@param user User address
@return Lowest and highest band the user deposited into
"""
ns: int256 = self.user_shares[user].ns
n2: int256 = unsafe_div(ns, 2**128)
n1: int256 = ns % 2**128
if n1 >= 2**127:
n1 = unsafe_sub(n1, 2**128)
n2 = unsafe_add(n2, 1)
return [n1, n2]
@external
@view
def p_oracle_up(n: int256) -> uint256:
"""
@notice Highest oracle price for the band to have liquidity when p = p_oracle
@param n Band number (can be negative)
@return Price at 1e18 base
"""
return self._p_oracle_up(n)
@external
@view
def p_oracle_down(n: int256) -> uint256:
"""
@notice Lowest oracle price for the band to have liquidity when p = p_oracle
@param n Band number (can be negative)
@return Price at 1e18 base
"""
return self._p_oracle_up(n + 1)
@internal
@view
def _p_oracle_up(n: int256) -> uint256:
"""
@notice Upper oracle price for the band to have liquidity when p = p_oracle
@param n Band number (can be negative)
@return Price at 1e18 base
"""
# p_oracle_up(n) = p_base * ((A - 1) / A) **n
# p_oracle_down(n) = p_base * ((A - 1) / A) **(n + 1) = p_oracle_up(n+1)
# return unsafe_div(self._base_price() * self.exp_int(-n * LOG_A_RATIO), 10**18)
power: int256 = -n * LOG_A_RATIO
# ((A - 1) / A) **n = exp(-n * A / (A - 1)) = exp(-n * LOG_A_RATIO)
## Exp implementation based on solmate's
assert power > -42139678854452767551
assert power < 135305999368893231589
x: int256 = unsafe_div(unsafe_mul(power, 2**96), 10**18)
k: int256 = unsafe_div(
unsafe_add(
unsafe_div(unsafe_mul(x, 2**96), 54916777467707473351141471128),
2**95),
2**96)
x = unsafe_sub(x, unsafe_mul(k, 54916777467707473351141471128))
y: int256 = unsafe_add(x, 1346386616545796478920950773328)
y = unsafe_add(unsafe_div(unsafe_mul(y, x), 2**96), 57155421227552351082224309758442)
p: int256 = unsafe_sub(unsafe_add(y, x), 94201549194550492254356042504812)
p = unsafe_add(unsafe_div(unsafe_mul(p, y), 2**96), 28719021644029726153956944680412240)
p = unsafe_add(unsafe_mul(p, x), (4385272521454847904659076985693276 * 2**96))
q: int256 = x - 2855989394907223263936484059900
q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 50020603652535783019961831881945)
q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 533845033583426703283633433725380)
q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 3604857256930695427073651918091429)
q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 14423608567350463180887372962807573)
q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 26449188498355588339934803723976023)
exp_result: uint256 = shift(
unsafe_mul(convert(unsafe_div(p, q), uint256), 3822833074963236453042738258902158003155416615667),
unsafe_sub(k, 195))
## End exp
return unsafe_div(self._base_price() * exp_result, 10**18)
```
This example shows the liquidation range of a user's loan. In this case, the liquidation range would be between 64,018 and 57,897.
```shell
>>> Controller.users_price(trader):
[6401870706098817273644, 5789737113118292909562]
```
::::
### `health`
::::description[`Controller.health(user: address, full: bool = False) -> int256`]
Getter for the health of `user`'s loan normalized to 1e18. If health is lower than 0, the loan can be hard-liquidated.
Returns: health (`int256`).
| Input | Type | Description |
| ------ | --------- | ------------------------------------------------------------------- |
| `user` | `address` | User address |
| `full` | `bool` | Whether to take into account the price difference above the highest user's band |
```vyper
@view
@external
@nonreentrant('lock')
def health(user: address, full: bool = False) -> int256:
"""
@notice Returns position health normalized to 1e18 for the user.
Liquidation starts when < 0, however devaluation of collateral doesn't cause liquidation
"""
return self._health(user, self._debt(user)[0], full, self.liquidation_discounts[user])
@internal
@view
def _health(user: address, debt: uint256, full: bool, liquidation_discount: uint256) -> int256:
"""
@notice Returns position health normalized to 1e18 for the user.
Liquidation starts when < 0, however devaluation of collateral doesn't cause liquidation
@param user User address to calculate health for
@param debt The amount of debt to calculate health for
@param full Whether to take into account the price difference above the highest user's band
@param liquidation_discount Liquidation discount to use (can be 0)
@return Health: > 0 = good.
"""
assert debt > 0, "Loan doesn't exist"
health: int256 = 10**18 - convert(liquidation_discount, int256)
health = unsafe_div(convert(AMM.get_x_down(user), int256) * health, convert(debt, int256)) - 10**18
if full:
ns0: int256 = AMM.read_user_tick_numbers(user)[0] # ns[1] > ns[0]
if ns0 > AMM.active_band(): # We are not in liquidation mode
p: uint256 = AMM.price_oracle()
p_up: uint256 = AMM.p_oracle_up(ns0)
if p > p_up:
health += convert(unsafe_div(unsafe_sub(p, p_up) * AMM.get_sum_xy(user)[1] * COLLATERAL_PRECISION, debt * BORROWED_PRECISION), int256)
return health
```
```vyper
@view
@external
@nonreentrant('lock')
def health(user: address, full: bool = False) -> int256:
"""
@notice Returns position health normalized to 1e18 for the user.
Liquidation starts when < 0, however devaluation of collateral doesn't cause liquidation
"""
return self._health(user, self._debt(user)[0], full, self.liquidation_discounts[user])
@internal
@view
def _health(user: address, debt: uint256, full: bool, liquidation_discount: uint256) -> int256:
"""
@notice Returns position health normalized to 1e18 for the user.
Liquidation starts when < 0, however devaluation of collateral doesn't cause liquidation
@param user User address to calculate health for
@param debt The amount of debt to calculate health for
@param full Whether to take into account the price difference above the highest user's band
@param liquidation_discount Liquidation discount to use (can be 0)
@return Health: > 0 = good.
"""
assert debt > 0, "Loan doesn't exist"
health: int256 = 10**18 - convert(liquidation_discount, int256)
health = unsafe_div(convert(AMM.get_x_down(user), int256) * health, convert(debt, int256)) - 10**18
if full:
ns0: int256 = AMM.read_user_tick_numbers(user)[0] # ns[1] > ns[0]
if ns0 > AMM.active_band(): # We are not in liquidation mode
p: uint256 = AMM.price_oracle()
p_up: uint256 = AMM.p_oracle_up(ns0)
if p > p_up:
health += convert(unsafe_div(unsafe_sub(p, p_up) * AMM.get_sum_xy(user)[1] * COLLATERAL_PRECISION, debt * BORROWED_PRECISION), int256)
return health
```
These examples show the health of a loan, once by taking the price differences above the user's highest band into account, and once without.
```shell
>>> Controller.health(trader, True)
6703636365754288577
>>> Controller.health(trader, False)
40947705194891925
```
::::
### `user_state`
::::description[`Controller.user_state(user: address) -> uint256[4]`]
Getter for `user`'s state.
Returns: collateral, stablecoin, debt, and number of bands (`uint256`).
| Input | Type | Description |
| ------ | --------- | ---------------------------------- |
| `user` | `address` | User address to return state for |
```vyper
@view
@external
@nonreentrant('lock')
def user_state(user: address) -> uint256[4]:
"""
@notice Return the user state in one call
@param user User to return the state for
@return (collateral, stablecoin, debt, N)
"""
xy: uint256[2] = AMM.get_sum_xy(user)
ns: int256[2] = AMM.read_user_tick_numbers(user) # ns[1] > ns[0]
return [xy[1], xy[0], self._debt_ro(user), convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)]
```
```vyper
@external
@view
@nonreentrant('lock')
def get_sum_xy(user: address) -> uint256[2]:
"""
@notice A low-gas function to measure amounts of stablecoins and collateral which user currently owns
@param user User address
@return Amounts of (stablecoin, collateral) in a tuple
"""
xy: DynArray[uint256, MAX_TICKS_UINT][2] = self._get_xy(user, True)
return [xy[0][0], xy[1][0]]
@external
@view
@nonreentrant('lock')
def read_user_tick_numbers(user: address) -> int256[2]:
"""
@notice Unpacks and reads user tick numbers
@param user User address
@return Lowest and highest band the user deposited into
"""
return self._read_user_tick_numbers(user)
@internal
@view
def _read_user_tick_numbers(user: address) -> int256[2]:
"""
@notice Unpacks and reads user tick numbers
@param user User address
@return Lowest and highest band the user deposited into
"""
ns: int256 = self.user_shares[user].ns
n2: int256 = unsafe_div(ns, 2**128)
n1: int256 = ns % 2**128
if n1 >= 2**127:
n1 = unsafe_sub(n1, 2**128)
n2 = unsafe_add(n2, 1)
return [n1, n2]
```
```vyper
@view
@external
@nonreentrant('lock')
def user_state(user: address) -> uint256[4]:
"""
@notice Return the user state in one call
@param user User to return the state for
@return (collateral, stablecoin, debt, N)
"""
xy: uint256[2] = AMM.get_sum_xy(user)
ns: int256[2] = AMM.read_user_tick_numbers(user) # ns[1] > ns[0]
return [xy[1], xy[0], self._debt_ro(user), convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)]
```
```vyper
@external
@view
@nonreentrant('lock')
def get_sum_xy(user: address) -> uint256[2]:
"""
@notice A low-gas function to measure amounts of stablecoins and collateral which user currently owns
@param user User address
@return Amounts of (stablecoin, collateral) in a tuple
"""
xy: DynArray[uint256, MAX_TICKS_UINT][2] = self._get_xy(user, True)
return [xy[0][0], xy[1][0]]
@external
@view
@nonreentrant('lock')
def read_user_tick_numbers(user: address) -> int256[2]:
"""
@notice Unpacks and reads user tick numbers
@param user User address
@return Lowest and highest band the user deposited into
"""
return self._read_user_tick_numbers(user)
@internal
@view
def _read_user_tick_numbers(user: address) -> int256[2]:
"""
@notice Unpacks and reads user tick numbers
@param user User address
@return Lowest and highest band the user deposited into
"""
ns: int256 = self.user_shares[user].ns
n2: int256 = unsafe_div(ns, 2**128)
n1: int256 = ns % 2**128
if n1 >= 2**127:
n1 = unsafe_sub(n1, 2**128)
n2 = unsafe_add(n2, 1)
return [n1, n2]
```
This example returns the state of a user's loan, including the collateral composition consisting of collateral and borrowable tokens, the debt, and the number of bands used.
```shell
>>> Controller.user_state(trader)
[2000000000000000000, 0, 11000001592726154783594, 10]
```
::::
### `loans`
::::description[`Controller.loans(arg0: uint256) -> address: view`]
Getter for the user address that created a loan at index `arg0`. Only loans with debt greater than 0 are included. Liquidated ones get removed.
Returns: user (`address`).
| Input | Type | Description |
| ------ | --------- | ----------- |
| `arg0` | `uint256` | Loan index |
```vyper
loans: public(address[2**64 - 1]) # Enumerate existing loans
@internal
def _remove_from_list(_for: address):
last_loan_ix: uint256 = self.n_loans - 1
loan_ix: uint256 = self.loan_ix[_for]
assert self.loans[loan_ix] == _for # dev: should never fail but safety first
self.loan_ix[_for] = 0
if loan_ix < last_loan_ix: # Need to replace
last_loan: address = self.loans[last_loan_ix]
self.loans[loan_ix] = last_loan
self.loan_ix[last_loan] = loan_ix
self.n_loans = last_loan_ix
```
```vyper
loans: public(address[2**64 - 1]) # Enumerate existing loans
@internal
def _remove_from_list(_for: address):
last_loan_ix: uint256 = self.n_loans - 1
loan_ix: uint256 = self.loan_ix[_for]
assert self.loans[loan_ix] == _for # dev: should never fail but safety first
self.loan_ix[_for] = 0
if loan_ix < last_loan_ix: # Need to replace
last_loan: address = self.loans[last_loan_ix]
self.loans[loan_ix] = last_loan
self.loan_ix[last_loan] = loan_ix
self.n_loans = last_loan_ix
```
```shell
>>> Controller.loans(0)
'0x10E47fC06ede0CD8C43E2A7ea438BEfcF45BCAa8'
>>> Controller.loans(21)
'0x3ee18B2214AFF97000D974cf647E7C347E8fa585'
```
::::
### `loan_ix`
::::description[`Controller.loan_ix(arg0: address) -> uint256: view`]
Getter for the user's loan in the list. Only loans with debt greater than 0 are included. Liquidated ones get removed.
Returns: index (`uint256`).
| Input | Type | Description |
| ------ | --------- | -------------- |
| `arg0` | `address` | User address. |
```vyper
loan_ix: public(HashMap[address, uint256]) # Position of the loan in the list
```
```vyper
loan_ix: public(HashMap[address, uint256]) # Position of the loan in the list
```
```shell
>>> Controller.loans_ix(trader)
21
```
::::
### `n_loans`
::::description[`Controller.n_loans() -> uint256: view`]
Getter for the total number of existing loans. This variable is increased by one when a loan is created and decreased by one when a loan is fully repaid.
Returns: number of active loans (`uint256`).
```vyper
n_loans: public(uint256) # Number of nonzero loans
@internal
def _remove_from_list(_for: address):
last_loan_ix: uint256 = self.n_loans - 1
loan_ix: uint256 = self.loan_ix[_for]
assert self.loans[loan_ix] == _for # dev: should never fail but safety first
self.loan_ix[_for] = 0
if loan_ix < last_loan_ix: # Need to replace
last_loan: address = self.loans[last_loan_ix]
self.loans[loan_ix] = last_loan
self.loan_ix[last_loan] = loan_ix
self.n_loans = last_loan_ix
```
```vyper
n_loans: public(uint256) # Number of nonzero loans
@internal
def _remove_from_list(_for: address):
last_loan_ix: uint256 = self.n_loans - 1
loan_ix: uint256 = self.loan_ix[_for]
assert self.loans[loan_ix] == _for # dev: should never fail but safety first
self.loan_ix[_for] = 0
if loan_ix < last_loan_ix: # Need to replace
last_loan: address = self.loans[last_loan_ix]
self.loans[loan_ix] = last_loan
self.loan_ix[last_loan] = loan_ix
self.n_loans = last_loan_ix
```
```shell
>>> Controller.n_loans()
22
```
::::
---
## Fees
*There are two types of fees:*
1. `Borrowing-based fee`: Borrowers pay **interest** on the debt borrowed.
2. `AMM-based fee`: **Swap fee** for trades within the AMM. There is also the option for an **admin fee**, but at the time of writing, admin fees are set to zero[^2], meaning all swap fees go to the liquidity providers, who are the borrowers themselves.
[^2]: Technically, admin fees within the AMMs are not zero. Currently, the admin fees of the AMMs are set to 1 (= 1/1e18), making them virtually nonexistent. The reason for this is to increase oracle manipulation resistance.
Both fees can be determined by the DAO. To change the borrowing-based fee, a new monetary policy contract needs to be set via [`set_monetary_policy`](#set_monetary_policy).
Changing the AMM fee can be done through [`set_amm_fee`](#set_amm_fee), and admin fees through [`set_admin_fee`](#set_amm_admin_fee).
### `admin_fees`
::::description[`Controller.admin_fees() -> uint256`]
Getter for the claimable admin fees. Claimable by calling [`colletct_fees`](#collect_fees).
Returns: admin fees (`uint256`).
```vyper
struct Loan:
initial_debt: uint256
rate_mul: uint256
@external
@view
def admin_fees() -> uint256:
"""
@notice Calculate the amount of fees obtained from the interest
"""
rate_mul: uint256 = AMM.get_rate_mul()
loan: Loan = self._total_debt
loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul + self.redeemed
minted: uint256 = self.minted
return unsafe_sub(max(loan.initial_debt, minted), minted)
```
```vyper
@external
@view
def get_rate_mul() -> uint256:
"""
@notice Rate multiplier which is 1.0 + integral(rate, dt)
@return Rate multiplier in units where 1.0 == 1e18
"""
return self._rate_mul()
@internal
@view
def _rate_mul() -> uint256:
"""
@notice Rate multiplier which is 1.0 + integral(rate, dt)
@return Rate multiplier in units where 1.0 == 1e18
"""
return unsafe_div(self.rate_mul * (10**18 + self.rate * (block.timestamp - self.rate_time)), 10**18)
```
```vyper
struct Loan:
initial_debt: uint256
rate_mul: uint256
@external
@view
def admin_fees() -> uint256:
"""
@notice Calculate the amount of fees obtained from the interest
"""
rate_mul: uint256 = AMM.get_rate_mul()
loan: Loan = self._total_debt
loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul + self.redeemed
minted: uint256 = self.minted
return unsafe_sub(max(loan.initial_debt, minted), minted)
```
```vyper
@external
@view
def get_rate_mul() -> uint256:
"""
@notice Rate multiplier which is 1.0 + integral(rate, dt)
@return Rate multiplier in units where 1.0 == 1e18
"""
return self._rate_mul()
@internal
@view
def _rate_mul() -> uint256:
"""
@notice Rate multiplier which is 1.0 + integral(rate, dt)
@return Rate multiplier in units where 1.0 == 1e18
"""
return unsafe_div(self.rate_mul * (10**18 + self.rate * (block.timestamp - self.rate_time)), 10**18)
```
```shell
>>> Controller.admin_fees()
1431079351921267396706
```
::::
### `set_amm_fee`
::::description[`Controller.set_amm_fee(fee: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the `Factory`.
:::
Function to set the AMM fee. The new fee value should be between `MIN_FEE` (10**6) and `MAX_FEE` (10**17).
Emits: `SetFee`
| Input | Type | Description |
| ----- | --------- | ------------- |
| `fee` | `uint256` | New fee value |
```vyper
MIN_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0
MAX_FEE: constant(uint256) = 10**17 # 10%
# AMM has nonreentrant decorator
@external
def set_amm_fee(fee: uint256):
"""
@notice Set the AMM fee (factory admin only)
@param fee The fee which should be no higher than MAX_FEE
"""
assert msg.sender == FACTORY.admin()
assert fee <= MAX_FEE and fee >= MIN_FEE, "Fee"
AMM.set_fee(fee)
```
```vyper
event SetFee:
fee: uint256
fee: public(uint256)
@external
@nonreentrant('lock')
def set_fee(fee: uint256):
"""
@notice Set AMM fee
@param fee Fee where 1e18 == 100%
"""
assert msg.sender == self.admin
self.fee = fee
log SetFee(fee)
```
```vyper
MIN_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0
MAX_FEE: constant(uint256) = 10**17 # 10%
# AMM has nonreentrant decorator
@external
def set_amm_fee(fee: uint256):
"""
@notice Set the AMM fee (factory admin only)
@param fee The fee which should be no higher than MAX_FEE
"""
assert msg.sender == FACTORY.admin()
assert fee <= MAX_FEE and fee >= MIN_FEE, "Fee"
AMM.set_fee(fee)
```
```vyper
event SetFee:
fee: uint256
fee: public(uint256)
@external
@nonreentrant('lock')
def set_fee(fee: uint256):
"""
@notice Set AMM fee
@param fee Fee where 1e18 == 100%
"""
assert msg.sender == self.admin
self.fee = fee
log SetFee(fee)
```
```shell
>>> soon
```
::::
### `set_amm_admin_fee`
::::description[`Controller.set_amm_admin_fee(fee: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the `Factory`.
:::
:::info[V1/V2 Only]
This function was removed in Controller V3. In V3 (and Llamalend), admin fees are set to zero and cannot be charged.
:::
Function to set the AMM admin fee. Maximum admin fee is 50%.
Emits: `SetAdminFee`
| Input | Type | Description |
| ----- | --------- | ------------- |
| `fee` | `uint256` | New admin fee |
```vyper
MAX_ADMIN_FEE: constant(uint256) = 5 * 10**17 # 50%
# AMM has nonreentrant decorator
@external
def set_amm_admin_fee(fee: uint256):
"""
@notice Set AMM's admin fee
@param fee New admin fee (not higher than MAX_ADMIN_FEE)
"""
assert msg.sender == FACTORY.admin()
assert fee <= MAX_ADMIN_FEE, "High fee"
AMM.set_admin_fee(fee)
```
```vyper
event SetAdminFee:
fee: uint256
admin_fee: public(uint256)
@external
@nonreentrant('lock')
def set_admin_fee(fee: uint256):
"""
@notice Set admin fee - fraction of the AMM fee to go to admin
@param fee Admin fee where 1e18 == 100%
"""
assert msg.sender == self.admin
self.admin_fee = fee
log SetAdminFee(fee)
```
```shell
>>> Controller.set_amm_admin_fee(1):
```
::::
### `collect_fees`
::::description[`Controller.collect_fees()`]
Function to collects all fees, including borrwing-based fees and AMM-based fees (if there are any). Collected fees are sent to the `fee_receiver` specified in the [Factory](./factory.md#fee-receiver).
Emits: `CollectFees`
```vyper
event CollectFees:
amount: uint256
new_supply: uint256
@external
@nonreentrant('lock')
def collect_fees() -> uint256:
"""
@notice Collect the fees charged as interest.
None of this fees are collected if factory has no fee_receiver - e.g. for lending
This is by design: lending does NOT earn interest, system makes money by using crvUSD
"""
# Calling fee_receiver will fail for lending markets because everything gets to lenders
_to: address = FACTORY.fee_receiver()
# Borrowing-based fees
rate_mul: uint256 = AMM.get_rate_mul()
loan: Loan = self._total_debt
loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul
loan.rate_mul = rate_mul
self._total_debt = loan
self._save_rate()
# Amount which would have been redeemed if all the debt was repaid now
to_be_redeemed: uint256 = loan.initial_debt + self.redeemed
# Amount which was minted when borrowing + all previously claimed admin fees
minted: uint256 = self.minted
# Difference between to_be_redeemed and minted amount is exactly due to interest charged
if to_be_redeemed > minted:
self.minted = to_be_redeemed
to_be_redeemed = unsafe_sub(to_be_redeemed, minted) # Now this is the fees to charge
self.transfer(BORROWED_TOKEN, _to, to_be_redeemed)
log CollectFees(to_be_redeemed, loan.initial_debt)
return to_be_redeemed
else:
log CollectFees(0, loan.initial_debt)
return 0
```
```vyper
admin_fees_x: public(uint256)
admin_fees_y: public(uint256)
@external
@nonreentrant('lock')
def reset_admin_fees():
"""
@notice Zero out AMM fees collected
"""
assert msg.sender == self.admin
self.admin_fees_x = 0
self.admin_fees_y = 0
```
```vyper
event CollectFees:
amount: uint256
new_supply: uint256
@external
@nonreentrant('lock')
def collect_fees() -> uint256:
"""
@notice Collect the fees charged as interest
@notice None of this fees are collected if factory has no fee_receiver - e.g. for lending
This is by design: lending does NOT earn interest, system makes money by using crvUSD
"""
# Calling fee_receiver will fail for lending markets because everything gets to lenders
_to: address = FACTORY.fee_receiver()
# AMM-based fees
borrowed_fees: uint256 = AMM.admin_fees_x()
collateral_fees: uint256 = AMM.admin_fees_y()
self.transferFrom(BORROWED_TOKEN, AMM.address, _to, borrowed_fees)
self.transferFrom(COLLATERAL_TOKEN, AMM.address, _to, collateral_fees)
AMM.reset_admin_fees()
# Borrowing-based fees
rate_mul: uint256 = AMM.get_rate_mul()
loan: Loan = self._total_debt
loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul
loan.rate_mul = rate_mul
self._total_debt = loan
self._save_rate()
# Amount which would have been redeemed if all the debt was repaid now
to_be_redeemed: uint256 = loan.initial_debt + self.redeemed
# Amount which was minted when borrowing + all previously claimed admin fees
minted: uint256 = self.minted
# Difference between to_be_redeemed and minted amount is exactly due to interest charged
if to_be_redeemed > minted:
self.minted = to_be_redeemed
to_be_redeemed = unsafe_sub(to_be_redeemed, minted) # Now this is the fees to charge
self.transfer(BORROWED_TOKEN, _to, to_be_redeemed)
log CollectFees(to_be_redeemed, loan.initial_debt)
return to_be_redeemed
else:
log CollectFees(0, loan.initial_debt)
return 0
```
```vyper
admin_fees_x: public(uint256)
admin_fees_y: public(uint256)
@external
@nonreentrant('lock')
def reset_admin_fees():
"""
@notice Zero out AMM fees collected
"""
assert msg.sender == self.admin
self.admin_fees_x = 0
self.admin_fees_y = 0
```
This example shows the effect of claiming fees. Before calling `collect_fees`, the total admin fees amounted to approximately 1,431 crvUSD. After claiming, the admin fees are reset to 0.
```shell
>>> Controller.admin_fees()
1431079351921267396706
>>> Controller.collect_fees()
>>> Controller.admin_fees()
0
```
::::
---
## Loan and Liquidation Discount
*New values for `loan_discount` and `liquidation_discount` can be assigned by the admin of the Factory, which is the DAO.*
The **loan discount** is the percentage used to discount the collateral for calculating the maximum borrowable amount when creating a loan.
The **liquidation discount** is used to discount the collateral for calculating the recoverable value upon liquidation at the current market price.
### `loan_discount`
::::description[`Controller.loan_discount() -> uint256: view`]
Getter for the discount of the maximum loan size compared to `get_x_down()` value. This value defines the LTV.
Returns: loan discount (`uint256`).
```vyper
loan_discount: public(uint256)
@external
def __init__(
collateral_token: address,
monetary_policy: address,
loan_discount: uint256,
liquidation_discount: uint256,
amm: address):
"""
@notice Controller constructor deployed by the factory from blueprint
@param collateral_token Token to use for collateral
@param monetary_policy Address of monetary policy
@param loan_discount Discount of the maximum loan size compare to get_x_down() value
@param liquidation_discount Discount of the maximum loan size compare to
get_x_down() for "bad liquidation" purposes
@param amm AMM address (Already deployed from blueprint)
"""
...
self.loan_discount = loan_discount
...
```
```vyper
loan_discount: public(uint256)
@external
def __init__(
collateral_token: address,
monetary_policy: address,
loan_discount: uint256,
liquidation_discount: uint256,
amm: address):
"""
@notice Controller constructor deployed by the factory from blueprint
@param collateral_token Token to use for collateral
@param monetary_policy Address of monetary policy
@param loan_discount Discount of the maximum loan size compare to get_x_down() value
@param liquidation_discount Discount of the maximum loan size compare to
get_x_down() for "bad liquidation" purposes
@param amm AMM address (Already deployed from blueprint)
"""
...
self.loan_discount = loan_discount
...
```
```shell
>>> Controller.loan_discount()
90000000000000000
```
::::
### `liquidation_discount`
::::description[`Controller.liquidation_discount() -> uint256: view`]
Getter for the liquidation discount. This value is used to discount the collateral value when calculating the health for liquidation puroses in order to incentivize liquidators.
Returns: liquidation discount (`uint256`).
```vyper
liquidation_discount: public(uint256)
@external
def __init__(
collateral_token: address,
monetary_policy: address,
loan_discount: uint256,
liquidation_discount: uint256,
amm: address):
"""
@notice Controller constructor deployed by the factory from blueprint
@param collateral_token Token to use for collateral
@param monetary_policy Address of monetary policy
@param loan_discount Discount of the maximum loan size compare to get_x_down() value
@param liquidation_discount Discount of the maximum loan size compare to
get_x_down() for "bad liquidation" purposes
@param amm AMM address (Already deployed from blueprint)
"""
...
self.liquidation_discount = liquidation_discount
...
```
```vyper
liquidation_discount: public(uint256)
@external
def __init__(
collateral_token: address,
monetary_policy: address,
loan_discount: uint256,
liquidation_discount: uint256,
amm: address):
"""
@notice Controller constructor deployed by the factory from blueprint
@param collateral_token Token to use for collateral
@param monetary_policy Address of monetary policy
@param loan_discount Discount of the maximum loan size compare to get_x_down() value
@param liquidation_discount Discount of the maximum loan size compare to
get_x_down() for "bad liquidation" purposes
@param amm AMM address (Already deployed from blueprint)
"""
...
self.liquidation_discount = liquidation_discount
...
```
```shell
>>> Controller.liquidation_discount()
60000000000000000
```
::::
### `liquidation_discounts`
::::description[`Controller.liquidation_discounts(arg0: address) -> uint256: view`]
Getter method for the liquidation discount of a user. This value is used to discount the collateral for calculating the recoverable value upon liquidation at the current market price. The discount is factored into the health calculation.
Returns: liquidation discount (`uint256`).
| Input | Type | Description |
| ------ | --------- | ------------ |
| `arg0` | `address` | User Address |
```vyper
liquidation_discounts: public(HashMap[address, uint256])
```
```vyper
liquidation_discounts: public(HashMap[address, uint256])
```
```shell
>>> Controller.liquidation_discounts(trader)
0
```
::::
### `set_borrowing_discounts`
::::description[`Controller.set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory.
:::
Function to set new values for `loan_discount` and `liquidation_discount`. This metric defines the max LTV and where bad liquidations start.
Emits: `SetBorrowingDiscount`
| Input | Type | Description |
| ---------------------- | --------- | -------------------------------------- |
| `loan_discount` | `uint256` | New value for the loan discount |
| `liquidation_discount` | `uint256` | New value for the liquidation discount |
```vyper
event SetBorrowingDiscounts:
loan_discount: uint256
liquidation_discount: uint256
@nonreentrant('lock')
@external
def set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint256):
"""
@notice Set discounts at which we can borrow (defines max LTV) and where bad liquidation starts
@param loan_discount Discount which defines LTV
@param liquidation_discount Discount where bad liquidation starts
"""
assert msg.sender == FACTORY.admin()
assert loan_discount > liquidation_discount
assert liquidation_discount >= MIN_LIQUIDATION_DISCOUNT
assert loan_discount <= MAX_LOAN_DISCOUNT
self.liquidation_discount = liquidation_discount
self.loan_discount = loan_discount
log SetBorrowingDiscounts(loan_discount, liquidation_discount)
```
```vyper
event SetBorrowingDiscounts:
loan_discount: uint256
liquidation_discount: uint256
@nonreentrant('lock')
@external
def set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint256):
"""
@notice Set discounts at which we can borrow (defines max LTV) and where bad liquidation starts
@param loan_discount Discount which defines LTV
@param liquidation_discount Discount where bad liquidation starts
"""
assert msg.sender == FACTORY.admin()
assert loan_discount > liquidation_discount
assert liquidation_discount >= MIN_LIQUIDATION_DISCOUNT
assert loan_discount <= MAX_LOAN_DISCOUNT
self.liquidation_discount = liquidation_discount
self.loan_discount = loan_discount
log SetBorrowingDiscounts(loan_discount, liquidation_discount)
```
```shell
>>> Controller.set_borrowing_discounts(90000000000000000, 60000000000000000)
```
::::
---
## Monetary Policy
Each controller has a monetary policy contract. This contract is responsible for the interest rates within the markets.
While [monetary policies for minting markets](./monetary-policy/monetary-policy.md) depend on several factors such as the price of crvUSD, pegkeeper debt, etc., the monetary policy for lending markets is solely based on a [semi-log monetary policy](../lending/contracts/semilog-mp.md) which determines the rate based on the utilization of the assets.
### `monetary_policy`
::::description[`Controller.monetary_policy() -> address: view`]
Getter for the monetary policy contract.
Returns: monetary policy contract (`address`).
```vyper
interface MonetaryPolicy:
def rate_write() -> uint256: nonpayable
monetary_policy: public(MonetaryPolicy)
```
```vyper
interface MonetaryPolicy:
def rate_write() -> uint256: nonpayable
monetary_policy: public(MonetaryPolicy)
```
```shell
>>> Controller.monetary_policy()
'0x8c5A7F011f733fBb0A6c969c058716d5CE9bc933'
```
::::
### `set_monetary_policy`
::::description[`Controller.set_monetary_policy(monetary_policy: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the Factory.
:::
Function to set the monetary policy contract. Initially, the monetary policy contract is configured when a new market is added via the Factory. However, this function allows the contract address to be changed later. When setting the new address, the function calls `rate_write()` from the monetary policy contract to verify if the ABI is correct.
Emits: `SetMonetaryPolicy`
| Input | Type | Description |
| ----------------- | --------- | ------------------------ |
| `monetary_policy` | `address` | Monetary policy contract |
```vyper
event SetMonetaryPolicy:
monetary_policy: address
monetary_policy: public(MonetaryPolicy)
@nonreentrant('lock')
@external
def set_monetary_policy(monetary_policy: address):
"""
@notice Set monetary policy contract
@param monetary_policy Address of the monetary policy contract
"""
assert msg.sender == FACTORY.admin()
self.monetary_policy = MonetaryPolicy(monetary_policy)
MonetaryPolicy(monetary_policy).rate_write()
log SetMonetaryPolicy(monetary_policy)
```
```vyper
@external
def rate_write(_for: address = msg.sender) -> uint256:
# Not needed here but useful for more automated policies
# which change rate0 - for example rate0 targeting some fraction pl_debt/total_debt
return self.calculate_rate(_for, PRICE_ORACLE.price_w())
```
```vyper
event SetMonetaryPolicy:
monetary_policy: address
monetary_policy: public(MonetaryPolicy)
@nonreentrant('lock')
@external
def set_monetary_policy(monetary_policy: address):
"""
@notice Set monetary policy contract
@param monetary_policy Address of the monetary policy contract
"""
assert msg.sender == FACTORY.admin()
self.monetary_policy = MonetaryPolicy(monetary_policy)
MonetaryPolicy(monetary_policy).rate_write()
log SetMonetaryPolicy(monetary_policy)
```
```vyper
@external
def rate_write(_for: address = msg.sender) -> uint256:
# Not needed here but useful for more automated policies
# which change rate0 - for example rate0 targeting some fraction pl_debt/total_debt
return self.calculate_rate(_for, PRICE_ORACLE.price_w())
```
```shell
>>> Controller.set_monetary_policy("0xc684432FD6322c6D58b6bC5d28B18569aA0AD0A1")
```
::::
---
## Contract Info Methods
### `FACTORY`
::::description[`Controller.factory() -> address: view`]
Getter of the Factory contract of the Controller. This variable is immutable and can not be changed.
Returns: Factory (`address`).
```vyper
interface Factory:
def stablecoin() -> address: view
def admin() -> address: view
def fee_receiver() -> address: view
def WETH() -> address: view
FACTORY: immutable(Factory)
@external
def __init__(
collateral_token: address,
monetary_policy: address,
loan_discount: uint256,
liquidation_discount: uint256,
amm: address):
"""
@notice Controller constructor deployed by the factory from blueprint
@param collateral_token Token to use for collateral
@param monetary_policy Address of monetary policy
@param loan_discount Discount of the maximum loan size compare to get_x_down() value
@param liquidation_discount Discount of the maximum loan size compare to
get_x_down() for "bad liquidation" purposes
@param amm AMM address (Already deployed from blueprint)
"""
FACTORY = Factory(msg.sender)
...
```
```vyper
interface Factory:
def stablecoin() -> address: view
def admin() -> address: view
def fee_receiver() -> address: view
def WETH() -> address: view
FACTORY: immutable(Factory)
@external
def __init__(
collateral_token: address,
monetary_policy: address,
loan_discount: uint256,
liquidation_discount: uint256,
amm: address):
"""
@notice Controller constructor deployed by the factory from blueprint
@param collateral_token Token to use for collateral
@param monetary_policy Address of monetary policy
@param loan_discount Discount of the maximum loan size compare to get_x_down() value
@param liquidation_discount Discount of the maximum loan size compare to
get_x_down() for "bad liquidation" purposes
@param amm AMM address (Already deployed from blueprint)
"""
FACTORY = Factory(msg.sender)
...
```
```shell
>>> Controller.factory()
'0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC
```
::::
### `amm`
::::description[`Controller.amm() -> address: view`]
Getter of the `AMM` contract of the `Controller`. This variable is immutable and can not be changed.
Returns: `AMM` contract (`address`).
```vyper
AMM: immutable(LLAMMA)
@external
def __init__(
collateral_token: address,
monetary_policy: address,
loan_discount: uint256,
liquidation_discount: uint256,
amm: address):
"""
@notice Controller constructor deployed by the factory from blueprint
@param collateral_token Token to use for collateral
@param monetary_policy Address of monetary policy
@param loan_discount Discount of the maximum loan size compare to get_x_down() value
@param liquidation_discount Discount of the maximum loan size compare to
get_x_down() for "bad liquidation" purposes
@param amm AMM address (Already deployed from blueprint)
"""
...
AMM = LLAMMA(amm)
...
```
```vyper
AMM: immutable(LLAMMA)
@external
def __init__(
collateral_token: address,
monetary_policy: address,
loan_discount: uint256,
liquidation_discount: uint256,
amm: address):
"""
@notice Controller constructor deployed by the factory from blueprint
@param collateral_token Token to use for collateral
@param monetary_policy Address of monetary policy
@param loan_discount Discount of the maximum loan size compare to get_x_down() value
@param liquidation_discount Discount of the maximum loan size compare to
get_x_down() for "bad liquidation" purposes
@param amm AMM address (Already deployed from blueprint)
"""
...
AMM = LLAMMA(amm)
...
```
```shell
>>> Controller.amm()
'0xf9bD9da2427a50908C4c6D1599D8e62837C2BCB0'
```
::::
### `collateral_token`
::::description[`Controller.collateral_token() -> address: view`]
Getter of the collateral token for the market. This variable is immutable and can not be changed.
Returns: collateral token (`address`).
```vyper
COLLATERAL_TOKEN: immutable(ERC20)
@external
def __init__(
collateral_token: address,
monetary_policy: address,
loan_discount: uint256,
liquidation_discount: uint256,
amm: address):
"""
@notice Controller constructor deployed by the factory from blueprint
@param collateral_token Token to use for collateral
@param monetary_policy Address of monetary policy
@param loan_discount Discount of the maximum loan size compare to get_x_down() value
@param liquidation_discount Discount of the maximum loan size compare to
get_x_down() for "bad liquidation" purposes
@param amm AMM address (Already deployed from blueprint)
"""
...
COLLATERAL_TOKEN = _collateral_token
...
```
```vyper
COLLATERAL_TOKEN: immutable(ERC20)
@external
def __init__(
collateral_token: address,
monetary_policy: address,
loan_discount: uint256,
liquidation_discount: uint256,
amm: address):
"""
@notice Controller constructor deployed by the factory from blueprint
@param collateral_token Token to use for collateral
@param monetary_policy Address of monetary policy
@param loan_discount Discount of the maximum loan size compare to get_x_down() value
@param liquidation_discount Discount of the maximum loan size compare to
get_x_down() for "bad liquidation" purposes
@param amm AMM address (Already deployed from blueprint)
"""
...
COLLATERAL_TOKEN = _collateral_token
...
```
```shell
>>> Controller.collateral_token()
'0x18084fbA666a33d37592fA2633fD49a74DD93a88'
```
::::
### `amm_price`
::::description[`Controller.amm_price() -> uint256: view`]
Getter for the current price from the AMM.
Returns: price (`uint256`).
```vyper
# AMM has a nonreentrant decorator
@view
@external
def amm_price() -> uint256:
"""
@notice Current price from the AMM
"""
return AMM.get_p()
```
```vyper
@external
@view
@nonreentrant('lock')
def get_p() -> uint256:
"""
@notice Get current AMM price in active_band
@return Current price at 1e18 base
"""
n: int256 = self.active_band
return self._get_p(n, self.bands_x[n], self.bands_y[n])
@internal
@view
def _get_p(n: int256, x: uint256, y: uint256) -> uint256:
"""
@notice Get current AMM price in band
@param n Band number
@param x Amount of stablecoin in band
@param y Amount of collateral in band
@return Current price at 1e18 base
"""
p_o_up: uint256 = self._p_oracle_up(n)
p_o: uint256 = self._price_oracle_ro()[0]
assert p_o_up != 0
# Special cases
if x == 0:
if y == 0: # x and y are 0
# Return mid-band
return unsafe_div((unsafe_div(unsafe_div(p_o**2, p_o_up) * p_o, p_o_up) * A), Aminus1)
# if x == 0: # Lowest point of this band -> p_current_down
return unsafe_div(unsafe_div(p_o**2, p_o_up) * p_o, p_o_up)
if y == 0: # Highest point of this band -> p_current_up
p_o_up = unsafe_div(p_o_up * Aminus1, A) # now this is _actually_ p_o_down
return unsafe_div(p_o**2 / p_o_up * p_o, p_o_up)
y0: uint256 = self._get_y0(x, y, p_o, p_o_up)
# ^ that call also checks that p_o != 0
# (f(y0) + x) / (g(y0) + y)
f: uint256 = unsafe_div(A * y0 * p_o, p_o_up) * p_o
g: uint256 = unsafe_div(Aminus1 * y0 * p_o_up, p_o)
return (f + x * 10**18) / (g + y)
```
```vyper
# AMM has a nonreentrant decorator
@view
@external
def amm_price() -> uint256:
"""
@notice Current price from the AMM
"""
return AMM.get_p()
```
```vyper
@external
@view
@nonreentrant('lock')
def get_p() -> uint256:
"""
@notice Get current AMM price in active_band
@return Current price at 1e18 base
"""
n: int256 = self.active_band
return self._get_p(n, self.bands_x[n], self.bands_y[n])
@internal
@view
def _get_p(n: int256, x: uint256, y: uint256) -> uint256:
"""
@notice Get current AMM price in band
@param n Band number
@param x Amount of stablecoin in band
@param y Amount of collateral in band
@return Current price at 1e18 base
"""
p_o_up: uint256 = self._p_oracle_up(n)
p_o: uint256 = self._price_oracle_ro()[0]
assert p_o_up != 0
# Special cases
if x == 0:
if y == 0: # x and y are 0
# Return mid-band
return unsafe_div((unsafe_div(unsafe_div(p_o**2, p_o_up) * p_o, p_o_up) * A), Aminus1)
# if x == 0: # Lowest point of this band -> p_current_down
return unsafe_div(unsafe_div(p_o**2, p_o_up) * p_o, p_o_up)
if y == 0: # Highest point of this band -> p_current_up
p_o_up = unsafe_div(p_o_up * Aminus1, A) # now this is _actually_ p_o_down
return unsafe_div(p_o**2 / p_o_up * p_o, p_o_up)
y0: uint256 = self._get_y0(x, y, p_o, p_o_up)
# ^ that call also checks that p_o != 0
# (f(y0) + x) / (g(y0) + y)
f: uint256 = unsafe_div(A * y0 * p_o, p_o_up) * p_o
g: uint256 = unsafe_div(Aminus1 * y0 * p_o_up, p_o)
return (f + x * 10**18) / (g + y)
```
```shell
>>> Controller.amm_price()
42852102383927213434085
```
::::
### `minted`
::::description[`Controller.minted() -> uint256: view`]
Getter for the total amount of crvUSD minted from this controller. Increments by the amount of debt when calling `create_loan` or `borrow_more`.
Returns: total minted (`uint256`).
```vyper
minted: public(uint256)
```
```vyper
minted: public(uint256)
```
```shell
>>> Controller.minted()
20682637249975500380405996
```
::::
### `redeemed`
::::description[`Controller.redeemed() -> uint256: view`]
Getter for the total amount of crvUSD redeemed from this controller. Increments by the amount of debt that is repayed when calling `repay` or `repay_extended`.
Returns: total redeemed (`uint256`).
```vyper
redeemed: public(uint256)
```
```vyper
redeemed: public(uint256)
```
```shell
>>> Controller.redeemed()
16646401312086830122157869
```
::::
---
## Callbacks
### `set_callback`
::::description[`Controller.set_callback(cb: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory.
:::
Function to set a callback for liquidity mining.
| Input | Type | Description |
| ----- | --------- | ----------- |
| `cb` | `address` | Callback |
```vyper
@external
@nonreentrant('lock')
def set_callback(cb: address):
"""
@notice Set liquidity mining callback
"""
assert msg.sender == FACTORY.admin()
AMM.set_callback(cb)
```
```vyper
@external
@nonreentrant('lock')
def set_callback(cb: address):
"""
@notice Set liquidity mining callback
"""
assert msg.sender == FACTORY.admin()
AMM.set_callback(cb)
```
```shell
>>> soon
```
::::
---
## crvUSD
:::vyper[`Stablecoin.vy`]
The source code of the `crvUSD` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/Stablecoin.vy).
:::
`crvUSD` has been [deployed](https://etherscan.io/tx/0x5f501b4e420485ac3a18be3b88ad73413ac17ee45c776bb48cfd26902585e9fd) on May, 14th 2024 on Ethereum Mainnet. Prior to the lauch, there have been several deployments for the stablecoin and its components. Please always make sure you are using the latest deployment. See [`deployment-logs/mainnet.log`](https://github.com/curvefi/curve-stablecoin/blob/master/deployment-logs/mainnet.log).
Since the initial deployment of crvUSD, the token was bridged to several chains, including the following:
| Chain | Token Address |
| :---------------------------: | :------------------: |
| :logos-ethereum: `Ethereum` | [0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E](https://etherscan.io/token/0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E) |
| :logos-arbitrum: `Arbitrum` | [0x498Bf2B1e120FeD3ad3D42EA2165E9b73f99C1e5](https://arbiscan.io/address/0x498Bf2B1e120FeD3ad3D42EA2165E9b73f99C1e5) |
| :logos-optimism: `Optimism` | [0xc52d7f23a2e460248db6ee192cb23dd12bddcbf6](https://optimistic.etherscan.io/address/0xc52d7f23a2e460248db6ee192cb23dd12bddcbf6) |
| :logos-base: `Base` | [0x417Ac0e078398C154EdFadD9Ef675d30Be60Af93](https://basescan.org/address/0x417Ac0e078398C154EdFadD9Ef675d30Be60Af93) |
| :logos-gnosis: `Gnosis` | [0xaBEf652195F98A91E490f047A5006B71c85f058d](https://gnosisscan.io/address/0xaBEf652195F98A91E490f047A5006B71c85f058d) |
| :logos-polygon: `Polygon` | [0xc4Ce1D6F5D98D65eE25Cf85e9F2E9DcFEe6Cb5d6](https://polygonscan.com/address/0xc4Ce1D6F5D98D65eE25Cf85e9F2E9DcFEe6Cb5d6) |
| :logos-xlayer: `X-Layer` | [0xda8f4eb4503acf5dec5420523637bb5b33a846f6](https://web3.okx.com/explorer/x-layer/address/0xda8f4eb4503acf5dec5420523637bb5b33a846f6) |
| :logos-fraxtal: `Fraxtal` | [0xB102f7Efa0d5dE071A8D37B3548e1C7CB148Caf3](https://fraxscan.com/address/0xB102f7Efa0d5dE071A8D37B3548e1C7CB148Caf3) |
| :logos-bsc: `BinanceSmartChain` | [0xe2fb3F127f5450DeE44afe054385d74C392BdeF4](https://bscscan.com/address/0xe2fb3F127f5450DeE44afe054385d74C392BdeF4) |
| :logos-mantle: `Mantle` | [0x0994206dfe8de6ec6920ff4d779b0d950605fb53](https://mantlescan.xyz/address/0x0994206dfe8de6ec6920ff4d779b0d950605fb53) |
| :logos-zksync: `zk-Sync` | [0x43cd37cc4b9ec54833c8ac362dd55e58bfd62b86](https://explorer.zksync.io/address/0x43cd37cc4b9ec54833c8ac362dd55e58bfd62b86) |
---
## Mint and Burn
- crvUSD can only be minted by the `minter` of the contract, which is the Factory contract
- crvUSD is minted in accordance with the `debt_ceiling`, either when **adding a new market** or when **raising its debt ceiling**. This is accomplished by calling the `set_new_debt_ceiling` function within the Factory contract.
- Burning crvUSD typically occurs when a **lower debt ceiling is set**, or if a user decides to burn their crvUSD for whatever reason.
### `minter`
::::description[`crvUSD.minter() -> address: view`]
Getter for the minter contract.
Returns: minter (`address`).
```vyper
minter: public(address)
```
```shell
>>> crvUSD.minter()
'0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC'
```
::::
### `mint`
::::description[`crvUSD.mint(_to: address, _value: uint256) -> bool`]
:::guard[Guarded Method]
This function is only callable by the `minter` of the contract, which is the Factory.
:::
Function to mint `_value` amount of tokens to `_to`.
| Input | Type | Description |
| ----------- | -------| ----|
| `_to` | `address` | Address newly minted tokens are credited to |
| `_value` | `uint256` | Amount of tokens to mint |
Returns: true (`bool`).
Emits: `Transfer`
```vyper
event Transfer:
sender: indexed(address)
receiver: indexed(address)
value: uint256
@external
def mint(_to: address, _value: uint256) -> bool:
"""
@notice Mint `_value` amount of tokens to `_to`.
@dev Only callable by an account with minter privileges.
@param _to The account newly minted tokens are credited to.
@param _value The amount of tokens to mint.
"""
assert msg.sender == self.minter
assert _to not in [self, empty(address)]
self.balanceOf[_to] += _value
self.totalSupply += _value
log Transfer(empty(address), _to, _value)
return True
```
```shell
>>> crvUSD.mint("0xec0820efafc41d8943ee8de495fc9ba8495b15cf", 10**22)
```
:::note
The `mint` function is only used when adding a new market or raising a market's debt ceiling. The function will revert if any EOA (Externally Owned Account) or contract other than the `admin` attempts to call it. Additionally, tokens cannot be minted to the `minter` itself or the `ZERO_ADDRESS`.
:::
::::
### `set_minter`
::::description[`crvUSD.set_minter(_minter: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the Factory.
:::
Function to set the minter address of the token.
| Input | Type | Description |
| ----------- | -------| ----|
| `_minter` | `address` | New minter address |
Emits: `SetMinter`
```vyper
event SetMinter:
minter: indexed(address)
minter: public(address)
@external
def set_minter(_minter: address):
assert msg.sender == self.minter
self.minter = _minter
log SetMinter(_minter)
```
```shell
>>> crvUSD.set_minter("")
```
:::note
The function will revert if any EOA (Externally Owned Account) or contract other than the `admin` attempts to call this function.
:::
::::
### `burn`
::::description[`crvUSD.burn(_value: uint256) -> bool`]
Function to burn `_value` amount of tokens from `msg.sender`.
| Input | Type | Description |
| ----------- | -------| ----|
| `_value` | `uint256` | Amount of tokens to burn |
Returns: true (`bool`).
Emits: `Transfer`
```vyper
event Transfer:
sender: indexed(address)
receiver: indexed(address)
value: uint256
@external
def burn(_value: uint256) -> bool:
"""
@notice Burn `_value` amount of tokens.
@param _value The amount of tokens to burn.
"""
self._burn(msg.sender, _value)
return True
@internal
def _burn(_from: address, _value: uint256):
self.balanceOf[_from] -= _value
self.totalSupply -= _value
log Transfer(_from, empty(address), _value)
```
```shell
>>> crvUSD.burn(10**18)
'True'
```
::::
### `burnFrom`
::::description[`crvUSD.burnFrom(_from: address, _value: uint256) -> bool`]
Function to burn `_value` amount of tokens from `_from`.
| Input | Type | Description |
| ----------- | -------| ----|
| `_from` | `address` | Address to burn tokens for |
| `_value` | `uint256` | Amount of tokens to burn |
Returns: true (`bool`).
Emits: `Transfer`
```vyper
event Transfer:
sender: indexed(address)
receiver: indexed(address)
value: uint256
@external
def burnFrom(_from: address, _value: uint256) -> bool:
"""
@notice Burn `_value` amount of tokens from `_from`.
@dev The caller must have previously been given an allowance by `_from`.
@param _from The account to burn the tokens from.
@param _value The amount of tokens to burn.
"""
allowance: uint256 = self.allowance[_from][msg.sender]
if allowance != max_value(uint256):
self._approve(_from, msg.sender, allowance - _value)
self._burn(_from, _value)
return True
@internal
def _burn(_from: address, _value: uint256):
self.balanceOf[_from] -= _value
self.totalSupply -= _value
log Transfer(_from, empty(address), _value)
```
```shell
>>> crvUSD.burn("0xec0820efafc41d8943ee8de495fc9ba8495b15cf", "25000000000000000000000000")
'True'
```
:::note
The `burnFrom` function is called when the debt ceiling is reduced via `set_debt_ceiling` within the Factory.
:::
::::
## Contract Info Methods
### `decimals`
::::description[`crvUSD.decimals() -> uint8: view`]
Getter for the decimals of the token.
Returns: decimals (`uint8`).
```vyper
decimals: public(constant(uint8)) = 18
```
```shell
>>> crvUSD.decimals()
18
```
::::
### `version`
::::description[`crvUSD.version() -> String[8]: view`]
Getter for the version of the contract.
Returns: version (`String[8]`).
```vyper
version: public(constant(String[8])) = "v1.0.0"
```
```shell
>>> crvUSD.version()
'v1.0.0'
```
::::
### `name`
::::description[`crvUSD.name() -> String[64]: view`]
Getter for the name of the token.
Returns: name (`String[64]`).
```vyper
name: public(immutable(String[64]))
@external
def __init__(_name: String[64], _symbol: String[32]):
name = _name
symbol = _symbol
NAME_HASH = keccak256(_name)
CACHED_CHAIN_ID = chain.id
salt = block.prevhash
CACHED_DOMAIN_SEPARATOR = keccak256(
_abi_encode(
EIP712_TYPEHASH,
keccak256(_name),
VERSION_HASH,
chain.id,
self,
block.prevhash,
)
)
self.minter = msg.sender
log SetMinter(msg.sender)
```
```shell
>>> crvUSD.name()
'Curve.Fi USD Stablecoin'
```
::::
### `symbol`
::::description[`crvUSD.symbol() -> String[32]: view`]
Getter for the symbol of the token.
Returns: symbol (`String[32]`).
```vyper
symbol: public(immutable(String[32]))
@external
def __init__(_name: String[64], _symbol: String[32]):
name = _name
symbol = _symbol
NAME_HASH = keccak256(_name)
CACHED_CHAIN_ID = chain.id
salt = block.prevhash
CACHED_DOMAIN_SEPARATOR = keccak256(
_abi_encode(
EIP712_TYPEHASH,
keccak256(_name),
VERSION_HASH,
chain.id,
self,
block.prevhash,
)
)
self.minter = msg.sender
log SetMinter(msg.sender)
```
```shell
>>> crvUSD.symbol()
'crvUSD'
```
::::
### `balanceOf`
::::description[`crvUSD.balanceOf(arg0: address) -> uint256: view`]
Getter for the crvUSD balance of address `arg0`.
Returns: balance (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `address` | Address to check balance for |
```vyper
balanceOf: public(HashMap[address, uint256])
```
```shell
>>> crvUSD.balanceOf("0x844Dc85EdD8492A56228D293cfEbb823EF3E10EC")
1002155725613742880120968
```
::::
### `totalSupply`
::::description[`crvUSD.totalSupply() -> uint256: view`]
Getter for the total supply of crvUSD.
Returns: total supply (`uint256`).
```vyper
totalSupply: public(uint256)
```
```shell
>>> crvUSD.totalSupply()
260000000000000000000000000
```
::::
## Allowances and Approvals
### `allowance`
::::description[`crvUSD.allowance(arg0: address, arg1: address) -> uint256: view`]
Getter method to check the allowance.
Returns: allowed tokens (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `address` | Address of the spender |
| `arg1` | `address` | Address of the token owner |
```vyper
allowance: public(HashMap[address, HashMap[address, uint256]])
```
```shell
>>> crvUSD.allowance("0x7a16fF8270133F063aAb6C9977183D9e72835428", "0x4dece678ceceb27446b35c672dc7d61f30bad69e")
115792089237316195423570985008687907853269984665640564039457584007913129639935
```
::::
### `approve`
::::description[`crvUSD.approve(_spender: address, _value: uint256) -> bool`]
Function to allow `_spender` to transfer up to `_value` amount of tokens from the caller's amount.
| Input | Type | Description |
| ----------- | -------| ----|
| `_spender` | `address` | Address permitted to spend up to `_value` amount of caller's funds |
| `_value` | `uint256` | Amount of tokens `_spender` is allowed to spend |
Returns: true (`bool`).
Emits: `Approval`
```vyper
event Approval:
owner: indexed(address)
spender: indexed(address)
value: uint256
allowance: public(HashMap[address, HashMap[address, uint256]])
@external
def approve(_spender: address, _value: uint256) -> bool:
"""
@notice Allow `_spender` to transfer up to `_value` amount of tokens from the caller's account.
@dev Non-zero to non-zero approvals are allowed, but should be used cautiously. The methods
increaseAllowance + decreaseAllowance are available to prevent any front-running that
may occur.
@param _spender The account permitted to spend up to `_value` amount of caller's funds.
@param _value The amount of tokens `_spender` is allowed to spend.
"""
self._approve(msg.sender, _spender, _value)
return True
@internal
def _approve(_owner: address, _spender: address, _value: uint256):
self.allowance[_owner][_spender] = _value
log Approval(_owner, _spender, _value)
```
```shell
>>> crvUSD.approve("0x4dece678ceceb27446b35c672dc7d61f30bad69e", 10**22)
'True'
```
::::
### `increaseAllowance`
::::description[`crvUSD.increaseAllowance(_spender: address, _add_value: uint256) -> bool`]
Function to increase the allowance granted to `_spender`.
| Input | Type | Description |
| ----------- | -------| ----|
| `_spender` | `address` | Address to increase the allowance of |
| `_add_value` | `uint256` | Amount to increase the allwance by |
Returns: true (`bool`).
Emits: `Approval`
:::note
This function will never overflow, and instead will bind allowance to `MAX_UINT256`. This has the potential to grant infinite approval.
:::
```vyper
allowance: public(HashMap[address, HashMap[address, uint256]])
@external
def increaseAllowance(_spender: address, _add_value: uint256) -> bool:
"""
@notice Increase the allowance granted to `_spender`.
@dev This function will never overflow, and instead will bound
allowance to MAX_UINT256. This has the potential to grant an
infinite approval.
@param _spender The account to increase the allowance of.
@param _add_value The amount to increase the allowance by.
"""
cached_allowance: uint256 = self.allowance[msg.sender][_spender]
allowance: uint256 = unsafe_add(cached_allowance, _add_value)
# check for an overflow
if allowance < cached_allowance:
allowance = max_value(uint256)
if allowance != cached_allowance:
self._approve(msg.sender, _spender, allowance)
return True
@internal
def _approve(_owner: address, _spender: address, _value: uint256):
self.allowance[_owner][_spender] = _value
log Approval(_owner, _spender, _value)
```
```shell
>>> crvUSD.increaseAllowance("0x4dece678ceceb27446b35c672dc7d61f30bad69e", 2**256-1)
'True'
```
::::
### `decreaseAllowance`
::::description[`crvUSD.decreaseAllowance(_spender: address, _sub_value: uint256) -> bool`]
Function to decrease the allowance granted to `_spender`.
| Input | Type | Description |
| ----------- | -------| ----|
| `_spender` | `address` | Address to decrease the allowance of |
| `_sub_value` | `uint256` | Amount to decrease the allwance by |
Returns: true (`bool`).
Emits: `Approval`
:::note
This function will never underflow, and instead will bound allowance to 0.
:::
```vyper
allowance: public(HashMap[address, HashMap[address, uint256]])
@external
def decreaseAllowance(_spender: address, _sub_value: uint256) -> bool:
"""
@notice Decrease the allowance granted to `_spender`.
@dev This function will never underflow, and instead will bound
allowance to 0.
@param _spender The account to decrease the allowance of.
@param _sub_value The amount to decrease the allowance by.
"""
cached_allowance: uint256 = self.allowance[msg.sender][_spender]
allowance: uint256 = unsafe_sub(cached_allowance, _sub_value)
# check for an underflow
if cached_allowance < allowance:
allowance = 0
if allowance != cached_allowance:
self._approve(msg.sender, _spender, allowance)
return True
@internal
def _approve(_owner: address, _spender: address, _value: uint256):
self.allowance[_owner][_spender] = _value
log Approval(_owner, _spender, _value)
```
```shell
>>> crvUSD.decreaseAllowance("0x4dece678ceceb27446b35c672dc7d61f30bad69e", 2**256-1)
'True'
```
::::
### `permit`
::::description[`crvUSD.permit(_owner: address, _spender: address, _value: uint256, _deadline: uint256, _v: uint8, _r: bytes32, _s: bytes32) -> bool`]
Function to permit `_spender` to spend up to `_value` amount of `_owner`'s tokens via a signature.
| Input | Type | Description |
| ----------- | -------| ----|
| `_owner` | `address` | Address which generated the signature and is granting an allowance |
| `_spender` | `uint256` | Address which will be granted an allowance |
| `_value` | `uint256` | Approved amount |
| `_deadline` | `uint256` | Deadline by which the signature must be submitted |
| `_v` | `uint256` | Last byte of the ECDSA signature |
| `_r` | `uint256` | First 32 bytes of the ECDSA signature |
| `_s` | `uint256` | Second 32 bytes of the ECDSA signature |
Returns: true (`bool`).
Emits: `Approval`
:::note
In the event of a chain fork, replay attacks are prevented as domain separator is recalculated.
However, this is only if the resulting chains update their chainId.
:::
```vyper
event Approval:
owner: indexed(address)
spender: indexed(address)
value: uint256
allowance: public(HashMap[address, HashMap[address, uint256]])
@external
def permit(
_owner: address,
_spender: address,
_value: uint256,
_deadline: uint256,
_v: uint8,
_r: bytes32,
_s: bytes32,
) -> bool:
"""
@notice Permit `_spender` to spend up to `_value` amount of `_owner`'s tokens via a signature.
@dev In the event of a chain fork, replay attacks are prevented as domain separator is recalculated.
However, this is only if the resulting chains update their chainId.
@param _owner The account which generated the signature and is granting an allowance.
@param _spender The account which will be granted an allowance.
@param _value The approval amount.
@param _deadline The deadline by which the signature must be submitted.
@param _v The last byte of the ECDSA signature.
@param _r The first 32 bytes of the ECDSA signature.
@param _s The second 32 bytes of the ECDSA signature.
"""
assert _owner != empty(address) and block.timestamp <= _deadline
nonce: uint256 = self.nonces[_owner]
digest: bytes32 = keccak256(
concat(
b"\x19\x01",
self._domain_separator(),
keccak256(_abi_encode(EIP2612_TYPEHASH, _owner, _spender, _value, nonce, _deadline)),
)
)
if _owner.is_contract:
sig: Bytes[65] = concat(_abi_encode(_r, _s), slice(convert(_v, bytes32), 31, 1))
assert ERC1271(_owner).isValidSignature(digest, sig) == ERC1271_MAGIC_VAL
else:
assert ecrecover(digest, _v, _r, _s) == _owner
self.nonces[_owner] = nonce + 1
self._approve(_owner, _spender, _value)
return True
@internal
def _approve(_owner: address, _spender: address, _value: uint256):
self.allowance[_owner][_spender] = _value
log Approval(_owner, _spender, _value)
```
```shell
>>> crvUSD.permit(todo)
```
::::
---
## MarketFactory
The crvUSD MarketFactory enables the **creation of new markets**and adjustments, including **setting a new fee receiver**, **modifying the debt ceiling**of an existing market, or **updating blueprint implementations**.
Other than the pool factory, this factory **does not allow permissionless deployment of new markets**. Only its **`admin`**, the CurveOwnershipAgent, can call to add a market. Therefore, adding a new market requires a successfully passed DAO vote.
:::vyper[`ControllerFactory.vy`]
The source code for the `ControllerFactory.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/ControllerFactory.vy).
The contract is deployed on :logos-ethereum: Ethereum at [`0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC`](https://etherscan.io/address/0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC#code).
:::
---
## Adding Markets
A new crvUSD market can be added by the CurveOwnershipAgent. Therefore, adding a new market requires a successfully passed DAO vote.
### `add_market`
::::description[`ControllerFactory.add_market(token: address, A: uint256, fee: uint256, admin_fee: uint256, _price_oracle_contract: address, monetary_policy: address, loan_discount: uint256, liquidation_discount: uint256, debt_ceiling: uint256) -> address[2]`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to add a new market and automatically deploy a new AMM and a Controller from the implementation contracts (see [Implementations](#implementations)). Additionally, when initializing a new market, **`rate_write()`**from the MonetaryPolicy contract is called to check if it has a correct ABI.
| Input | Type | Description |
| ------------------------ | --------- | ------------------------------------------------------------ |
| `token` | `address` | Collateral token address |
| `A` | `uint256` | Amplification coefficient. One band size is 1/n |
| `fee` | `uint256` | AMM fee in the market's AMM |
| `admin_fee` | `uint256` | AMM admin fee |
| `_price_oracle_contract` | `address` | Address of the price oracle contract for the market |
| `monetary_policy` | `address` | Monetary policy for the market |
| `loan_discount` | `uint256` | Loan Discount: allowed to borrow only up to x_down * (1 - loan_discount) |
| `liquidation_discount` | `uint256` | Discount defining a bad liquidation threshold |
| `debt_ceiling` | `uint256` | Debt ceiling for the market |
:::warning
There are some limitation values for adding new markets regarding `fee`, `A` and `liquidation_discount`.
:::
Returns: AMM and Controller contracts (`address`).
Emits: `AddNewMarket`
```vyper
# Limits
MIN_A: constant(uint256) = 2
MAX_A: constant(uint256) = 10000
MIN_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0
MAX_FEE: constant(uint256) = 10**17 # 10%
MAX_ADMIN_FEE: constant(uint256) = 10**18 # 100%
MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17
MIN_LIQUIDATION_DISCOUNT: constant(uint256) = 10**16
@external
@nonreentrant('lock')
def add_market(token: address, A: uint256, fee: uint256, admin_fee: uint256,
_price_oracle_contract: address,
monetary_policy: address, loan_discount: uint256, liquidation_discount: uint256,
debt_ceiling: uint256) -> address[2]:
"""
@notice Add a new market, creating an AMM and a Controller from a blueprint
@param token Collateral token address
@param A Amplification coefficient; one band size is 1/A
@param fee AMM fee in the market's AMM
@param admin_fee AMM admin fee
@param _price_oracle_contract Address of price oracle contract for this market
@param monetary_policy Monetary policy for this market
@param loan_discount Loan discount: allowed to borrow only up to x_down * (1 - loan_discount)
@param liquidation_discount Discount which defines a bad liquidation threshold
@param debt_ceiling Debt ceiling for this market
@return (Controller, AMM)
"""
assert msg.sender == self.admin, "Only admin"
assert A >= MIN_A and A <= MAX_A, "Wrong A"
assert fee <= MAX_FEE, "Fee too high"
assert fee >= MIN_FEE, "Fee too low"
assert admin_fee < MAX_ADMIN_FEE, "Admin fee too high"
assert liquidation_discount >= MIN_LIQUIDATION_DISCOUNT, "Liquidation discount too low"
assert loan_discount <= MAX_LOAN_DISCOUNT, "Loan discount too high"
assert loan_discount > liquidation_discount, "need loan_discount>liquidation_discount"
MonetaryPolicy(monetary_policy).rate_write() # Test that MonetaryPolicy has correct ABI
p: uint256 = PriceOracle(_price_oracle_contract).price() # This also validates price oracle ABI
assert p > 0
assert PriceOracle(_price_oracle_contract).price_w() == p
A_ratio: uint256 = 10**18 * A / (A - 1)
amm: address = create_from_blueprint(
self.amm_implementation,
STABLECOIN.address, 10**(18 - STABLECOIN.decimals()),
token, 10**(18 - ERC20(token).decimals()), # <- This validates ERC20 ABI
A, isqrt(A_ratio * 10**18), self.ln_int(A_ratio),
p, fee, admin_fee, _price_oracle_contract,
code_offset=3)
controller: address = create_from_blueprint(
self.controller_implementation,
token, monetary_policy, loan_discount, liquidation_discount, amm,
code_offset=3)
AMM(amm).set_admin(controller)
self._set_debt_ceiling(controller, debt_ceiling, True)
N: uint256 = self.n_collaterals
self.collaterals[N] = token
for i in range(1000):
if self.collaterals_index[token][i] == 0:
self.collaterals_index[token][i] = 2**128 + N
break
assert i != 999, "Too many controllers for same collateral"
self.controllers[N] = controller
self.amms[N] = amm
self.n_collaterals = N + 1
log AddMarket(token, controller, amm, monetary_policy, N)
return [controller, amm]
```
```vyper
@internal
@view
def calculate_rate() -> uint256:
sigma: int256 = self.sigma
target_debt_fraction: uint256 = self.target_debt_fraction
p: int256 = convert(PRICE_ORACLE.price(), int256)
pk_debt: uint256 = 0
for pk in self.peg_keepers:
if pk.address == empty(address):
break
pk_debt += pk.debt()
power: int256 = (10**18 - p) * 10**18 / sigma # high price -> negative pow -> low rate
if pk_debt > 0:
total_debt: uint256 = CONTROLLER_FACTORY.total_debt()
if total_debt == 0:
return 0
else:
power -= convert(pk_debt * 10**18 / total_debt * 10**18 / target_debt_fraction, int256)
return self.rate0 * min(self.exp(power), MAX_EXP) / 10**18
@external
def rate_write() -> uint256:
# Not needed here but useful for more automated policies
# which change rate0 - for example rate0 targeting some fraction pl_debt/total_debt
return self.calculate_rate()
```
```vyper
@external
@view
def price() -> uint256:
n: uint256 = self.n_price_pairs
prices: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
D: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
Dsum: uint256 = 0
DPsum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
price_pair: PricePair = self.price_pairs[i]
pool_supply: uint256 = price_pair.pool.totalSupply()
if pool_supply >= MIN_LIQUIDITY:
p: uint256 = price_pair.pool.price_oracle()
if price_pair.is_inverse:
p = 10**36 / p
prices[i] = p
_D: uint256 = price_pair.pool.get_virtual_price() * pool_supply / 10**18
D[i] = _D
Dsum += _D
DPsum += _D * p
if Dsum == 0:
return 10**18 # Placeholder for no active pools
p_avg: uint256 = DPsum / Dsum
e: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
e_min: uint256 = max_value(uint256)
for i in range(MAX_PAIRS):
if i == n:
break
p: uint256 = prices[i]
e[i] = (max(p, p_avg) - min(p, p_avg))**2 / (SIGMA**2 / 10**18)
e_min = min(e[i], e_min)
wp_sum: uint256 = 0
w_sum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
w: uint256 = D[i] * self.exp(-convert(e[i] - e_min, int256)) / 10**18
w_sum += w
wp_sum += w * prices[i]
return wp_sum / w_sum
```
```vyper
@external
def set_admin(_admin: address):
"""
@notice Set admin of the AMM. Typically it's a controller (unless it's tests)
@param _admin Admin address
"""
assert self.admin == empty(address)
self.admin = _admin
self.approve_max(BORROWED_TOKEN, _admin)
self.approve_max(COLLATERAL_TOKEN, _admin)
```
```shell
>>> ControllerFactory.add_market("0xae78736cd615f374d3085123a210448e74fc6393",
100,
6000000000000000,
0,
"price oracle contract",
"monetary policy contract",
90000000000000000,
60000000000000000,
10000000000000000000000000):
"returns AMM and Controller contract"
```
::::
---
## Debt Ceilings
### `debt_ceiling`
::::description[`ControllerFactory.debt_ceiling(arg0: address) -> uint256: view`]
Getter for the current debt ceiling of a market.
Returns: debt ceiling (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `address` | Address of the controller |
```vyper
debt_ceiling: public(HashMap[address, uint256])
```
```shell
>>> ControllerFactory.debt_ceiling("0x8472A9A7632b173c8Cf3a86D3afec50c35548e76")
10000000000000000000000000
```
::::
### `debt_ceiling_residual`
::::description[`ControllerFactory.debt_ceiling_residual(arg0: address) -> uint256: view`]
Getter for the residual debt ceiling for a market.
Returns: debt ceiling residual (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `address` | Address of the controller |
```vyper
debt_ceiling: public(HashMap[address, uint256])
```
```shell
>>> ControllerFactory.debt_ceiling("0x8472A9A7632b173c8Cf3a86D3afec50c35548e76")
10000000000000000000000000
```
::::
### `rug_debt_ceiling`
::::description[`ControllerFactory.rug_debt_ceiling(_to: address)`]
Function to remove stablecoins above the debt seiling from a controller and burn them. This function is used to burn residual crvUSD when the debt ceiling was lowered.
| Input | Type | Description |
| ----------- | -------| ----|
| `_to` | `address` | Address of the controller to remove stablecoins from |
```vyper
@external
@nonreentrant('lock')
def rug_debt_ceiling(_to: address):
"""
@notice Remove stablecoins above the debt ceiling from the address and burn them
@param _to Address to remove stablecoins from
"""
self._set_debt_ceiling(_to, self.debt_ceiling[_to], False)
@internal
def _set_debt_ceiling(addr: address, debt_ceiling: uint256, update: bool):
"""
@notice Set debt ceiling for a market
@param addr Controller address
@param debt_ceiling Value for stablecoin debt ceiling
@param update Whether to actually update the debt ceiling (False is used for burning the residuals)
"""
old_debt_residual: uint256 = self.debt_ceiling_residual[addr]
if debt_ceiling > old_debt_residual:
to_mint: uint256 = debt_ceiling - old_debt_residual
STABLECOIN.mint(addr, to_mint)
self.debt_ceiling_residual[addr] = debt_ceiling
log MintForMarket(addr, to_mint)
if debt_ceiling < old_debt_residual:
diff: uint256 = min(old_debt_residual - debt_ceiling, STABLECOIN.balanceOf(addr))
STABLECOIN.burnFrom(addr, diff)
self.debt_ceiling_residual[addr] = old_debt_residual - diff
log RemoveFromMarket(addr, diff)
if update:
self.debt_ceiling[addr] = debt_ceiling
log SetDebtCeiling(addr, debt_ceiling)
```
```shell
>>> ControllerFactory.rug_debt_ceiling("controller address")
```
::::
### `set_debt_ceiling`
::::description[`ControllerFactory.set_debt_ceiling(_to: address, debt_ceiling: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set the debt ceiling of a market and mint the token amount given for it.
| Input | Type | Description |
| ----------- | -------| ----|
| `_to` | `address` | Address to set debt ceiling for |
| `debt_ceiling` | `uint256` | Maximum to be allowed to mint |
Emits: `MintForMarket` or `RemoveFromMarket` or `SetDebtCeiling`
```vyper
event SetDebtCeiling:
addr: indexed(address)
debt_ceiling: uint256
event MintForMarket:
addr: indexed(address)
amount: uint256
event RemoveFromMarket:
addr: indexed(address)
amount: uint256
@internal
def _set_debt_ceiling(addr: address, debt_ceiling: uint256, update: bool):
"""
@notice Set debt ceiling for a market
@param addr Controller address
@param debt_ceiling Value for stablecoin debt ceiling
@param update Whether to actually update the debt ceiling (False is used for burning the residuals)
"""
old_debt_residual: uint256 = self.debt_ceiling_residual[addr]
if debt_ceiling > old_debt_residual:
to_mint: uint256 = debt_ceiling - old_debt_residual
STABLECOIN.mint(addr, to_mint)
self.debt_ceiling_residual[addr] = debt_ceiling
log MintForMarket(addr, to_mint)
if debt_ceiling < old_debt_residual:
diff: uint256 = min(old_debt_residual - debt_ceiling, STABLECOIN.balanceOf(addr))
STABLECOIN.burnFrom(addr, diff)
self.debt_ceiling_residual[addr] = old_debt_residual - diff
log RemoveFromMarket(addr, diff)
if update:
self.debt_ceiling[addr] = debt_ceiling
log SetDebtCeiling(addr, debt_ceiling)
@external
@nonreentrant('lock')
def set_debt_ceiling(_to: address, debt_ceiling: uint256):
"""
@notice Set debt ceiling of the address - mint the token amount given for it
@param _to Address to allow borrowing for
@param debt_ceiling Maximum allowed to be allowed to mint for it
"""
assert msg.sender == self.admin
self._set_debt_ceiling(_to, debt_ceiling, True)
```
```vyper
@external
def mint(_to: address, _value: uint256) -> bool:
"""
@notice Mint `_value` amount of tokens to `_to`.
@dev Only callable by an account with minter privileges.
@param _to The account newly minted tokens are credited to.
@param _value The amount of tokens to mint.
"""
assert msg.sender == self.minter
assert _to not in [self, empty(address)]
self.balanceOf[_to] += _value
self.totalSupply += _value
log Transfer(empty(address), _to, _value)
return True
@external
def burn(_value: uint256) -> bool:
"""
@notice Burn `_value` amount of tokens.
@param _value The amount of tokens to burn.
"""
self._burn(msg.sender, _value)
return True
```
```shell
>>> ControllerFactory.set_debt_ceiling(20000000000000000000000000)
```
::::
---
## Fee Receiver
The fee receiver is the address that receives the claimed fees when calling `collect_fees()` on the Controller.
A new receiver can be set by the `admin` of the contract, which is the CurveOwnershipAgent.
### `fee_receiver`
::::description[`ControllerFactory.fee_receiver() -> address: view`]
Getter for the fee receiver address.
Returns: `address` of fee receiver.
```vyper
fee_receiver: public(address)
@external
def __init__(stablecoin: ERC20,
admin: address,
fee_receiver: address,
weth: address):
"""
@notice Factory which creates both controllers and AMMs from blueprints
@param stablecoin Stablecoin address
@param admin Admin of the factory (ideally DAO)
@param fee_receiver Receiver of interest and admin fees
@param weth Address of WETH contract address
"""
STABLECOIN = stablecoin
self.admin = admin
self.fee_receiver = fee_receiver
WETH = weth
```
```shell
>>> ControllerFactory.fee_receiver()
'0xeCb456EA5365865EbAb8a2661B0c503410e9B347'
```
::::
### `set_fee_receiver`
::::description[`ControllerFactory.set_fee_receiver(fee_receiver: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set the fee receiver address.
| Input | Type | Description |
| ----------- | -------| ----|
| `fee_receiver` | `address` | Address of the receiver |
Emits: `SetFeeReceiver`
```vyper
event SetFeeReceiver:
fee_receiver: address
fee_receiver: public(address)
@external
@nonreentrant('lock')
def set_fee_receiver(fee_receiver: address):
"""
@notice Set fee receiver who earns interest (DAO)
@param fee_receiver Address of the receiver
"""
assert msg.sender == self.admin
assert fee_receiver != empty(address)
self.fee_receiver = fee_receiver
log SetFeeReceiver(fee_receiver)
```
```shell
>>> ControllerFactory.set_fee_receiver("0xeCb456EA5365865EbAb8a2661B0c503410e9B347")
```
::::
### `collect_fees_above_ceiling`
::::description[`ControllerFactory.collect_fees_above_ceiling(_to: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to claim fees above the debt ceiling. This function will automatically increase the debt ceiling if there is not enough to claim admin fees.
| Input | Type | Description |
| ----------- | -------| ----|
| `_to` | `address` | Address of the controller |
```vyper
@external
@nonreentrant('lock')
def collect_fees_above_ceiling(_to: address):
"""
@notice If the receiver is the controller - increase the debt ceiling if it's not enough to claim admin fees
and claim them
@param _to Address of the controller
"""
assert msg.sender == self.admin
old_debt_residual: uint256 = self.debt_ceiling_residual[_to]
assert self.debt_ceiling[_to] > 0 or old_debt_residual > 0
admin_fees: uint256 = Controller(_to).total_debt() + Controller(_to).redeemed() - Controller(_to).minted()
b: uint256 = STABLECOIN.balanceOf(_to)
if admin_fees > b:
to_mint: uint256 = admin_fees - b
STABLECOIN.mint(_to, to_mint)
self.debt_ceiling_residual[_to] = old_debt_residual + to_mint
Controller(_to).collect_fees()
```
```shell
>>> ControllerFactory.collect_fees_above_ceiling("0x100dAa78fC509Db39Ef7D04DE0c1ABD299f4C6CE")
```
::::
---
## Implementations
Implementations are blueprint contracts used to deploy new markets. When calling `add_market`, Controller and AMM are created from the current implementations.
### `controller_implementation`
::::description[`ControllerFactory.controller_implementation() -> address: view`]
Getter for controller implementation address.
Returns: implementation (`address`).
```vyper
collaterals: public(address[MAX_CONTROLLERS])
```
```shell
>>> ControllerFactory.controller_implementation()
'0x6340678b2bab22a37d781Cd8da958a3cD1d97cdD'
```
::::
### `amm_implementation`
::::description[`ControllerFactory.amm_implementation() -> address: view`]
Getter for amm implementation address.
Returns: implementation (`address`).
```vyper
amm_implementation: public(address)
```
```shell
>>> ControllerFactory.amm_implementation()
'0x3da7fF6C15C0c97D9C2dF4AF82a9910384b372FD'
```
::::
### `set_implementations`
::::description[`ControllerFactory.set_implementations(controller: address, amm: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set new implementations (blueprints) for Controller and AMM. Setting new implementations for Controller and AMM does not affect the existing ones.
| Input | Type | Description |
| ----------- | -------| ----|
| `controller` | `address` | Address of the controller blueprint |
| `amm` | `address` | Address of the amm blueprint |
Emits: `SetImplementations`
```vyper
event SetImplementations:
amm: address
controller: address
controller_implementation: public(address)
amm_implementation: public(address)
@external
@nonreentrant('lock')
def set_implementations(controller: address, amm: address):
"""
@notice Set new implementations (blueprints) for controller and amm. Doesn't change existing ones
@param controller Address of the controller blueprint
@param amm Address of the AMM blueprint
"""
assert msg.sender == self.admin
assert controller != empty(address)
assert amm != empty(address)
self.controller_implementation = controller
self.amm_implementation = amm
log SetImplementations(amm, controller)
```
```shell
>>> ControllerFactory.set_implementations("new controller implementation", "new amm implementation")
```
::::
---
## Contract Info Methods
### `stablecoin`
::::description[`ControllerFactory.stablecoin() -> address: view`]
Getter for the stablecoin address.
Returns: stablecoin (`address`).
```vyper
STABLECOIN: immutable(ERC20)
@external
def __init__(stablecoin: ERC20,
admin: address,
fee_receiver: address,
weth: address):
"""
@notice Factory which creates both controllers and AMMs from blueprints
@param stablecoin Stablecoin address
@param admin Admin of the factory (ideally DAO)
@param fee_receiver Receiver of interest and admin fees
@param weth Address of WETH contract address
"""
STABLECOIN = stablecoin
self.admin = admin
self.fee_receiver = fee_receiver
WETH = weth
```
```shell
>>> ControllerFactory.stablecoin()
'0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'
```
::::
### `total_debt`
::::description[`ControllerFactory.total_debt() -> uint256: view`]
Getter for the sum of all debts across the controllers.
Returns: total amount of debt (`uint256`).
```vyper
@external
@view
def total_debt() -> uint256:
"""
@notice Sum of all debts across controllers
"""
total: uint256 = 0
n_collaterals: uint256 = self.n_collaterals
for i in range(MAX_CONTROLLERS):
if i == n_collaterals:
break
total += Controller(self.controllers[i]).total_debt()
return total
```
```shell
>>> ControllerFactory.total_debt()
37565735180665889485176526
```
::::
### `get_controller`
::::description[`ControllerFactory.get_controller(collateral: address, i: uint256 = 0) -> address: view`]
Getter for the controller address for `collateral`.
Returns: controller `address`.
| Input | Type | Description |
| ----------- | -------| ----|
| `collateral` | `address` | Address of collateral token |
| `i` | `uint256` | Index to iterate over several controller for the same collateral if needed |
```vyper
@external
@view
def get_controller(collateral: address, i: uint256 = 0) -> address:
"""
@notice Get controller address for collateral
@param collateral Address of collateral token
@param i Iterate over several controllers for collateral if needed
"""
return self.controllers[self.collaterals_index[collateral][i] - 2**128]
```
```shell
>>> ControllerFactory.get_controller("0xac3E018457B222d93114458476f3E3416Abbe38F", 0)
'0x8472A9A7632b173c8Cf3a86D3afec50c35548e76'
```
::::
### `get_amm`
::::description[`ControllerFactory.get_amm(collateral: address, i: uint256 = 0) -> address: view`]
Getter for the amm address for `collateral`.
Returns: amm `address`.
| Input | Type | Description |
| ----------- | -------| ----|
| `collateral` | `address` | Address of collateral token |
| `i` | `uint256` | Index to iterate over several amms for the same collateral if needed |
```vyper
@external
@view
def get_amm(collateral: address, i: uint256 = 0) -> address:
"""
@notice Get AMM address for collateral
@param collateral Address of collateral token
@param i Iterate over several AMMs for collateral if needed
"""
return self.amms[self.collaterals_index[collateral][i] - 2**128]
```
```shell
>>> ControllerFactory.get_amm("0xac3E018457B222d93114458476f3E3416Abbe38F", 0)
'0x136e783846ef68C8Bd00a3369F787dF8d683a696'
```
::::
### `controllers`
::::description[`ControllerFactory.controllers(arg0: uint256) -> address: view`]
Getter for the controller address at index `arg0`.
Returns: controller `address` at specific index.
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `uint256` | Index |
```vyper
MAX_CONTROLLERS: constant(uint256) = 50000
controllers: public(address[MAX_CONTROLLERS])
```
```shell
>>> ControllerFactory.controllers(0)
'0x8472A9A7632b173c8Cf3a86D3afec50c35548e76'
```
::::
### `amms`
::::description[`ControllerFactory.amms(arg0: uint256) -> address: view`]
Getter for the amm address at index `arg0`.
Returns: AMM `address` at specific index.
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `uint256` | Index |
```vyper
amms: public(address[MAX_CONTROLLERS])
```
```shell
>>> ControllerFactory.amms(0)
'0x136e783846ef68C8Bd00a3369F787dF8d683a696'
```
::::
### `n_collaterals`
::::description[`ControllerFactory.n_collaterals() -> uint256: view`]
Getter for the number of collaterals.
Returns: number of collaterals (`uint256`).
```vyper
n_collaterals: public(uint256)
```
```shell
>>> ControllerFactory.n_collaterals()
2
```
::::
### `collaterals`
::::description[`ControllerFactory.collaterals(arg0: uint256) -> address: view`]
Getter for the collateral addresses at index `arg0`.
Returns: `address` of collateral.
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `uint256` | Index |
```vyper
collaterals: public(address[MAX_CONTROLLERS])
```
```shell
>>> ControllerFactory.collaterals(0)
'0xac3E018457B222d93114458476f3E3416Abbe38F'
```
::::
### `collaterals_index`
::::description[`ControllerFactory.collaterals_index(arg0: address, arg1: uint256) -> uint256: view`]
Getter for the index of a controller for `arg0`.
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `address` | Address of collateral |
| `arg1` | `uint256` | Index |
Returns: index (`uint256`).
:::note
The returned value is $2^{128}$ + index.
:::
```vyper
collaterals_index: public(HashMap[address, uint256[1000]])
```
```shell
>>> ControllerFactory.collaterals_index(0xac3E018457B222d93114458476f3E3416Abbe38F, 0)
340282366920938463463374607431768211456
```
::::
### `WETH`
::::description[`ControllerFactory.WETH() -> address: view`]
Getter for WETH address.
Returns: `address` of WETH.
```vyper
WETH: public(immutable(address))
@external
def __init__(stablecoin: ERC20,
admin: address,
fee_receiver: address,
weth: address):
"""
@notice Factory which creates both controllers and AMMs from blueprints
@param stablecoin Stablecoin address
@param admin Admin of the factory (ideally DAO)
@param fee_receiver Receiver of interest and admin fees
@param weth Address of WETH contract address
"""
STABLECOIN = stablecoin
self.admin = admin
self.fee_receiver = fee_receiver
WETH = weth
```
```shell
>>> ControllerFactory.WETH()
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
```
::::
---
## Admin Ownership
### `admin`
::::description[`ControllerFactory.admin() -> address: view`]
Getter for the admin of the contract.
Returns: admin (`address`).
```vyper
admin: public(address)
@external
def __init__(stablecoin: ERC20,
admin: address,
fee_receiver: address,
weth: address):
"""
@notice Factory which creates both controllers and AMMs from blueprints
@param stablecoin Stablecoin address
@param admin Admin of the factory (ideally DAO)
@param fee_receiver Receiver of interest and admin fees
@param weth Address of WETH contract address
"""
STABLECOIN = stablecoin
self.admin = admin
self.fee_receiver = fee_receiver
WETH = weth
```
```shell
>>> ControllerFactory.admin()
'0x40907540d8a6C65c637785e8f8B742ae6b0b9968'
```
::::
### `set_admin`
::::description[`ControllerFactory.set_admin(admin: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set the admin of the contract.
| Input | Type | Description |
| ----------- | -------| ----|
| `admin` | `address` | New admin |
Emits: `SetAdmin`
```vyper
event SetAdmin:
admin: address
admin: public(address)
@external
@nonreentrant('lock')
def set_admin(admin: address):
"""
@notice Set admin of the factory (should end up with DAO)
@param admin Address of the admin
"""
assert msg.sender == self.admin
self.admin = admin
log SetAdmin(admin)
```
```shell
>>> ControllerFactory.set_admin("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
```
::::
---
## FlashLender
The `FlashLender.vy` contract is an [`ERC-3156`](https://eips.ethereum.org/EIPS/eip-3156) contract that allows users to take out a flash loan for `crvUSD`. The flash loan must be repaid within the same transaction; otherwise, the transaction will revert.
:::vyper[`FlashLender.vy`]
The source code for the `FlashLender.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/flashloan/FlashLender.vy). Additionally, a `DummyFlashBorrower.vy` contract showcasing a potential usage of a flash loan can also be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/testing/DummyFlashBorrower.vy).
The `FlashLender.vy` is deployed on :logos-ethereum: Ethereum at [`0x26dE7861e213A5351F6ED767d00e0839930e9eE1`](https://etherscan.io/address/0x26dE7861e213A5351F6ED767d00e0839930e9eE1).
:::
The contract does not charge any fees on flash loans. The `fee` and `flashFee` methods are implemented to comply with the `ERC-3156` standard.
---
### `flashLoan`
::::description[`FlashLender.flashLoan(receiver: ERC3156FlashBorrower, token: address, amount: uint256, data: Bytes[10**5]) -> bool`]
Function to take out a flash loan of `amount` of `token` and send them to the `receiver`. The `receiver` address must be a contract that implements the `onFlashLoan(initiator: address, token: address, amount: uint256, fee: uint256, data: Bytes[10**5])` interface. A flash loan must be repaid within the same transaction; otherwise, the transaction will revert. Additionally, the method allows passing custom `data` to the `receiver` contract.
| Input | Type | Description |
| ---------- | ---------------------- | --------------------------------------------- |
| `receiver` | `ERC3156FlashBorrower` | Contract to receive the flash loan |
| `token` | `address` | Address of the token to take the flash loan in|
| `amount` | `uint256` | Amount of tokens to flash loan |
| `data` | `Bytes[10**5]` | Custom data to pass to the receiver contract |
Returns: `True` (`bool`).
Emits: `FlashLoan`
The following source code includes all changes up to commit hash [`e771f43`](https://github.com/curvefi/curve-stablecoin/tree/e771f437f42fbc5ab73990866000f610bffe1df2); any changes made after this commit are not included.
```py
from vyper.interfaces import ERC20
interface Factory:
def stablecoin() -> address: view
def debt_ceiling_residual(_to: address) -> uint256: view
event FlashLoan:
caller: indexed(address)
receiver: indexed(address)
amount: uint256
CRVUSD: immutable(address)
fee: public(constant(uint256)) = 0 # 1 == 0.01 %
@external
@nonreentrant('lock')
def flashLoan(receiver: ERC3156FlashBorrower, token: address, amount: uint256, data: Bytes[10**5]) -> bool:
"""
@notice Loan `amount` tokens to `receiver`, and takes it back plus a `flashFee` after the callback
@param receiver The contract receiving the tokens, needs to implement the
`onFlashLoan(initiator: address, token: address, amount: uint256, fee: uint256, data: Bytes[10**5])` interface.
@param token The loan currency.
@param amount The amount of tokens lent.
@param data A data parameter to be passed on to the `receiver` for any custom use.
"""
assert token == CRVUSD, "FlashLender: Unsupported currency"
ERC20(CRVUSD).transfer(receiver.address, amount)
receiver.onFlashLoan(msg.sender, CRVUSD, amount, 0, data)
assert ERC20(CRVUSD).balanceOf(self) >= FACTORY.debt_ceiling_residual(self), "FlashLender: Repay failed"
log FlashLoan(msg.sender, receiver.address, amount)
return True
```
::::
### `maxFlashLoan`
::::description[`FlashLender.maxFlashLoan(token: address) -> uint256: view`]
Getter for the maximum amount of flash-loanable tokens. This corresponds to the token balance of the contract (`token.balanceOf(FlashLender)`).
Returns: maximum flash-loanable amount (`uint256`).
| Input | Type | Description |
| ------- | --------- | ------------------------------------------------------------ |
| `token` | `address` | Token address to check the maximum flash-loanable amount for |
The following source code includes all changes up to commit hash [`e771f43`](https://github.com/curvefi/curve-stablecoin/tree/e771f437f42fbc5ab73990866000f610bffe1df2); any changes made after this commit are not included.
```py
CRVUSD: immutable(address)
fee: public(constant(uint256)) = 0 # 1 == 0.01 %
@external
@view
def maxFlashLoan(token: address) -> uint256:
"""
@notice The amount of currency available to be lent.
@param token The loan currency.
@return The amount of `token` that can be borrowed.
"""
if token == CRVUSD:
return ERC20(CRVUSD).balanceOf(self)
else:
return 0
```
Calling the function with the `crvUSD` address as input will return the flash-loanable amount. Calling the function with any other token than `crvUSD` will return `0`.
```shell
>>> FlashLender.maxFlashLoan('0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E')
1000000000000000000000000
```
::::
### `fee`
::::description[`FlashLender.fee() -> uint256: view`]
Getter for the fee charged on the flash loan. This variable is a constant set to `0` and cannot be changed. The fees for the contract will always remain at `0`.
Returns: fee (`uint256`).
The following source code includes all changes up to commit hash [`e771f43`](https://github.com/curvefi/curve-stablecoin/tree/e771f437f42fbc5ab73990866000f610bffe1df2); any changes made after this commit are not included.
```py
fee: public(constant(uint256)) = 0 # 1 == 0.01 %
```
```shell
>>> FlashLender.fee()
0
```
::::
### `flashFee`
::::description[`FlashLender.flashFee(token: address, amount: uint256) -> uint256: view`]
Getter for the flash fee when taking out a flash loan of `amount` of `token`. This method will always return `0` and will always revert if `token != CRVUSD`.
Returns: total fee charged on the flashloan (`uint256`).
| Input | Type | Description |
| -------- | --------- | ----------------------------------------- |
| `token` | `address` | Address of the token for the flash loan |
| `amount` | `uint256` | Amount of tokens to be flash loaned |
The following source code includes all changes up to commit hash [`e771f43`](https://github.com/curvefi/curve-stablecoin/tree/e771f437f42fbc5ab73990866000f610bffe1df2); any changes made after this commit are not included.
```py
CRVUSD: immutable(address)
@external
@view
def flashFee(token: address, amount: uint256) -> uint256:
"""
@notice The fee to be charged for a given loan.
@param token The loan currency.
@param amount The amount of tokens lent.
@return The amount of `token` to be charged for the loan, on top of the returned principal.
"""
assert token == CRVUSD, "FlashLender: Unsupported currency"
return 0
```
This method will always return `0` for any amount of flash loaned `crvUSD` because the `fee` is `0`. If the function is called with any token other than `crvUSD`, the function will revert.
```shell
>>> FlashLender.flashFee('0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E', 100000 * 10**18)
0
>>> FlashLender.flashFee('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 100000 * 10**18)
reverts: "FlashLender: Unsupported currency"
```
::::
### `supportedTokens`
::::description[`FlashLender.supportedTokens(token: address) -> bool: view`]
Getter for the supported token by the `FlashLender`. The only supported token is `crvUSD`. Due to the immutability of the contract, no further supported tokens can be added.
Returns: `True` or `False` (`bool`).
| Input | Type | Description |
| ------- | --------- | ----------------------------------------- |
| `token` | `address` | Token address to check support status for |
The following source code includes all changes up to commit hash [`e771f43`](https://github.com/curvefi/curve-stablecoin/tree/e771f437f42fbc5ab73990866000f610bffe1df2); any changes made after this commit are not included.
```py
CRVUSD: immutable(address)
@external
@view
def supportedTokens(token: address) -> bool:
return token == CRVUSD
```
This method returns a `bool` based on whether the token is supported or not.
```shell
>>> FlashLender.supportedTokens('0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E') # crvusd
'true'
>>> FlashLender.supportedTokens('0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E') # usdc
'false'
```
::::
### `version`
::::description[`FlashLender.version() -> String[8]: view`]
Getter for the version of the contract.
Returns: contract version (`String[8]`).
The following source code includes all changes up to commit hash [`e771f43`](https://github.com/curvefi/curve-stablecoin/tree/e771f437f42fbc5ab73990866000f610bffe1df2); any changes made after this commit are not included.
```py
version: public(constant(String[8])) = "1.0.0" # Initial
```
```shell
>>> FlashLender.version()
"1.0.0"
```
::::
---
## LeverageZap1inch.vy
This Zap contract is specifically designed to **create or repay leveraged loans** using the [**1inch router**](https://1inch.io/aggregation-protocol/).
:::vyper[`LeverageZap1inch.vy`]
The source code for the `LeverageZap1inch.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/lending/contracts/zaps/LeverageZap1inch.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
The contract is deployed on :logos-ethereum: Ethereum at [`0x3294514B78Df4Bb90132567fcf8E5e99f390B687`](https://etherscan.io/address/0x3294514B78Df4Bb90132567fcf8E5e99f390B687).
An accompanying JavaScript library for Curve Lending can be found here: [GitHub](https://github.com/curvefi/curve-lending-js).
:::
Previously, building leverage for crvUSD markets relied solely on predefined routes using only Curve pools. Leveraging large positions often led to significant price impact due to the exclusive use of Curve liquidity pools. This new Zap contract allows users to leverage loans for crvUSD and lending markets using the 1inch router, which considers liquidity sources across DeFi.[^1]
[^1]: The premise is that these liquidity sources are integrated within the 1inch router.
---
Leverage is built using a **callback method**. The function to execute callbacks is located in the `Controller.vy` contract:
:::bug
`callback_sig` is the `method_id` of the function from the `LeverageZap1inch.vy` contract which needs to be called. While this value is obtained by using Vyper's built-in [`method_id`](https://docs.vyperlang.org/en/stable/built-in-functions.html?highlight=raw_call#method_id) function for the `callback_deposit` function, it does not work for the `callback_repay` function due to a bug. The reason for the bug is a `0` at the beginning of the method_id. That's why the method ID for `CALLBACK_REPAY_WITH_BYTES` is hardcoded to `0x008ae188`.
:::
```py
struct CallbackData:
active_band: int256
stablecoins: uint256
collateral: uint256
CALLBACK_DEPOSIT: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_REPAY: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_LIQUIDATE: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_DEPOSIT_WITH_BYTES: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
# CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4) <-- BUG! The reason is 0 at the beginning of method_id
CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = 0x008ae188
CALLBACK_LIQUIDATE_WITH_BYTES: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
@internal
def execute_callback(callbacker: address, callback_sig: bytes4,
user: address, stablecoins: uint256, collateral: uint256, debt: uint256,
callback_args: DynArray[uint256, 5], callback_bytes: Bytes[10**4]) -> CallbackData:
assert callbacker != COLLATERAL_TOKEN.address
data: CallbackData = empty(CallbackData)
data.active_band = AMM.active_band()
band_x: uint256 = AMM.bands_x(data.active_band)
band_y: uint256 = AMM.bands_y(data.active_band)
# Callback
response: Bytes[64] = raw_call(
callbacker,
concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)),
max_outsize=64
)
data.stablecoins = convert(slice(response, 0, 32), uint256)
data.collateral = convert(slice(response, 32, 32), uint256)
# Checks after callback
assert data.active_band == AMM.active_band()
assert band_x == AMM.bands_x(data.active_band)
assert band_y == AMM.bands_y(data.active_band)
return data
```
:::info[Required Changes to `Controller.vy`]
This zap only works for crvUSD and lending markets which were deployed using the blueprint implementation at [`0x4c5d4F542765B66154B2E789abd8E69ed4504112`](https://etherscan.io/address/0x4c5d4F542765B66154B2E789abd8E69ed4504112). Markets deployed prior to that can only make use of the regular [`LeverageZap.vy`](./leverage-zap.md).
To enable the functionality of such Zap contracts, minor modifications were necessary in the `Controller.vy` contract. Functions such as `create_loan_extended`, `borrow_more_extended`, `repay_extended`, `_liquidity`, and `liquidate_extended` were enhanced with an additional constructor argument `callback_bytes: Bytes[10**4]`. This allows users to pass bytes to the Zap contract. Additionally, the internal `execute_callback` function, which manages the callbacks, was also updated.
:::
---
## Building Leverage
To build up leverage, the `LeverageZap1inch.vy` contract uses the `callback_deposit` function. Additionally, there is a `max_borrowable` function that calculates the maximum borrowable amount when using leverage. For an accompanying JavaScript library, see [GitHub](https://github.com/curvefi/curve-lending-js?tab=readme-ov-file#leverage-createloan-borrowmore-repay).
*Flow of building leverage:*
1. User calls [`create_loan_extended`](../controller.md#create_loan_extended) or [`borrow_more_extended`](../controller.md#borrow_more_extended) and passes `collateral`, `debt`, `N`, `callbacker`, `callback_args`, and `callback_bytes` into the function.[^2]
2. The debt which is taken on by the user is then transferred to the `callbacker`, in our case the `LeverageZap1inch.vy` contract.
3. After the transfer, the callback is executed using the internal `execute_callback` in the `Controller.vy` contract. This step builds up the leverage.
```py
struct CallbackData:
active_band: int256
stablecoins: uint256
collateral: uint256
CALLBACK_DEPOSIT: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_REPAY: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_LIQUIDATE: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_DEPOSIT_WITH_BYTES: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
# CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4) <-- BUG! The reason is 0 at the beginning of method_id
CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = 0x008ae188
CALLBACK_LIQUIDATE_WITH_BYTES: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
@internal
def execute_callback(callbacker: address, callback_sig: bytes4,
user: address, stablecoins: uint256, collateral: uint256, debt: uint256,
callback_args: DynArray[uint256, 5], callback_bytes: Bytes[10**4]) -> CallbackData:
assert callbacker != COLLATERAL_TOKEN.address
data: CallbackData = empty(CallbackData)
data.active_band = AMM.active_band()
band_x: uint256 = AMM.bands_x(data.active_band)
band_y: uint256 = AMM.bands_y(data.active_band)
# Callback
response: Bytes[64] = raw_call(
callbacker,
concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)),
max_outsize=64
)
data.stablecoins = convert(slice(response, 0, 32), uint256)
data.collateral = convert(slice(response, 32, 32), uint256)
# Checks after callback
assert data.active_band == AMM.active_band()
assert band_x == AMM.bands_x(data.active_band)
assert band_y == AMM.bands_y(data.active_band)
return data
```
The function uses Vyper's built-in [`raw_call`](https://docs.vyperlang.org/en/stable/built-in-functions.html?highlight=raw_call#raw_call) function to call the desired method (in this case `callback_deposit`) with the according `callback_bytes`.
4. After executing the callback, the Controller either creates a new loan or adds the additional collateral borrowed to the already existing loan and deposits the collateral into the AMM.
[^2]: `collateral` is the amount of collateral tokens used, `debt` is the amount of debt to take on, `N` represents the number of bands, `callbacker` is the callback contract, `callback_args` are some extra arguments passed to the callbacker, and `callback_bytes`.
### `callback_deposit`
::::description[`LeverageZap1inch.callback_deposit(user: address, stablecoins: uint256, user_collateral: uint256, d_debt: uint256, callback_args: DynArray[uint256, 10], callback_bytes: Bytes[10**4] = b"")]
:::guard[Guarded Method]
This function is only callable by the `Controller` from where tokens are borrowed from.
:::
Function to create a leveraged loan using a callback.
The following callback arguments need to be passed to this function via `create_loan_extended` or `borrow_more_extended`:
- `callback_args[0] = factory_id`: depending on which factory (crvusd or lending).
- `callback_args[1] = controller_id`: index of the controller in the factory contract fetched from `Factory.controllers(controller_id)`.
- `callback_args[2] = user_borrowed`: amount of borrowed token provided by the user (which is exchanged for the collateral token).
| Input | Type | Description |
| ----------------- | ----------------------- | ------------ |
| `user` | `address` | User address to create a leveraged position for. |
| `stablecoins` | `uint256` | Always 0. |
| `user_collateral` | `uint256` | Amount of collateral token provided by the user. |
| `d_debt` | `uint256` | Amount to be borrowed (in addition to what has already been borrowed). |
| `callback_args` | `DynArray[uint256, 10]` | Callback arguments. |
| `callback_bytes` | `Bytes[10**4] = b""` | Callback bytes. |
Returns: 0 and additional collateral (`uint256[2]`).
Emits: `Deposit`
```vyper
event Deposit:
user: indexed(address)
user_collateral: uint256
user_borrowed: uint256
user_collateral_from_borrowed: uint256
debt: uint256
leverage_collateral: uint256
@external
@nonreentrant('lock')
def callback_deposit(user: address, stablecoins: uint256, user_collateral: uint256, d_debt: uint256,
callback_args: DynArray[uint256, 10], callback_bytes: Bytes[10**4] = b"") -> uint256[2]:
"""
@notice Callback method which should be called by controller to create leveraged position
@param user Address of the user
@param stablecoins Always 0
@param user_collateral The amount of collateral token provided by user
@param d_debt The amount to be borrowed (in addition to what has already been borrowed)
@param callback_args [factory_id, controller_id, user_borrowed]
0-1. factory_id, controller_id are needed to check that msg.sender is the one of our controllers
1. user_borrowed - the amount of borrowed token provided by user (needs to be exchanged for collateral)
return [0, user_collateral_from_borrowed + leverage_collateral]
"""
controller: address = Factory(self.FACTORIES[callback_args[0]]).controllers(callback_args[1])
assert msg.sender == controller, "wrong controller"
amm: LLAMMA = LLAMMA(Controller(controller).amm())
borrowed_token: address = amm.coins(0)
collateral_token: address = amm.coins(1)
self._approve(borrowed_token, ROUTER_1INCH)
self._approve(collateral_token, controller)
user_borrowed: uint256 = callback_args[2]
self._transferFrom(borrowed_token, user, self, user_borrowed)
raw_call(ROUTER_1INCH, callback_bytes) # buys leverage_collateral for user_borrowed + dDebt
additional_collateral: uint256 = ERC20(collateral_token).balanceOf(self)
leverage_collateral: uint256 = d_debt * 10**18 / (d_debt + user_borrowed) * additional_collateral / 10**18
user_collateral_from_borrowed: uint256 = additional_collateral - leverage_collateral
log Deposit(user, user_collateral, user_borrowed, user_collateral_from_borrowed, d_debt, leverage_collateral)
return [0, additional_collateral]
@internal
def _approve(coin: address, spender: address):
if ERC20(coin).allowance(self, spender) == 0:
ERC20(coin).approve(spender, max_value(uint256))
@internal
def _transferFrom(token: address, _from: address, _to: address, amount: uint256):
if amount > 0:
assert ERC20(token).transferFrom(_from, _to, amount, default_return_value=True)
```
```vyper
@external
@nonreentrant('lock')
def create_loan_extended(collateral: uint256, debt: uint256, N: uint256, callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b""):
"""
@notice Create loan but pass stablecoin to a callback first so that it can build leverage
@param collateral Amount of collateral to use
@param debt Stablecoin debt to take
@param N Number of bands to deposit into (to do autoliquidation-deliquidation),
can be from MIN_TICKS to MAX_TICKS
@param callbacker Address of the callback contract
@param callback_args Extra arguments for the callback (up to 5) such as min_amount etc
"""
# Before callback
self.transfer(BORROWED_TOKEN, callbacker, debt)
# For compatibility
callback_sig: bytes4 = CALLBACK_DEPOSIT_WITH_BYTES
if callback_bytes == b"":
callback_sig = CALLBACK_DEPOSIT
# Callback
# If there is any unused debt, callbacker can send it to the user
more_collateral: uint256 = self.execute_callback(
callbacker, callback_sig, msg.sender, 0, collateral, debt, callback_args, callback_bytes).collateral
# After callback
self._create_loan(collateral + more_collateral, debt, N, False)
self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral)
self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral)
@internal
def execute_callback(callbacker: address, callback_sig: bytes4,
user: address, stablecoins: uint256, collateral: uint256, debt: uint256,
callback_args: DynArray[uint256, 5], callback_bytes: Bytes[10**4]) -> CallbackData:
assert callbacker != COLLATERAL_TOKEN.address
data: CallbackData = empty(CallbackData)
data.active_band = AMM.active_band()
band_x: uint256 = AMM.bands_x(data.active_band)
band_y: uint256 = AMM.bands_y(data.active_band)
# Callback
response: Bytes[64] = raw_call(
callbacker,
concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)),
max_outsize=64
)
data.stablecoins = convert(slice(response, 0, 32), uint256)
data.collateral = convert(slice(response, 32, 32), uint256)
# Checks after callback
assert data.active_band == AMM.active_band()
assert band_x == AMM.bands_x(data.active_band)
assert band_y == AMM.bands_y(data.active_band)
return data
```
```shell
>>> soon
```
::::
### `max_borrowable`
::::description[`LeverageZap1inch.max_borrowable(controller: address, _user_collateral: uint256, _leverage_collateral: uint256, N: uint256, p_avg: uint256) -> uint256: view`]
Function to calculate the maximum borrowable using leverage. The maximum borrowable amount essentially comes down to:
$$\text{max\_borrowable} = \frac{\text{collateral}}{\frac{1}{\text{k\_effective} \times \text{max\_p\_base}} - \frac{1}{\text{p\_avg}}}$$
with $\text{k\_effective}$ and $\text{max\_p\_base}$ being calculated with the internal `_get_k_effective` and `_max_p_base` methods. $\text{p\_avg}$ is the average price of the collateral.
Returns: maximum amount to borrow (`uint256`). The maximum value to return is either the maximum a user can borrow and is ultimately limited by the amount of coins the Controller has.
| Input | Type | Description |
| ---------------------- | --------- | ------------ |
| `controller` | `address` | Controller of the market to borrow from. |
| `_user_collateral` | `uint256` | Amount of collateral at its native precision. |
| `_leverage_collateral` | `uint256` | Additional collateral to use for leveraging. |
| `N` | `uint256` | Number of bands to deposit into. |
| `p_avg` | `uint256` | Average price of the collateral. |
```vyper
@external
@view
def max_borrowable(controller: address, _user_collateral: uint256, _leverage_collateral: uint256, N: uint256, p_avg: uint256) -> uint256:
"""
@notice Calculation of maximum which can be borrowed with leverage
@param collateral Amount of collateral (at its native precision)
@param N Number of bands to deposit into
@param route_idx Index of the route which should be use for exchange stablecoin to collateral
@return Maximum amount of stablecoin to borrow with leverage
"""
# max_borrowable = collateral / (1 / (k_effective * max_p_base) - 1 / p_avg)
AMM: LLAMMA = LLAMMA(Controller(controller).amm())
BORROWED_TOKEN: address = AMM.coins(0)
COLLATERAL_TOKEN: address = AMM.coins(1)
COLLATERAL_PRECISION: uint256 = pow_mod256(10, 18 - ERC20(COLLATERAL_TOKEN).decimals())
user_collateral: uint256 = _user_collateral * COLLATERAL_PRECISION
leverage_collateral: uint256 = _leverage_collateral * COLLATERAL_PRECISION
k_effective: uint256 = self._get_k_effective(controller, user_collateral + leverage_collateral, N)
max_p_base: uint256 = self._max_p_base(controller)
max_borrowable: uint256 = user_collateral * 10**18 / (10**36 / k_effective * 10**18 / max_p_base - 10**36 / p_avg)
return min(max_borrowable * 999 / 1000, ERC20(BORROWED_TOKEN).balanceOf(controller)) # Cannot borrow beyond the amount of coins Controller has
@internal
@view
def _get_k_effective(controller: address, collateral: uint256, N: uint256) -> uint256:
"""
@notice Intermediary method which calculates k_effective defined as x_effective / p_base / y,
however discounted by loan_discount.
x_effective is an amount which can be obtained from collateral when liquidating
@param N Number of bands the deposit is made into
@return k_effective
"""
# x_effective = sum_{i=0..N-1}(y / N * p(n_{n1+i})) =
# = y / N * p_oracle_up(n1) * sqrt((A - 1) / A) * sum_{0..N-1}(((A-1) / A)**k)
# === d_y_effective * p_oracle_up(n1) * sum(...) === y * k_effective * p_oracle_up(n1)
# d_k_effective = 1 / N / sqrt(A / (A - 1))
# d_k_effective: uint256 = 10**18 * unsafe_sub(10**18, discount) / (SQRT_BAND_RATIO * N)
# Make some extra discount to always deposit lower when we have DEAD_SHARES rounding
CONTROLLER: Controller = Controller(controller)
A: uint256 = LLAMMA(CONTROLLER.amm()).A()
SQRT_BAND_RATIO: uint256 = isqrt(unsafe_div(10 **36 * A, unsafe_sub(A, 1)))
discount: uint256 = CONTROLLER.loan_discount()
d_k_effective: uint256 = 10**18 * unsafe_sub(
10**18, min(discount + (DEAD_SHARES * 10**18) / max(collateral / N, DEAD_SHARES), 10**18)
) / (SQRT_BAND_RATIO * N)
k_effective: uint256 = d_k_effective
for i in range(1, MAX_TICKS_UINT):
if i == N:
break
d_k_effective = unsafe_div(d_k_effective * (A - 1), A)
k_effective = unsafe_add(k_effective, d_k_effective)
return k_effective
@internal
@view
def _max_p_base(controller: address) -> uint256:
"""
@notice Calculate max base price including skipping bands
"""
AMM: LLAMMA = LLAMMA(Controller(controller).amm())
A: uint256 = AMM.A()
LOGN_A_RATIO: int256 = self.wad_ln(A * 10**18 / (A - 1))
p_oracle: uint256 = AMM.price_oracle()
# Should be correct unless price changes suddenly by MAX_P_BASE_BANDS+ bands
n1: int256 = self.wad_ln(AMM.get_base_price() * 10**18 / p_oracle)
if n1 < 0:
n1 -= LOGN_A_RATIO - 1 # This is to deal with vyper's rounding of negative numbers
n1 = unsafe_div(n1, LOGN_A_RATIO) + MAX_P_BASE_BANDS
n_min: int256 = AMM.active_band_with_skip()
n1 = max(n1, n_min + 1)
p_base: uint256 = AMM.p_oracle_up(n1)
for i in range(MAX_SKIP_TICKS + 1):
n1 -= 1
if n1 <= n_min:
break
p_base_prev: uint256 = p_base
p_base = unsafe_div(p_base * A, A - 1)
if p_base > p_oracle:
return p_base_prev
return p_base
```
```shell
>>> soon
```
::::
---
## Unwinding Leverage
To deleverage loans, the `LeverageZap1inch.vy` contract uses the `callback_repay` function.
For an accompanying JavaScript library, see [GitHub](https://github.com/curvefi/curve-lending-js?tab=readme-ov-file#leverage-createloan-borrowmore-repay).
*Flow of deleveraging:*
1. User calls `repay_extended` and passes `callbacker`, `callback_args`, and `callback_bytes` into the function.
2. The Controller withdraws 100% of the collateral from the AMM and transfers all of it to the `callbacker` contract.
3. After the transfer, the callback is executed using the internal `execute_callback` in the `Controller.vy` contract. This function unwinds the leverage.
```py
struct CallbackData:
active_band: int256
stablecoins: uint256
collateral: uint256
CALLBACK_DEPOSIT: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_REPAY: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_LIQUIDATE: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_DEPOSIT_WITH_BYTES: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
# CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4) <-- BUG! The reason is 0 at the beginning of method_id
CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = 0x008ae188
CALLBACK_LIQUIDATE_WITH_BYTES: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
@internal
def execute_callback(callbacker: address, callback_sig: bytes4,
user: address, stablecoins: uint256, collateral: uint256, debt: uint256,
callback_args: DynArray[uint256, 5], callback_bytes: Bytes[10**4]) -> CallbackData:
assert callbacker != COLLATERAL_TOKEN.address
data: CallbackData = empty(CallbackData)
data.active_band = AMM.active_band()
band_x: uint256 = AMM.bands_x(data.active_band)
band_y: uint256 = AMM.bands_y(data.active_band)
# Callback
response: Bytes[64] = raw_call(
callbacker,
concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)),
max_outsize=64
)
data.stablecoins = convert(slice(response, 0, 32), uint256)
data.collateral = convert(slice(response, 32, 32), uint256)
# Checks after callback
assert data.active_band == AMM.active_band()
assert band_x == AMM.bands_x(data.active_band)
assert band_y == AMM.bands_y(data.active_band)
return data
```
The function uses Vyper's built-in [`raw_call`](https://docs.vyperlang.org/en/stable/built-in-functions.html?highlight=raw_call#raw_call) function to call the desired method (in this case `callback_repay`) with the according `callback_bytes`.
4. After executing the callback, the Controller checks and does a full repayment and closes the position when possible. Else, it does a partial repayment (deleverage).
### `callback_repay`
::::description[`LeverageZap1inch.callback_repay(user: address, stablecoins: uint256, collateral: uint256, debt: uint256, callback_args: DynArray[uint256, 10], callback_bytes: Bytes[10**4] = b"")]
:::guard[Guarded Method]
This function is only callable by the `Controller` from where tokens were borrowed from.
:::
Function to de-leverage a loan using a callback.
The following `callback_args` need to be passed to this function via `repay_extended`:
- `callback_args[0] = factory_id`: depending on which factory (crvusd or lending).
- `callback_args[1] = controller_id`: index of the controller in the factory contract fetched from `Factory.controllers(controller_id)`.
- `callback_args[2] = user_collateral`: amount of collateral token provided by the user (which is exchanged for the borrowed token).
- `callback_args[3] = user_borrowed`: amount of borrowed tokens to repay.
| Input | Type | Description |
| ----------------- | ----------------------- | ------------ |
| `user` | `address` | User address to unwind a leveraged position for. |
| `stablecoins` | `uint256` | Value returned from `user_state`. |
| `user_collateral` | `uint256` | Value returned from `user_state`. |
| `d_debt` | `uint256` | Value returned from `user_state`. |
| `callback_args` | `DynArray[uint256, 10]` | Callback arguments. |
| `callback_bytes` | `Bytes[10**4] = b""` | Callback bytes. |
Returns: borrowed_from_state_collateral + borrowed_from_user_collateral + user_borrowed and remaining_collateral (`uint256[2]`).
Emits: `Repay`
```vyper
event Repay:
user: indexed(address)
state_collateral_used: uint256
borrowed_from_state_collateral: uint256
user_collateral: uint256
user_collateral_used: uint256
borrowed_from_user_collateral: uint256
user_borrowed: uint256
@external
@nonreentrant('lock')
def callback_repay(user: address, stablecoins: uint256, collateral: uint256, debt: uint256,
callback_args: DynArray[uint256,10], callback_bytes: Bytes[10 **4] = b"") -> uint256[2]:
"""
@notice Callback method which should be called by controller to create leveraged position
@param user Address of the user
@param stablecoins The value from user_state
@param collateral The value from user_state
@param debt The value from user_state
@param callback_args [factory_id, controller_id, user_collateral, user_borrowed]
0-1. factory_id, controller_id are needed to check that msg.sender is the one of our controllers
1. user_collateral - the amount of collateral token provided by user (needs to be exchanged for borrowed)
2. user_borrowed - the amount of borrowed token to repay from user's wallet
return [user_borrowed + borrowed_from_collateral, remaining_collateral]
"""
controller: address = Factory(self.FACTORIES[callback_args[0]]).controllers(callback_args[1])
assert msg.sender == controller, "wrong controller"
amm: LLAMMA = LLAMMA(Controller(controller).amm())
borrowed_token: address = amm.coins(0)
collateral_token: address = amm.coins(1)
self._approve(collateral_token, ROUTER_1INCH)
self._approve(borrowed_token, controller)
self._approve(collateral_token, controller)
initial_collateral: uint256 = ERC20(collateral_token).balanceOf(self)
user_collateral: uint256 = callback_args[2]
if callback_bytes != b"":
self._transferFrom(collateral_token, user, self, user_collateral)
# Buys borrowed token for collateral from user's position + from user's wallet.
# The amount to be spent is specified inside callback_bytes.
raw_call(ROUTER_1INCH, callback_bytes)
else:
assert user_collateral == 0
remaining_collateral: uint256 = ERC20(collateral_token).balanceOf(self)
state_collateral_used: uint256 = 0
borrowed_from_state_collateral: uint256 = 0
user_collateral_used: uint256 = user_collateral
borrowed_from_user_collateral: uint256 = ERC20(borrowed_token).balanceOf(self) # here it's total borrowed_from_collateral
if remaining_collateral < initial_collateral:
state_collateral_used = initial_collateral - remaining_collateral
borrowed_from_state_collateral = state_collateral_used * 10**18 / (state_collateral_used + user_collateral_used) * borrowed_from_user_collateral / 10**18
borrowed_from_user_collateral = borrowed_from_user_collateral - borrowed_from_state_collateral
else:
user_collateral_used = user_collateral - (remaining_collateral - initial_collateral)
user_borrowed: uint256 = callback_args[3]
self._transferFrom(borrowed_token, user, self, user_borrowed)
log Repay(user, state_collateral_used, borrowed_from_state_collateral, user_collateral, user_collateral_used, borrowed_from_user_collateral, user_borrowed)
return [borrowed_from_state_collateral + borrowed_from_user_collateral + user_borrowed, remaining_collateral]
@internal
def _approve(coin: address, spender: address):
if ERC20(coin).allowance(self, spender) == 0:
ERC20(coin).approve(spender, max_value(uint256))
@internal
def _transferFrom(token: address, _from: address, _to: address, amount: uint256):
if amount > 0:
assert ERC20(token).transferFrom(_from, _to, amount, default_return_value=True)
```
```shell
>>> soon
```
::::
---
## Contract Info Methods
The contract has two public getters, one for the 1inch router contract and one for the two factory contracts for crvUSD and lending markets.
### `ROUTER_1INCH`
::::description[`LeverageZap1inch.ROUTER_1INCH() -> address: view`]
Getter method for the 1inch router contract. This variable is immutable and can not be changed.
Returns: 1inch router (`address`).
```vyper
ROUTER_1INCH: public(immutable(address))
@external
def __init__(_router_1inch: address, _factories: DynArray[address, 2]):
ROUTER_1INCH = _router_1inch
self.FACTORIES = _factories
```
```shell
>>> LeverageZap1inch.ROUTER_1INCH()
'todo'
```
::::
### `FACTORIES`
::::description[`LeverageZap1inch.FACTORIES(arg0: uint256) -> address: view`]
Getter method for the factory contract at index `arg0`.
Returns: Factory contract (`address`).
| Input | Type | Description |
| ------ | --------- | -------------------- |
| `arg0` | `uint256` | Index of the Factory contract to use. |
```vyper
FACTORIES: public(DynArray[address, 2])
@external
def __init__(_router_1inch: address, _factories: DynArray[address, 2]):
ROUTER_1INCH = _router_1inch
self.FACTORIES = _factories
```
```shell
>>> LeverageZap1inch.FACTORIES(0)
'todo'
>>> LeverageZap1inch.FACTORIES(0)
'todo'
```
::::
---
## LeverageZap.vy
This Zap contract is specifically designed to **create leveraged loans**using **predetermined routes that only utilize Curve pools**.
:::vyper[`LeverageZap.vy`]
The source code for the `LeverageZap.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/lending/contracts/zaps/LeverageZap.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
An accompanying JavaScript library for Curve Lending can be found here: [GitHub](https://github.com/curvefi/curve-lending-js?tab=readme-ov-file#leverage-createloan-borrowmore-repay).
:::
---
## Callback
This leverage zap allows up to five values to be passed for `callback_args`, but only the first two are needed:
- `callback_args[0]` represents the route used for leveraging.
- `callback_args[1]` is the minimum amount of collateral tokens to receive.
:::notebook[Jupyter Notebook]
A simple Jupyter notebook on how to create a leveraged position using this zap contract can be found here: [https://try.vyperlang.org/hub/user-redirect/lab/tree/shared/mo-anon/curve%20lending/loans/create_loan_extended.ipynb](https://try.vyperlang.org/hub/user-redirect/lab/tree/shared/mo-anon/curve%20lending/loans/create_loan_extended.ipynb)
:::
### `callback_deposit`
::::description[`LeverageZap.callback_deposit(user: address, stablecoins: uint256, collateral: uint256, debt: uint256, callback_args: DynArray[uint256, 5]) -> uint256[2]`]
:::guard[Guarded Method]
This function is only callable by the `Controller` which is used to create the leveraged position.
:::
Function to perform a callback method to create a leveraged position. The functions input arguments are passed from the `Controller` contract.
| Input | Type | Description |
| --------------- | ---------------------- | ------------ |
| `user` | `address` | User address to create a leveraged position. |
| `stablecoins` | `uint256` | Amount of stablecoins. Always 0 when calling this method. |
| `collateral` | `uint256` | Amount of collateral tokens provided by the user. |
| `debt` | `uint256` | Amount of be borrowed. |
| `callback_args` | `DynArray[uint256, 5]` | Array of callback arguments consisting of `[route_idx, min_recv]` |
Returns: [0 and leveraged collateral] (`uint256[2]`), which is the amount of collateral received as a result of leveraging up.
```vyper
@external
@nonreentrant('lock')
def callback_deposit(user: address, stablecoins: uint256, collateral: uint256, debt: uint256, callback_args: DynArray[uint256, 5]) -> uint256[2]:
"""
@notice Callback method which should be called by controller to create leveraged position
@param user Address of the user
@param stablecoins Amount of stablecoin (always = 0)
@param collateral Amount of collateral given by user
@param debt Borrowed amount
@param callback_args [route_idx, min_recv]
return [0, leverage_collateral], leverage_collateral is the amount of collateral got as a result of selling borrowed stablecoin
"""
assert msg.sender == CONTROLLER
route_idx: uint256 = callback_args[0]
min_recv: uint256 = callback_args[1]
leverage_collateral: uint256 = ROUTER.exchange_multiple(self.routes[route_idx], self.route_params[route_idx], debt, min_recv, self.route_pools[route_idx])
return [0, leverage_collateral]
```
```py
@external
@payable
def exchange_multiple(
_route: address[9],
_swap_params: uint256[3][4],
_amount: uint256,
_expected: uint256,
_pools: address[4]=[ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS],
_receiver: address=msg.sender
) -> uint256:
"""
@notice Perform up to four swaps in a single transaction
@dev Routing and swap params must be determined off-chain. This
functionality is designed for gas efficiency over ease-of-use.
@param _route Array of [initial token, pool, token, pool, token, ...]
The array is iterated until a pool address of 0x00, then the last
given token is transferred to `_receiver`
@param _swap_params Multidimensional array of [i, j, swap type] where i and j are the correct
values for the n'th pool in `_route`. The swap type should be
1 for a stableswap `exchange`,
2 for stableswap `exchange_underlying`,
3 for a cryptoswap `exchange`,
4 for a cryptoswap `exchange_underlying`,
5 for factory metapools with lending base pool `exchange_underlying`,
6 for factory crypto-meta pools underlying exchange (`exchange` method in zap),
7-11 for wrapped coin (underlying for lending or fake pool) -> LP token "exchange" (actually `add_liquidity`),
12-14 for LP token -> wrapped coin (underlying for lending pool) "exchange" (actually `remove_liquidity_one_coin`)
15 for WETH -> ETH "exchange" (actually deposit/withdraw)
@param _amount The amount of `_route[0]` token being sent.
@param _expected The minimum amount received after the final swap.
@param _pools Array of pools for swaps via zap contracts. This parameter is only needed for
Polygon meta-factories underlying swaps.
@param _receiver Address to transfer the final output token to.
@return Received amount of the final output token
"""
input_token: address = _route[0]
amount: uint256 = _amount
output_token: address = ZERO_ADDRESS
# validate / transfer initial token
if input_token == ETH_ADDRESS:
assert msg.value == amount
else:
assert msg.value == 0
response: Bytes[32] = raw_call(
input_token,
_abi_encode(
msg.sender,
self,
amount,
method_id=method_id("transferFrom(address,address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
for i in range(1,5):
# 4 rounds of iteration to perform up to 4 swaps
swap: address = _route[i*2-1]
pool: address = _pools[i-1] # Only for Polygon meta-factories underlying swap (swap_type == 4)
output_token = _route[i*2]
params: uint256[3] = _swap_params[i-1] # i, j, swap type
if not self.is_approved[input_token][swap]:
# approve the pool to transfer the input token
response: Bytes[32] = raw_call(
input_token,
_abi_encode(
swap,
MAX_UINT256,
method_id=method_id("approve(address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
self.is_approved[input_token][swap] = True
eth_amount: uint256 = 0
if input_token == ETH_ADDRESS:
eth_amount = amount
# perform the swap according to the swap type
if params[2] == 1:
CurvePool(swap).exchange(convert(params[0], int128), convert(params[1], int128), amount, 0, value=eth_amount)
elif params[2] == 2:
CurvePool(swap).exchange_underlying(convert(params[0], int128), convert(params[1], int128), amount, 0, value=eth_amount)
elif params[2] == 3:
if input_token == ETH_ADDRESS or output_token == ETH_ADDRESS:
CryptoPoolETH(swap).exchange(params[0], params[1], amount, 0, True, value=eth_amount)
else:
CryptoPool(swap).exchange(params[0], params[1], amount, 0)
elif params[2] == 4:
CryptoPool(swap).exchange_underlying(params[0], params[1], amount, 0, value=eth_amount)
elif params[2] == 5:
LendingBasePoolMetaZap(swap).exchange_underlying(pool, convert(params[0], int128), convert(params[1], int128), amount, 0)
elif params[2] == 6:
use_eth: bool = input_token == ETH_ADDRESS or output_token == ETH_ADDRESS
CryptoMetaZap(swap).exchange(pool, params[0], params[1], amount, 0, use_eth)
elif params[2] == 7:
_amounts: uint256[2] = [0, 0]
_amounts[params[0]] = amount
BasePool2Coins(swap).add_liquidity(_amounts, 0)
elif params[2] == 8:
_amounts: uint256[3] = [0, 0, 0]
_amounts[params[0]] = amount
BasePool3Coins(swap).add_liquidity(_amounts, 0)
elif params[2] == 9:
_amounts: uint256[3] = [0, 0, 0]
_amounts[params[0]] = amount
LendingBasePool3Coins(swap).add_liquidity(_amounts, 0, True) # example: aave on Polygon
elif params[2] == 10:
_amounts: uint256[4] = [0, 0, 0, 0]
_amounts[params[0]] = amount
BasePool4Coins(swap).add_liquidity(_amounts, 0)
elif params[2] == 11:
_amounts: uint256[5] = [0, 0, 0, 0, 0]
_amounts[params[0]] = amount
BasePool5Coins(swap).add_liquidity(_amounts, 0)
elif params[2] == 12:
# The number of coins doesn't matter here
BasePool3Coins(swap).remove_liquidity_one_coin(amount, convert(params[1], int128), 0)
elif params[2] == 13:
# The number of coins doesn't matter here
LendingBasePool3Coins(swap).remove_liquidity_one_coin(amount, convert(params[1], int128), 0, True) # example: aave on Polygon
elif params[2] == 14:
# The number of coins doesn't matter here
CryptoBasePool3Coins(swap).remove_liquidity_one_coin(amount, params[1], 0) # example: atricrypto3 on Polygon
elif params[2] == 15:
if input_token == ETH_ADDRESS:
wETH(swap).deposit(value=amount)
elif output_token == ETH_ADDRESS:
wETH(swap).withdraw(amount)
else:
raise "One of the coins must be ETH for swap type 15"
else:
raise "Bad swap type"
# update the amount received
if output_token == ETH_ADDRESS:
amount = self.balance
else:
amount = ERC20(output_token).balanceOf(self)
# sanity check, if the routing data is incorrect we will have a 0 balance and that is bad
assert amount != 0, "Received nothing"
# check if this was the last swap
if i == 4 or _route[i*2+1] == ZERO_ADDRESS:
break
# if there is another swap, the output token becomes the input for the next round
input_token = output_token
# validate the final amount received
assert amount >= _expected
# transfer the final token to the receiver
if output_token == ETH_ADDRESS:
raw_call(_receiver, b"", value=amount)
else:
response: Bytes[32] = raw_call(
output_token,
_abi_encode(
_receiver,
amount,
method_id=method_id("transfer(address,uint256)"),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
log ExchangeMultiple(msg.sender, _receiver, _route, _swap_params, _pools, _amount, amount)
return amount
```
::::
---
## Helper Functions
*The contract includes various helper functions:*
### `get_collateral`
::::description[`LeverageZap.get_collateral(stablecoin: uint256, route_idx: uint256) -> uint256: view`]
Function to calculate the expected amount of collateral tokens for 'exchanging' a given amount of stablecoins using a specific route.
Returns: expected amount of collateral (`uint256`).
| Input | Type | Description |
| ------------ | --------- | ---------------------------------- |
| `stablecoin` | `uint256` | Amount of stablecoins to exchange. |
| `route_idx` | `uint256` | Index of the route to use. |
```vyper
interface Router:
def exchange_multiple(_route: address[9], _swap_params: uint256[3][4], _amount: uint256, _expected: uint256, _pools: address[4]) -> uint256: payable
def get_exchange_multiple_amount(_route: address[9], _swap_params: uint256[3][4], _amount: uint256, _pools: address[4]) -> uint256: view
@view
@external
@nonreentrant('lock')
def get_collateral(stablecoin: uint256, route_idx: uint256) -> uint256:
"""
@notice Calculate the expected amount of collateral by given stablecoin amount
@param stablecoin Amount of stablecoin
@param route_idx Index of the route to use
@return Amount of collateral
"""
return self._get_collateral(stablecoin, route_idx)
@view
@internal
def _get_collateral(stablecoin: uint256, route_idx: uint256) -> uint256:
return ROUTER.get_exchange_multiple_amount(self.routes[route_idx], self.route_params[route_idx], stablecoin, self.route_pools[route_idx])
```
```vyper
@view
@external
def get_exchange_multiple_amount(
_route: address[9],
_swap_params: uint256[3][4],
_amount: uint256,
_pools: address[4]=[ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS]
) -> uint256:
"""
@notice Get the current number the final output tokens received in an exchange
@dev Routing and swap params must be determined off-chain. This
functionality is designed for gas efficiency over ease-of-use.
@param _route Array of [initial token, pool, token, pool, token, ...]
The array is iterated until a pool address of 0x00, then the last
given token is transferred to `_receiver`
@param _swap_params Multidimensional array of [i, j, swap type] where i and j are the correct
values for the n'th pool in `_route`. The swap type should be
1 for a stableswap `exchange`,
2 for stableswap `exchange_underlying`,
3 for a cryptoswap `exchange`,
4 for a cryptoswap `exchange_underlying`,
5 for factory metapools with lending base pool `exchange_underlying`,
6 for factory crypto-meta pools underlying exchange (`exchange` method in zap),
7-11 for wrapped coin (underlying for lending pool) -> LP token "exchange" (actually `add_liquidity`),
12-14 for LP token -> wrapped coin (underlying for lending or fake pool) "exchange" (actually `remove_liquidity_one_coin`)
15 for WETH -> ETH "exchange" (actually deposit/withdraw)
@param _amount The amount of `_route[0]` token to be sent.
@param _pools Array of pools for swaps via zap contracts. This parameter is only needed for
Polygon meta-factories underlying swaps.
@return Expected amount of the final output token
"""
amount: uint256 = _amount
for i in range(1,5):
# 4 rounds of iteration to perform up to 4 swaps
swap: address = _route[i*2-1]
pool: address = _pools[i-1] # Only for Polygon meta-factories underlying swap (swap_type == 4)
params: uint256[3] = _swap_params[i-1] # i, j, swap type
# Calc output amount according to the swap type
if params[2] == 1:
amount = CurvePool(swap).get_dy(convert(params[0], int128), convert(params[1], int128), amount)
elif params[2] == 2:
amount = CurvePool(swap).get_dy_underlying(convert(params[0], int128), convert(params[1], int128), amount)
elif params[2] == 3:
amount = CryptoPool(swap).get_dy(params[0], params[1], amount)
elif params[2] == 4:
amount = CryptoPool(swap).get_dy_underlying(params[0], params[1], amount)
elif params[2] == 5:
amount = CurvePool(pool).get_dy_underlying(convert(params[0], int128), convert(params[1], int128), amount)
elif params[2] == 6:
amount = CryptoMetaZap(swap).get_dy(pool, params[0], params[1], amount)
elif params[2] == 7:
_amounts: uint256[2] = [0, 0]
_amounts[params[0]] = amount
amount = BasePool2Coins(swap).calc_token_amount(_amounts, True)
elif params[2] in [8, 9]:
_amounts: uint256[3] = [0, 0, 0]
_amounts[params[0]] = amount
amount = BasePool3Coins(swap).calc_token_amount(_amounts, True)
elif params[2] == 10:
_amounts: uint256[4] = [0, 0, 0, 0]
_amounts[params[0]] = amount
amount = BasePool4Coins(swap).calc_token_amount(_amounts, True)
elif params[2] == 11:
_amounts: uint256[5] = [0, 0, 0, 0, 0]
_amounts[params[0]] = amount
amount = BasePool5Coins(swap).calc_token_amount(_amounts, True)
elif params[2] in [12, 13]:
# The number of coins doesn't matter here
amount = BasePool3Coins(swap).calc_withdraw_one_coin(amount, convert(params[1], int128))
elif params[2] == 14:
# The number of coins doesn't matter here
amount = CryptoBasePool3Coins(swap).calc_withdraw_one_coin(amount, params[1])
elif params[2] == 15:
# ETH <--> WETH rate is 1:1
pass
else:
raise "Bad swap type"
# check if this was the last swap
if i == 4 or _route[i*2+1] == ZERO_ADDRESS:
break
return amount
```
```shell
>>> LeverageZap.get_collateral(100000000000000000000000, 0) # 100,000 crvUSD using route 0
154124094 # 1.54 wBTC
>>> LeverageZap.get_collateral(100000000000000000000000, 1) # 100,000 crvUSD using route 1
155160443 # 1.55 wBTC
```
::::
### `get_collateral_underlying`
::::description[`LeverageZap.get_collateral_underlying(stablecoin: uint256, route_idx: uint256) -> uint256: view`]
Function to calculate the expected amount of collateral for a given amount of `stablecoin`. This is exactly the same function as `get_collateral` but is needed to make the ABI the same as the ABI for sfrxETH and wstETH.
Returns: amount of collateral (`uint256`).
| Input | Type | Description |
| ------------ | --------- | ---------------------------------- |
| `stablecoin` | `uint256` | Amount of stablecoins to exchange. |
| `route_idx` | `uint256` | Index of the route to use. |
```vyper
interface Router:
def exchange_multiple(_route: address[9], _swap_params: uint256[3][4], _amount: uint256, _expected: uint256, _pools: address[4]) -> uint256: payable
def get_exchange_multiple_amount(_route: address[9], _swap_params: uint256[3][4], _amount: uint256, _pools: address[4]) -> uint256: view
@view
@external
@nonreentrant('lock')
def get_collateral_underlying(stablecoin: uint256, route_idx: uint256) -> uint256:
"""
@notice This method is needed just to make ABI the same as ABI for sfrxETH and wstETH
"""
return self._get_collateral(stablecoin, route_idx)
@view
@internal
def _get_collateral(stablecoin: uint256, route_idx: uint256) -> uint256:
return ROUTER.get_exchange_multiple_amount(self.routes[route_idx], self.route_params[route_idx], stablecoin, self.route_pools[route_idx])
```
```vyper
@view
@external
def get_exchange_multiple_amount(
_route: address[9],
_swap_params: uint256[3][4],
_amount: uint256,
_pools: address[4]=[ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS]
) -> uint256:
"""
@notice Get the current number the final output tokens received in an exchange
@dev Routing and swap params must be determined off-chain. This
functionality is designed for gas efficiency over ease-of-use.
@param _route Array of [initial token, pool, token, pool, token, ...]
The array is iterated until a pool address of 0x00, then the last
given token is transferred to `_receiver`
@param _swap_params Multidimensional array of [i, j, swap type] where i and j are the correct
values for the n'th pool in `_route`. The swap type should be
1 for a stableswap `exchange`,
2 for stableswap `exchange_underlying`,
3 for a cryptoswap `exchange`,
4 for a cryptoswap `exchange_underlying`,
5 for factory metapools with lending base pool `exchange_underlying`,
6 for factory crypto-meta pools underlying exchange (`exchange` method in zap),
7-11 for wrapped coin (underlying for lending pool) -> LP token "exchange" (actually `add_liquidity`),
12-14 for LP token -> wrapped coin (underlying for lending or fake pool) "exchange" (actually `remove_liquidity_one_coin`)
15 for WETH -> ETH "exchange" (actually deposit/withdraw)
@param _amount The amount of `_route[0]` token to be sent.
@param _pools Array of pools for swaps via zap contracts. This parameter is only needed for
Polygon meta-factories underlying swaps.
@return Expected amount of the final output token
"""
amount: uint256 = _amount
for i in range(1,5):
# 4 rounds of iteration to perform up to 4 swaps
swap: address = _route[i*2-1]
pool: address = _pools[i-1] # Only for Polygon meta-factories underlying swap (swap_type == 4)
params: uint256[3] = _swap_params[i-1] # i, j, swap type
# Calc output amount according to the swap type
if params[2] == 1:
amount = CurvePool(swap).get_dy(convert(params[0], int128), convert(params[1], int128), amount)
elif params[2] == 2:
amount = CurvePool(swap).get_dy_underlying(convert(params[0], int128), convert(params[1], int128), amount)
elif params[2] == 3:
amount = CryptoPool(swap).get_dy(params[0], params[1], amount)
elif params[2] == 4:
amount = CryptoPool(swap).get_dy_underlying(params[0], params[1], amount)
elif params[2] == 5:
amount = CurvePool(pool).get_dy_underlying(convert(params[0], int128), convert(params[1], int128), amount)
elif params[2] == 6:
amount = CryptoMetaZap(swap).get_dy(pool, params[0], params[1], amount)
elif params[2] == 7:
_amounts: uint256[2] = [0, 0]
_amounts[params[0]] = amount
amount = BasePool2Coins(swap).calc_token_amount(_amounts, True)
elif params[2] in [8, 9]:
_amounts: uint256[3] = [0, 0, 0]
_amounts[params[0]] = amount
amount = BasePool3Coins(swap).calc_token_amount(_amounts, True)
elif params[2] == 10:
_amounts: uint256[4] = [0, 0, 0, 0]
_amounts[params[0]] = amount
amount = BasePool4Coins(swap).calc_token_amount(_amounts, True)
elif params[2] == 11:
_amounts: uint256[5] = [0, 0, 0, 0, 0]
_amounts[params[0]] = amount
amount = BasePool5Coins(swap).calc_token_amount(_amounts, True)
elif params[2] in [12, 13]:
# The number of coins doesn't matter here
amount = BasePool3Coins(swap).calc_withdraw_one_coin(amount, convert(params[1], int128))
elif params[2] == 14:
# The number of coins doesn't matter here
amount = CryptoBasePool3Coins(swap).calc_withdraw_one_coin(amount, params[1])
elif params[2] == 15:
# ETH <--> WETH rate is 1:1
pass
else:
raise "Bad swap type"
# check if this was the last swap
if i == 4 or _route[i*2+1] == ZERO_ADDRESS:
break
return amount
```
This function is used for markets like sfrxETH or wstETH to fetch the amount of underlying ETH. For markets that do not use "underlying" tokens, the function will return the same value as `get_collateral`.
```shell
# wstETH market:
>>> LeverageZap.get_collateral(100000000000000000000000, 0) # 100,000 crvUSD using route 0
27065166978322615717 # 27.07 wstETH
>>> LeverageZap.get_collateral_underlying(100000000000000000000000, 0) # 100,000 crvUSD using route 0
31551027792084938361 # 31.55 ETH
```
::::
### `max_borrowable`
::::description[`LeverageZap.max_borrowable(collateral: uint256, N: uint256, route_idx: uint256) -> uint256: view`]
:::warning
`max_borrowable` will return different values based on the route chosen.
:::
Function to calculate the maximum amount of crvUSD to be borrowed using leverage.
| Input | Type | Description |
| ------------ | --------- | --------------------------------------------------- |
| `collateral` | `uint256` | Amount of collateral (at its native precision). |
| `N` | `uint256` | Number of bands to deposit into. |
| `route_idx` | `uint256` | Index of the route to be used for exchanging stablecoin to collateral. |
Returns: maximum borrowable amount (`uint256`).
```vyper
@external
@view
def max_borrowable(collateral: uint256, N: uint256, route_idx: uint256) -> uint256:
"""
@notice Calculation of maximum which can be borrowed with leverage
@param collateral Amount of collateral (at its native precision)
@param N Number of bands to deposit into
@param route_idx Index of the route which should be use for exchange stablecoin to collateral
@return Maximum amount of stablecoin to borrow with leverage
"""
return self._max_borrowable(collateral, N ,route_idx)
@internal
@view
def _max_borrowable(collateral: uint256, N: uint256, route_idx: uint256) -> uint256:
"""
@notice Calculation of maximum which can be borrowed with leverage
@param collateral Amount of collateral (at its native precision)
@param N Number of bands to deposit into
@param route_idx Index of the route which should be use for exchange stablecoin to collateral
@return Maximum amount of stablecoin to borrow with leverage
"""
# max_borrowable = collateral / (1 / (k_effective * max_p_base) - 1 / p_avg)
user_collateral: uint256 = collateral * COLLATERAL_PRECISION
leverage_collateral: uint256 = 0
k_effective: uint256 = self._get_k_effective(user_collateral + leverage_collateral, N)
max_p_base: uint256 = self._max_p_base()
p_avg: uint256 = AMM.price_oracle()
max_borrowable_prev: uint256 = 0
max_borrowable: uint256 = 0
for i in range(10):
max_borrowable_prev = max_borrowable
max_borrowable = user_collateral * 10**18 / (10**36 / k_effective * 10**18 / max_p_base - 10**36 / p_avg)
if max_borrowable > max_borrowable_prev:
if max_borrowable - max_borrowable_prev <= 1:
return max_borrowable
else:
if max_borrowable_prev - max_borrowable <= 1:
return max_borrowable
res: uint256[2] = self._get_collateral_and_avg_price(max_borrowable, route_idx)
leverage_collateral = res[0]
p_avg = res[1]
k_effective = self._get_k_effective(user_collateral + leverage_collateral, N)
return min(max_borrowable * 999 / 1000, ERC20(CRVUSD).balanceOf(CONTROLLER)) # Cannot borrow beyond the amount of coins Controller has
```
```shell
>>> LeverageZap.max_borrowable(100000000, 4, 0) # 1 wBTC with 4 bands using route id 0
361562517762983346937868 # 361562.52 crvUSD max borrowable
>>> LeverageZap.max_borrowable(100000000, 4, 1) # 1 wBTC with 4 bands using route id 1
368244180550171738607454 # 368244.18 crvUSD max borrowable
>>> LeverageZap.max_borrowable(100000000, 4, 2) # 1 wBTC with 4 bands using route id 2
72242814877726777613187 # 72242.81 crvUSD max borrowable
```
::::
### `max_collateral`
::::description[`LeverageZap.max_collateral(collateral: uint256, N: uint256, route_idx: uint256) -> uint256: view`]
:::warning
`max_collateral` will return different values based on the route chosen.
:::
Function to calculate the maximum collateral position that can be created using leverage.
| Input | Type | Description |
| ------------ | --------- | --------------------------------------------------- |
| `collateral` | `uint256` | Amount of collateral (at its native precision). |
| `N` | `uint256` | Number of bands to deposit into. |
| `route_idx` | `uint256` | Index of the route to be used for exchanging stablecoin to collateral. |
Returns: total amount of collateral, i.e., user_collateral + max_leverage collateral (`uint256`).
```vyper
@external
@view
def max_collateral(collateral: uint256, N: uint256, route_idx: uint256) -> uint256:
"""
@notice Calculation of maximum collateral position which can be created with leverage
@param collateral Amount of collateral (at its native precision)
@param N Number of bands to deposit into
@param route_idx Index of the route which should be use for exchange stablecoin to collateral
@return user_collateral + max_leverage_collateral
"""
max_borrowable: uint256 = self._max_borrowable(collateral, N, route_idx)
max_leverage_collateral: uint256 = self._get_collateral(max_borrowable, route_idx)
return collateral + max_leverage_collateral
@internal
@view
def _max_borrowable(collateral: uint256, N: uint256, route_idx: uint256) -> uint256:
"""
@notice Calculation of maximum which can be borrowed with leverage
@param collateral Amount of collateral (at its native precision)
@param N Number of bands to deposit into
@param route_idx Index of the route which should be use for exchange stablecoin to collateral
@return Maximum amount of stablecoin to borrow with leverage
"""
# max_borrowable = collateral / (1 / (k_effective * max_p_base) - 1 / p_avg)
user_collateral: uint256 = collateral * COLLATERAL_PRECISION
leverage_collateral: uint256 = 0
k_effective: uint256 = self._get_k_effective(user_collateral + leverage_collateral, N)
max_p_base: uint256 = self._max_p_base()
p_avg: uint256 = AMM.price_oracle()
max_borrowable_prev: uint256 = 0
max_borrowable: uint256 = 0
for i in range(10):
max_borrowable_prev = max_borrowable
max_borrowable = user_collateral * 10**18 / (10**36 / k_effective * 10**18 / max_p_base - 10**36 / p_avg)
if max_borrowable > max_borrowable_prev:
if max_borrowable - max_borrowable_prev <= 1:
return max_borrowable
else:
if max_borrowable_prev - max_borrowable <= 1:
return max_borrowable
res: uint256[2] = self._get_collateral_and_avg_price(max_borrowable, route_idx)
leverage_collateral = res[0]
p_avg = res[1]
k_effective = self._get_k_effective(user_collateral + leverage_collateral, N)
return min(max_borrowable * 999 / 1000, ERC20(CRVUSD).balanceOf(CONTROLLER)) # Cannot borrow beyond the amount of coins Controller has
@view
@internal
def _get_collateral(stablecoin: uint256, route_idx: uint256) -> uint256:
return ROUTER.get_exchange_multiple_amount(self.routes[route_idx], self.route_params[route_idx], stablecoin, self.route_pools[route_idx])
```
```vyper
@view
@external
def get_exchange_multiple_amount(
_route: address[9],
_swap_params: uint256[3][4],
_amount: uint256,
_pools: address[4]=[ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS]
) -> uint256:
"""
@notice Get the current number the final output tokens received in an exchange
@dev Routing and swap params must be determined off-chain. This
functionality is designed for gas efficiency over ease-of-use.
@param _route Array of [initial token, pool, token, pool, token, ...]
The array is iterated until a pool address of 0x00, then the last
given token is transferred to `_receiver`
@param _swap_params Multidimensional array of [i, j, swap type] where i and j are the correct
values for the n'th pool in `_route`. The swap type should be
1 for a stableswap `exchange`,
2 for stableswap `exchange_underlying`,
3 for a cryptoswap `exchange`,
4 for a cryptoswap `exchange_underlying`,
5 for factory metapools with lending base pool `exchange_underlying`,
6 for factory crypto-meta pools underlying exchange (`exchange` method in zap),
7-11 for wrapped coin (underlying for lending pool) -> LP token "exchange" (actually `add_liquidity`),
12-14 for LP token -> wrapped coin (underlying for lending or fake pool) "exchange" (actually `remove_liquidity_one_coin`)
15 for WETH -> ETH "exchange" (actually deposit/withdraw)
@param _amount The amount of `_route[0]` token to be sent.
@param _pools Array of pools for swaps via zap contracts. This parameter is only needed for
Polygon meta-factories underlying swaps.
@return Expected amount of the final output token
"""
amount: uint256 = _amount
for i in range(1,5):
# 4 rounds of iteration to perform up to 4 swaps
swap: address = _route[i*2-1]
pool: address = _pools[i-1] # Only for Polygon meta-factories underlying swap (swap_type == 4)
params: uint256[3] = _swap_params[i-1] # i, j, swap type
# Calc output amount according to the swap type
if params[2] == 1:
amount = CurvePool(swap).get_dy(convert(params[0], int128), convert(params[1], int128), amount)
elif params[2] == 2:
amount = CurvePool(swap).get_dy_underlying(convert(params[0], int128), convert(params[1], int128), amount)
elif params[2] == 3:
amount = CryptoPool(swap).get_dy(params[0], params[1], amount)
elif params[2] == 4:
amount = CryptoPool(swap).get_dy_underlying(params[0], params[1], amount)
elif params[2] == 5:
amount = CurvePool(pool).get_dy_underlying(convert(params[0], int128), convert(params[1], int128), amount)
elif params[2] == 6:
amount = CryptoMetaZap(swap).get_dy(pool, params[0], params[1], amount)
elif params[2] == 7:
_amounts: uint256[2] = [0, 0]
_amounts[params[0]] = amount
amount = BasePool2Coins(swap).calc_token_amount(_amounts, True)
elif params[2] in [8, 9]:
_amounts: uint256[3] = [0, 0, 0]
_amounts[params[0]] = amount
amount = BasePool3Coins(swap).calc_token_amount(_amounts, True)
elif params[2] == 10:
_amounts: uint256[4] = [0, 0, 0, 0]
_amounts[params[0]] = amount
amount = BasePool4Coins(swap).calc_token_amount(_amounts, True)
elif params[2] == 11:
_amounts: uint256[5] = [0, 0, 0, 0, 0]
_amounts[params[0]] = amount
amount = BasePool5Coins(swap).calc_token_amount(_amounts, True)
elif params[2] in [12, 13]:
# The number of coins doesn't matter here
amount = BasePool3Coins(swap).calc_withdraw_one_coin(amount, convert(params[1], int128))
elif params[2] == 14:
# The number of coins doesn't matter here
amount = CryptoBasePool3Coins(swap).calc_withdraw_one_coin(amount, params[1])
elif params[2] == 15:
# ETH <--> WETH rate is 1:1
pass
else:
raise "Bad swap type"
# check if this was the last swap
if i == 4 or _route[i*2+1] == ZERO_ADDRESS:
break
return amount
```
```shell
>>> LeverageZap.max_collateral(100000000, 4, 0) # 1 wBTC with 4 bands using route id 0
645147830 # 6.45 wBTC as max collateral
>>> LeverageZap.max_collateral(100000000, 20, 0) # 1 wBTC with 20 bands using route id 0
472496460 # 4.72 wBTC as max collateral
>>> LeverageZap.max_collateral(100000000, 50, 0) # 1 wBTC with 50 bands using route id 0
322177677 # 3.22 wBTC as max collateral
```
::::
### `max_borrowable_and_collateral`
::::description[`LeverageZap.max_borrowable_and_collateral(collateral: uint256, N: uint256, route_idx: uint256) -> uint256[2]: view`]
:::warning
`max_borrowable` and `max_collateral` will return different values based on the route chosen.
:::
Function to calculate the maximum amount of crvUSD to be borrowed and the maximum amount of collateral for the position when using leverage. This function combines `max_borrowable` and `max_collateral` into one.
| Input | Type | Description |
| ------------ | --------- | --------------------------------------------------- |
| `collateral` | `uint256` | Amount of collateral (at its native precision). |
| `N` | `uint256` | Number of bands to deposit into. |
| `route_idx` | `uint256` | Index of the route to be used for exchanging stablecoin to collateral. |
Returns: maximum borrowable crvUSD and maximum collateral for the position.
```vyper
@external
@view
def max_borrowable_and_collateral(collateral: uint256, N: uint256, route_idx: uint256) -> uint256[2]:
"""
@notice Calculation of maximum which can be borrowed with leverage and maximum collateral position which can be created then
@param collateral Amount of collateral (at its native precision)
@param N Number of bands to deposit into
@param route_idx Index of the route which should be use for exchange stablecoin to collateral
@return [max_borrowable, user_collateral + max_leverage_collateral]
"""
max_borrowable: uint256 = self._max_borrowable(collateral, N, route_idx)
max_leverage_collateral: uint256 = self._get_collateral(max_borrowable, route_idx)
return [max_borrowable, collateral + max_leverage_collateral]
```
```shell
>>> LeverageZap.max_borrowable_and_collateral(100000000, 4, 0)
360350604468393712411123, 642982517
>>> LeverageZap.max_borrowable_and_collateral(100000000, 20, 0)
244175455173940727227428, 471426060
>>> LeverageZap.max_borrowable_and_collateral(100000000, 50, 0)
144607094555240096128757, 321798242
```
::::
### `calculate_debt_n1`
::::description[`LeverageZap.calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, route_idx: uint256) -> int256: view`]
Function to calculate the upper band number for the deposit to sit in, to support the given debt with full leverage. This essentially means that all borrowed stablecoin is converted to the collateral token and deposited in addition to the collateral provided by the user. The method reverts if the requested debt is too high.
Returns: upper band to deposit into (`int256`).
| Input | Type | Description |
| ------------ | --------- | -------------------------------------------- |
| `collateral` | `address` | Address of the collateral token. |
| `debt` | `uint256` | Amount of requested debt. |
| `N` | `uint256` | Number of bands to deposit into. |
| `route_idx` | `uint256` | Index of the route to be used for conversion.|
```vyper
@external
@view
def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, route_idx: uint256) -> int256:
"""
@notice Calculate the upper band number for the deposit to sit in to support
the given debt with full leverage, which means that all borrowed
stablecoin is converted to collateral coin and deposited in addition
to collateral provided by user. Reverts if requested debt is too high.
@param collateral Amount of collateral (at its native precision)
@param debt Amount of requested debt
@param N Number of bands to deposit into
@param route_idx Index of the route which should be use for exchange stablecoin to collateral
@return Upper band n1 (n1 <= n2) to deposit into. Signed integer
"""
leverage_collateral: uint256 = self._get_collateral(debt, route_idx)
return Controller(CONTROLLER).calculate_debt_n1(collateral + leverage_collateral, debt, N)
```
```py
@external
@view
@nonreentrant('lock')
def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256) -> int256:
"""
@notice Calculate the upper band number for the deposit to sit in to support
the given debt. Reverts if requested debt is too high.
@param collateral Amount of collateral (at its native precision)
@param debt Amount of requested debt
@param N Number of bands to deposit into
@return Upper band n1 (n1 <= n2) to deposit into. Signed integer
"""
return self._calculate_debt_n1(collateral, debt, N)
@internal
@view
def _calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256) -> int256:
"""
@notice Calculate the upper band number for the deposit to sit in to support
the given debt. Reverts if requested debt is too high.
@param collateral Amount of collateral (at its native precision)
@param debt Amount of requested debt
@param N Number of bands to deposit into
@return Upper band n1 (n1 <= n2) to deposit into. Signed integer
"""
assert debt > 0, "No loan"
n0: int256 = AMM.active_band()
p_base: uint256 = AMM.p_oracle_up(n0)
# x_effective = y / N * p_oracle_up(n1) * sqrt((A - 1) / A) * sum_{0..N-1}(((A-1) / A)**k)
# === d_y_effective * p_oracle_up(n1) * sum(...) === y_effective * p_oracle_up(n1)
# d_y_effective = y / N / sqrt(A / (A - 1))
y_effective: uint256 = self.get_y_effective(collateral * COLLATERAL_PRECISION, N, self.loan_discount)
# p_oracle_up(n1) = base_price * ((A - 1) / A)**n1
# We borrow up until min band touches p_oracle,
# or it touches non-empty bands which cannot be skipped.
# We calculate required n1 for given (collateral, debt),
# and if n1 corresponds to price_oracle being too high, or unreachable band
# - we revert.
# n1 is band number based on adiabatic trading, e.g. when p_oracle ~ p
y_effective = y_effective * p_base / (debt + 1) # Now it's a ratio
# n1 = floor(log2(y_effective) / self.logAratio)
# EVM semantics is not doing floor unlike Python, so we do this
assert y_effective > 0, "Amount too low"
n1: int256 = self.log2(y_effective) # <- switch to faster ln() XXX?
if n1 < 0:
n1 -= LOG2_A_RATIO - 1 # This is to deal with vyper's rounding of negative numbers
n1 /= LOG2_A_RATIO
n1 = min(n1, 1024 - convert(N, int256)) + n0
if n1 <= n0:
assert AMM.can_skip_bands(n1 - 1), "Debt too high"
# Let's not rely on active_band corresponding to price_oracle:
# this will be not correct if we are in the area of empty bands
assert AMM.p_oracle_up(n1) < AMM.price_oracle(), "Debt too high"
return n1
```
```shell
>>> LeverageZap.calculate_debt_n1(100000000, 300000000000000000000000. 4, 0)
-60
```
::::
---
## Routes
Routes are predetermined paths for token exchanges. These routes are added when initializing the contract. Additional routes cannot be added after the contract's deployment.
### `routes`
::::description[`LeverageZap.routes(arg0: uint256, arg1: uint256) -> address: view`]
Getter for the specific route of a route index. The route consists of alternating tokens and pools, formatted as `token -> pool -> token -> pool`, etc.
Returns: address of the pool or coin (`address`).
| Input | Type | Description |
| ------ | --------- | ------------------------------------ |
| `arg0` | `uint256` | Index of the route. |
| `arg1` | `uint256` | Position in the route to retrieve the pool or coin. |
```vyper
routes: public(HashMap[uint256, address[9]])
@external
def __init__(
_controller: address,
_collateral: address,
_router: address,
_routes: DynArray[address[9], 20],
_route_params: DynArray[uint256[3][4], 20],
_route_pools: DynArray[address[4], 20],
_route_names: DynArray[String[64], 20],
):
CONTROLLER = _controller
ROUTER = Router(_router)
amm: address = Controller(_controller).amm()
AMM = LLAMMA(amm)
_A: uint256 = LLAMMA(amm).A()
A = _A
Aminus1 = _A - 1
LOG2_A_RATIO = self.log2(_A * 10 **18 / unsafe_sub(_A, 1))
SQRT_BAND_RATIO = isqrt(unsafe_div(10 **36 * _A, unsafe_sub(_A, 1)))
COLLATERAL_PRECISION = pow_mod256(10, 18 - ERC20(_collateral).decimals())
for i in range(20):
if i >= len(_routes):
break
self.routes[i] = _routes[i]
self.route_params[i] = _route_params[i]
self.route_pools[i] = _route_pools[i]
self.route_names[i] = _route_names[i]
self.routes_count = len(_routes)
ERC20(CRVUSD).approve(_router, max_value(uint256), default_return_value=True)
ERC20(_collateral).approve(_controller, max_value(uint256), default_return_value=True)
```
*This example shows the route for the route at index 0 `'crvusd/USDC --> 3pool --> tricrypto2'`.*
```shell
>>> LeverageZap.route_name(0)
'crvusd/USDC --> 3pool --> tricrypto2'
>>> LeverageZap.routes(0, 0)
'0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E' # crvUSD
>>> LeverageZap.routes(0, 1)
'0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E' # crvusd/USDC pool
>>> LeverageZap.routes(0, 2)
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' # USDC
>>> LeverageZap.routes(0, 3)
'0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7' # threpool (DAI<>USDC<>USDT) pool
>>> LeverageZap.routes(0, 4)
'0xdAC17F958D2ee523a2206206994597C13D831ec7' # USDT
>>> LeverageZap.routes(0, 5)
'0xD51a44d3FaE010294C616388b506AcdA1bfAAE46' # tricrypto2 (USDT, wETH, wBTC) pool
>>> LeverageZap.routes(0, 6)
'0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599' # wBTC
```
::::
### `route_params`
::::description[`LeverageZap.route_params(arg0: uint256, arg1: uint256, arg2: uint256) -> uint256: view`]
Getter for the route parameters.
Returns: route parameter (`uint256`).
| Input | Type | Description |
| ------ | --------- | ---------------------------------------------------- |
| `arg0` | `uint256` | Index of the route. |
| `arg1` | `uint256` | Exchange index within the route. The first exchange is indexed as 0, the second as 1, etc. |
| `arg2` | `uint256` | Route parameter value. `0` for input token, `1` for output token, `2` for swap type. |
```vyper
route_params: public(HashMap[uint256, uint256[3][4]])
@external
def __init__(
_controller: address,
_collateral: address,
_router: address,
_routes: DynArray[address[9], 20],
_route_params: DynArray[uint256[3][4], 20],
_route_pools: DynArray[address[4], 20],
_route_names: DynArray[String[64], 20],
):
CONTROLLER = _controller
ROUTER = Router(_router)
amm: address = Controller(_controller).amm()
AMM = LLAMMA(amm)
_A: uint256 = LLAMMA(amm).A()
A = _A
Aminus1 = _A - 1
LOG2_A_RATIO = self.log2(_A * 10 **18 / unsafe_sub(_A, 1))
SQRT_BAND_RATIO = isqrt(unsafe_div(10 **36 * _A, unsafe_sub(_A, 1)))
COLLATERAL_PRECISION = pow_mod256(10, 18 - ERC20(_collateral).decimals())
for i in range(20):
if i >= len(_routes):
break
self.routes[i] = _routes[i]
self.route_params[i] = _route_params[i]
self.route_pools[i] = _route_pools[i]
self.route_names[i] = _route_names[i]
self.routes_count = len(_routes)
ERC20(CRVUSD).approve(_router, max_value(uint256), default_return_value=True)
ERC20(_collateral).approve(_controller, max_value(uint256), default_return_value=True)
```
```shell
# first exchange: exchanging crvUSD for USDC using crvusd/USDC pool
>>> LeverageZap.route_params(0, 0, 0) # route 0, first exchange (index 0), fist parameter value (index 0)
1 # i = crvUSD
>>> LeverageZap.route_params(0, 0, 1) # route 0, first exchange (index 0), second parameter value (index 1)
0 # j = USDC
>>> LeverageZap.route_params(0, 0, 2) # route 0, first exchange (index 0), third parameter value (index 2)
1 # swap type 1 = stableswap exchange
# second exchange: exchanging USDC for USDT using threepool
>>> LeverageZap.route_params(0, 1, 0) # route 0, second exchange (index 1), fist parameter value (index 0)
1 # i = USDC
>>> LeverageZap.route_params(0, 1, 1) # route 0, second exchange (index 1), second parameter value (index 1)
2 # j = USDT
>>> LeverageZap.route_params(0, 1, 2) # route 0, second exchange (index 1), third parameter value (index 2)
1 # swap type 1 = stableswap exchange
# third exchange: exchanging USDT for BTC using tricrypto2 pool
>>> LeverageZap.route_params(0, 2, 0) # route 0, third exchange (index 2), fist parameter value (index 0)
0 # i = USDT
>>> LeverageZap.route_params(0, 2, 1) # route 0, third exchange (index 2), second parameter value (index 1)
1 # j = wBTC
>>> LeverageZap.route_params(0, 2, 2) # route 0, third exchange (index 2), third parameter value (index 2)
3 # swap type 3 = cryptoswap exchange
```
::::
### `route_pools`
::::description[`LeverageZap.route_pools(arg0: uint256, arg1: uint256) -> address: view`]
Getter for the zap contracts used for a specific exchange in a route, if there are any.
Returns: zap contract (`address`).
| Input | Type | Description |
| ------ | --------- | --------------------------------------------------- |
| `arg0` | `uint256` | Index of the route. |
| `arg1` | `uint256` | Index of the exchange. The first exchange is index 0, the second exchange is index 1, etc. |
```vyper
route_pools: public(HashMap[uint256, address[4]])
@external
def __init__(
_controller: address,
_collateral: address,
_router: address,
_routes: DynArray[address[9], 20],
_route_params: DynArray[uint256[3][4], 20],
_route_pools: DynArray[address[4], 20],
_route_names: DynArray[String[64], 20],
):
CONTROLLER = _controller
ROUTER = Router(_router)
amm: address = Controller(_controller).amm()
AMM = LLAMMA(amm)
_A: uint256 = LLAMMA(amm).A()
A = _A
Aminus1 = _A - 1
LOG2_A_RATIO = self.log2(_A * 10 **18 / unsafe_sub(_A, 1))
SQRT_BAND_RATIO = isqrt(unsafe_div(10 **36 * _A, unsafe_sub(_A, 1)))
COLLATERAL_PRECISION = pow_mod256(10, 18 - ERC20(_collateral).decimals())
for i in range(20):
if i >= len(_routes):
break
self.routes[i] = _routes[i]
self.route_params[i] = _route_params[i]
self.route_pools[i] = _route_pools[i]
self.route_names[i] = _route_names[i]
self.routes_count = len(_routes)
ERC20(CRVUSD).approve(_router, max_value(uint256), default_return_value=True)
ERC20(_collateral).approve(_controller, max_value(uint256), default_return_value=True)
```
```shell
>>> LeverageZap.route_pools(0, 0)
'0x0000000000000000000000000000000000000000'
>>> LeverageZap.route_pools(0, 1)
'0x0000000000000000000000000000000000000000'
```
::::
### `route_names`
::::description[`LeverageZap.route_names(arg0: uint256) -> String[64]: view`]
Getter for the route name of a route.
Returns: route name (`String[64]`).
| Input | Type | Description |
| ------| --------- | -------------------- |
| `arg0`| `uint256` | Index of the route. |
```vyper
route_names: public(HashMap[uint256, String[64]])
@external
def __init__(
_controller: address,
_collateral: address,
_router: address,
_routes: DynArray[address[9], 20],
_route_params: DynArray[uint256[3][4], 20],
_route_pools: DynArray[address[4], 20],
_route_names: DynArray[String[64], 20],
):
...
for i in range(20):
if i >= len(_routes):
break
self.routes[i] = _routes[i]
self.route_params[i] = _route_params[i]
self.route_pools[i] = _route_pools[i]
self.route_names[i] = _route_names[i]
self.routes_count = len(_routes)
...
```
```shell
>>> LeverageZap.route_names(0)
'crvusd/USDC --> 3pool --> tricrypto2'
>>> LeverageZap.route_names(1)
'crvusd/USDT --> tricrypto2'
```
::::
### `route_count`
::::description[`LeverageZap.route_count() -> uint256: view`]
Getter for the total amount of routes included.
Returns: amount of routes (`uint256`).
```vyper
routes_count: public(uint256)
@external
def __init__(
_controller: address,
_collateral: address,
_router: address,
_routes: DynArray[address[9], 20],
_route_params: DynArray[uint256[3][4], 20],
_route_pools: DynArray[address[4], 20],
_route_names: DynArray[String[64], 20],
):
...
for i in range(20):
if i >= len(_routes):
break
self.routes[i] = _routes[i]
self.route_params[i] = _route_params[i]
self.route_pools[i] = _route_pools[i]
self.route_names[i] = _route_names[i]
self.routes_count = len(_routes)
...
```
```shell
>>> LeverageZap.route_count()
5
```
::::
---
## LeverageZapOdos
This Zap contract is specifically designed to **create or repay leveraged loans** using the [**Odos router**](https://odos.xyz/).
:::vyper[`LeverageZapOdos.vy`]
The source code for the `LlamaLendOdosLeverageZap.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/lending/contracts/zaps/LeverageZapOdos.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
The contract is deployed on :logos-ethereum: Ethereum at [`0xc5898606bdb494a994578453b92e7910a90aa873`](https://etherscan.io/address/0xc5898606bdb494a994578453b92e7910a90aa873).
An accompanying JavaScript library for Curve Lending can be found here: [GitHub](https://github.com/curvefi/curve-lending-js).
:::
Previously, building leverage for crvUSD markets relied solely on predefined routes using only Curve pools. Leveraging large positions often led to significant price impact due to the exclusive use of Curve liquidity pools. This new Zap contract allows users to leverage loans for crvUSD and lending markets using the Odos router, which considers liquidity sources across DeFi.[^1]
[^1]: The premise is that these liquidity sources are integrated within the Odos router.
---
Leverage is built using a **callback method**. The function to execute callbacks is located in the `Controller.vy` contract:
:::bug
`callback_sig` is the `method_id` of the function from the `LlamaLendOdosLeverageZap.vy` contract which needs to be called. While this value is obtained by using Vyper's built-in [`method_id`](https://docs.vyperlang.org/en/stable/built-in-functions.html?highlight=raw_call#method_id) function for the `callback_deposit` function, it does not work for the `callback_repay` function due to a bug. The reason for the bug is a `0` at the beginning of the method_id. That's why the method ID for `CALLBACK_REPAY_WITH_BYTES` is hardcoded to `0x008ae188`.
:::
```py
struct CallbackData:
active_band: int256
stablecoins: uint256
collateral: uint256
CALLBACK_DEPOSIT: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_REPAY: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_LIQUIDATE: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_DEPOSIT_WITH_BYTES: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
# CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4) <-- BUG! The reason is 0 at the beginning of method_id
CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = 0x008ae188
CALLBACK_LIQUIDATE_WITH_BYTES: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
@internal
def execute_callback(callbacker: address, callback_sig: bytes4,
user: address, stablecoins: uint256, collateral: uint256, debt: uint256,
callback_args: DynArray[uint256, 5], callback_bytes: Bytes[10**4]) -> CallbackData:
assert callbacker != COLLATERAL_TOKEN.address
data: CallbackData = empty(CallbackData)
data.active_band = AMM.active_band()
band_x: uint256 = AMM.bands_x(data.active_band)
band_y: uint256 = AMM.bands_y(data.active_band)
# Callback
response: Bytes[64] = raw_call(
callbacker,
concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)),
max_outsize=64
)
data.stablecoins = convert(slice(response, 0, 32), uint256)
data.collateral = convert(slice(response, 32, 32), uint256)
# Checks after callback
assert data.active_band == AMM.active_band()
assert band_x == AMM.bands_x(data.active_band)
assert band_y == AMM.bands_y(data.active_band)
return data
```
:::info[Required Changes to `Controller.vy`]
This zap only works for crvUSD and lending markets which were deployed using the blueprint implementation at [`0x4c5d4F542765B66154B2E789abd8E69ed4504112`](https://etherscan.io/address/0x4c5d4F542765B66154B2E789abd8E69ed4504112). Markets deployed prior to that can only make use of the regular [`LeverageZap.vy`](./leverage-zap.md).
To enable the functionality of such Zap contracts, minor modifications were necessary in the `Controller.vy` contract. Functions such as `create_loan_extended`, `borrow_more_extended`, `repay_extended`, `_liquidity`, and `liquidate_extended` were enhanced with an additional constructor argument `callback_bytes: Bytes[10**4]`. This allows users to pass bytes to the Zap contract. Additionally, the internal `execute_callback` function, which manages the callbacks, was also updated.
:::
---
## Building Leverage
To build up leverage, the `LlamaLendOdosLeverageZap.vy` contract uses the `callback_deposit` function. Additionally, there is a `max_borrowable` function that calculates the maximum borrowable amount when using leverage.
*Flow of building leverage:*
1. User calls [`create_loan_extended`](../controller.md#create_loan_extended) or [`borrow_more_extended`](../controller.md#borrow_more_extended) and passes `collateral`, `debt`, `N`, `callbacker`, `callback_args`, and `callback_bytes` into the function.[^2]
2. The debt which is taken on by the user is then transferred to the `callbacker`, in our case the `LlamaLendOdosLeverageZap.vy` contract.
3. After the transfer, the callback is executed using the internal `execute_callback` in the `Controller.vy` contract. This step builds up the leverage.
```py
struct CallbackData:
active_band: int256
stablecoins: uint256
collateral: uint256
CALLBACK_DEPOSIT: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_REPAY: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_LIQUIDATE: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_DEPOSIT_WITH_BYTES: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
# CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4) <-- BUG! The reason is 0 at the beginning of method_id
CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = 0x008ae188
CALLBACK_LIQUIDATE_WITH_BYTES: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
@internal
def execute_callback(callbacker: address, callback_sig: bytes4,
user: address, stablecoins: uint256, collateral: uint256, debt: uint256,
callback_args: DynArray[uint256, 5], callback_bytes: Bytes[10**4]) -> CallbackData:
assert callbacker != COLLATERAL_TOKEN.address
data: CallbackData = empty(CallbackData)
data.active_band = AMM.active_band()
band_x: uint256 = AMM.bands_x(data.active_band)
band_y: uint256 = AMM.bands_y(data.active_band)
# Callback
response: Bytes[64] = raw_call(
callbacker,
concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)),
max_outsize=64
)
data.stablecoins = convert(slice(response, 0, 32), uint256)
data.collateral = convert(slice(response, 32, 32), uint256)
# Checks after callback
assert data.active_band == AMM.active_band()
assert band_x == AMM.bands_x(data.active_band)
assert band_y == AMM.bands_y(data.active_band)
return data
```
The function uses Vyper's built-in [`raw_call`](https://docs.vyperlang.org/en/stable/built-in-functions.html?highlight=raw_call#raw_call) function to call the desired method (in this case `callback_deposit`) with the according `callback_bytes`.
4. After executing the callback, the Controller either creates a new loan or adds the additional collateral borrowed to the already existing loan and deposits the collateral into the AMM.
[^2]: `collateral` is the amount of collateral tokens used, `debt` is the amount of debt to take on, `N` represents the number of bands, `callbacker` is the callback contract, `callback_args` are some extra arguments passed to the callbacker, and `callback_bytes`.
### `callback_deposit`
::::description[`LlamaLendOdosLeverageZap.callback_deposit(user: address, stablecoins: uint256, user_collateral: uint256, d_debt: uint256, callback_args: DynArray[uint256, 10], callback_bytes: Bytes[10**4] = b"")]
:::guard[Guarded Method]
This function is only callable by the `Controller` from where tokens are borrowed from.
:::
Function to create a leveraged loan using a callback.
The following callback arguments need to be passed to this function via `create_loan_extended` or `borrow_more_extended`:
- `callback_args[0] = factory_id`: depending on which factory (crvusd or lending).
- `callback_args[1] = controller_id`: index of the controller in the factory contract fetched from `Factory.controllers(controller_id)`.
- `callback_args[2] = user_borrowed`: amount of borrowed token provided by the user (which is exchanged for the collateral token).
| Input | Type | Description |
| ----------------- | ----------------------- | ------------ |
| `user` | `address` | User address to create a leveraged position for. |
| `stablecoins` | `uint256` | Always 0. |
| `user_collateral` | `uint256` | Amount of collateral token provided by the user. |
| `d_debt` | `uint256` | Amount to be borrowed (in addition to what has already been borrowed). |
| `callback_args` | `DynArray[uint256, 10]` | Callback arguments. |
| `callback_bytes` | `Bytes[10**4] = b""` | Callback bytes. |
Returns: 0 and additional collateral (`uint256[2]`).
Emits: `Deposit`
```vyper
event Deposit:
user: indexed(address)
user_collateral: uint256
user_borrowed: uint256
user_collateral_from_borrowed: uint256
debt: uint256
leverage_collateral: uint256
@external
@nonreentrant('lock')
def callback_deposit(user: address, stablecoins: uint256, user_collateral: uint256, d_debt: uint256,
callback_args: DynArray[uint256, 10], callback_bytes: Bytes[10**4] = b"") -> uint256[2]:
"""
@notice Callback method which should be called by controller to create leveraged position
@param user Address of the user
@param stablecoins Always 0
@param user_collateral The amount of collateral token provided by user
@param d_debt The amount to be borrowed (in addition to what has already been borrowed)
@param callback_args [factory_id, controller_id, user_borrowed]
0-1. factory_id, controller_id are needed to check that msg.sender is the one of our controllers
2. user_borrowed - the amount of borrowed token provided by user (needs to be exchanged for collateral)
return [0, user_collateral_from_borrowed + leverage_collateral]
"""
controller: address = Factory(self.FACTORIES[callback_args[0]]).controllers(callback_args[1])
assert msg.sender == controller, "wrong controller"
amm: LLAMMA = LLAMMA(Controller(controller).amm())
borrowed_token: address = amm.coins(0)
collateral_token: address = amm.coins(1)
self._approve(borrowed_token, ROUTER)
self._approve(collateral_token, controller)
user_borrowed: uint256 = callback_args[2]
self._transferFrom(borrowed_token, user, self, user_borrowed)
raw_call(ROUTER, callback_bytes) # buys leverage_collateral for user_borrowed + dDebt
additional_collateral: uint256 = ERC20(collateral_token).balanceOf(self)
leverage_collateral: uint256 = d_debt * 10**18 / (d_debt + user_borrowed) * additional_collateral / 10**18
user_collateral_from_borrowed: uint256 = additional_collateral - leverage_collateral
log Deposit(user, user_collateral, user_borrowed, user_collateral_from_borrowed, d_debt, leverage_collateral)
return [0, additional_collateral]
@internal
def _transferFrom(token: address, _from: address, _to: address, amount: uint256):
if amount > 0:
assert ERC20(token).transferFrom(_from, _to, amount, default_return_value=True)
@internal
def _approve(coin: address, spender: address):
if ERC20(coin).allowance(self, spender) == 0:
assert ERC20(coin).approve(spender, max_value(uint256), default_return_value=True)
```
```vyper
@external
@nonreentrant('lock')
def create_loan_extended(collateral: uint256, debt: uint256, N: uint256, callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b""):
"""
@notice Create loan but pass stablecoin to a callback first so that it can build leverage
@param collateral Amount of collateral to use
@param debt Stablecoin debt to take
@param N Number of bands to deposit into (to do autoliquidation-deliquidation),
can be from MIN_TICKS to MAX_TICKS
@param callbacker Address of the callback contract
@param callback_args Extra arguments for the callback (up to 5) such as min_amount etc
"""
# Before callback
self.transfer(BORROWED_TOKEN, callbacker, debt)
# For compatibility
callback_sig: bytes4 = CALLBACK_DEPOSIT_WITH_BYTES
if callback_bytes == b"":
callback_sig = CALLBACK_DEPOSIT
# Callback
# If there is any unused debt, callbacker can send it to the user
more_collateral: uint256 = self.execute_callback(
callbacker, callback_sig, msg.sender, 0, collateral, debt, callback_args, callback_bytes).collateral
# After callback
self._create_loan(collateral + more_collateral, debt, N, False)
self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral)
self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral)
@internal
def execute_callback(callbacker: address, callback_sig: bytes4,
user: address, stablecoins: uint256, collateral: uint256, debt: uint256,
callback_args: DynArray[uint256, 5], callback_bytes: Bytes[10**4]) -> CallbackData:
assert callbacker != COLLATERAL_TOKEN.address
data: CallbackData = empty(CallbackData)
data.active_band = AMM.active_band()
band_x: uint256 = AMM.bands_x(data.active_band)
band_y: uint256 = AMM.bands_y(data.active_band)
# Callback
response: Bytes[64] = raw_call(
callbacker,
concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)),
max_outsize=64
)
data.stablecoins = convert(slice(response, 0, 32), uint256)
data.collateral = convert(slice(response, 32, 32), uint256)
# Checks after callback
assert data.active_band == AMM.active_band()
assert band_x == AMM.bands_x(data.active_band)
assert band_y == AMM.bands_y(data.active_band)
return data
```
::::
### `max_borrowable`
::::description[`LlamaLendOdosLeverageZap.max_borrowable(controller: address, _user_collateral: uint256, _leverage_collateral: uint256, N: uint256, p_avg: uint256) -> uint256: view`]
Function to calculate the maximum borrowable using leverage. The maximum borrowable amount essentially comes down to:
$$\text{max\_borrowable} = \frac{\text{collateral}}{\frac{1}{\text{k\_effective} \times \text{max\_p\_base}} - \frac{1}{\text{p\_avg}}}$$
with $\text{k\_effective}$ and $\text{max\_p\_base}$ being calculated with the internal `_get_k_effective` and `_max_p_base` methods. $\text{p\_avg}$ is the average price of the collateral.
| Input | Type | Description |
| ---------------------- | --------- | ------------ |
| `controller` | `address` | Controller of the market to borrow from. |
| `_user_collateral` | `uint256` | Amount of collateral at its native precision. |
| `_leverage_collateral` | `uint256` | Additional collateral to use for leveraging. |
| `N` | `uint256` | Number of bands to deposit into. |
| `p_avg` | `uint256` | Average price of the collateral. |
Returns: maximum amount to borrow (`uint256`). The maximum value to return is either the maximum a user can borrow and is ultimately limited by the amount of coins the Controller has.
```vyper
DEAD_SHARES: constant(uint256) = 1000
MAX_TICKS_UINT: constant(uint256) = 50
MAX_P_BASE_BANDS: constant(int256) = 5
MAX_SKIP_TICKS: constant(uint256) = 1024
@external
@view
def max_borrowable(controller: address, _user_collateral: uint256, _leverage_collateral: uint256, N: uint256, p_avg: uint256) -> uint256:
"""
@notice Calculation of maximum which can be borrowed with leverage
"""
# max_borrowable = collateral / (1 / (k_effective * max_p_base) - 1 / p_avg)
AMM: LLAMMA = LLAMMA(Controller(controller).amm())
BORROWED_TOKEN: address = AMM.coins(0)
COLLATERAL_TOKEN: address = AMM.coins(1)
COLLATERAL_PRECISION: uint256 = pow_mod256(10, 18 - ERC20(COLLATERAL_TOKEN).decimals())
user_collateral: uint256 = _user_collateral * COLLATERAL_PRECISION
leverage_collateral: uint256 = _leverage_collateral * COLLATERAL_PRECISION
k_effective: uint256 = self._get_k_effective(controller, user_collateral + leverage_collateral, N)
max_p_base: uint256 = self._max_p_base(controller)
max_borrowable: uint256 = user_collateral * 10**18 / (10**36 / k_effective * 10**18 / max_p_base - 10**36 / p_avg)
return min(max_borrowable * 999 / 1000, ERC20(BORROWED_TOKEN).balanceOf(controller)) # Cannot borrow beyond the amount of coins Controller has
@internal
@view
def _get_k_effective(controller: address, collateral: uint256, N: uint256) -> uint256:
"""
@notice Intermediary method which calculates k_effective defined as x_effective / p_base / y,
however discounted by loan_discount.
x_effective is an amount which can be obtained from collateral when liquidating
@param N Number of bands the deposit is made into
@return k_effective
"""
# x_effective = sum_{i=0..N-1}(y / N * p(n_{n1+i})) =
# = y / N * p_oracle_up(n1) * sqrt((A - 1) / A) * sum_{0..N-1}(((A-1) / A)**k)
# === d_y_effective * p_oracle_up(n1) * sum(...) === y * k_effective * p_oracle_up(n1)
# d_k_effective = 1 / N / sqrt(A / (A - 1))
# d_k_effective: uint256 = 10**18 * unsafe_sub(10**18, discount) / (SQRT_BAND_RATIO * N)
# Make some extra discount to always deposit lower when we have DEAD_SHARES rounding
CONTROLLER: Controller = Controller(controller)
A: uint256 = LLAMMA(CONTROLLER.amm()).A()
SQRT_BAND_RATIO: uint256 = isqrt(unsafe_div(10 **36 * A, unsafe_sub(A, 1)))
discount: uint256 = CONTROLLER.loan_discount()
d_k_effective: uint256 = 10**18 * unsafe_sub(
10**18, min(discount + (DEAD_SHARES * 10**18) / max(collateral / N, DEAD_SHARES), 10**18)
) / (SQRT_BAND_RATIO * N)
k_effective: uint256 = d_k_effective
for i in range(1, MAX_TICKS_UINT):
if i == N:
break
d_k_effective = unsafe_div(d_k_effective * (A - 1), A)
k_effective = unsafe_add(k_effective, d_k_effective)
return k_effective
@internal
@view
def _max_p_base(controller: address) -> uint256:
"""
@notice Calculate max base price including skipping bands
"""
AMM: LLAMMA = LLAMMA(Controller(controller).amm())
A: uint256 = AMM.A()
LOGN_A_RATIO: int256 = self.wad_ln(A * 10**18 / (A - 1))
p_oracle: uint256 = AMM.price_oracle()
# Should be correct unless price changes suddenly by MAX_P_BASE_BANDS+ bands
n1: int256 = self.wad_ln(AMM.get_base_price() * 10**18 / p_oracle)
if n1 < 0:
n1 -= LOGN_A_RATIO - 1 # This is to deal with vyper's rounding of negative numbers
n1 = unsafe_div(n1, LOGN_A_RATIO) + MAX_P_BASE_BANDS
n_min: int256 = AMM.active_band_with_skip()
n1 = max(n1, n_min + 1)
p_base: uint256 = AMM.p_oracle_up(n1)
for i in range(MAX_SKIP_TICKS + 1):
n1 -= 1
if n1 <= n_min:
break
p_base_prev: uint256 = p_base
p_base = unsafe_div(p_base * A, A - 1)
if p_base > p_oracle:
return p_base_prev
return p_base
```
::::
---
## Unwinding Leverage
To deleverage loans, the `LlamaLendOdosLeverageZap.vy` contract uses the `callback_repay` function.
*Flow of deleveraging:*
1. User calls `repay_extended` and passes `callbacker`, `callback_args`, and `callback_bytes` into the function.
2. The Controller withdraws 100% of the collateral from the AMM and transfers all of it to the `callbacker` contract.
3. After the transfer, the callback is executed using the internal `execute_callback` in the `Controller.vy` contract. This function unwinds the leverage.
```py
struct CallbackData:
active_band: int256
stablecoins: uint256
collateral: uint256
CALLBACK_DEPOSIT: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_REPAY: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_LIQUIDATE: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[])", output_type=bytes4)
CALLBACK_DEPOSIT_WITH_BYTES: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
# CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4) <-- BUG! The reason is 0 at the beginning of method_id
CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = 0x008ae188
CALLBACK_LIQUIDATE_WITH_BYTES: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4)
@internal
def execute_callback(callbacker: address, callback_sig: bytes4,
user: address, stablecoins: uint256, collateral: uint256, debt: uint256,
callback_args: DynArray[uint256, 5], callback_bytes: Bytes[10**4]) -> CallbackData:
assert callbacker != COLLATERAL_TOKEN.address
data: CallbackData = empty(CallbackData)
data.active_band = AMM.active_band()
band_x: uint256 = AMM.bands_x(data.active_band)
band_y: uint256 = AMM.bands_y(data.active_band)
# Callback
response: Bytes[64] = raw_call(
callbacker,
concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)),
max_outsize=64
)
data.stablecoins = convert(slice(response, 0, 32), uint256)
data.collateral = convert(slice(response, 32, 32), uint256)
# Checks after callback
assert data.active_band == AMM.active_band()
assert band_x == AMM.bands_x(data.active_band)
assert band_y == AMM.bands_y(data.active_band)
return data
```
The function uses Vyper's built-in [`raw_call`](https://docs.vyperlang.org/en/stable/built-in-functions.html?highlight=raw_call#raw_call) function to call the desired method (in this case `callback_repay`) with the according `callback_bytes`.
4. After executing the callback, the Controller checks and does a full repayment and closes the position when possible. Else, it does a partial repayment (deleverage).
### `callback_repay`
::::description[`LlamaLendOdosLeverageZap.callback_repay(user: address, stablecoins: uint256, collateral: uint256, debt: uint256, callback_args: DynArray[uint256, 10], callback_bytes: Bytes[10**4] = b"")]
:::guard[Guarded Method]
This function is only callable by the `Controller` from where tokens were borrowed from.
:::
Function to de-leverage a loan using a callback.
The following `callback_args` need to be passed to this function via `repay_extended`:
- `callback_args[0] = factory_id`: depending on which factory (crvusd or lending).
- `callback_args[1] = controller_id`: index of the controller in the factory contract fetched from `Factory.controllers(controller_id)`.
- `callback_args[2] = user_collateral`: amount of collateral token provided by the user (which is exchanged for the borrowed token).
- `callback_args[3] = user_borrowed`: amount of borrowed tokens to repay.
| Input | Type | Description |
| ----------------- | ----------------------- | ------------ |
| `user` | `address` | User address to unwind a leveraged position for. |
| `stablecoins` | `uint256` | Value returned from `user_state`. |
| `user_collateral` | `uint256` | Value returned from `user_state`. |
| `d_debt` | `uint256` | Value returned from `user_state`. |
| `callback_args` | `DynArray[uint256, 10]` | Callback arguments. |
| `callback_bytes` | `Bytes[10**4] = b""` | Callback bytes. |
Returns: borrowed_from_state_collateral + borrowed_from_user_collateral + user_borrowed and remaining_collateral (`uint256[2]`).
Emits: `Repay`
```vyper
event Repay:
user: indexed(address)
state_collateral_used: uint256
borrowed_from_state_collateral: uint256
user_collateral: uint256
user_collateral_used: uint256
borrowed_from_user_collateral: uint256
user_borrowed: uint256
@external
@nonreentrant('lock')
def callback_repay(user: address, stablecoins: uint256, collateral: uint256, debt: uint256,
callback_args: DynArray[uint256,10], callback_bytes: Bytes[10 **4] = b"") -> uint256[2]:
"""
@notice Callback method which should be called by controller to create leveraged position
@param user Address of the user
@param stablecoins The value from user_state
@param collateral The value from user_state
@param debt The value from user_state
@param callback_args [factory_id, controller_id, user_collateral, user_borrowed]
0-1. factory_id, controller_id are needed to check that msg.sender is the one of our controllers
2. user_collateral - the amount of collateral token provided by user (needs to be exchanged for borrowed)
3. user_borrowed - the amount of borrowed token to repay from user's wallet
return [user_borrowed + borrowed_from_collateral, remaining_collateral]
"""
controller: address = Factory(self.FACTORIES[callback_args[0]]).controllers(callback_args[1])
assert msg.sender == controller, "wrong controller"
amm: LLAMMA = LLAMMA(Controller(controller).amm())
borrowed_token: address = amm.coins(0)
collateral_token: address = amm.coins(1)
self._approve(collateral_token, ROUTER)
self._approve(borrowed_token, controller)
self._approve(collateral_token, controller)
initial_collateral: uint256 = ERC20(collateral_token).balanceOf(self)
user_collateral: uint256 = callback_args[2]
if callback_bytes != b"":
self._transferFrom(collateral_token, user, self, user_collateral)
# Buys borrowed token for collateral from user's position + from user's wallet.
# The amount to be spent is specified inside callback_bytes.
raw_call(ROUTER, callback_bytes)
else:
assert user_collateral == 0
remaining_collateral: uint256 = ERC20(collateral_token).balanceOf(self)
state_collateral_used: uint256 = 0
borrowed_from_state_collateral: uint256 = 0
user_collateral_used: uint256 = user_collateral
borrowed_from_user_collateral: uint256 = ERC20(borrowed_token).balanceOf(self) # here it's total borrowed_from_collateral
if remaining_collateral < initial_collateral:
state_collateral_used = initial_collateral - remaining_collateral
borrowed_from_state_collateral = state_collateral_used * 10**18 / (state_collateral_used + user_collateral_used) * borrowed_from_user_collateral / 10**18
borrowed_from_user_collateral = borrowed_from_user_collateral - borrowed_from_state_collateral
else:
user_collateral_used = user_collateral - (remaining_collateral - initial_collateral)
user_borrowed: uint256 = callback_args[3]
self._transferFrom(borrowed_token, user, self, user_borrowed)
log Repay(user, state_collateral_used, borrowed_from_state_collateral, user_collateral, user_collateral_used, borrowed_from_user_collateral, user_borrowed)
return [borrowed_from_state_collateral + borrowed_from_user_collateral + user_borrowed, remaining_collateral]
@internal
def _transferFrom(token: address, _from: address, _to: address, amount: uint256):
if amount > 0:
assert ERC20(token).transferFrom(_from, _to, amount, default_return_value=True)
@internal
def _approve(coin: address, spender: address):
if ERC20(coin).allowance(self, spender) == 0:
assert ERC20(coin).approve(spender, max_value(uint256), default_return_value=True)
```
::::
---
## Contract Info Methods
The contract has two public getters, one for the [Odos Router](https://docs.odos.xyz/build/quickstart/sor) contract and one for the two factory contracts for crvUSD and lending markets.
### `ROUTER`
::::description[`LlamaLendOdosLeverageZap.ROUTER() -> address: view`]
Getter method for the [Odos Router](https://docs.odos.xyz/build/quickstart/sor) contract. This variable is immutable, set at initialization and can not be changed.
Returns: Odos Router (`address`).
```vyper
ROUTER: public(immutable(address))
@external
def __init__(_router: address, _factories: DynArray[address, 2]):
ROUTER = _router
self.FACTORIES = _factories
```
This example returns the contract address of the Odos Router.
```shell
>>> LlamaLendOdosLeverageZap.ROUTER()
'0xCf5540fFFCdC3d510B18bFcA6d2b9987b0772559'
```
::::
### `FACTORIES`
::::description[`LlamaLendOdosLeverageZap.FACTORIES(arg0: uint256) -> address: view`]
Getter method for the `Factory` contract at index `arg0`.
Returns: Factory contract (`address`).
| Input | Type | Description |
| ------ | --------- | -------------------- |
| `arg0` | `uint256` | Index of the Factory contract to use. |
```vyper
FACTORIES: public(DynArray[address, 2])
@external
def __init__(_router: address, _factories: DynArray[address, 2]):
ROUTER = _router
self.FACTORIES = _factories
```
This example returns the contract address of the `Factory` contract at a specific index.
```shell
>>> LlamaLendOdosLeverageZap.FACTORIES(0)
'0xeA6876DDE9e3467564acBeE1Ed5bac88783205E0'
>>> LlamaLendOdosLeverageZap.FACTORIES(1)
'0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC'
```
::::
---
## Leverage Overview
When using leverage, a user uses their borrowed assets (debt) to buy more of the collateral assets. To do this, Curve employs `LeverageZap` contracts, which automatically loop the position. Currently, there are two different ones:
:::warning[Zap Integrations]
Leverage zaps only function properly if the Controller's features are synchronized with the zap contracts. While the regular `LeverageZap.vy` is compatible with all crvUSD and lending markets so far, the `LeverageZap1inch.vy` only works with newer lending markets using the [latest controller blueprint implementation](https://etherscan.io/address/0x4c5d4F542765B66154B2E789abd8E69ed4504112). This requirement is due to the 1inch contract needing specific byte data to build leverage.
The new controller implementation, which facilitates leveraging through the 1inch router, was [added to the `OneWayLendingFactory`](https://etherscan.io/tx/0x7a17babdfe5d171abf8bbbe6a00a82f1b19cdbcd2e71b93ccbe93cd1002635fe) on May 03, 2024, at 06:31:11 AM UTC. Any market deployed using this implementation can utilize both the regular and the 1inch leverage zap.
:::
First integration of leverage for crvUSD markets. This zap contract uses predefined routes on **Curve pools** to exchange debt for collateral token.
This zap makes use of the **1inch router** and works for crvUSD and lending markets. This allows users to tap into liquidity sources beyond just Curve pools.
This zap makes use of the **Odos router** and works for crvUSD and lending markets. This allows users to tap into liquidity sources beyond just Curve pools.
---
## How Leverage is Built
Leverage is built through a process known as "looping." The concept is straightforward: A user puts up some collateral (e.g., ETH) and takes on debt against it, let's say crvUSD. Because they want to use leverage and increase their exposure to ETH, they loop their position by selling crvUSD for more ETH and adding it as collateral to their loan. They then borrow more crvUSD again, sell it for ETH, and add it again. This process can be repeated as often as they wish.
1. Supply ETH as collateral and borrow crvUSD against it.
2. Exchange crvUSD for more ETH.
3. Add the newly acquired ETH as collateral again and borrow more crvUSD.
4. Exchange the new crvUSD for more ETH and add it as collateral.
5. Repeat the process.
---
## LLAMMA Explainer
:::info[Disclaimer: Examples]
Examples used in this section refer to an `ETH <> crvUSD` market, where `ETH` is the collateral and `crvUSD` is the borrowed token. Additionally, the examples in this section are simplified in terms of numbers and other details. In reality, it is more complex, but for the sake of explaining the system, simplified examples are sufficient.
:::
LLAMMA (Lending Liquidating Automated Market Maker Algorithm) is a market-making algorithm that rebalances the collateral of a loan using an AMM. The AMM contract creates opportunities for liquidating and de-liquidating collateral via arbitrage by offering better prices within the AMM compared to external markets. These collateral rebalances smoothly swap - dependant on asset prices - the collateral asset and the borrowed asset accordingly to keep the position sufficiently collateralized while recovering the collateral when the collateral price goes back up.
Unlike other liquidation mechanisms that have a single liquidation price at which the loan is liquidated once reached, **LLAMMA has a liquidation range in which collateral is continuously being rebalanced**. In short, when the price of the collateral asset goes down, the AMM starts converting `ETH` for `crvUSD`. If the price eventually recovers, the AMM converts the `crvUSD` back into `ETH`. More on the liquidation mechanism here: [Liquidation](#liquidation).
Each individual market has its own AMM containing the collateral and borrowable asset, such as the `ETH <> crvUSD` AMM consisting of `ETH` and `crvUSD`.
Before explaining the heart of the system, the liquidation process, in more detail, it is crucial to understand how the general structure of the AMM works. The AMM is similar to a **Uniswap V3-style AMM** using bands/ticks. Liquidity can be deposited into these bands, which have upper and lower price ranges. When opening a loan and adding collateral, this collateral is deposited into these bands. Users can choose the number of bands at loan creation, ranging from a **minimum of `4`** to a **maximum of `50`**. The liquidity sits idle in these bands and is only accessible for trading when the collateral price falls within the price range of a band.
---
## Bands
Bands in LLAMMA function similarly to Uniswap V3, concentrating liquidity between two prices. A band is a range of prices into which liquidity is deposited. LLAMMA consists of multiple bands forming a fixed price grid, and when creating a loan, liquidity is equally distributed across the number of bands (`N`) chosen when opening the loan. The minimum number is `4`, and the maximum is `50`. These bands where liquidity is deposited define the total liquidation range of the loan.[^1]
[^1]: For now, do not worry about how liquidation works. This mechanism will be explained in a section further down below. For now, just think of liquidation as changing the token composition that is backing your loan.
The graph shows how liquidity is distributed equally across the chosen number of bands (in this example, `N = 4`). While each single band has its own range, combining all bands with deposited liquidity forms the total liquidation range of the loan. In our example, the total liquidation range would be from `$1000` to `$600`. When the price of the collateral falls within this range, the collateral of the loan is being liquidated.
---
## Liquidation
The liquidation aspect of LLAMMA is very different from other lending protocols. LLAMMA has a liquidation range, whereas most other lending protocols have a single liquidation price, and their loan can be liquidated when that price is crossed.
:::tip[Liquidation only happens when prices are within the users liquidation range]
This liquidation mechanism only occurs if the price of the collateral is within the user's liquidation range. If not, there is no need to liquidate any assets backing the loan.
:::
In LLAMMA, your collateral is continuously being rebalanced if the price of the collateral asset is within the users liquidation range. Liquidated in this case does not refer to a "hard-liquidation" where your loan is immeadiatly closed, but rather a "partial liquidation" which rebalances the assets backing your loan. So, what does this exaclty mean? In short, if the price of the collateral goes down, the LLAMMA starts selling the collateral asset for the borrowed asset (in our example selling ETH for crvUSD). Now the loan is backed by ETH and crvUSD. This process is called [**soft-Liquidation**](#soft-liquidation). When the price of the collateral eventually goes up again, LLAMMA starts converting the previously bought crvUSD back into ETH. This is called [**de-Liquidation**](#de-liquidation).
Liquidation is not done on a user basis but rather on a band basis, making the liquidation mechanism scalable. Not individual users are soft- or de-liquidated, but rather bands in which multiple users have deposited liquidity.
*For simplicity purposes, let's ignore [hard-liquidation](#hard-liquidation) and how the AMM actually liquidates the collateral for now.*
---
:::info[`ETH <> crvUSD` market as Example]
*Let's look at the different states of liquidation, using a fictive example of a loan backed by 2 ETH (ETH price = $1000) and a total debt of 1750 crvUSD. The loan uses 4 bands. In the following examples, whenever stating that price goes down, its meant that the collateral asset price goes down. But e.g. if there is a market with a stable asset as collateral such as crvUSD, then soft-liquidation would happen when the borrowed asset goes up.*
:::
### Soft-Liquidation
Soft-liquidation is the process which sells the collateral asset (`ETH`) for the borrowable (`crvUSD`) asset because the asset price of the collateral is going down. This reduces the overall exposure to the volatile asset (ETH) of the loan. But keep in mind, as long as the price is above the liquidation range, there is no need to liquidate anything; all the bands are 100% in ETH.
Now let's assume the price of the collateral falls into the liquidation range within the first band. Now, the LLAMMA starts selling off ETH into crvUSD. The further down the price of the collateral goes, the more ETH will be sold for crvUSD. For example, if 0.5 ETH is deposited in this first band (because 2 ETH is evenly spread across 4 bands) and the price of the collateral is approximately in the middle of this band, then 0.25 ETH will be converted into crvUSD. If the price goes further down, say exactly to the bottom range of the first band (which at the same time is also the top range of the band below), then a total of 0.5 (the entire band) will be soft-liquidated into crvUSD. At this point, LLAMMA sold 0.5 ETH for, let's say, 490 crvUSD[^2]. If the price goes further down, it enters the second band and does exactly the same.
This can lead to the following: It is possible that the price of the collateral goes down through the entire liquidation range (essentially below the lower liquidation range of $800). At this point, the entire collateral will be soft-liquidated for crvUSD, and now the loan is fully backed by crvUSD. NOTE: This is only possible if the health of the loan is still above 0% (more on this in the hard-liquidation section).
[^2]: You might wonder why the LLAMMA did not convert 0.5 ETH into 500 crvUSD as the price of ETH is $1000. The explanation is that losses occur during this soft-liquidation.
*Summary: If the price goes down, LLAMMA starts selling the collateral asset for the borrow asset.*
### De-Liquidation
De-liquidation is essentially exactly the same as soft-liquidation, but the other way around when the price of the collateral asset rises again. De-liquidation means converting the crvUSD backing the loan obtained through earlier soft-liquidation back into the collateral token (ETH) again. Logically, for a de-liquidation to happen, the loan first needs to be soft-liquidated. Otherwise, there is no crvUSD to convert to ETH.
De-liquidation can happen until all the assets backing the loan are converted back into the original asset (ETH).
*Summary: If the price of the collateral rises, LLAMMA starts buying back the collateral for the borrow asset.*
### Hard-Liquidation
Hard-liquidation is the process where other users can repay the debt of a user and in exchange receive their collateral. A loan is only eligible for hard-liquidation when the health of the loan is below 0%. It is very important to understand that the liquidation range does not reflect prices where a loan is hard-liquidated. It really only depends on the health of the loan.
:::tip
Losses through soft- and de-liquidation only occur when the loan is in liquidation mode. If the price of the collateral is outside the liquidation range and there is no need to liquidate the position, no losses occur because the collaterals are not traded. On the other hand, losses accrued through interest rates happen regardless of whether the loan is being liquidated or not.
:::
*There are two factors that decrease the health of the loan:*
- **Interest rate**: A user borrowing assets needs to pay an interest rate. This constantly (per block) increases the debt of the position and therefore reduces its health.
- **More importantly: Losses when the position is soft- or de-liquidated**. When converting the collateral back and forth, losses occur. It is very hard to quantify the gravity of losses as it depends on various factors. Observations so far lead to the following conclusions:
- The more bands used for a loan, the fewer the losses through soft- and de-liquidation.
- The more liquidity, the fewer the losses.
- The more efficient the arbitrage, the fewer the losses.
To counter the losses from liquidation and the interest rate, there is a swap fee in the AMM. Arbitrage traders pay this fee when exchanging tokens within the AMM. The earned swap fees are given to the liquidity providers, who are the users that took out a loan and deposited collateral into bands in the AMM.
A loan's health can be read directly from the `Controller.vy` contract of the corresponding market using the [`health`](controller.md#health) method:
```vyper
>>> Controller.health('0xc92D575eB77C8AAe8e841bF5040346E34ad12d
37', true)
372983744062357570 # ≈37.3%
```
## Possible Scenarios of Band Compositions
*Now that we know how the liquidation mechanism of LLAMMA works, we can define three possible scenarios for bands regarding their asset composition:*
1. **Band contains both collateral and borrowable token:** Indicates continuous liquidation mode. The band in which the collateral price is currently located is defined as the [`active_band`](amm.md#active_band).
2. **Band contains only the collateral token:** This band has not been soft-liquidated. The collateral price is higher than the upper price of the band and is therefore outside the band. These are the bands above the [`active_band`](amm.md#active_band).
3. **Band contains only the borrowable token:** This band has already been soft-liquidated. The collateral price is below the band, and arbitrage trades have exchanged all the ETH for crvUSD in the band. These are the bands below the [`active_band`](amm.md#active_band).
*A full set of bands can look like the following:*
---
## How does LLAMMA liquidate the Collateral?
Liquidation happens on a band basis, not on an individual user basis. This means that if multiple users have liquidity deposited into the same band that is currently being soft- or de-liquidated, the liquidation happens for all users together and not just for a single user.
Soft- and de-liquidation is not automatically triggered by the smart contract. Instead, the AMM creates an arbitrage opportunity by utilizing the following two prices:
- **`price_oracle`**: The collateral price fetched from a price oracle contract.
- **`get_p`**: The oracle price of the AMM itself.
When the price oracle fetched from an external price source (using oracles of Curve liquidity pools), the AMM's "internal price `get_p`" is adjusted to be more sensitive, creating arbitrage opportunities. Arbitrage traders are incentivized to maintain `get_p = price_oracle` within the LLAMMA. When `price_oracle` equals `get_p`, the external oracle price and the AMM price are identical, indicating no need and possibility for arbitrage.
### Oracle Price Decrease (Soft-Liquidation)
When the `price_oracle` starts to fall, `get_p` falls faster than `price_oracle`. For example, if `get_p` is 830 and `price_oracle` is 850, arbitrage traders can buy ETH for 830 in the LLAMMA and sell it for 850 elsewhere. This process decreases ETH in the bands (because it’s bought) and increases crvUSD (because it’s being sold).
### Price Oracle Increase (De-Liquidation)
When the `price_oracle` rises, `get_p` increases faster than `price_oracle`. For example, if `get_p` is 860 and `price_oracle` is 850, arbitrage traders can buy ETH outside of the AMM for 850 and sell it in the AMM for 860. This results in ETH being deposited into the AMM and crvUSD being removed.
### How is it arbitraged?
The arbitrage opportunity lies within the difference between `get_p` and `price_oracle`. As long as `get_p ≠ price_oracle`, there is an arbitrage opportunity. The static exchange fee can be adjusted by governance using the `set_fee` function.
- **`get_p < price_oracle`**: The ETH price in the AMM is lower, making it favorable to buy ETH from the AMM and sell it elsewhere. This removes ETH from the bands and adds crvUSD, leading to soft-liquidation.
- **`get_p > price_oracle`**: The ETH price in the AMM is higher, making it favorable to buy ETH elsewhere and sell it to the AMM. This adds ETH to the AMM and removes crvUSD, leading to de-liquidation.
- **`get_p = price_oracle`**: There is no arbitrage opportunity.
Arbitrage traders should observe `get_p` and `price_oracle` inside the AMM. The `get_amount_for_price` function helps check how much of the assets need to be exchanged to reach a certain price. This function returns a `uint256` value representing the amount to sell/buy and a boolean indicating whether to pump or dump the collateral.
---
## Loan Parameters
### Maximum LTV
The loan-to-value (LTV) ratio depends on the number of bands (`N`) and the band width factor (`A`). The higher the number of bands, the lower the LTV. The maximum LTV can be approximated using the following function:
$$LTV = 100\% - \text{loan\_discount} - 100 \cdot \frac{N}{2 \cdot A}$$
The loan discount is the percentage used to discount the collateral for calculating the maximum borrowable amount when creating a loan.
*Two examples approximating the maximum LTV using 4 and 50 bands with a loan discount of 9% and an A value of 100:*
$\text{LTV (4 bands)} = 1 - 0.09 - 1 \times \frac{4}{2 \times 100} = 0.89 ≈ \text{89%}$
$\text{LTV (50 bands)} = 1 - 0.09 - 1 \times \frac{50}{2 \times 100} = 0.66 ≈ \text{66%}$
### Liquidation Range
The start of the liquidation range is also determined by the LTV:
$$\text{starting\_price} = \frac{debt}{collateral \cdot LTV}$$
To obtain the actual starting price value in dollars, multiply the value by the `price_oracle` at the time of creating the loan.
---
## Resources and Further Reading
For a basic understanding of how LLAMMAs work, consider the following articles:
- [Introduction to the Principles and Architecture of Curve Stablecoin](https://mirror.xyz/albertlin.eth/H0m3nyq65anotTWhTdWDIWEfMPOofNPy-0qyARYXNF4) by Albert Lin
- [crvUSD - Curve's StableCoin](https://mirror.xyz/0x290101596c9f85eB7194f6090a8c94fF5AAa32ca/esqF1zwoaZ4ZSIjt-faZZiuKwLLw34nD0SGlqD2fZ6Q) by GeekRunner
- [crvUSD: Just What the User Needs to Know](https://github.com/chanhosuh/curvefi-math/blob/master/LLAMMA.ipynb) by Chanho Suh
- [From Uniswap v3 to crvUSD LLAMMA](https://www.curve.wiki/post/from-uniswap-v3-to-crvusd-llamma-%E8%8B%B1%E6%96%87%E7%89%88)
For more articles and resources on LLAMMA, see [Useful Resources](../resources/useful.md#curve-stablecoin-crvusd).
---
## AggMonetaryPolicy (v4)
The `AggMonetaryPolicy4` contract is the latest version of the aggregated monetary policy for crvUSD markets. It builds on the original [`AggMonetaryPolicy`](monetary-policy.md) with several key improvements: **EMA-smoothed PegKeeper debt ratios** (reducing manipulation risk), **debt candles** (using the minimum debt over half-day periods for more stable rate calculations), and **per-market rate adjustments** (scaling rates based on how close a market is to its debt ceiling).
:::vyper[`AggMonetaryPolicy4.vy`]
The source code for the `AggMonetaryPolicy4.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/mpolicies/AggMonetaryPolicy4.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.4.3`.
The contract is deployed on :logos-ethereum: Ethereum at [`0x07491D124ddB3Ef59a8938fCB3EE50F9FA0b9251`](https://etherscan.io/address/0x07491D124ddB3Ef59a8938fCB3EE50F9FA0b9251).
```json
[{"anonymous":false,"inputs":[{"indexed":false,"name":"admin","type":"address"}],"name":"SetAdmin","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"peg_keeper","type":"address"}],"name":"AddPegKeeper","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"peg_keeper","type":"address"}],"name":"RemovePegKeeper","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"rate","type":"uint256"}],"name":"SetRate","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sigma","type":"int256"}],"name":"SetSigma","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"target_debt_fraction","type":"uint256"}],"name":"SetTargetDebtFraction","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"extra_const","type":"uint256"}],"name":"SetExtraConst","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"debt_ratio_ema_time","type":"uint256"}],"name":"SetDebtRatioEmaTime","type":"event"},{"inputs":[{"name":"admin","type":"address"},{"name":"price_oracle","type":"address"},{"name":"controller_factory","type":"address"},{"name":"peg_keepers","type":"address[5]"},{"name":"rate","type":"uint256"},{"name":"sigma","type":"int256"},{"name":"target_debt_fraction","type":"uint256"},{"name":"extra_const","type":"uint256"},{"name":"_debt_ratio_ema_time","type":"uint256"}],"outputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"name":"admin","type":"address"}],"name":"set_admin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"pk","type":"address"}],"name":"add_peg_keeper","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"pk","type":"address"}],"name":"remove_peg_keeper","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"rate","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_for","type":"address"}],"name":"rate","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"rate_write","outputs":[{"name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_for","type":"address"}],"name":"rate_write","outputs":[{"name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"rate","type":"uint256"}],"name":"set_rate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"sigma","type":"int256"}],"name":"set_sigma","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"target_debt_fraction","type":"uint256"}],"name":"set_target_debt_fraction","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"extra_const","type":"uint256"}],"name":"set_extra_const","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_debt_ratio_ema_time","type":"uint256"}],"name":"set_debt_ratio_ema_time","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"debt_ratio_ema_time","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"admin","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"rate0","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"sigma","outputs":[{"name":"","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"target_debt_fraction","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"extra_const","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint256"}],"name":"peg_keepers","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PRICE_ORACLE","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"CONTROLLER_FACTORY","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"n_controllers","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint256"}],"name":"controllers","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"}],"name":"min_debt_candles","outputs":[{"name":"candle0","type":"uint256"},{"name":"candle1","type":"uint256"},{"name":"timestamp","type":"uint256"}],"stateMutability":"view","type":"function"}]
```
:::
## Interest Rate Mechanics
This version of the monetary policy uses the same core formula as the original but adds several refinements for more robust and market-aware rate calculation.
### Base Rate Formula
The base interest rate is computed as:
$$r = rate0 \cdot e^{\text{power}} + extra\_const$$
:::info
`extra_const` is currently set to `0` and is not actively used. The effective formula simplifies to $r = rate0 \cdot e^{\text{power}}$.
:::
Where:
$$\text{power} = \frac{price\_peg - price\_crvusd}{\sigma} - \frac{EMA(DebtRatio)}{TargetFraction}$$
Key variables:
- $rate0$: the baseline rate when PegKeepers are debt-free and crvUSD price equals 1
- $price\_peg$: target crvUSD price (1.00)
- $price\_crvusd$: actual crvUSD price from `PRICE_ORACLE.price()`
- $\sigma$: sensitivity parameter controlling how much price deviations affect the rate (see [intuition behind sigma](overview.md#intuition-behind-sigma))
- $EMA(DebtRatio)$: exponentially smoothed PegKeeper debt ratio (see below)
- $TargetFraction$: target debt fraction for PegKeepers
- $extra\_const$: an additional constant added to the rate
### EMA-Smoothed Debt Ratio
Instead of using the raw PegKeeper debt fraction directly, this version smooths it with an **exponential moving average**. The EMA uses a queueing mechanism where the value from the *previous* update is used as input, and new values are queued for the *next* update. This reduces manipulation risk—a flash loan attacker would have to sustain their position beyond a single transaction to impact the EMA.
### Per-Market Rate Adjustment
After computing the base rate, it is further adjusted based on how close an individual market's debt is to its debt ceiling:
$$rate\_adjusted = rate \cdot \frac{(1 - target) + target \cdot \frac{1}{1 - f}}{1}$$
Where $f = \frac{debt\_for}{ceiling}$ and $target = 0.1$ (`TARGET_REMAINDER`). This means:
- At 0% utilization: rate stays the same ($1.0 \times rate$)
- At 90% of ceiling: rate doubles ($1.9 \times rate$)
- At 100% of ceiling: rate is capped at `MAX_RATE` (300% APY)
### Debt Candles
The contract tracks **minimum debt** over half-day periods using a candle-like structure. Instead of using the current (potentially manipulated) debt values, `calculate_rate` uses the minimum observed debt from recent candle periods, providing more stable rate calculations.
### Annualized Rate
Both `rate` and `rate0` are expressed per second with $10^{18}$ precision. The annualized rate is:
$$\text{annualRate} = (1 + \frac{rate}{10^{18}})^{365 \times 24 \times 60 \times 60} - 1$$
---
## Interest Rates
### `rate`
::::description[`AggMonetaryPolicy4.rate(_for: address = msg.sender) -> uint256: view`]
Getter for the current interest rate paid per second for a given controller. If no `_for` address is provided, it defaults to `msg.sender`. The rate is calculated using EMA-smoothed PegKeeper debt ratios and adjusted based on the controller's debt ceiling utilization.
| Input | Type | Description |
| ------ | --------- | ------------------------------------------------ |
| `_for` | `address` | Controller address to calculate rate for (optional, defaults to `msg.sender`) |
Returns: rate (`uint256`).
```vyper
@view
@external
def rate(_for: address = msg.sender) -> uint256:
rate: uint256 = 0
_: uint256 = 0
rate, _ = self.calculate_rate(_for, staticcall PRICE_ORACLE.price(), True)
return rate
@internal
@view
def calculate_rate(_for: address, _price: uint256, ro: bool) -> (uint256, uint256):
sigma: int256 = self.sigma
target_debt_fraction: uint256 = self.target_debt_fraction
p: int256 = convert(_price, int256)
pk_debt: uint256 = 0
for pk: PegKeeper in self.peg_keepers:
if pk.address == empty(address):
break
pk_debt += staticcall pk.debt()
total_debt: uint256 = 0
debt_for: uint256 = 0
total_debt, debt_for = self.read_debt(_for, ro)
power: int256 = (10**18 - p) * 10**18 // sigma # high price -> negative pow -> low rate
ratio: uint256 = 0
if pk_debt > 0:
if total_debt == 0:
return 0, 0
else:
ratio = pk_debt * 10**18 // total_debt
power -= convert(ema.read(DEBT_RATIO_EMA_ID) * 10**18 // target_debt_fraction, int256)
# Rate accounting for crvUSD price and PegKeeper debt
rate: uint256 = self.rate0 * min(self.exp(power), MAX_EXP) // 10**18 + self.extra_const
# Account for individual debt ceiling to dynamically tune rate depending on filling the market
ceiling: uint256 = staticcall CONTROLLER_FACTORY.debt_ceiling(_for)
if ceiling > 0:
f: uint256 = min(debt_for * 10**18 // ceiling, 10**18 - TARGET_REMAINDER // 1000)
rate = min(rate * ((10**18 - TARGET_REMAINDER) + TARGET_REMAINDER * 10**18 // (10**18 - f)) // 10**18, MAX_RATE)
return rate, ratio
```
::::
### `rate_write`
::::description[`AggMonetaryPolicy4.rate_write(_for: address = msg.sender) -> uint256`]
State-changing version of `rate` that also updates the controller cache, debt candles, and the EMA of the PegKeeper debt ratio. This function is called by controllers when they need the current rate and want to ensure state is updated.
| Input | Type | Description |
| ------ | --------- | ------------------------------------------------ |
| `_for` | `address` | Controller address to calculate rate for (optional, defaults to `msg.sender`) |
Returns: the current rate (`uint256`).
```vyper
@external
def rate_write(_for: address = msg.sender) -> uint256:
# Update controller list
n_controllers: uint256 = self.n_controllers
n_factory_controllers: uint256 = staticcall CONTROLLER_FACTORY.n_collaterals()
if n_factory_controllers > n_controllers:
self.n_controllers = n_factory_controllers
for i: uint256 in range(MAX_CONTROLLERS):
self.controllers[n_controllers] = staticcall CONTROLLER_FACTORY.controllers(n_controllers)
n_controllers += 1
if n_controllers >= n_factory_controllers:
break
# Update candles
total_debt: uint256 = 0
debt_for: uint256 = 0
total_debt, debt_for = self.get_total_debt(_for)
self.save_candle(empty(address), total_debt)
self.save_candle(_for, debt_for)
rate: uint256 = 0
ratio: uint256 = 0
rate, ratio = self.calculate_rate(_for, extcall PRICE_ORACLE.price_w(), False)
ema.update(DEBT_RATIO_EMA_ID, ratio)
return rate
```
```shell
>>> AggMonetaryPolicy4.rate_write()
3488503937
```
::::
### `rate0`
::::description[`AggMonetaryPolicy4.rate0() -> uint256: view`]
Getter for the `rate0` baseline rate. `rate0` must be less than or equal to `MAX_RATE` (300% APY = `43959106799`).
Returns: rate0 (`uint256`).
```vyper
MAX_RATE: constant(uint256) = 43959106799 # 300% APY
rate0: public(uint256)
@deploy
def __init__(admin: address,
price_oracle: PriceOracle,
controller_factory: ControllerFactory,
peg_keepers: PegKeeper[5],
rate: uint256,
sigma: int256,
target_debt_fraction: uint256,
extra_const: uint256,
_debt_ratio_ema_time: uint256):
...
assert rate <= MAX_RATE
self.rate0 = rate
...
```
::::
### `set_rate`
::::description[`AggMonetaryPolicy4.set_rate(rate: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the CurveOwnershipAgent.
:::
Function to set a new `rate0`. New value must be less than or equal to `MAX_RATE` (`43959106799`, i.e. 300% APY).
| Input | Type | Description |
| ------ | --------- | --------------- |
| `rate` | `uint256` | New rate0 value |
Emits: `SetRate` event.
```vyper
event SetRate:
rate: uint256
MAX_RATE: constant(uint256) = 43959106799 # 300% APY
rate0: public(uint256)
@external
def set_rate(rate: uint256):
assert msg.sender == self.admin
assert rate <= MAX_RATE
self.rate0 = rate
log SetRate(rate=rate)
```
```shell
>>> AggMonetaryPolicy4.set_rate(3488077118)
```
::::
## Rate Parameters
### `sigma`
::::description[`AggMonetaryPolicy4.sigma() -> int256: view`]
Getter for the sigma value, which controls how sensitive the interest rate is to crvUSD price deviations. Must satisfy: $10^{14} \leq \sigma \leq 10^{18}$.
Returns: sigma (`int256`).
```vyper
sigma: public(int256) # 2 * 10**16 for example
MAX_SIGMA: constant(int256) = 10**18
MIN_SIGMA: constant(int256) = 10**14
```
::::
### `set_sigma`
::::description[`AggMonetaryPolicy4.set_sigma(sigma: int256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the CurveOwnershipAgent.
:::
Function to set a new sigma value. New value must be between `MIN_SIGMA` ($10^{14}$) and `MAX_SIGMA` ($10^{18}$).
| Input | Type | Description |
| ------- | -------- | --------------- |
| `sigma` | `int256` | New sigma value |
Emits: `SetSigma` event.
```vyper
event SetSigma:
sigma: int256
sigma: public(int256)
MAX_SIGMA: constant(int256) = 10**18
MIN_SIGMA: constant(int256) = 10**14
@external
def set_sigma(sigma: int256):
assert msg.sender == self.admin
assert sigma >= MIN_SIGMA
assert sigma <= MAX_SIGMA
self.sigma = sigma
log SetSigma(sigma=sigma)
```
```shell
>>> AggMonetaryPolicy4.set_sigma(30000000000000000)
```
::::
### `target_debt_fraction`
::::description[`AggMonetaryPolicy4.target_debt_fraction() -> uint256: view`]
Getter for the target PegKeeper debt fraction. This is the desired ratio of PegKeeper debt to total debt.
Returns: target debt fraction (`uint256`).
```vyper
MAX_TARGET_DEBT_FRACTION: constant(uint256) = 10**18
target_debt_fraction: public(uint256)
```
::::
### `set_target_debt_fraction`
::::description[`AggMonetaryPolicy4.set_target_debt_fraction(target_debt_fraction: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the CurveOwnershipAgent.
:::
Function to set a new target debt fraction. Must be greater than 0 and less than or equal to `MAX_TARGET_DEBT_FRACTION` ($10^{18}$).
| Input | Type | Description |
| ---------------------- | --------- | ------------------------------ |
| `target_debt_fraction` | `uint256` | New target debt fraction value |
Emits: `SetTargetDebtFraction` event.
```vyper
event SetTargetDebtFraction:
target_debt_fraction: uint256
MAX_TARGET_DEBT_FRACTION: constant(uint256) = 10**18
target_debt_fraction: public(uint256)
@external
def set_target_debt_fraction(target_debt_fraction: uint256):
assert msg.sender == self.admin
assert target_debt_fraction <= MAX_TARGET_DEBT_FRACTION
assert target_debt_fraction > 0
self.target_debt_fraction = target_debt_fraction
log SetTargetDebtFraction(target_debt_fraction=target_debt_fraction)
```
```shell
>>> AggMonetaryPolicy4.set_target_debt_fraction(200000000000000000)
```
::::
### `extra_const`
::::description[`AggMonetaryPolicy4.extra_const() -> uint256: view`]
Getter for the `extra_const` value, an additional constant that is added to the computed rate. This allows setting a minimum floor rate independent of the exponential formula. Must be less than or equal to `MAX_EXTRA_CONST` (which equals `MAX_RATE`). Currently set to `0` and not actively used.
Returns: extra_const (`uint256`).
```vyper
MAX_EXTRA_CONST: constant(uint256) = MAX_RATE
extra_const: public(uint256)
```
::::
### `set_extra_const`
::::description[`AggMonetaryPolicy4.set_extra_const(extra_const: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the CurveOwnershipAgent.
:::
Function to set a new `extra_const` value. Must be less than or equal to `MAX_EXTRA_CONST`.
| Input | Type | Description |
| ------------- | --------- | -------------------- |
| `extra_const` | `uint256` | New extra_const value |
Emits: `SetExtraConst` event.
```vyper
event SetExtraConst:
extra_const: uint256
MAX_EXTRA_CONST: constant(uint256) = MAX_RATE
extra_const: public(uint256)
@external
def set_extra_const(extra_const: uint256):
assert msg.sender == self.admin
assert extra_const <= MAX_EXTRA_CONST
self.extra_const = extra_const
log SetExtraConst(extra_const=extra_const)
```
```shell
>>> AggMonetaryPolicy4.set_extra_const(1000000000)
```
::::
### `debt_ratio_ema_time`
::::description[`AggMonetaryPolicy4.debt_ratio_ema_time() -> uint256: view`]
Getter for the EMA smoothing time (in seconds) used for the PegKeeper debt ratio. A longer EMA time means the debt ratio changes more slowly, providing more stability but less responsiveness.
Returns: EMA time in seconds (`uint256`).
```vyper
DEBT_RATIO_EMA_ID: constant(String[4]) = "pkr"
@external
@view
def debt_ratio_ema_time() -> uint256:
return ema._emas[DEBT_RATIO_EMA_ID].ema_time
```
```vyper
struct EMA:
ema_time: uint256
prev_value: uint256
prev_timestamp: uint256
queued_value: uint256
_emas: HashMap[String[4], EMA]
```
::::
### `set_debt_ratio_ema_time`
::::description[`AggMonetaryPolicy4.set_debt_ratio_ema_time(_debt_ratio_ema_time: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the CurveOwnershipAgent.
:::
Function to set a new EMA smoothing time for the PegKeeper debt ratio. Before updating, it first computes the current EMA value to avoid discontinuities. The new value must be greater than 0.
| Input | Type | Description |
| ----------------------- | --------- | ------------------------------ |
| `_debt_ratio_ema_time` | `uint256` | New EMA time in seconds |
Emits: `SetDebtRatioEmaTime` event.
```vyper
event SetDebtRatioEmaTime:
debt_ratio_ema_time: uint256
@external
def set_debt_ratio_ema_time(_debt_ratio_ema_time: uint256):
assert msg.sender == self.admin
ema.set_ema_time(DEBT_RATIO_EMA_ID, _debt_ratio_ema_time)
log SetDebtRatioEmaTime(debt_ratio_ema_time=_debt_ratio_ema_time)
```
```vyper
@internal
def set_ema_time(_ema_id: String[4], _ema_time: uint256):
assert self._is_allowed(_ema_id)
self.update(_ema_id, self._emas[_ema_id].queued_value)
assert _ema_time > 0
ema: EMA = self._emas[_ema_id]
ema.ema_time = _ema_time
self._emas[_ema_id] = ema
```
```shell
>>> AggMonetaryPolicy4.set_debt_ratio_ema_time(86400)
```
::::
## PegKeepers
PegKeepers must be added to the MonetaryPolicy contract to calculate the rate, as it depends on the PegKeeper *DebtFraction*. They can be added by calling `add_peg_keeper` and removed via `remove_peg_keeper`.
### `peg_keepers`
::::description[`AggMonetaryPolicy4.peg_keepers(arg0: uint256) -> address: view`]
Getter for the PegKeeper contract at index `arg0`.
| Input | Type | Description |
| ------ | --------- | ------------------------ |
| `arg0` | `uint256` | Index of the PegKeeper |
Returns: PegKeeper contract (`address`).
```vyper
interface PegKeeper:
def debt() -> uint256: view
peg_keepers: public(PegKeeper[1001])
```
::::
### `add_peg_keeper`
::::description[`AggMonetaryPolicy4.add_peg_keeper(pk: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the CurveOwnershipAgent.
:::
Function to add an existing PegKeeper to the monetary policy contract.
| Input | Type | Description |
| ----- | --------- | ------------------------- |
| `pk` | `address` | PegKeeper address to add |
Emits: `AddPegKeeper` event.
```vyper
event AddPegKeeper:
peg_keeper: indexed(address)
peg_keepers: public(PegKeeper[1001])
@external
def add_peg_keeper(pk: PegKeeper):
assert msg.sender == self.admin
assert pk.address != empty(address)
for i: uint256 in range(1000):
_pk: PegKeeper = self.peg_keepers[i]
assert _pk != pk, "Already added"
if _pk.address == empty(address):
self.peg_keepers[i] = pk
log AddPegKeeper(peg_keeper=pk.address)
break
```
```shell
>>> AggMonetaryPolicy4.add_peg_keeper("0x1234567890abcdef1234567890abcdef12345678")
```
::::
### `remove_peg_keeper`
::::description[`AggMonetaryPolicy4.remove_peg_keeper(pk: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the CurveOwnershipAgent.
:::
Function to remove an existing PegKeeper from the monetary policy contract.
| Input | Type | Description |
| ----- | --------- | ---------------------------- |
| `pk` | `address` | PegKeeper address to remove |
Emits: `RemovePegKeeper` event.
```vyper
event RemovePegKeeper:
peg_keeper: indexed(address)
peg_keepers: public(PegKeeper[1001])
@external
def remove_peg_keeper(pk: PegKeeper):
assert msg.sender == self.admin
replaced_peg_keeper: uint256 = 10000
for i: uint256 in range(1001): # 1001th element is always 0x0
_pk: PegKeeper = self.peg_keepers[i]
if _pk == pk:
replaced_peg_keeper = i
log RemovePegKeeper(peg_keeper=pk.address)
if _pk.address == empty(address):
if replaced_peg_keeper < i:
if replaced_peg_keeper < i - 1:
self.peg_keepers[replaced_peg_keeper] = self.peg_keepers[i - 1]
self.peg_keepers[i - 1] = PegKeeper(empty(address))
break
```
```shell
>>> AggMonetaryPolicy4.remove_peg_keeper("0x1234567890abcdef1234567890abcdef12345678")
```
::::
## Admin Ownership
### `admin`
::::description[`AggMonetaryPolicy4.admin() -> address: view`]
Getter for the admin of the contract, which is the CurveOwnershipAgent.
Returns: admin (`address`).
```vyper
admin: public(address)
@deploy
def __init__(admin: address, ...):
self.admin = admin
...
```
::::
### `set_admin`
::::description[`AggMonetaryPolicy4.set_admin(admin: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the CurveOwnershipAgent.
:::
Function to set a new admin.
| Input | Type | Description |
| ------- | --------- | ----------------- |
| `admin` | `address` | New admin address |
Emits: `SetAdmin` event.
```vyper
event SetAdmin:
admin: address
admin: public(address)
@external
def set_admin(admin: address):
assert msg.sender == self.admin
self.admin = admin
log SetAdmin(admin=admin)
```
```shell
>>> AggMonetaryPolicy4.set_admin("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
```
::::
## Contract Info Methods
### `PRICE_ORACLE`
::::description[`AggMonetaryPolicy4.PRICE_ORACLE() -> address: view`]
Getter for the price oracle contract. Immutable variable set at deployment.
Returns: price oracle contract (`address`).
```vyper
PRICE_ORACLE: public(immutable(PriceOracle))
@deploy
def __init__(admin: address,
price_oracle: PriceOracle,
controller_factory: ControllerFactory, ...):
...
PRICE_ORACLE = price_oracle
...
```
::::
### `CONTROLLER_FACTORY`
::::description[`AggMonetaryPolicy4.CONTROLLER_FACTORY() -> address: view`]
Getter for the controller factory contract. Immutable variable set at deployment.
Returns: controller factory contract (`address`).
```vyper
CONTROLLER_FACTORY: public(immutable(ControllerFactory))
@deploy
def __init__(admin: address,
price_oracle: PriceOracle,
controller_factory: ControllerFactory, ...):
...
CONTROLLER_FACTORY = controller_factory
...
```
::::
### `n_controllers`
::::description[`AggMonetaryPolicy4.n_controllers() -> uint256: view`]
Getter for the number of controllers cached in this contract. The controller list is automatically updated when `rate_write` is called and the factory has added new controllers.
Returns: number of cached controllers (`uint256`).
```vyper
MAX_CONTROLLERS: constant(uint256) = 50000
n_controllers: public(uint256)
controllers: public(address[MAX_CONTROLLERS])
```
::::
### `controllers`
::::description[`AggMonetaryPolicy4.controllers(arg0: uint256) -> address: view`]
Getter for the cached controller address at index `arg0`.
| Input | Type | Description |
| ------ | --------- | ------------------------- |
| `arg0` | `uint256` | Index of the controller |
Returns: controller address (`address`).
```vyper
MAX_CONTROLLERS: constant(uint256) = 50000
controllers: public(address[MAX_CONTROLLERS])
```
::::
### `min_debt_candles`
::::description[`AggMonetaryPolicy4.min_debt_candles(arg0: address) -> (uint256, uint256, uint256): view`]
Getter for the debt candle data for a given controller address. Returns a struct with:
- `candle0`: the earlier half-day candle (minimum debt observed)
- `candle1`: the later half-day candle (minimum debt observed)
- `timestamp`: the last update timestamp
The zero address (`0x0000...`) stores the *total* debt candle across all controllers.
| Input | Type | Description |
| ------ | --------- | ---------------------------------- |
| `arg0` | `address` | Controller address (or zero for total) |
Returns: candle0 (`uint256`), candle1 (`uint256`), timestamp (`uint256`).
```vyper
struct DebtCandle:
candle0: uint256 # earlier 1/2 day candle
candle1: uint256 # later 1/2 day candle
timestamp: uint256
DEBT_CANDLE_TIME: constant(uint256) = 86400 // 2
min_debt_candles: public(HashMap[address, DebtCandle])
@internal
@view
def read_candle(_for: address) -> uint256:
out: uint256 = 0
candle: DebtCandle = self.min_debt_candles[_for]
if block.timestamp < candle.timestamp // DEBT_CANDLE_TIME * DEBT_CANDLE_TIME + DEBT_CANDLE_TIME:
if candle.candle0 > 0:
out = min(candle.candle0, candle.candle1)
else:
out = candle.candle1
elif block.timestamp < candle.timestamp // DEBT_CANDLE_TIME * DEBT_CANDLE_TIME + DEBT_CANDLE_TIME * 2:
out = candle.candle1
return out
@internal
def save_candle(_for: address, _value: uint256):
candle: DebtCandle = self.min_debt_candles[_for]
if candle.timestamp == 0 and _value == 0:
return
if block.timestamp >= candle.timestamp // DEBT_CANDLE_TIME * DEBT_CANDLE_TIME + DEBT_CANDLE_TIME:
if block.timestamp < candle.timestamp // DEBT_CANDLE_TIME * DEBT_CANDLE_TIME + DEBT_CANDLE_TIME * 2:
candle.candle0 = candle.candle1
candle.candle1 = _value
else:
candle.candle0 = _value
candle.candle1 = _value
else:
candle.candle1 = min(candle.candle1, _value)
candle.timestamp = block.timestamp
self.min_debt_candles[_for] = candle
```
::::
---
## Monetary Policy
MonetaryPolicy contracts are integrated into the crvUSD ecosystem, where they play a pivotal role in determining the interest rates for crvUSD markets.
:::vyper[`AggMonetaryPolicy.vy`]
The source code for the `AggMonetaryPolicy.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/mpolicies/AggMonetaryPolicy4.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.3.7`.
The contract is deployed on :logos-ethereum: Ethereum at [`0xc684432fd6322c6d58b6bc5d28b18569aa0ad0a1`](https://etherscan.io/address/0xc684432fd6322c6d58b6bc5d28b18569aa0ad0a1).
```json
[{"name": "SetAdmin", "inputs": [{"name": "admin", "type": "address", "indexed": false}], "anonymous": false, "type": "event"}, {"name": "AddPegKeeper", "inputs": [{"name": "peg_keeper", "type": "address", "indexed": true}], "anonymous": false, "type": "event"}, {"name": "RemovePegKeeper", "inputs": [{"name": "peg_keeper", "type": "address", "indexed": true}], "anonymous": false, "type": "event"}, {"name": "SetRate", "inputs": [{"name": "rate", "type": "uint256", "indexed": false}], "anonymous": false, "type": "event"}, {"name": "SetSigma", "inputs": [{"name": "sigma", "type": "uint256", "indexed": false}], "anonymous": false, "type": "event"}, {"name": "SetTargetDebtFraction", "inputs": [{"name": "target_debt_fraction", "type": "uint256", "indexed": false}], "anonymous": false, "type": "event"}, {"stateMutability": "nonpayable", "type": "constructor", "inputs": [{"name": "admin", "type": "address"}, {"name": "price_oracle", "type": "address"}, {"name": "controller_factory", "type": "address"}, {"name": "peg_keepers", "type": "address[5]"}, {"name": "rate", "type": "uint256"}, {"name": "sigma", "type": "uint256"}, {"name": "target_debt_fraction", "type": "uint256"}], "outputs": []}, {"stateMutability": "nonpayable", "type": "function", "name": "set_admin", "inputs": [{"name": "admin", "type": "address"}], "outputs": []}, {"stateMutability": "nonpayable", "type": "function", "name": "add_peg_keeper", "inputs": [{"name": "pk", "type": "address"}], "outputs": []}, {"stateMutability": "nonpayable", "type": "function", "name": "remove_peg_keeper", "inputs": [{"name": "pk", "type": "address"}], "outputs": []}, {"stateMutability": "view", "type": "function", "name": "rate", "inputs": [], "outputs": [{"name": "", "type": "uint256"}]}, {"stateMutability": "nonpayable", "type": "function", "name": "rate_write", "inputs": [], "outputs": [{"name": "", "type": "uint256"}]}, {"stateMutability": "nonpayable", "type": "function", "name": "set_rate", "inputs": [{"name": "rate", "type": "uint256"}], "outputs": []}, {"stateMutability": "nonpayable", "type": "function", "name": "set_sigma", "inputs": [{"name": "sigma", "type": "uint256"}], "outputs": []}, {"stateMutability": "nonpayable", "type": "function", "name": "set_target_debt_fraction", "inputs": [{"name": "target_debt_fraction", "type": "uint256"}], "outputs": []}, {"stateMutability": "view", "type": "function", "name": "admin", "inputs": [], "outputs": [{"name": "", "type": "address"}]}, {"stateMutability": "view", "type": "function", "name": "rate0", "inputs": [], "outputs": [{"name": "", "type": "uint256"}]}, {"stateMutability": "view", "type": "function", "name": "sigma", "inputs": [], "outputs": [{"name": "", "type": "int256"}]}, {"stateMutability": "view", "type": "function", "name": "target_debt_fraction", "inputs": [], "outputs": [{"name": "", "type": "uint256"}]}, {"stateMutability": "view", "type": "function", "name": "peg_keepers", "inputs": [{"name": "arg0", "type": "uint256"}], "outputs": [{"name": "", "type": "address"}]}, {"stateMutability": "view", "type": "function", "name": "PRICE_ORACLE", "inputs": [], "outputs": [{"name": "", "type": "address"}]}, {"stateMutability": "view", "type": "function", "name": "CONTROLLER_FACTORY", "inputs": [], "outputs": [{"name": "", "type": "address"}]}]
```
:::
## Interest Rate Mechanics
The interest rates in crvUSD markets are not static but fluctuate based on a set of factors, including:
- The price of crvUSD, which is determined through an aggregated oracle price from multiple Curve Stableswap pools ([details here](../oracles/price-aggregator.md)).
- The variables `sigma`, `rate0`, `TargetFraction`, and the `DebtFraction` specific to PegKeepers.
:::tip
A useful tool to explore and understand how the rate is affected by [0xreviews](https://twitter.com/0xreviews_xyz) is avaliable at: https://crvusd-rate.0xreviews.xyz/
:::
The formula for calculating the interest rate (`r`) is as follows:
$$r = rate0 \cdot e^{\text{power}}$$
Where:
$$\text{power} = \frac{price_{peg} - price_{crvusd}}{sigma} - \frac{DebtFraction}{TargetFraction}$$
And the DebtFraction is defined by:
$$DebtFraction = \frac{PegKeeperDebt}{TotalDebt}$$
Key variables in this calculation include:
- `r`: the interest rate
- `rate0`: the baseline rate, applicable when PegKeepers are debt-free and the crvUSD price equals 1
- `price_peg`: the target crvUSD price, set at 1.00
- `price_crvusd`: the actual crvUSD price, aggregated from `PRICE_ORACLE.price()`
- `DebtFraction`: the portion of PegKeeper debt relative to the total outstanding debt
- `TargetFraction`: the designated target fraction
- `PegKeeperDebt`: the cumulative debt of all PegKeepers
- `TotalDebt`: the aggregate crvUSD debt
For accuracy and consistency, both `rate` and `rate0` are expressed in terms of $10^{18}$ to denote precision and are calculated per second.
The annualized interest rate can be computed as:
$$\text{annualRate} = (1 + \frac{rate}{10^{18}})^{365 \times 24 \times 60 \times 60} - 1$$
---
### `rate`
::::description[`MonetaryPolicy.rate() -> uint256: view`]
Getter for the rate of the monetary policy contract. This is the current interest rate paid per second.
Returns: rate (`uint256`).
```vyper
@view
@external
def rate() -> uint256:
return self.calculate_rate()
@internal
@view
def calculate_rate() -> uint256:
sigma: int256 = self.sigma
target_debt_fraction: uint256 = self.target_debt_fraction
p: int256 = convert(PRICE_ORACLE.price(), int256)
pk_debt: uint256 = 0
for pk in self.peg_keepers:
if pk.address == empty(address):
break
pk_debt += pk.debt()
power: int256 = (10**18 - p) * 10**18 / sigma # high price -> negative pow -> low rate
if pk_debt > 0:
total_debt: uint256 = CONTROLLER_FACTORY.total_debt()
if total_debt == 0:
return 0
else:
power -= convert(pk_debt * 10**18 / total_debt * 10**18 / target_debt_fraction, int256)
return self.rate0 * min(self.exp(power), MAX_EXP) / 10**18
```
::::
### `rate0`
::::description[`MonetaryPolicy.rate0() -> uint256: view`]
Getter for the `rate0` of the monetary policy contract. `rate0` has to be less than or equal to `MAX_RATE` (400% APY).
Returns: rate0 (`uint256`).
```vyper
MAX_RATE: constant(uint256) = 43959106799 # 400% APY
rate0: public(uint256)
@external
def __init__(admin: address,
price_oracle: PriceOracle,
controller_factory: ControllerFactory,
peg_keepers: PegKeeper[5],
rate: uint256,
sigma: uint256,
target_debt_fraction: uint256):
...
assert rate <= MAX_RATE
self.rate0 = rate
...
```
::::
### `set_rate`
::::description[`MonetaryPolicy.set_rate(rate: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the CurveOwnershipAgent.
:::
Function to set a new rate0. New `rate0` has to be less than or equal to `MAX_RATE (=43959106799)`.
| Input | Type | Description |
| ----------- | -------| ----|
| `rate` | `uint256` | New rate0 value |
Emits: `SetRate` event.
```vyper
event SetRate:
rate: uint256
MAX_RATE: constant(uint256) = 43959106799 # 400% APY
rate0: public(uint256)
@external
def set_rate(rate: uint256):
assert msg.sender == self.admin
assert rate <= MAX_RATE
self.rate0 = rate
log SetRate(rate)
```
```shell
>>> MonetaryPolicy.set_rate(3488077118)
```
::::
### `sigma`
::::description[`MonetaryPolicy.sigma() -> int256: view`]
Getter for the sigma value. The following needs to hold: $10^{14} \leq \sigma \leq 10^{18}$.
Returns: sigma (`int256`).
```vyper
sigma: public(int256) # 2 * 10**16 for example
MAX_SIGMA: constant(uint256) = 10**18
MIN_SIGMA: constant(uint256) = 10**14
@external
def __init__(admin: address,
price_oracle: PriceOracle,
controller_factory: ControllerFactory,
peg_keepers: PegKeeper[5],
rate: uint256,
sigma: uint256,
target_debt_fraction: uint256):
...
assert sigma >= MIN_SIGMA
assert sigma <= MAX_SIGMA
...
```
::::
### `set_sigma`
::::description[`MonetaryPolicy.set_sigma(sigma: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the CurveOwnershipAgent.
:::
Function to set a new sigma value. New value must be inbetween `MIN_SIGMA` and `MAX_SIGMA`.
| Input | Type | Description |
| ----------- | -------| ----|
| `sigma` | `uint256` | New sigma value |
Emits: `SetSigma` event.
```vyper
event SetSigma:
sigma: uint256
sigma: public(int256) # 2 * 10**16 for example
MAX_SIGMA: constant(uint256) = 10**18
MIN_SIGMA: constant(uint256) = 10**14
@external
def set_sigma(sigma: uint256):
assert msg.sender == self.admin
assert sigma >= MIN_SIGMA
assert sigma <= MAX_SIGMA
self.sigma = convert(sigma, int256)
log SetSigma(sigma)
```
```shell
>>> MonetaryPolicy.set_sigma(30000000000000000)
```
::::
### `target_debt_fraction`
::::description[`MonetaryPolicy.target_debt_fraction() -> uint256: view`]
Getter for the debt fraction target.
Returns: target debt fraction (`uint256`).
```vyper
MAX_TARGET_DEBT_FRACTION: constant(uint256) = 10**18
target_debt_fraction: public(uint256)
@external
def __init__(admin: address,
price_oracle: PriceOracle,
controller_factory: ControllerFactory,
peg_keepers: PegKeeper[5],
rate: uint256,
sigma: uint256,
target_debt_fraction: uint256):
...
self.target_debt_fraction = target_debt_fraction
```
::::
### `set_target_debt_fraction`
::::description[`MonetaryPolicy.set_target_debt_fraction(target_debt_fraction: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the CurveOwnershipAgent.
:::
Function to set a new value for the debt fraction target. New value needs to be less than or equal to `MAX_TARGET_DEBT_FRACTION`.
| Input | Type | Description |
| ----------- | -------| ----|
| `target_debt_fraction` | `uint256` | New debt fraction target value |
Emits: `SetTargetDebtFraction` event.
```vyper
event SetTargetDebtFraction:
target_debt_fraction: uint256
MAX_TARGET_DEBT_FRACTION: constant(uint256) = 10**18
target_debt_fraction: public(uint256)
@external
def set_target_debt_fraction(target_debt_fraction: uint256):
assert msg.sender == self.admin
assert target_debt_fraction <= MAX_TARGET_DEBT_FRACTION
self.target_debt_fraction = target_debt_fraction
log SetTargetDebtFraction(target_debt_fraction)
```
```shell
>>> MonetaryPolicy.set_target_debt_fraction(200000000000000000)
```
::::
## PegKeepers
PegKeepers must be added to the MonetaryPolicy contract to calculate the rate as it depends on the *DebtFraction*. They can be added by calling `add_peg_keeper` and removed via `remove_peg_keeper`.
### `peg_keepers`
::::description[`MonetaryPolicy.peg_keepers(arg0: uint256) -> address: view`]
Getter for the PegKeeper contract at index `arg0`.
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `uint256` | Index of the PegKeeper |
Returns: PegKeeper contracts (`address`).
```vyper
interface PegKeeper:
def debt() -> uint256: view
peg_keepers: public(PegKeeper[1001])
@external
def __init__(admin: address,
price_oracle: PriceOracle,
controller_factory: ControllerFactory,
peg_keepers: PegKeeper[5],
rate: uint256,
sigma: uint256,
target_debt_fraction: uint256):
...
for i in range(5):
if peg_keepers[i].address == empty(address):
break
self.peg_keepers[i] = peg_keepers[i]
...
```
::::
### `add_peg_keeper`
::::description[`MonetaryPolicy.add_peg_keeper(pk: PegKeeper)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to add an existing PegKeeper to the monetary policy contract.
| Input | Type | Description |
| ----------- | -------| ----|
| `pk` | `PegKeeper` | PegKeeper address to add |
Emits: `AddPegKeeper` event.
```vyper
event AddPegKeeper:
peg_keeper: indexed(address)
peg_keepers: public(PegKeeper[1001])
@external
def add_peg_keeper(pk: PegKeeper):
assert msg.sender == self.admin
assert pk.address != empty(address)
for i in range(1000):
_pk: PegKeeper = self.peg_keepers[i]
assert _pk != pk, "Already added"
if _pk.address == empty(address):
self.peg_keepers[i] = pk
log AddPegKeeper(pk.address)
break
```
```shell
>>> MonetaryPolicy.add_peg_keeper("PegKeeper address")
```
::::
### `remove_peg_keeper`
::::description[`MonetaryPolicy.remove_peg_keeper(pk: PegKeeper)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to remove an existing PegKeeper from the monetary policy contract.
| Input | Type | Description |
| ----------- | -------| ----|
| `pk` | `PegKeeper` | PegKeeper address to remove |
Emits: `RemovePegKeeper` event.
```vyper
event RemovePegKeeper:
peg_keeper: indexed(address)
peg_keepers: public(PegKeeper[1001])
@external
def remove_peg_keeper(pk: PegKeeper):
assert msg.sender == self.admin
replaced_peg_keeper: uint256 = 10000
for i in range(1001): # 1001th element is always 0x0
_pk: PegKeeper = self.peg_keepers[i]
if _pk == pk:
replaced_peg_keeper = i
log RemovePegKeeper(pk.address)
if _pk.address == empty(address):
if replaced_peg_keeper < i:
if replaced_peg_keeper < i - 1:
self.peg_keepers[replaced_peg_keeper] = self.peg_keepers[i - 1]
self.peg_keepers[i - 1] = PegKeeper(empty(address))
break
```
```shell
>>> MonetaryPolicy.remove_peg_keeper("PegKeeper address")
```
::::
## Admin Ownership
### `admin`
::::description[`MonetaryPolicy.admin() -> address: view`]
Getter for the admin of the contract, which is the CurveOwnershipAgent.
Returns: admin (`address`).
```vyper
admin: public(address)
@external
def __init__(admin: address,
price_oracle: PriceOracle,
controller_factory: ControllerFactory,
peg_keepers: PegKeeper[5],
rate: uint256,
sigma: uint256,
target_debt_fraction: uint256):
self.admin = admin
...
```
::::
### `set_admin`
::::description[`MonetaryPolicy.set_admin(admin: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract, which is the CurveOwnershipAgent.
:::
Function to set a new admin.
| Input | Type | Description |
| ----------- | -------| ----|
| `admin` | `address` | New admin address |
Emits: `SetAdmin` event.
```vyper
event SetAdmin:
admin: address
admin: public(address)
@external
def set_admin(admin: address):
assert msg.sender == self.admin
self.admin = admin
log SetAdmin(admin)
```
```shell
>>> MonetaryPolicy.set_admin("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
```
::::
## Contract Info Methods
### `PRICE_ORACLE`
::::description[`MonetaryPolicy.PRICE_ORACLE() -> address: view`]
Getter for the price oracle contract.
Returns: price oracle contract (`address`).
```vyper
PRICE_ORACLE: public(immutable(PriceOracle))
@external
def __init__(admin: address,
price_oracle: PriceOracle,
controller_factory: ControllerFactory,
peg_keepers: PegKeeper[5],
rate: uint256,
sigma: uint256,
target_debt_fraction: uint256):
...
PRICE_ORACLE = price_oracle
...
```
::::
### `CONTROLLER_FACTORY`
::::description[`MonetaryPolicy.CONTROLLER_FACTORY() -> address: view`]
Getter for the controller factory contract. immutable variable!
Returns: controller factory contract (`address`).
```vyper
CONTROLLER_FACTORY: public(immutable(ControllerFactory))
@external
def __init__(admin: address,
price_oracle: PriceOracle,
controller_factory: ControllerFactory,
peg_keepers: PegKeeper[5],
rate: uint256,
sigma: uint256,
target_debt_fraction: uint256):
...
CONTROLLER_FACTORY = controller_factory
...
```
::::
### `rate_write`
::::description[`MonetaryPolicy.rate_write() -> uint256`]
When adding a new market via the factory contract, `rate_write` is called to check if the MonetaryPolicy contract has the correct ABI.
Returns: the current rate (`uint256`).
```vyper
@external
def rate_write() -> uint256:
# Not needed here but useful for more automated policies
# which change rate0 - for example rate0 targeting some fraction pl_debt/total_debt
return self.calculate_rate()
@internal
@view
def calculate_rate() -> uint256:
sigma: int256 = self.sigma
target_debt_fraction: uint256 = self.target_debt_fraction
p: int256 = convert(PRICE_ORACLE.price(), int256)
pk_debt: uint256 = 0
for pk in self.peg_keepers:
if pk.address == empty(address):
break
pk_debt += pk.debt()
power: int256 = (10**18 - p) * 10**18 / sigma # high price -> negative pow -> low rate
if pk_debt > 0:
total_debt: uint256 = CONTROLLER_FACTORY.total_debt()
if total_debt == 0:
return 0
else:
power -= convert(pk_debt * 10**18 / total_debt * 10**18 / target_debt_fraction, int256)
return self.rate0 * min(self.exp(power), MAX_EXP) / 10**18
```
```shell
>>> MonetaryPolicy.rate_write()
3488503937
```
::::
---
## Monetary Policy Overview
Monetary policy contracts determine the **borrow interest rate** for each crvUSD mint market. They are called by the Controller on every user interaction (`rate_write`) and can be queried for the current rate at any time (`rate`).
The current production monetary policy is the [**AggMonetaryPolicy (v4)**](agg-monetary-policy-v4.md), deployed at [`0x0749...251`](https://etherscan.io/address/0x07491D124ddB3Ef59a8938fCB3EE50F9FA0b9251). It is used by all active crvUSD mint markets. The original [AggMonetaryPolicy](monetary-policy.md) ([`0xc684...0a1`](https://etherscan.io/address/0xc684432fd6322c6d58b6bc5d28b18569aa0ad0a1)) is only used by one deprecated market.
## Rate Formula
Both versions share the same core formula:
$$r = rate0 \cdot e^{\text{power}} \quad \text{where} \quad \text{power} = \frac{1 - price\_crvusd}{\sigma} - \frac{DebtFraction}{TargetFraction}$$
The v4 contract uses an EMA of the debt ratio instead of the raw value and adjusts rates per-market based on debt ceiling utilization. See the [AggMonetaryPolicy (v4)](agg-monetary-policy-v4.md) page for full details.
### Intuition Behind Sigma
While sigma is often described as a "sensitivity parameter," it has a concrete financial interpretation. Consider the price deviation component of the formula in isolation (ignoring the debt ratio term):
$$r \approx rate0 \cdot e^{d/\sigma}$$
where $d = 1 - price\_crvusd$ is the depeg. This exponential can be approximated as:
$$e^{d/\sigma} \approx \left(\frac{1}{1 - d}\right)^{1/\sigma}$$
When this rate is applied over a period of length $T = \sigma$, the total interest cost equals the depeg itself. In other words, **sigma represents the time period over which a borrower effectively pays for the current depeg through elevated interest rates**.
This gives a practical decision rule: if a borrower expects crvUSD to remain depegged and can borrow elsewhere at `rate0`, they should compare their expected loan duration to sigma. If they plan to borrow for **longer** than sigma, it may be cheaper to close the loan now and reborrow elsewhere. If **shorter**, sitting through the elevated rate could be acceptable.
A smaller sigma means the rate spikes more sharply during a depeg (borrowers pay for it quickly), while a larger sigma spreads the cost over a longer period with a gentler rate increase.
## Market → Monetary Policy Mapping
The table below is fetched live from on-chain data. Each Controller's `monetary_policy()` is queried to show which policy version is currently active for each market.
---
## CryptoFromPoolVaultWAgg
This oracle contract derives the price of **ERC-4626 vault token collateral** by reading a price oracle from a Curve liquidity pool and applying the vault's redemption rate (`convertToAssets`). It then multiplies by the [aggregated crvUSD price](./price-aggregator.md) to produce a USD-denominated oracle price, making it suitable for **crvUSD mint markets**.
:::vyper[`CryptoFromPoolVaultWAgg.vy`]
The source code for the `CryptoFromPoolVaultWAgg.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/price_oracles/CryptoFromPoolVaultWAgg.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.3.10`.
Each crvUSD market using this oracle has its own deployment. The oracle address can be fetched by calling `price_oracle_contract` on the market's Controller.
```json
[{"stateMutability":"view","type":"function","name":"price","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"price_w","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"POOL","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"BORROWED_IX","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"COLLATERAL_IX","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"N_COINS","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"NO_ARGUMENT","inputs":[],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"VAULT","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"AGG","inputs":[],"outputs":[{"name":"","type":"address"}]}]
```
:::
:::warning[Oracle Suitability]
`CryptoFromPoolVaultWAgg.vy` is **only suitable for vaults which cannot be affected by [donation attacks](https://mixbytes.io/blog/overview-of-the-inflation-attack)** (e.g., sFRAX). Vaults vulnerable to donation attacks should use a different oracle type.
:::
:::danger[Oracle Immutability]
The oracle contract is **fully immutable**. Once deployed, it cannot change any parameters, stop price updates, or alter the pool used to calculate prices. All relevant data is passed into the `__init__` function during deployment.
```vyper
@external
def __init__(
pool: Pool,
N: uint256,
borrowed_ix: uint256,
collateral_ix: uint256,
vault: Vault,
agg: StableAggregator
):
assert borrowed_ix != collateral_ix
assert borrowed_ix < N
assert collateral_ix < N
POOL = pool
N_COINS = N
BORROWED_IX = borrowed_ix
COLLATERAL_IX = collateral_ix
VAULT = vault
AGG = agg
no_argument: bool = False
if N == 2:
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pool.address,
_abi_encode(empty(uint256), method_id=method_id("price_oracle(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
no_argument = True
NO_ARGUMENT = no_argument
```
:::
:::tip[Lending Variant]
A base variant without the aggregated crvUSD price ([`CryptoFromPoolVault.vy`](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/price_oracles/CryptoFromPoolVault.vy)) is also available. Both variants are documented together on the [lending oracle page](../../lending/contracts/crypto-from-pool-vault.md), which includes additional detail on rate limiting and side-by-side code comparisons.
:::
---
## How the Price is Calculated
The oracle computes the collateral price in three steps:
1. **Pool price oracle** — reads the internal price oracle from the Curve pool, extracting the relative price between the collateral and borrowed assets
2. **Vault redemption** — multiplies by `VAULT.convertToAssets(10**18)` to convert from vault shares to underlying
3. **USD normalization** — multiplies by the aggregated crvUSD/USD price from `AGG`
$$\text{price} = \frac{p_{\text{collateral}}}{p_{\text{borrowed}}} \times \frac{\text{convertToAssets}(10^{18})}{10^{18}} \times \frac{\text{AGG.price}()}{10^{18}}$$
The internal `_raw_price()` function handles steps 1-2, while `price()` and `price_w()` apply step 3.
```vyper
@internal
@view
def _raw_price() -> uint256:
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT:
p: uint256 = POOL.price_oracle()
if COLLATERAL_IX > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX > 0:
p_borrowed = POOL.price_oracle(BORROWED_IX - 1)
if COLLATERAL_IX > 0:
p_collateral = POOL.price_oracle(COLLATERAL_IX - 1)
return p_collateral * VAULT.convertToAssets(10**18) / p_borrowed
```
---
## Price Functions
### `price`
::::description[`CryptoFromPoolVaultWAgg.price() -> uint256: view`]
Returns the current USD price of the collateral token. Reads the pool's price oracle, applies the vault redemption rate, and multiplies by the aggregated crvUSD price.
Returns: collateral price in USD (`uint256`).
```vyper
@external
@view
def price() -> uint256:
return self._raw_price() * AGG.price() / 10**18
@internal
@view
def _raw_price() -> uint256:
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT:
p: uint256 = POOL.price_oracle()
if COLLATERAL_IX > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX > 0:
p_borrowed = POOL.price_oracle(BORROWED_IX - 1)
if COLLATERAL_IX > 0:
p_collateral = POOL.price_oracle(COLLATERAL_IX - 1)
return p_collateral * VAULT.convertToAssets(10**18) / p_borrowed
```
```shell
>>> CryptoFromPoolVaultWAgg.price()
1020187011799809503
```
::::
### `price_w`
::::description[`CryptoFromPoolVaultWAgg.price_w() -> uint256`]
Write version of `price()`. Calls `AGG.price_w()` instead of `AGG.price()`, which updates the aggregator's internal state (EMA of crvUSD prices). This function is called by the AMM during regular operations to keep the aggregator up to date.
Returns: collateral price in USD (`uint256`).
```vyper
@external
def price_w() -> uint256:
return self._raw_price() * AGG.price_w() / 10**18
@internal
@view
def _raw_price() -> uint256:
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT:
p: uint256 = POOL.price_oracle()
if COLLATERAL_IX > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX > 0:
p_borrowed = POOL.price_oracle(BORROWED_IX - 1)
if COLLATERAL_IX > 0:
p_collateral = POOL.price_oracle(COLLATERAL_IX - 1)
return p_collateral * VAULT.convertToAssets(10**18) / p_borrowed
```
```shell
>>> CryptoFromPoolVaultWAgg.price_w()
1020187011799809503
```
::::
---
## Contract Info Methods
### `POOL`
::::description[`CryptoFromPoolVaultWAgg.POOL() -> address: view`]
Getter for the Curve pool used as the price source.
Returns: pool address (`address`).
```vyper
POOL: public(immutable(Pool))
```
```shell
>>> CryptoFromPoolVaultWAgg.POOL()
'0x...'
```
::::
### `BORROWED_IX`
::::description[`CryptoFromPoolVaultWAgg.BORROWED_IX() -> uint256: view`]
Getter for the index of the borrowed asset in the pool.
Returns: coin index (`uint256`).
```vyper
BORROWED_IX: public(immutable(uint256))
```
```shell
>>> CryptoFromPoolVaultWAgg.BORROWED_IX()
0
```
::::
### `COLLATERAL_IX`
::::description[`CryptoFromPoolVaultWAgg.COLLATERAL_IX() -> uint256: view`]
Getter for the index of the collateral asset in the pool.
Returns: coin index (`uint256`).
```vyper
COLLATERAL_IX: public(immutable(uint256))
```
```shell
>>> CryptoFromPoolVaultWAgg.COLLATERAL_IX()
1
```
::::
### `N_COINS`
::::description[`CryptoFromPoolVaultWAgg.N_COINS() -> uint256: view`]
Getter for the number of coins in the pool.
Returns: number of coins (`uint256`).
```vyper
N_COINS: public(immutable(uint256))
```
```shell
>>> CryptoFromPoolVaultWAgg.N_COINS()
2
```
::::
### `NO_ARGUMENT`
::::description[`CryptoFromPoolVaultWAgg.NO_ARGUMENT() -> bool: view`]
Getter that indicates whether the pool's `price_oracle()` function is called without arguments (true for 2-coin pools where the no-argument variant is available) or with a coin index argument.
Returns: true if no argument needed (`bool`).
```vyper
NO_ARGUMENT: public(immutable(bool))
```
```shell
>>> CryptoFromPoolVaultWAgg.NO_ARGUMENT()
False
```
::::
### `VAULT`
::::description[`CryptoFromPoolVaultWAgg.VAULT() -> address: view`]
Getter for the ERC-4626 vault contract whose redemption rate is applied to the price.
Returns: vault address (`address`).
```vyper
VAULT: public(immutable(Vault))
```
```shell
>>> CryptoFromPoolVaultWAgg.VAULT()
'0x...'
```
::::
### `AGG`
::::description[`CryptoFromPoolVaultWAgg.AGG() -> address: view`]
Getter for the crvUSD price aggregator contract ([`PriceAggregator`](./price-aggregator.md)).
Returns: aggregator address (`address`).
```vyper
AGG: public(immutable(StableAggregator))
```
```shell
>>> CryptoFromPoolVaultWAgg.AGG()
'0x...'
```
::::
---
## CryptoFromPoolsRateWAgg
This oracle contract **chains together price oracles from multiple Curve liquidity pools** and optionally applies `stored_rates` to account for tokens with rate oracles (e.g., interest-bearing tokens). It multiplies the result by the [aggregated crvUSD price](./price-aggregator.md) to produce a USD-denominated oracle price, making it suitable for **crvUSD mint markets**.
:::vyper[`CryptoFromPoolsRateWAgg.vy`]
The source code for the `CryptoFromPoolsRateWAgg.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/price_oracles/CryptoFromPoolsRateWAgg.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.3.10`.
Each crvUSD market using this oracle has its own deployment. The oracle address can be fetched by calling `price_oracle_contract` on the market's Controller.
```json
[{"stateMutability":"view","type":"function","name":"price","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"price_w","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"stored_rate","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"POOLS","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"POOL_COUNT","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"BORROWED_IX","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"COLLATERAL_IX","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"NO_ARGUMENT","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"USE_RATES","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"AGG","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"cached_rate","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"cached_timestamp","inputs":[],"outputs":[{"name":"","type":"uint256"}]}]
```
:::
:::danger[Oracle Immutability]
The oracle contract is **fully immutable**. Once deployed, it cannot change any parameters, stop price updates, or alter the pools used to calculate prices. All relevant data is passed into the `__init__` function during deployment.
```vyper
@external
def __init__(
pools: DynArray[Pool, MAX_POOLS],
borrowed_ixs: DynArray[uint256, MAX_POOLS],
collateral_ixs: DynArray[uint256, MAX_POOLS],
agg: StableAggregator
):
POOLS = pools
pool_count: uint256 = 0
no_arguments: DynArray[bool, MAX_POOLS] = empty(DynArray[bool, MAX_POOLS])
use_rates: DynArray[bool, MAX_POOLS] = empty(DynArray[bool, MAX_POOLS])
AGG = agg
for i in range(MAX_POOLS):
if i == len(pools):
assert i != 0, "Wrong pool counts"
pool_count = i
break
# Find N
N: uint256 = 0
for j in range(MAX_COINS + 1):
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pools[i].address,
_abi_encode(j, method_id=method_id("coins(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
assert j != 0, "No coins(0)"
N = j
break
assert borrowed_ixs[i] != collateral_ixs[i]
assert borrowed_ixs[i] < N
assert collateral_ixs[i] < N
# Init variables for raw call
success: bool = False
# Check and record if pool requires coin id in argument or no
if N == 2:
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pools[i].address,
_abi_encode(empty(uint256), method_id=method_id("price_oracle(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
no_arguments.append(True)
else:
no_arguments.append(False)
else:
no_arguments.append(False)
res: Bytes[1024] = empty(Bytes[1024])
success, res = raw_call(pools[i].address, method_id("stored_rates()"), max_outsize=1024, is_static_call=True, revert_on_failure=False)
stored_rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
if success and len(res) > 0:
stored_rates = _abi_decode(res, DynArray[uint256, MAX_COINS])
u: bool = False
for r in stored_rates:
if r != 10**18:
u = True
use_rates.append(u)
NO_ARGUMENT = no_arguments
BORROWED_IX = borrowed_ixs
COLLATERAL_IX = collateral_ixs
if pool_count == 0:
pool_count = MAX_POOLS
POOL_COUNT = pool_count
USE_RATES = use_rates
```
:::
:::tip[Lending Variant]
A base variant without the aggregated crvUSD price ([`CryptoFromPoolsRate.vy`](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/price_oracles/CryptoFromPoolsRate.vy)) is also available. Both variants are documented together on the [lending oracle page](../../lending/contracts/crypto-from-pools-rate.md), which includes side-by-side code comparisons.
:::
---
## How the Price is Calculated
The oracle computes the collateral price by chaining price oracles across multiple pools:
1. **Unscaled price** — iterates through all configured pools, reading each pool's `price_oracle()` and computing `p_collateral / p_borrowed` for that pool, then multiplying all ratios together
2. **Rate adjustment** — if any pool uses `stored_rates` (for interest-bearing tokens), the ratio `rates[COLLATERAL_IX] / rates[BORROWED_IX]` is applied, subject to rate limiting
3. **USD normalization** — multiplies by the aggregated crvUSD/USD price from `AGG`
$$\text{price} = \frac{\text{unscaled\_price} \times \text{stored\_rate} \times \text{AGG.price}()}{10^{36}}$$
Where:
$$\text{unscaled\_price} = \prod_{i=0}^{N-1} \frac{p_{\text{collateral},i}}{p_{\text{borrowed},i}}$$
$$\text{stored\_rate} = \prod_{i \in \text{USE\_RATES}} \frac{\text{rates}_i[\text{COLLATERAL\_IX}_i]}{\text{rates}_i[\text{BORROWED\_IX}_i]}$$
---
## Price Functions
### `price`
::::description[`CryptoFromPoolsRateWAgg.price() -> uint256: view`]
Returns the current USD price of the collateral token. Chains price oracles across all configured pools, applies rate adjustments if applicable, and multiplies by the aggregated crvUSD price.
Returns: collateral price in USD (`uint256`).
```vyper
@external
@view
def price() -> uint256:
return self._unscaled_price() * self._stored_rate()[0] / 10**18 * AGG.price() / 10**18
@internal
@view
def _unscaled_price() -> uint256:
_price: uint256 = 10**18
for i in range(MAX_POOLS):
if i >= POOL_COUNT:
break
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT[i]:
p: uint256 = POOLS[i].price_oracle()
if COLLATERAL_IX[i] > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX[i] > 0:
p_borrowed = POOLS[i].price_oracle(unsafe_sub(BORROWED_IX[i], 1))
if COLLATERAL_IX[i] > 0:
p_collateral = POOLS[i].price_oracle(unsafe_sub(COLLATERAL_IX[i], 1))
_price = _price * p_collateral / p_borrowed
return _price
@internal
@view
def _stored_rate() -> (uint256, bool):
use_rates: bool = False
rate: uint256 = 0
rate, use_rates = self._raw_stored_rate()
if not use_rates:
return rate, use_rates
cached_rate: uint256 = self.cached_rate
if cached_rate == 0 or cached_rate == rate:
return rate, use_rates
if rate > cached_rate:
return min(rate, cached_rate * (10**18 + RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18), use_rates
else:
return max(rate, cached_rate * (10**18 - min(RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp), 10**18)) / 10**18), use_rates
```
```shell
>>> CryptoFromPoolsRateWAgg.price()
67019544503887498803322
```
::::
### `price_w`
::::description[`CryptoFromPoolsRateWAgg.price_w() -> uint256`]
Write version of `price()`. Calls `AGG.price_w()` and `_stored_rate_w()`, which update the aggregator's and rate cache's internal state respectively. This function is called by the AMM during regular operations.
Returns: collateral price in USD (`uint256`).
```vyper
@external
def price_w() -> uint256:
return self._unscaled_price() * self._stored_rate_w() / 10**18 * AGG.price_w() / 10**18
@internal
def _stored_rate_w() -> uint256:
rate: uint256 = 0
use_rates: bool = False
rate, use_rates = self._stored_rate()
if use_rates:
self.cached_rate = rate
self.cached_timestamp = block.timestamp
return rate
```
```shell
>>> CryptoFromPoolsRateWAgg.price_w()
67019544503887498803322
```
::::
---
## Rates
The contract includes a **rate caching mechanism** to prevent rapid price manipulation through sudden rate changes. When pools use `stored_rates` (for interest-bearing tokens like crvUSD in certain pools), the oracle tracks the rate and limits how fast it can change.
The maximum rate of change is defined by `RATE_MAX_SPEED`:
```vyper
RATE_MAX_SPEED: constant(uint256) = 10**16 / 60 # Max speed of Rate change
```
This translates to a maximum of **1% per 60 seconds**. If the actual rate moves faster than this, the oracle clamps it to the maximum allowed change since the last cache update.
### `stored_rate`
::::description[`CryptoFromPoolsRateWAgg.stored_rate() -> uint256: view`]
Returns the current stored rate, subject to rate limiting. This is the product of `stored_rates[COLLATERAL_IX] / stored_rates[BORROWED_IX]` across all pools that use rates, clamped by `RATE_MAX_SPEED`.
Returns: rate-limited stored rate (`uint256`).
```vyper
@external
@view
def stored_rate() -> uint256:
return self._stored_rate()[0]
@internal
@view
def _stored_rate() -> (uint256, bool):
use_rates: bool = False
rate: uint256 = 0
rate, use_rates = self._raw_stored_rate()
if not use_rates:
return rate, use_rates
cached_rate: uint256 = self.cached_rate
if cached_rate == 0 or cached_rate == rate:
return rate, use_rates
if rate > cached_rate:
return min(rate, cached_rate * (10**18 + RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18), use_rates
else:
return max(rate, cached_rate * (10**18 - min(RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp), 10**18)) / 10**18), use_rates
@internal
@view
def _raw_stored_rate() -> (uint256, bool):
rate: uint256 = 10**18
use_rates: bool = False
for i in range(MAX_POOLS):
if i == POOL_COUNT:
break
if USE_RATES[i]:
use_rates = True
rates: DynArray[uint256, MAX_COINS] = POOLS[i].stored_rates()
rate = rate * rates[COLLATERAL_IX[i]] / rates[BORROWED_IX[i]]
return rate, use_rates
```
```shell
>>> CryptoFromPoolsRateWAgg.stored_rate()
1000000000000000000
```
::::
### `cached_rate`
::::description[`CryptoFromPoolsRateWAgg.cached_rate() -> uint256: view`]
Getter for the last cached rate value, updated each time `price_w()` is called.
Returns: cached rate (`uint256`).
```vyper
cached_rate: public(uint256)
```
```shell
>>> CryptoFromPoolsRateWAgg.cached_rate()
1000000000000000000
```
::::
### `cached_timestamp`
::::description[`CryptoFromPoolsRateWAgg.cached_timestamp() -> uint256: view`]
Getter for the timestamp when the rate was last cached (updated each time `price_w()` is called).
Returns: timestamp (`uint256`).
```vyper
cached_timestamp: public(uint256)
```
```shell
>>> CryptoFromPoolsRateWAgg.cached_timestamp()
1700000000
```
::::
---
## Contract Info Methods
### `POOLS`
::::description[`CryptoFromPoolsRateWAgg.POOLS(arg0: uint256) -> address: view`]
Getter for the Curve pool at index `arg0` used as a price source.
| Input | Type | Description |
| ------ | --------- | ----------- |
| `arg0` | `uint256` | Pool index |
Returns: pool address (`address`).
```vyper
POOLS: public(immutable(DynArray[Pool, MAX_POOLS]))
```
```shell
>>> CryptoFromPoolsRateWAgg.POOLS(0)
'0x...'
```
::::
### `POOL_COUNT`
::::description[`CryptoFromPoolsRateWAgg.POOL_COUNT() -> uint256: view`]
Getter for the total number of pools configured in this oracle.
Returns: number of pools (`uint256`).
```vyper
POOL_COUNT: public(immutable(uint256))
```
```shell
>>> CryptoFromPoolsRateWAgg.POOL_COUNT()
2
```
::::
### `BORROWED_IX`
::::description[`CryptoFromPoolsRateWAgg.BORROWED_IX(arg0: uint256) -> uint256: view`]
Getter for the index of the borrowed asset in the pool at index `arg0`.
| Input | Type | Description |
| ------ | --------- | ----------- |
| `arg0` | `uint256` | Pool index |
Returns: coin index (`uint256`).
```vyper
BORROWED_IX: public(immutable(DynArray[uint256, MAX_POOLS]))
```
```shell
>>> CryptoFromPoolsRateWAgg.BORROWED_IX(0)
0
```
::::
### `COLLATERAL_IX`
::::description[`CryptoFromPoolsRateWAgg.COLLATERAL_IX(arg0: uint256) -> uint256: view`]
Getter for the index of the collateral asset in the pool at index `arg0`.
| Input | Type | Description |
| ------ | --------- | ----------- |
| `arg0` | `uint256` | Pool index |
Returns: coin index (`uint256`).
```vyper
COLLATERAL_IX: public(immutable(DynArray[uint256, MAX_POOLS]))
```
```shell
>>> CryptoFromPoolsRateWAgg.COLLATERAL_IX(0)
1
```
::::
### `NO_ARGUMENT`
::::description[`CryptoFromPoolsRateWAgg.NO_ARGUMENT(arg0: uint256) -> bool: view`]
Getter that indicates whether the pool at index `arg0` uses a no-argument `price_oracle()` call (true for certain 2-coin pools) or requires a coin index argument.
| Input | Type | Description |
| ------ | --------- | ----------- |
| `arg0` | `uint256` | Pool index |
Returns: true if no argument needed (`bool`).
```vyper
NO_ARGUMENT: public(immutable(DynArray[bool, MAX_POOLS]))
```
```shell
>>> CryptoFromPoolsRateWAgg.NO_ARGUMENT(0)
false
```
::::
### `USE_RATES`
::::description[`CryptoFromPoolsRateWAgg.USE_RATES(arg0: uint256) -> bool: view`]
Getter that indicates whether the pool at index `arg0` has non-trivial `stored_rates` that need to be applied to the price calculation.
| Input | Type | Description |
| ------ | --------- | ----------- |
| `arg0` | `uint256` | Pool index |
Returns: true if the pool uses stored rates (`bool`).
```vyper
USE_RATES: public(immutable(DynArray[bool, MAX_POOLS]))
```
```shell
>>> CryptoFromPoolsRateWAgg.USE_RATES(0)
true
```
::::
### `AGG`
::::description[`CryptoFromPoolsRateWAgg.AGG() -> address: view`]
Getter for the crvUSD price aggregator contract ([`PriceAggregator`](./price-aggregator.md)).
Returns: aggregator address (`address`).
```vyper
AGG: public(immutable(StableAggregator))
```
```shell
>>> CryptoFromPoolsRateWAgg.AGG()
'0x...'
```
::::
---
## Overview(Oracles)
As crvUSD markets use internal oracles, they utilize in-house liquidity pools to aggregate the price of collateral. But there is a possibility to use Chainlink oracle prices as safety limits.
:::vyper[`CryptoWithStablePrice*.vy`]
Each market has its own oracle contract. Source code is available on [GitHub](https://github.com/curvefi/curve-stablecoin/tree/master/curve_stablecoin/price_oracles). Relevant deployments can be found [here](../../deployments.md).
:::
:::warning
Every market has its own price oracle contract, which can be fetched by calling `price_oracle_contract` within the controller of the market. The [wstETH oracle](https://etherscan.io/address/0xc1793A29609ffFF81f10139fa0A7A444c9e106Ad#code) will be used for the purpose of this documentation. Please be aware that oracle contracts can vary based on the collateral token.
:::
:::tip
The formulas below use slightly different terminologies than the code to make them easier to read.
For abbreviations, see [here](#terminology-used-in-code).
:::
## CryptoWithStablePrice\* Variants
The `CryptoWithStablePrice` oracle family is specifically designed for **crvUSD mint markets**. Each variant is tailored to a particular collateral type — the wstETH variant shown on this page is the most complex, using TVL-weighted tricrypto pools, staked ETH pricing, and optional Chainlink safety limits. Other variants are simpler, but all share the same core pattern: **read price oracles from Curve pools, apply collateral-specific adjustments, and optionally bound with Chainlink**.
| Variant | Collateral | Vyper | Pools | TVL Weighting |
| ------- | ---------- | ----- | ----- | ------------- |
| [`CryptoWithStablePrice`](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/price_oracles/CryptoWithStablePrice.vy) | sfrxETH | `0.3.10` | 2 tricrypto + 2 stableswap | Yes |
| [`CryptoWithStablePriceTBTC`](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/price_oracles/CryptoWithStablePriceTBTC.vy) | tBTC | `0.3.10` | 2 tricrypto + 2 stableswap | Yes |
| [`CryptoWithStablePriceWstethN`](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/price_oracles/CryptoWithStablePriceWstethN.vy) | wstETH | `0.3.10` | 2 tricrypto + 2 stableswap + staked swap | Yes |
| [`CryptoWithStablePriceWBTC`](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/price_oracles/CryptoWithStablePriceWBTC.vy) | WBTC | `0.3.10` | 2 tricrypto + 2 stableswap | Yes |
| [`CryptoWithStablePriceAndChainlink`](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/price_oracles/CryptoWithStablePriceAndChainlink.vy) | ETH | `0.3.10` | 1 tricrypto + 1 stableswap | No |
| [`CryptoWithStablePriceFrxethN`](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/price_oracles/CryptoWithStablePriceFrxethN.vy) | sfrxETH v2 | `0.3.10` | 2 tricrypto + 2 stableswap + staked swap | Yes |
Not all crvUSD markets use `CryptoWithStablePrice*` oracles. Some markets use oracle contracts from the `CryptoFromPool*` family, which are shared with the lending system:
- **[CryptoFromPoolVaultWAgg](./crypto-from-pool-vault-w-agg.md)** — for ERC-4626 vault token collateral (e.g., sFRAX)
- **[CryptoFromPoolsRateWAgg](./crypto-from-pools-rate-w-agg.md)** — for collateral requiring multi-pool price chaining with rate adjustments
## EMA of TVL
`_ema_tvl()` calculates the *exponential moving average* (EMA) of the *total value locked* (TVL) for `TRICRYPTO` pools.
This value is subsequently used in the internal function `_raw_price()` to compute the *weighted price of ETH*.
```vyper
last_timestamp: public(uint256)
last_tvl: public(uint256[N_POOLS])
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
@internal
@view
def _ema_tvl() -> uint256[N_POOLS]:
last_timestamp: uint256 = self.last_timestamp
last_tvl: uint256[N_POOLS] = self.last_tvl
if last_timestamp < block.timestamp:
alpha: uint256 = self.exp(- convert((block.timestamp - last_timestamp) * 10**18 / TVL_MA_TIME, int256))
# alpha = 1.0 when dt = 0
# alpha = 0.0 when dt = inf
for i in range(N_POOLS):
tvl: uint256 = TRICRYPTO[i].totalSupply() * TRICRYPTO[i].virtual_price() / 10**18
last_tvl[i] = (tvl * (10**18 - alpha) + last_tvl[i] * alpha) / 10**18
return last_tvl
```
$$tvl_{i} = \frac{TS_i \cdot VP_i}{10^{18}}$$
$$\text{last\_tvl}_i = \frac{tvl_i \cdot (10^{18} - \alpha) + \text{last\_tvl}_i \cdot \alpha}{10^{18}}$$
$tvl_i = \text{TVL of i-th pool}$ in `TRICRYPTO[N_POOLS]`
$TS_i = \text{total supply of i-th pool}$ in `TRICRYPTO[N_POOLS]`
$VP_i = \text{virtual price of i-th pool}$ in `TRICRYPTO[N_POOLS]`
$\text{last\_tvl}_i = \text{smoothed TVL of i-th pool}$ in `TRICRYPTO[N_POOLS]`
### `ema_tvl`
::::description[`Oracle.ema_tvl() -> uint256[N_POOLS]: view`]
Function to calculate the Total-Value-Locked (TVL) Exponential-Moving-Average (EMA) of the `TRICRYPTO` pools.
Returns: `last_tvl` (`uint256[N_POOLS]`).
```vyper hl_lines="3 4 8 20"
@external
@view
def ema_tvl() -> uint256[N_POOLS]:
return self._ema_tvl()
@internal
@view
def _ema_tvl() -> uint256[N_POOLS]:
last_timestamp: uint256 = self.last_timestamp
last_tvl: uint256[N_POOLS] = self.last_tvl
if last_timestamp < block.timestamp:
alpha: uint256 = self.exp(- convert((block.timestamp - last_timestamp) * 10**18 / TVL_MA_TIME, int256))
# alpha = 1.0 when dt = 0
# alpha = 0.0 when dt = inf
for i in range(N_POOLS):
tvl: uint256 = TRICRYPTO[i].totalSupply() * TRICRYPTO[i].virtual_price() / 10**18
last_tvl[i] = (tvl * (10**18 - alpha) + last_tvl[i] * alpha) / 10**18
return last_tvl
```
```shell
>>> Oracle.ema_tvl()
38652775551183170655949, 40849321168337010409906
```
::::
### `last_tvl`
::::description[`Oracle.last_tvl(arg0: uint256) -> uint256: view`]
Getter for the `last_tvl` of the tricrypto pool at index `arg0`.
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `uint256` | Index |
Returns: last tvl value (`uint256`).
```vyper hl_lines="3"
last_tvl: public(uint256[N_POOLS])
```
```shell
>>> Oracle.last_tvl(0)
38650114241563018578505
```
::::
## Calculate Raw Price
The internal `_raw_price()` function calculates the *raw price of the collateral token*.
```vyper
@internal
@view
def _raw_price(tvls: uint256[N_POOLS], agg_price: uint256) -> uint256:
weighted_price: uint256 = 0
weights: uint256 = 0
for i in range(N_POOLS):
p_crypto_r: uint256 = TRICRYPTO[i].price_oracle(TRICRYPTO_IX[i]) # d_usdt/d_eth
p_stable_r: uint256 = STABLESWAP[i].price_oracle() # d_usdt/d_st
p_stable_agg: uint256 = agg_price # d_usd/d_st
if IS_INVERSE[i]:
p_stable_r = 10**36 / p_stable_r
weight: uint256 = tvls[i]
# Prices are already EMA but weights - not so much
weights += weight
weighted_price += p_crypto_r * p_stable_agg / p_stable_r * weight # d_usd/d_eth
crv_p: uint256 = weighted_price / weights
use_chainlink: bool = self.use_chainlink
# Limit ETH price
if use_chainlink:
chainlink_lrd: ChainlinkAnswer = CHAINLINK_AGGREGATOR_ETH.latestRoundData()
if block.timestamp - min(chainlink_lrd.updated_at, block.timestamp) <= CHAINLINK_STALE_THRESHOLD:
chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 / CHAINLINK_PRICE_PRECISION_ETH
lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) / 10**18
upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) / 10**18
crv_p = min(max(crv_p, lower), upper)
p_staked: uint256 = STAKEDSWAP.price_oracle() # d_eth / d_steth
# Limit STETH price
if use_chainlink:
chainlink_lrd: ChainlinkAnswer = CHAINLINK_AGGREGATOR_STETH.latestRoundData()
if block.timestamp - min(chainlink_lrd.updated_at, block.timestamp) <= CHAINLINK_STALE_THRESHOLD:
chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 / CHAINLINK_PRICE_PRECISION_STETH
lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) / 10**18
upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) / 10**18
p_staked = min(max(p_staked, lower), upper)
p_staked = min(p_staked, 10**18) * WSTETH.stEthPerToken() / 10**18 # d_eth / d_wsteth
return p_staked * crv_p / 10**18
```
$$price_{weighted} = \frac{price_{eth} \cdot price_{crvusd}}{price_{usd}} \cdot weight$$
$$totalPrice_{weighted} = \frac{\sum{price_{weighted}}}{\sum{weight}}$$
$$price_{stETH} = \min(price_{stETH}, 10^{18}) \cdot \frac{rate_{wstETH}}{10^{18}}$$
$$price = price_{stETH} \cdot totalPrice_{weighted}$$
$price_{weighted} =$ weighted price of ETH
$totalPrice_{weighted} =$ total weighted price of ETH
$price_{eth} =$ price oracle of eth in the tricrypto pools w.r.t usdc/usdt
$price_{usd} =$ price oracle of stableswap pool
$price_{crvusd} =$ price oracle of crvusd
$price_{stETH} =$ price of stETH w.r.t ETH
$rate_{wstETH} =$ amount of stETH for 1 wstETH
### `raw_price`
::::description[`Oracle.raw_price() -> uint256: view`]
Function to calculate the raw price.
Returns: raw price (`uint256`).
```vyper hl_lines="1 16 18"
@external
@view
def raw_price() -> uint256:
return self._raw_price()
@internal
@view
def _raw_price(tvls: uint256[N_POOLS], agg_price: uint256) -> uint256:
weighted_price: uint256 = 0
weights: uint256 = 0
for i in range(N_POOLS):
p_crypto_r: uint256 = TRICRYPTO[i].price_oracle(TRICRYPTO_IX[i]) # d_usdt/d_eth
p_stable_r: uint256 = STABLESWAP[i].price_oracle() # d_usdt/d_st
p_stable_agg: uint256 = agg_price # d_usd/d_st
if IS_INVERSE[i]:
p_stable_r = 10**36 / p_stable_r
weight: uint256 = tvls[i]
# Prices are already EMA but weights - not so much
weights += weight
weighted_price += p_crypto_r * p_stable_agg / p_stable_r * weight # d_usd/d_eth
crv_p: uint256 = weighted_price / weights
use_chainlink: bool = self.use_chainlink
# Limit ETH price
if use_chainlink:
chainlink_lrd: ChainlinkAnswer = CHAINLINK_AGGREGATOR_ETH.latestRoundData()
if block.timestamp - min(chainlink_lrd.updated_at, block.timestamp) <= CHAINLINK_STALE_THRESHOLD:
chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 / CHAINLINK_PRICE_PRECISION_ETH
lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) / 10**18
upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) / 10**18
crv_p = min(max(crv_p, lower), upper)
p_staked: uint256 = STAKEDSWAP.price_oracle() # d_eth / d_steth
# Limit STETH price
if use_chainlink:
chainlink_lrd: ChainlinkAnswer = CHAINLINK_AGGREGATOR_STETH.latestRoundData()
if block.timestamp - min(chainlink_lrd.updated_at, block.timestamp) <= CHAINLINK_STALE_THRESHOLD:
chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 / CHAINLINK_PRICE_PRECISION_STETH
lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) / 10**18
upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) / 10**18
p_staked = min(max(p_staked, lower), upper)
p_staked = min(p_staked, 10**18) * WSTETH.stEthPerToken() / 10**18 # d_eth / d_wsteth
return p_staked * crv_p / 10**18
```
```shell
>>> Oracle.raw_price()
1970446024043370547236
```
::::
## Chainlink Limits
The oracle contracts have the option to utilize Chainlink prices, which serve as safety limits. When enabled, these limits are triggered if the Chainlink price deviates by more than 1.5% (represented by `BOUND_SIZE`) from the internal price oracles.
Chainlink limits can be turned on and off by calling `set_use_chainlink(do_it: bool)`, which can only be done by the admin of the Factory contract.
Chainlink vs Internal Oracle
### `use_chainlink`
::::description[`Oracle.use_chainlink() -> bool: view`]
Getter method to check if chainlink oracles are turned on or off.
Returns: True or False (`bool`).
```vyper hl_lines="1"
use_chainlink: public(bool)
```
```shell
>>> Oracle.use_chainlink()
'False'
```
::::
### `set_use_chainlink`
::::description[`Oracle.set_use_chainlink(do_it: bool)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the Factory contract.
:::
Function to toggle the usage of chainlink limits.
| Input | Type | Description |
| ----------- | -------| ----|
| `do_it` | `bool` | Bool to toggle the usage of chainlink oracles |
```vyper hl_lines="1 4 6"
use_chainlink: public(bool)
@external
def set_use_chainlink(do_it: bool):
assert msg.sender == FACTORY.admin()
self.use_chainlink = do_it
```
```shell
>>> Oracle.set_use_chainlink('False')
```
::::
## Terminology used in Code
| terminology used in code | |
|-----------|----------------|
| $\alpha$ | `alpha` |
| $\exp$ | `exp(power: int256) -> uint256:` |
| $TS_i$ | `TRICRYPTO[i].totalSupply()`
| $VP_i$ | `TRICRYPTO[i].virtual_price()` |
| $price_{eth}$ | `p_crypto_r` |
| $price_{usd}$ | `p_stable_agg` |
| $price_{crvusd}$ | `p_stable_r` |
| $price_{weighted}$ | `weighted_price` |
| $totalETH_{price}$ | `crv_p` |
## Contract Info Methods
### `N_POOLS`
::::description[`Oracle.N_POOLS() -> uint256: view`]
Getter for the number of external pools used by the oracle.
Returns: number of pools (`uint256`).
```vyper hl_lines="1"
N_POOLS: public(constant(uint256)) = 2
```
```shell
>>> Oracle.N_POOLS()
2
```
::::
### `TRICRYPTO`
::::description[`Oracle.TRICRYPTO(arg0: uint256) -> address: view`]
Getter for the tricrypto pool at index `arg0`.
Returns: tricrypto pool address (`address`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `uint256` | Index |
```vyper hl_lines="1"
TRICRYPTO: public(immutable(Tricrypto[N_POOLS]))
```
```shell
>>> Oracle.TRICRYPTO(0)
'0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B'
```
::::
### `TRICRYPTO_IX`
::::description[`Oracle.TRICRYPTO_IX(arg0: uint256) -> uint256: view`]
Getter for the index of ETH in the tricrypto pool w.r.t the coin at index 0.
Returns: Index of ETH price oracle in the tricrypto pool (`uint256`).
:::tip
Returns 1, as ETH price oracle index in the tricrypto pool is 1. If the same index would be 0, it would return the price oracle of ETH. Their prices are all w.r.t the coin at index 0 (USDC or USDT).
:::
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `uint256` | Index of `TRICRYPTO` |
```vyper hl_lines="1"
TRICRYPTO_IX: public(immutable(uint256[N_POOLS]))
```
```shell
>>> Oracle.TRICRYPTO_IX(0)
1
```
::::
### `STABLESWAP_AGGREGATOR`
::::description[`Oracle.STABLESWAP_AGGREGATOR() -> address: view`]
Getter for contract of the crvusd price aggregator.
Returns: contract (`address`).
```vyper hl_lines="1"
STABLESWAP_AGGREGATOR: public(immutable(StableAggregator))
```
```shell
>>> Oracle.STABLESWAP_AGGREGATOR()
'0x18672b1b0c623a30089A280Ed9256379fb0E4E62'
```
::::
### `STABLESWAP`
::::description[`Oracle.STABLESWAP(arg0: uint256) -> address: view`]
Getter for the stableswap pool at index `arg0`.
Returns: stableswap pool (`address`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `uint256` | Index of `STABLESWAP` |
```vyper hl_lines="1"
STABLESWAP: public(immutable(Stableswap[N_POOLS]))
```
```shell
>>> Oracle.STABLESWAP(0)
'0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E'
```
::::
### `STABLECOIN`
::::description[`Oracle.STABLECOIN() -> address: view`]
Getter for the contract address of crvUSD.
Returns: crvUSD contract (`address`).
```vyper hl_lines="1"
STABLECOIN: public(immutable(address))
```
```shell
>>> Oracle.STABLECOIN()
'0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'
```
::::
### `FACTORY`
::::description[`Oracle.FACTORY() -> address: view`]
Getter for the contract address of the Factory.
Returns: factory contract (`address`).
```vyper hl_lines="1"
FACTORY: public(immutable(ControllerFactory))
```
```shell
>>> Oracle.FACTORY()
'0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC'
```
::::
### `BOUND_SIZE`
::::description[`Oracle.BOUND_SIZE() -> uint256: view`]
Getter for the bound size of the chainlink oracle limits. This essentially is the size of the safety limits.
Returns: bound size (`uint256`).
```vyper hl_lines="1"
BOUND_SIZE: public(immutable(uint256))
```
```shell
>>> Oracle.BOUND_SIZE()
15000000000000000
```
::::
### `STAKEDSWAP`
::::description[`Oracle.STAKEDSWAP() -> address: view`]
Getter for the stETH/ETH stableswap pool.
Returns: pool contract (`address`).
```vyper hl_lines="1"
STAKEDSWAP: public(immutable(Stableswap))
```
```shell
>>> Oracle.STAKEDSWAP()
'0x21E27a5E5513D6e65C4f830167390997aA84843a'
```
::::
### `WSTETH`
::::description[`Oracle.WSTETH() -> address: view`]
Getter for the wstETH contract address.
Returns: wstETH contract (`address`).
```vyper hl_lines="1"
WSTETH: public(immutable(wstETH))
```
```shell
>>> Oracle.WSTETH()
'0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0'
```
::::
### `last_timestamp`
::::description[`Oracle.last_timestamp() -> uint256: view`]
Getter for the last timestamp when `price_w()` was called.
Returns: timestamp (`uint256`).
```vyper hl_lines="1"
last_timestamp: public(uint256)
```
```shell
>>> Oracle.last_timestamp()
1692613703
```
::::
### `TVL_MA_TIME`
::::description[`Oracle.TVL_MA_TIME() -> uint256: view`]
Getter for the Exponential-Moving-Average time.
Returns: ema time (`uint256`).
```vyper hl_lines="1"
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
```
```shell
>>> Oracle.TVL_MA_TIME()
50000
```
::::
### `price`
::::description[`Oracle.price() -> uint256: view`]
Function to calculate the raw price of the collateral token.
Returns: raw price (`uint256`).
```vyper hl_lines="3 4 8 47"
@external
@view
def price() -> uint256:
return self._raw_price(self._ema_tvl(), STABLESWAP_AGGREGATOR.price())
@internal
@view
def _raw_price(tvls: uint256[N_POOLS], agg_price: uint256) -> uint256:
weighted_price: uint256 = 0
weights: uint256 = 0
for i in range(N_POOLS):
p_crypto_r: uint256 = TRICRYPTO[i].price_oracle(TRICRYPTO_IX[i]) # d_usdt/d_eth
p_stable_r: uint256 = STABLESWAP[i].price_oracle() # d_usdt/d_st
p_stable_agg: uint256 = agg_price # d_usd/d_st
if IS_INVERSE[i]:
p_stable_r = 10**36 / p_stable_r
weight: uint256 = tvls[i]
# Prices are already EMA but weights - not so much
weights += weight
weighted_price += p_crypto_r * p_stable_agg / p_stable_r * weight # d_usd/d_eth
crv_p: uint256 = weighted_price / weights
use_chainlink: bool = self.use_chainlink
# Limit ETH price
if use_chainlink:
chainlink_lrd: ChainlinkAnswer = CHAINLINK_AGGREGATOR_ETH.latestRoundData()
if block.timestamp - min(chainlink_lrd.updated_at, block.timestamp) <= CHAINLINK_STALE_THRESHOLD:
chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 / CHAINLINK_PRICE_PRECISION_ETH
lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) / 10**18
upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) / 10**18
crv_p = min(max(crv_p, lower), upper)
p_staked: uint256 = STAKEDSWAP.price_oracle() # d_eth / d_steth
# Limit STETH price
if use_chainlink:
chainlink_lrd: ChainlinkAnswer = CHAINLINK_AGGREGATOR_STETH.latestRoundData()
if block.timestamp - min(chainlink_lrd.updated_at, block.timestamp) <= CHAINLINK_STALE_THRESHOLD:
chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 / CHAINLINK_PRICE_PRECISION_STETH
lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) / 10**18
upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) / 10**18
p_staked = min(max(p_staked, lower), upper)
p_staked = min(p_staked, 10**18) * WSTETH.stEthPerToken() / 10**18 # d_eth / d_wsteth
return p_staked * crv_p / 10**18
```
```shell
>>> Oracle.price()
1970446024043370547236
```
::::
### `price_w`
::::description[`Oracle.price_w() -> uint256`]
Function to obtain the oracle price of the collateral token and update `last_tvl` and `last_timestamp`. This function is used in the AMM.
Returns: oracle price of the collateral token (`uint256`).
```vyper hl_lines="2 7"
@external
def price_w() -> uint256:
tvls: uint256[N_POOLS] = self._ema_tvl()
if self.last_timestamp < block.timestamp:
self.last_timestamp = block.timestamp
self.last_tvl = tvls
return self._raw_price(tvls, STABLESWAP_AGGREGATOR.price_w())
```
```shell
>>> Oracle.price_w()
```
::::
---
## PriceAggregator (Old)
The AggregatorStablePrice contract is designed to **aggregate the prices of crvUSD based on multiple Curve Stableswap pools**. This price is primarily used as an oracle for calculating the interest rate but also for [PegKeepers](../pegkeepers/overview.md) to determine whether to mint and deposit or withdraw and burn.
:::vyper[`AggregateStablePrice.vy`]
The source code for the `AggregateStablePrice.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/price_oracles/AggregateStablePrice2.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.3.7`.
The contract is deployed on :logos-ethereum: Ethereum at [`0xe5Afcf332a5457E8FafCD668BcE3dF953762Dfe7`](https://etherscan.io/address/0xe5Afcf332a5457E8FafCD668BcE3dF953762Dfe7).
```json
[{"name":"AddPricePair","inputs":[{"name":"n","type":"uint256","indexed":false},{"name":"pool","type":"address","indexed":false},{"name":"is_inverse","type":"bool","indexed":false}],"anonymous":false,"type":"event"},{"name":"RemovePricePair","inputs":[{"name":"n","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"MovePricePair","inputs":[{"name":"n_from","type":"uint256","indexed":false},{"name":"n_to","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"SetAdmin","inputs":[{"name":"admin","type":"address","indexed":false}],"anonymous":false,"type":"event"},{"stateMutability":"nonpayable","type":"constructor","inputs":[{"name":"stablecoin","type":"address"},{"name":"sigma","type":"uint256"},{"name":"admin","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_admin","inputs":[{"name":"_admin","type":"address"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"sigma","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"stablecoin","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"nonpayable","type":"function","name":"add_price_pair","inputs":[{"name":"_pool","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"remove_price_pair","inputs":[{"name":"n","type":"uint256"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"price","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"price_pairs","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"tuple","components":[{"name":"pool","type":"address"},{"name":"is_inverse","type":"bool"}]}]},{"stateMutability":"view","type":"function","name":"admin","inputs":[],"outputs":[{"name":"","type":"address"}]}]
```
:::
---
## Calculations
The `PriceAggregator` contract calculates the weighted average price of crvUSD across multiple liquidity pools, considering only those pools with sufficient liquidity (`MIN_LIQUIDITY` = 100,000 * 10**18). This calculation is based on the exponential moving-average (EMA) of the Total Value Locked (TVL) for each pool, determining the liquidity considered in the price aggregation.
## EMA TVL Calculation
The price calculation begins with determining the EMA of the TVL from different Curve Stableswap liquidity pools using the `_ema_tvl` function. This internal function computes the EMA TVLs based on the following formula, which adjusts for the time since the last update to smooth out short-term volatility in the TVL data, providing a more stable and representative average value over the specified time window (`TVL_MA_TIME` set to 50,000 seconds):
$$\alpha =
\begin{cases}
1 & \text{if last\_timestamp} = \text{current\_timestamp}, \\
e^{-\frac{(\text{current\_timestamp} - \text{last\_timestamp}) \cdot 10^{18}}{\text{TVL\_MA\_TIME}}} & \text{otherwise}.
\end{cases}
$$
$$\text{ema\_tvl}_{i} = \frac{\text{new\_tvl}_i \cdot (10^{18} - \alpha) + \text{tvl}_i \cdot \alpha}{10^{18}}$$
*The code snippet provided illustrates the implementation of the above formula in the contract.*
```vyper
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
@internal
@view
def _ema_tvl() -> DynArray[uint256, MAX_PAIRS]:
tvls: DynArray[uint256, MAX_PAIRS] = []
last_timestamp: uint256 = self.last_timestamp
alpha: uint256 = 10**18
if last_timestamp < block.timestamp:
alpha = self.exp(- convert((block.timestamp - last_timestamp) * 10**18 / TVL_MA_TIME, int256))
n_price_pairs: uint256 = self.n_price_pairs
for i in range(MAX_PAIRS):
if i == n_price_pairs:
break
tvl: uint256 = self.last_tvl[i]
if alpha != 10**18:
# alpha = 1.0 when dt = 0
# alpha = 0.0 when dt = inf
new_tvl: uint256 = self.price_pairs[i].pool.totalSupply() # We don't do virtual price here to save on gas
tvl = (new_tvl * (10**18 - alpha) + tvl * alpha) / 10**18
tvls.append(tvl)
return tvls
```
## Aggregated Price Calculation
The `_price` function then uses these EMA TVLs to calculate the aggregated prices by considering the liquidity of each pool. A pool's liquidity must meet or exceed `100,000 * 10**18` to be included in the calculation. The function adjusts the price from the pool's `price_oracle` based on the position of crvUSD in the liquidity pair, ensuring consistent price representation across pools.
```vyper
@internal
@view
def _price(tvls: DynArray[uint256, MAX_PAIRS]) -> uint256:
n: uint256 = self.n_price_pairs
prices: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
D: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
Dsum: uint256 = 0
DPsum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
price_pair: PricePair = self.price_pairs[i]
pool_supply: uint256 = tvls[i]
if pool_supply >= MIN_LIQUIDITY:
p: uint256 = price_pair.pool.price_oracle()
if price_pair.is_inverse:
p = 10**36 / p
prices[i] = p
D[i] = pool_supply
Dsum += pool_supply
DPsum += pool_supply * p
if Dsum == 0:
return 10**18 # Placeholder for no active pools
p_avg: uint256 = DPsum / Dsum
e: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
e_min: uint256 = max_value(uint256)
for i in range(MAX_PAIRS):
if i == n:
break
p: uint256 = prices[i]
e[i] = (max(p, p_avg) - min(p, p_avg))**2 / (SIGMA**2 / 10**18)
e_min = min(e[i], e_min)
wp_sum: uint256 = 0
w_sum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
w: uint256 = D[i] * self.exp(-convert(e[i] - e_min, int256)) / 10**18
w_sum += w
wp_sum += w * prices[i]
return wp_sum / w_sum
```
*The process involves:*
- Storing the price of `crvUSD` in a `prices[i]` array for each qualifying pool.
- Recording each qualifying pool's supply (TVL) in `D[i]`, adding this supply to `Dsum`, and accumulating the product of the `crvUSD` price and pool supply in `DPsum`.
- Iterating over all price pairs to perform the above steps.
*Finally, the contract:*
- Calculates an average price:
$$\text{average price} = \frac{\text{DPSum}}{\text{DSum}}$$
- Computes a variance measure `e` for each pool's price relative to the average, adjusting by `SIGMA` to normalize:
$$\text{e}_i = \frac{(\max(p, p_{\text{avg}}) - \min(p, p_{\text{avg}}))^2}{\frac{\text{SIGMA}^2}{10^{18}}}$$
$$\text{e}_{\min} = \min(\text{e}_i, \text{max\_value(uint256)})$$
- Applies an exponential decay based on these variance measures to weigh each pool's contribution to the final average price, reducing the influence of prices far from the minimum variance.
$$w = \frac{\text{D}_i \cdot e^{-(\text{e}_i - e_{\min})}}{10^{18}}$$
- Sums up all `w` to store it in `w_sum` and calculates the product of `w * prices[i]`, which is stored in `wp_sum`.
- Finally calculates the weighted average price as `wp_sum / w_sum`, with weights adjusted for both liquidity and price variance.
$$\text{final price} = \frac{\text{wp_sum}}{\text{w_sum}}$$
---
## Prices
### `price`
::::description[`PriceAggregator.price() -> uint256: view`]
Function to calculate the weighted price of crvUSD.
Returns: price (`uint256`).
```vyper
MAX_PAIRS: constant(uint256) = 20
MIN_LIQUIDITY: constant(uint256) = 100_000 * 10**18 # Only take into account pools with enough liquidity
STABLECOIN: immutable(address)
SIGMA: immutable(uint256)
price_pairs: public(PricePair[MAX_PAIRS])
n_price_pairs: uint256
@external
@view
def price() -> uint256:
return self._price(self._ema_tvl())
@internal
@view
def _price(tvls: DynArray[uint256, MAX_PAIRS]) -> uint256:
n: uint256 = self.n_price_pairs
prices: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
D: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
Dsum: uint256 = 0
DPsum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
price_pair: PricePair = self.price_pairs[i]
pool_supply: uint256 = tvls[i]
if pool_supply >= MIN_LIQUIDITY:
p: uint256 = price_pair.pool.price_oracle()
if price_pair.is_inverse:
p = 10**36 / p
prices[i] = p
D[i] = pool_supply
Dsum += pool_supply
DPsum += pool_supply * p
if Dsum == 0:
return 10**18 # Placeholder for no active pools
p_avg: uint256 = DPsum / Dsum
e: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
e_min: uint256 = max_value(uint256)
for i in range(MAX_PAIRS):
if i == n:
break
p: uint256 = prices[i]
e[i] = (max(p, p_avg) - min(p, p_avg))**2 / (SIGMA**2 / 10**18)
e_min = min(e[i], e_min)
wp_sum: uint256 = 0
w_sum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
w: uint256 = D[i] * self.exp(-convert(e[i] - e_min, int256)) / 10**18
w_sum += w
wp_sum += w * prices[i]
return wp_sum / w_sum
```
::::
### `last_price`
::::description[`PriceAggregator.last_price() -> uint256: view`]
Getter for the last price. This variable was set to $10^{18}$ (1.00) when initializing the contract and is now updated every time [`price_w`](#price_w) is called.
Returns: last price of crvUSD (`uint256`).
```vyper
last_price: public(uint256)
@external
def __init__(stablecoin: address, sigma: uint256, admin: address):
STABLECOIN = stablecoin
SIGMA = sigma # The change is so rare that we can change the whole thing altogether
self.admin = admin
self.last_price = 10**18
self.last_timestamp = block.timestamp
@external
def price_w() -> uint256:
if self.last_timestamp == block.timestamp:
return self.last_price
else:
ema_tvl: DynArray[uint256, MAX_PAIRS] = self._ema_tvl()
self.last_timestamp = block.timestamp
for i in range(MAX_PAIRS):
if i == len(ema_tvl):
break
self.last_tvl[i] = ema_tvl[i]
p: uint256 = self._price(ema_tvl)
self.last_price = p
return p
```
::::
### `ema_tvl`
::::description[`PriceAggregator.ema_tvl() -> DynArray[uint256, MAX_PAIRS]: view`]
Getter for the exponential moving-average of the TVL in `price_pairs`.
Returns: array of ema tvls (`DynArray[uint256, MAX_PAIRS]`).
```vyper
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
last_tvl: public(uint256[MAX_PAIRS])
@external
@view
def ema_tvl() -> DynArray[uint256, MAX_PAIRS]:
return self._ema_tvl()
@internal
@view
def _ema_tvl() -> DynArray[uint256, MAX_PAIRS]:
tvls: DynArray[uint256, MAX_PAIRS] = []
last_timestamp: uint256 = self.last_timestamp
alpha: uint256 = 10**18
if last_timestamp < block.timestamp:
alpha = self.exp(- convert((block.timestamp - last_timestamp) * 10**18 / TVL_MA_TIME, int256))
n_price_pairs: uint256 = self.n_price_pairs
for i in range(MAX_PAIRS):
if i == n_price_pairs:
break
tvl: uint256 = self.last_tvl[i]
if alpha != 10**18:
# alpha = 1.0 when dt = 0
# alpha = 0.0 when dt = inf
new_tvl: uint256 = self.price_pairs[i].pool.totalSupply() # We don't do virtual price here to save on gas
tvl = (new_tvl * (10**18 - alpha) + tvl * alpha) / 10**18
tvls.append(tvl)
return tvls
```
::::
### `last_tvl`
::::description[`PriceAggregator.last_tvl(arg0: uint256) -> uint256: view`]
Getter for the total value locked of price pair (pool).
Returns: total value locked (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `uint256` | Index of the price pair |
```vyper
last_tvl: public(uint256[MAX_PAIRS])
```
::::
### `price_w`
::::description[`PriceAggregator.price_w() -> uint256`]
Function to calculate and write the price. If called successfully, updates `last_tvl`, `last_price` and `last_timestamp`.
Returns: price (`uint256`).
```vyper
@external
def price_w() -> uint256:
if self.last_timestamp == block.timestamp:
return self.last_price
else:
ema_tvl: DynArray[uint256, MAX_PAIRS] = self._ema_tvl()
self.last_timestamp = block.timestamp
for i in range(MAX_PAIRS):
if i == len(ema_tvl):
break
self.last_tvl[i] = ema_tvl[i]
p: uint256 = self._price(ema_tvl)
self.last_price = p
return p
@internal
@view
def _price(tvls: DynArray[uint256, MAX_PAIRS]) -> uint256:
n: uint256 = self.n_price_pairs
prices: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
D: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
Dsum: uint256 = 0
DPsum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
price_pair: PricePair = self.price_pairs[i]
pool_supply: uint256 = tvls[i]
if pool_supply >= MIN_LIQUIDITY:
p: uint256 = price_pair.pool.price_oracle()
if price_pair.is_inverse:
p = 10**36 / p
prices[i] = p
D[i] = pool_supply
Dsum += pool_supply
DPsum += pool_supply * p
if Dsum == 0:
return 10**18 # Placeholder for no active pools
p_avg: uint256 = DPsum / Dsum
e: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
e_min: uint256 = max_value(uint256)
for i in range(MAX_PAIRS):
if i == n:
break
p: uint256 = prices[i]
e[i] = (max(p, p_avg) - min(p, p_avg))**2 / (SIGMA**2 / 10**18)
e_min = min(e[i], e_min)
wp_sum: uint256 = 0
w_sum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
w: uint256 = D[i] * self.exp(-convert(e[i] - e_min, int256)) / 10**18
w_sum += w
wp_sum += w * prices[i]
return wp_sum / w_sum
```
```shell
>>> PriceAggregator.price_w()
999385898759491513
```
::::
---
## Adding and Removing Price Pairs
All price pairs added to the contract are considered when calculating the `price` of crvUSD. Adding or removing price pairs can only be done by the `admin` of the contract, which is the Curve DAO.
### `price_pairs`
::::description[`PriceAggregator.price_pairs(arg0: uint256) -> PricePair: view`]
Getter for the price pair at index `arg0` and whether the price pair is inverse.
Returns: price pair (`address`) and true or false (`bool`).
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `uint256` | Index of the price pair |
```vyper
price_pairs: public(PricePair[MAX_PAIRS])
```
::::
### `add_price_pair`
::::description[`PriceAggregator.add_price_pair(_pool: Stableswap)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to add a price pair to the PriceAggregator.
| Input | Type | Description |
| ----------- | -------| ----|
| `_pool` | `Stableswap` | Price pair to add |
Emits: `AddPricePair` event.
```vyper
event AddPricePair:
n: uint256
pool: Stableswap
is_inverse: bool
@external
def add_price_pair(_pool: Stableswap):
assert msg.sender == self.admin
price_pair: PricePair = empty(PricePair)
price_pair.pool = _pool
coins: address[2] = [_pool.coins(0), _pool.coins(1)]
if coins[0] == STABLECOIN:
price_pair.is_inverse = True
else:
assert coins[1] == STABLECOIN
n: uint256 = self.n_price_pairs
self.price_pairs[n] = price_pair # Should revert if too many pairs
self.last_tvl[n] = _pool.totalSupply()
self.n_price_pairs = n + 1
log AddPricePair(n, _pool, price_pair.is_inverse)
```
```shell
>>> PriceAggregator.add_price_pair("0x0cd6f267b2086bea681e922e19d40512511be538")
```
::::
### `remove_price_pair`
::::description[`PriceAggregator.remove_price_pair(n: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to remove a price pair from the contract. If a prior pool than the latest added one gets removed, the function will move the latest added price pair to the removed pair pairs index to not mess up `price_pairs`.
| Input | Type | Description |
| ----------- | -------| ----|
| `n` | `uint256` | Index of the price pair to remove |
Emits: `RemovePricePair` event and possibly `MovePricePair` event.
```vyper
event RemovePricePair:
n: uint256
event MovePricePair:
n_from: uint256
n_to: uint256
@external
def remove_price_pair(n: uint256):
assert msg.sender == self.admin
n_max: uint256 = self.n_price_pairs - 1
assert n <= n_max
if n < n_max:
self.price_pairs[n] = self.price_pairs[n_max]
log MovePricePair(n_max, n)
self.n_price_pairs = n_max
log RemovePricePair(n)
```
```shell
>>> PriceAggregator.remove_price_pair(0)
```
::::
---
## Admin Ownership
### `admin`
::::description[`PriceAggregator.admin() -> address: view`]
Getter for the admin of the contract, which is the Curve DAO OwnershipAgent.
Returns: admin (`address`).
```vyper
admin: public(address)
@external
def __init__(stablecoin: address, sigma: uint256, admin: address):
STABLECOIN = stablecoin
SIGMA = sigma # The change is so rare that we can change the whole thing altogether
self.admin = admin
```
::::
### `set_admin`
::::description[`PriceAggregator.set_admin(_admin: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new admin.
| Input | Type | Description |
| ----------- | -------| ----|
| `_admin` | `address` | New admin address |
Emits: `SetAdmin` event.
```vyper
event SetAdmin:
admin: address
admin: public(address)
@external
def set_admin(_admin: address):
# We are not doing commit / apply because the owner will be a voting DAO anyway
# which has vote delays
assert msg.sender == self.admin
self.admin = _admin
log SetAdmin(_admin)
```
```shell
>>> PriceAggregator.set_admin("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
```
::::
---
## Contract Info Methods
### `sigma`
::::description[`PriceAggregator.sigma() -> uint256: view`]
Getter for the sigma value. SIGMA is a predefined constant that influences the adjustment of price deviations, affecting how variations in individual stablecoin prices contribute to the overall average stablecoin price.
Returns: sigma (`uint256`).
```vyper
SIGMA: immutable(uint256)
@external
@view
def sigma() -> uint256:
return SIGMA
@external
def __init__(stablecoin: address, sigma: uint256, admin: address):
STABLECOIN = stablecoin
SIGMA = sigma # The change is so rare that we can change the whole thing altogether
self.admin = admin
```
::::
### `stablecoin`
::::description[`PriceAggregator.stablecoin() -> address: view`]
Getter for the stablecoin contract address.
Returns: crvUSD contract (`address`).
```vyper
STABLECOIN: immutable(address)
@external
@view
def stablecoin() -> address:
return STABLECOIN
@external
def __init__(stablecoin: address, sigma: uint256, admin: address):
STABLECOIN = stablecoin
SIGMA = sigma # The change is so rare that we can change the whole thing altogether
self.admin = admin
```
::::
### `last_timestamp`
::::description[`PriceAggregator.last_timestamp() -> uint256: view`]
Getter for the latest timestamp. Variable is updated when [`price_w`](#price_w) is called.
Returns: timestamp (`uint256`).
```vyper
last_timestamp: public(uint256)
```
::::
### `TVL_MA_TIME`
::::description[`PriceAggregator.TVL_MA_TIME() -> uint256: view`]
Getter for the time period for the calculation of the EMA prices.
Returns: timestamp (`uint256`).
```vyper
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
```
::::
---
## PriceAggregator
The `AggregateStablePrice.vy` contract is designed to **get an aggregated price of crvUSD based on multiple stableswap pools weighted by their TVL**.
:::vyper[`AggregateStablePrice.vy`]
There are three iterations of the `AggregateStablePrice` contract. Source code for the contracts can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/tree/master/contracts/price_oracles). The contract is written in [Vyper](https://vyperlang.org/) version `0.3.7`.
- :logos-ethereum: Ethereum: [`0x18672b1b0c623a30089A280Ed9256379fb0E4E62`](https://etherscan.io/address/0x18672b1b0c623a30089A280Ed9256379fb0E4E62)
- :logos-arbitrum: Arbitrum: [`0x44a4FdFb626Ce98e36396d491833606309520330`](https://arbiscan.io/address/0x44a4FdFb626Ce98e36396d491833606309520330)
```json
[
{
"name": "AddPricePair",
"inputs": [
{
"name": "n",
"type": "uint256",
"indexed": false
},
{
"name": "pool",
"type": "address",
"indexed": false
},
{
"name": "is_inverse",
"type": "bool",
"indexed": false
}
],
"anonymous": false,
"type": "event"
},
{
"name": "RemovePricePair",
"inputs": [
{
"name": "n",
"type": "uint256",
"indexed": false
}
],
"anonymous": false,
"type": "event"
},
{
"name": "MovePricePair",
"inputs": [
{
"name": "n_from",
"type": "uint256",
"indexed": false
},
{
"name": "n_to",
"type": "uint256",
"indexed": false
}
],
"anonymous": false,
"type": "event"
},
{
"name": "SetAdmin",
"inputs": [
{
"name": "admin",
"type": "address",
"indexed": false
}
],
"anonymous": false,
"type": "event"
},
{
"stateMutability": "nonpayable",
"type": "constructor",
"inputs": [
{
"name": "stablecoin",
"type": "address"
},
{
"name": "sigma",
"type": "uint256"
},
{
"name": "admin",
"type": "address"
}
],
"outputs": []
},
{
"stateMutability": "nonpayable",
"type": "function",
"name": "set_admin",
"inputs": [
{
"name": "_admin",
"type": "address"
}
],
"outputs": []
},
{
"stateMutability": "view",
"type": "function",
"name": "sigma",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256"
}
]
},
{
"stateMutability": "view",
"type": "function",
"name": "stablecoin",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address"
}
]
},
{
"stateMutability": "nonpayable",
"type": "function",
"name": "add_price_pair",
"inputs": [
{
"name": "_pool",
"type": "address"
}
],
"outputs": []
},
{
"stateMutability": "nonpayable",
"type": "function",
"name": "remove_price_pair",
"inputs": [
{
"name": "n",
"type": "uint256"
}
],
"outputs": []
},
{
"stateMutability": "view",
"type": "function",
"name": "ema_tvl",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256[]"
}
]
},
{
"stateMutability": "view",
"type": "function",
"name": "price",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256"
}
]
},
{
"stateMutability": "nonpayable",
"type": "function",
"name": "price_w",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256"
}
]
},
{
"stateMutability": "view",
"type": "function",
"name": "price_pairs",
"inputs": [
{
"name": "arg0",
"type": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "tuple",
"components": [
{
"name": "pool",
"type": "address"
},
{
"name": "is_inverse",
"type": "bool"
}
]
}
]
},
{
"stateMutability": "view",
"type": "function",
"name": "last_timestamp",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256"
}
]
},
{
"stateMutability": "view",
"type": "function",
"name": "last_tvl",
"inputs": [
{
"name": "arg0",
"type": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "uint256"
}
]
},
{
"stateMutability": "view",
"type": "function",
"name": "TVL_MA_TIME",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256"
}
]
},
{
"stateMutability": "view",
"type": "function",
"name": "last_price",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256"
}
]
},
{
"stateMutability": "view",
"type": "function",
"name": "admin",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address"
}
]
}
]
```
:::
This aggregated price of crvUSD is used in multiple different components in the system such as in [monetary policy contracts](../monetary-policy/monetary-policy.md), [PegKeepers](../pegkeepers/overview.md) or [oracles for lending markets](../../lending/contracts/oracle-overview.md).
---
## Calculations
The `AggregateStablePrice` contract calculates the **weighted average price of crvUSD across multiple liquidity pools**, considering only those pools with sufficient liquidity (`MIN_LIQUIDITY = 100,000 * 10**18`). The calculation is based on the **exponential moving average (EMA) of the Total-Value-Locked (TVL)** for each pool, determining the liquidity considered in the price aggregation.
## EMA TVL Calculation
The price calculation starts with determining the EMA of the TVL from different Curve Stableswap liquidity pools using the `_ema_tvl` function. This internal function computes the EMA TVLs based on the formula below, which adjusts for the time since the last update to smooth out short-term volatility in the TVL data, providing a more stable and representative average value over the specified time window (`TVL_MA_TIME = 50000`):
$$\alpha =
\begin{cases}
1 & \text{if last\_timestamp} = \text{current\_timestamp}, \\
e^{-\frac{(\text{current\_timestamp} - \text{last\_timestamp}) \cdot 10^{18}}{\text{TVL\_MA\_TIME}}} & \text{otherwise}.
\end{cases}
$$
$$\text{ema\_tvl}_{i} = \frac{\text{new\_tvl}_i \cdot (10^{18} - \alpha) + \text{tvl}_i \cdot \alpha}{10^{18}}$$
*The code snippet provided illustrates the implementation of the above formula in the contract.*
```vyper
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
@internal
@view
def _ema_tvl() -> DynArray[uint256, MAX_PAIRS]:
tvls: DynArray[uint256, MAX_PAIRS] = []
last_timestamp: uint256 = self.last_timestamp
alpha: uint256 = 10**18
if last_timestamp < block.timestamp:
alpha = self.exp(- convert((block.timestamp - last_timestamp) * 10**18 / TVL_MA_TIME, int256))
n_price_pairs: uint256 = self.n_price_pairs
for i in range(MAX_PAIRS):
if i == n_price_pairs:
break
tvl: uint256 = self.last_tvl[i]
if alpha != 10**18:
# alpha = 1.0 when dt = 0
# alpha = 0.0 when dt = inf
new_tvl: uint256 = self.price_pairs[i].pool.totalSupply() # We don't do virtual price here to save on gas
tvl = (new_tvl * (10**18 - alpha) + tvl * alpha) / 10**18
tvls.append(tvl)
return tvls
```
## Aggregated crvUSD Price Calculation
The `_price` function then uses these EMA TVLs to calculate the aggregated price of `crvUSD` by considering the liquidity of each pool. The function adjusts the price from the pool's `price_oracle` based on the coin index of `crvUSD` in the liquidity pool.
```vyper
@internal
@view
def _price(tvls: DynArray[uint256, MAX_PAIRS]) -> uint256:
n: uint256 = self.n_price_pairs
prices: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
D: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
Dsum: uint256 = 0
DPsum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
price_pair: PricePair = self.price_pairs[i]
pool_supply: uint256 = tvls[i]
if pool_supply >= MIN_LIQUIDITY:
p: uint256 = price_pair.pool.price_oracle()
if price_pair.is_inverse:
p = 10**36 / p
prices[i] = p
D[i] = pool_supply
Dsum += pool_supply
DPsum += pool_supply * p
if Dsum == 0:
return 10**18 # Placeholder for no active pools
p_avg: uint256 = DPsum / Dsum
e: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
e_min: uint256 = max_value(uint256)
for i in range(MAX_PAIRS):
if i == n:
break
p: uint256 = prices[i]
e[i] = (max(p, p_avg) - min(p, p_avg))**2 / (SIGMA**2 / 10**18)
e_min = min(e[i], e_min)
wp_sum: uint256 = 0
w_sum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
w: uint256 = D[i] * self.exp(-convert(e[i] - e_min, int256)) / 10**18
w_sum += w
wp_sum += w * prices[i]
return wp_sum / w_sum
```
*In the calculation process, the contract iterates over all price pairs to perform the following steps:*
- Storing the price of `crvUSD` in a `prices[i]` array for each pool with enough liquidity.
- Storing each pool's TVL in `D[i]`, adding this TVL to `Dsum`, and summing up the product of the `crvUSD` price and pool supply in `DPsum`.
*Finally, the contract calculates an average price:*
$$\text{average price} = \frac{\text{DPsum}}{\text{Dsum}}$$
*Next, a variance measure `e` is computed for each pool's price relative to the average, adjusting by `SIGMA` to normalize:*
$$\text{e}_i = \frac{(\max(p, p_{\text{avg}}) - \min(p, p_{\text{avg}}))^2}{\frac{\text{SIGMA}^2}{10^{18}}}$$
$$\text{e}_{\min} = \min(\text{e}_i, \text{max\_value(uint256)})$$
Applying an exponential decay based on these variance measures to weigh each pool's contribution to the final average price, reducing the influence of prices far from the minimum variance.
$$w = \frac{\text{D}_i \cdot e^{-(\text{e}_i - e_{\min})}}{10^{18}}$$
Next, sum up all `w` to store it in `w_sum` and calculate the product of `w * prices[i]`, which is stored in `wp_sum`.
*Finally, the weighted average price of `crvUSD` is calculated:*
$$\text{final price} = \frac{\text{wp_sum}}{\text{w_sum}}$$
---
## Price and TVL Methods
### `price`
::::description[`PriceAggregator3.price() -> uint256: view`]
Getter for the aggregated price of crvUSD based on the prices of crvUSD within different `price_pairs`.
Returns: aggregated crvUSD price (`uint256`).
```vyper
MAX_PAIRS: constant(uint256) = 20
MIN_LIQUIDITY: constant(uint256) = 100_000 * 10**18 # Only take into account pools with enough liquidity
STABLECOIN: immutable(address)
SIGMA: immutable(uint256)
price_pairs: public(PricePair[MAX_PAIRS])
n_price_pairs: uint256
last_timestamp: public(uint256)
last_tvl: public(uint256[MAX_PAIRS])
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
last_price: public(uint256)
@external
@view
def price() -> uint256:
return self._price(self._ema_tvl())
@internal
@view
def _price(tvls: DynArray[uint256, MAX_PAIRS]) -> uint256:
n: uint256 = self.n_price_pairs
prices: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
D: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
Dsum: uint256 = 0
DPsum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
price_pair: PricePair = self.price_pairs[i]
pool_supply: uint256 = tvls[i]
if pool_supply >= MIN_LIQUIDITY:
p: uint256 = price_pair.pool.price_oracle()
if price_pair.is_inverse:
p = 10**36 / p
prices[i] = p
D[i] = pool_supply
Dsum += pool_supply
DPsum += pool_supply * p
if Dsum == 0:
return 10**18 # Placeholder for no active pools
p_avg: uint256 = DPsum / Dsum
e: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
e_min: uint256 = max_value(uint256)
for i in range(MAX_PAIRS):
if i == n:
break
p: uint256 = prices[i]
e[i] = (max(p, p_avg) - min(p, p_avg))**2 / (SIGMA**2 / 10**18)
e_min = min(e[i], e_min)
wp_sum: uint256 = 0
w_sum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
w: uint256 = D[i] * self.exp(-convert(e[i] - e_min, int256)) / 10**18
w_sum += w
wp_sum += w * prices[i]
return wp_sum / w_sum
@internal
@view
def _ema_tvl() -> DynArray[uint256, MAX_PAIRS]:
tvls: DynArray[uint256, MAX_PAIRS] = []
last_timestamp: uint256 = self.last_timestamp
alpha: uint256 = 10**18
if last_timestamp < block.timestamp:
alpha = self.exp(- convert((block.timestamp - last_timestamp) * 10**18 / TVL_MA_TIME, int256))
n_price_pairs: uint256 = self.n_price_pairs
for i in range(MAX_PAIRS):
if i == n_price_pairs:
break
tvl: uint256 = self.last_tvl[i]
if alpha != 10**18:
# alpha = 1.0 when dt = 0
# alpha = 0.0 when dt = inf
new_tvl: uint256 = self.price_pairs[i].pool.totalSupply() # We don't do virtual price here to save on gas
tvl = (new_tvl * (10**18 - alpha) + tvl * alpha) / 10**18
tvls.append(tvl)
return tvls
```
::::
### `price_w`
::::description[`PriceAggregator3.price_w() -> uint256`]
Function to calculate the aggregated price of crvUSD based on the prices of crvUSD within different `price_pairs`. This function writes the price on the blockchain and additionally updates `last_timestamp`, `last_tvl` and `last_price`.
Returns: aggregated crvUSD price (`uint256`).
```vyper
MAX_PAIRS: constant(uint256) = 20
MIN_LIQUIDITY: constant(uint256) = 100_000 * 10**18 # Only take into account pools with enough liquidity
STABLECOIN: immutable(address)
SIGMA: immutable(uint256)
price_pairs: public(PricePair[MAX_PAIRS])
n_price_pairs: uint256
last_timestamp: public(uint256)
last_tvl: public(uint256[MAX_PAIRS])
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
last_price: public(uint256)
@external
def price_w() -> uint256:
if self.last_timestamp == block.timestamp:
return self.last_price
else:
ema_tvl: DynArray[uint256, MAX_PAIRS] = self._ema_tvl()
self.last_timestamp = block.timestamp
for i in range(MAX_PAIRS):
if i == len(ema_tvl):
break
self.last_tvl[i] = ema_tvl[i]
p: uint256 = self._price(ema_tvl)
self.last_price = p
return p
@internal
@view
def _price(tvls: DynArray[uint256, MAX_PAIRS]) -> uint256:
n: uint256 = self.n_price_pairs
prices: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
D: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
Dsum: uint256 = 0
DPsum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
price_pair: PricePair = self.price_pairs[i]
pool_supply: uint256 = tvls[i]
if pool_supply >= MIN_LIQUIDITY:
p: uint256 = price_pair.pool.price_oracle()
if price_pair.is_inverse:
p = 10**36 / p
prices[i] = p
D[i] = pool_supply
Dsum += pool_supply
DPsum += pool_supply * p
if Dsum == 0:
return 10**18 # Placeholder for no active pools
p_avg: uint256 = DPsum / Dsum
e: uint256[MAX_PAIRS] = empty(uint256[MAX_PAIRS])
e_min: uint256 = max_value(uint256)
for i in range(MAX_PAIRS):
if i == n:
break
p: uint256 = prices[i]
e[i] = (max(p, p_avg) - min(p, p_avg))**2 / (SIGMA**2 / 10**18)
e_min = min(e[i], e_min)
wp_sum: uint256 = 0
w_sum: uint256 = 0
for i in range(MAX_PAIRS):
if i == n:
break
w: uint256 = D[i] * self.exp(-convert(e[i] - e_min, int256)) / 10**18
w_sum += w
wp_sum += w * prices[i]
return wp_sum / w_sum
@internal
@view
def _ema_tvl() -> DynArray[uint256, MAX_PAIRS]:
tvls: DynArray[uint256, MAX_PAIRS] = []
last_timestamp: uint256 = self.last_timestamp
alpha: uint256 = 10**18
if last_timestamp < block.timestamp:
alpha = self.exp(- convert((block.timestamp - last_timestamp) * 10**18 / TVL_MA_TIME, int256))
n_price_pairs: uint256 = self.n_price_pairs
for i in range(MAX_PAIRS):
if i == n_price_pairs:
break
tvl: uint256 = self.last_tvl[i]
if alpha != 10**18:
# alpha = 1.0 when dt = 0
# alpha = 0.0 when dt = inf
new_tvl: uint256 = self.price_pairs[i].pool.totalSupply() # We don't do virtual price here to save on gas
tvl = (new_tvl * (10**18 - alpha) + tvl * alpha) / 10**18
tvls.append(tvl)
return tvls
```
```shell
>>> PriceAggregator3.price_w()
996396341581883374
```
::::
### `last_price`
::::description[`PriceAggregator3.last_price() -> uint256: view`]
Getter for the last aggregated price of crvUSD. This variable was set to $10^{18}$ (1.00) when initializing the contract and is updated to the current aggregated crvUSD price every time [`price_w`](#price_w) is called.
Returns: last aggregated price of crvUSD (`uint256`).
```vyper
last_price: public(uint256)
@external
def __init__(stablecoin: address, sigma: uint256, admin: address):
STABLECOIN = stablecoin
SIGMA = sigma # The change is so rare that we can change the whole thing altogether
self.admin = admin
self.last_price = 10**18
self.last_timestamp = block.timestamp
```
::::
### `last_timestamp`
::::description[`PriceAggregator3.last_timestamp() -> uint256: view`]
Getter for the last timestamp when the aggregated price of crvUSD was updated. This variable was populated with `block.timestamp` when initializing the contract and is updated to the current timestamp every time [`price_w`](#price_w) is called. When adding a new price pair, its value is set to the `totalSupply` of the pair.
Returns: timestamp of the last price write (`uint256`).
```vyper
last_timestamp: public(uint256)
```
::::
### `ema_tvl`
::::description[`PriceAggregator3.ema_tvl() -> DynArray[uint256, MAX_PAIRS]: view`]
Getter for the exponential moving-average value of TVL across all `price_pairs`.
Returns: array of ema tvls (`DynArray[uint256, MAX_PAIRS]`).
```vyper
MAX_PAIRS: constant(uint256) = 20
MIN_LIQUIDITY: constant(uint256) = 100_000 * 10**18 # Only take into account pools with enough liquidity
price_pairs: public(PricePair[MAX_PAIRS])
n_price_pairs: uint256
last_timestamp: public(uint256)
last_tvl: public(uint256[MAX_PAIRS])
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
@external
@view
def ema_tvl() -> DynArray[uint256, MAX_PAIRS]:
return self._ema_tvl()
@internal
@view
def _ema_tvl() -> DynArray[uint256, MAX_PAIRS]:
tvls: DynArray[uint256, MAX_PAIRS] = []
last_timestamp: uint256 = self.last_timestamp
alpha: uint256 = 10**18
if last_timestamp < block.timestamp:
alpha = self.exp(- convert((block.timestamp - last_timestamp) * 10**18 / TVL_MA_TIME, int256))
n_price_pairs: uint256 = self.n_price_pairs
for i in range(MAX_PAIRS):
if i == n_price_pairs:
break
tvl: uint256 = self.last_tvl[i]
if alpha != 10**18:
# alpha = 1.0 when dt = 0
# alpha = 0.0 when dt = inf
new_tvl: uint256 = self.price_pairs[i].pool.totalSupply() # We don't do virtual price here to save on gas
tvl = (new_tvl * (10**18 - alpha) + tvl * alpha) / 10**18
tvls.append(tvl)
return tvls
```
::::
### `last_tvl`
::::description[`PriceAggregator3.last_tvl(arg0: uint256) -> uint256: view`]
Getter for the last ema tvl value of a `price_pair`. This variable is updated to the current ema tvl of the pool every time [`price_w`](#price_w) is called. When adding a new price pair, its value is set to the `totalSupply` of the pair.
Returns: last ema tvl (`uint256`).
| Input | Type | Description |
| ------ | --------- | ----------------------- |
| `arg0` | `uint256` | Index of the price pair |
```vyper
last_tvl: public(uint256[MAX_PAIRS])
```
::::
### `TVL_MA_TIME`
::::description[`PriceAggregator3.TVL_MA_TIME() -> uint256: view`]
Getter for the time periodicity used to calculate the exponential moving-average of TVL.
Returns: ema periodicity (`uint256`).
```vyper
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
```
::::
---
## Contract Info Methods
### `sigma`
::::description[`PriceAggregator3.sigma() -> uint256: view`]
Getter for the sigma value. SIGMA is a predefined constant that influences the adjustment of price deviations, affecting how variations in individual stablecoin prices contribute to the overall average stablecoin price. The value of `sigma` was set to `1000000000000000` when initializing the contract and the variable is immutable, meaning it can not be adjusted.
Returns: sigma value (`uint256`).
```vyper
SIGMA: immutable(uint256)
@external
@view
def sigma() -> uint256:
return SIGMA
@external
def __init__(stablecoin: address, sigma: uint256, admin: address):
STABLECOIN = stablecoin
SIGMA = sigma # The change is so rare that we can change the whole thing altogether
self.admin = admin
self.last_price = 10**18
self.last_timestamp = block.timestamp
```
::::
### `stablecoin`
::::description[`PriceAggregator3.stablecoin() -> address: view`]
Getter for the crvUSD contract address.
Returns: crvUSD contract (`address`).
```vyper
STABLECOIN: immutable(address)
@external
@view
def stablecoin() -> address:
return STABLECOIN
@external
def __init__(stablecoin: address, sigma: uint256, admin: address):
STABLECOIN = stablecoin
SIGMA = sigma # The change is so rare that we can change the whole thing altogether
self.admin = admin
self.last_price = 10**18
self.last_timestamp = block.timestamp
```
::::
---
## Price Pairs
All liquidity pools used to calculate the aggregated price are stored in `price_pairs`. New price pairs can be added or removed by the DAO using `add_price_pair` and `remove_price_pair`.
### `price_pairs`
::::description[`PriceAggregator3.price_pairs(arg0: uint256) -> PricePair: view`]
Getter for the price pairs added to the `PriceAggregator` contract. New pairs can be added using the [`add_price_pair`](#add_price_pair) function.
Returns: `PricePair` struct consisting of the pool (`address`) and if it is inverse (`bool`).
| Input | Type | Description |
| ------ | --------- | ----------------------- |
| `arg0` | `uint256` | Index of the price pair |
```vyper
struct PricePair:
pool: Stableswap
is_inverse: bool
price_pairs: public(PricePair[MAX_PAIRS])
```
::::
### `add_price_pair`
::::description[`PriceAggregator3.add_price_pair(_pool: Stableswap)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract. The `admin` is the Curve DAO (via the CurveOwnershipAgent).
:::
Function to add a new price pair to the `PriceAggregator`.
| Input | Type | Description |
| ------- | ------------ | ------------------------- |
| `_pool` | `Stableswap` | Pool to add as price pair |
Emits: `AddPricePair` event.
```vyper
event AddPricePair:
n: uint256
pool: Stableswap
is_inverse: bool
price_pairs: public(PricePair[MAX_PAIRS])
n_price_pairs: uint256
@external
def add_price_pair(_pool: Stableswap):
assert msg.sender == self.admin
price_pair: PricePair = empty(PricePair)
price_pair.pool = _pool
coins: address[2] = [_pool.coins(0), _pool.coins(1)]
if coins[0] == STABLECOIN:
price_pair.is_inverse = True
else:
assert coins[1] == STABLECOIN
n: uint256 = self.n_price_pairs
self.price_pairs[n] = price_pair # Should revert if too many pairs
self.last_tvl[n] = _pool.totalSupply()
self.n_price_pairs = n + 1
log AddPricePair(n, _pool, price_pair.is_inverse)
```
```shell
>>> PriceAggregator3.add_price_pair("0xpool_address")
```
::::
### `remove_price_pair`
::::description[`PriceAggregator3.remove_price_pair(n: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract. The `admin` is the Curve DAO (via the CurveOwnershipAgent).
:::
Function to remove the price pair at index `n` from the `PriceAggregator`.
| Input | Type | Description |
| ----- | --------- | --------------------------------- |
| `n` | `uint256` | Index of the price pair to remove |
Emits: `RemovePricePair` event and conditionally `MovePricePair` event.[^1]
[^1]: `MovePricePair` event is emitted when the removed price pair is not the last one which was added. In this case, price pairs need to be adjusted accordingly.
```vyper
event RemovePricePair:
n: uint256
event MovePricePair:
n_from: uint256
n_to: uint256
price_pairs: public(PricePair[MAX_PAIRS])
n_price_pairs: uint256
@external
def remove_price_pair(n: uint256):
assert msg.sender == self.admin
n_max: uint256 = self.n_price_pairs - 1
assert n <= n_max
if n < n_max:
self.price_pairs[n] = self.price_pairs[n_max]
log MovePricePair(n_max, n)
self.n_price_pairs = n_max
log RemovePricePair(n)
```
```shell
>>> PriceAggregator3.remove_price_pair(0)
```
::::
---
## Contract Ownership
The contract follows the classical two-step ownership model used in various other Curve contracts:
### `admin`
::::description[`PriceAggregator3.admin() -> address: view`]
Getter for the current admin of the contract.
Returns: current admin (`address`).
```vyper
admin: public(address)
@external
def __init__(stablecoin: address, sigma: uint256, admin: address):
STABLECOIN = stablecoin
SIGMA = sigma # The change is so rare that we can change the whole thing altogether
self.admin = admin
self.last_price = 10**18
self.last_timestamp = block.timestamp
```
::::
### `set_admin`
::::description[`PriceAggregator3.set_admin(_admin: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract. The `admin` is the Curve DAO (via the CurveOwnershipAgent).
:::
Function to set a new address as the `admin` of the contract.
| Input | Type | Description |
| -------- | --------- | ------------------------------- |
| `_admin` | `address` | New address to set the admin to |
Emits: `SetAdmin` event.
```vyper
event SetAdmin:
admin: address
admin: public(address)
@external
def set_admin(_admin: address):
# We are not doing commit / apply because the owner will be a voting DAO anyway
# which has vote delays
assert msg.sender == self.admin
self.admin = _admin
log SetAdmin(_admin)
```
```shell
>>> PriceAggregator3.set_admin("0xnew_admin_address")
```
::::
---
## Curve Stablecoin: Overview
Curve Stablecoin infrastructure enables users to **mint crvUSD using a selection of crypto collaterals**. Adding new collaterals is subject to DAO approval.
`crvUSD` is designed to provide a more **capital-efficient** stablecoin mechanism and **smoother liquidations**, while maintaining a decentralized design which the Curve DAO governs.
:::github[GitHub]
The source code for all releveant stablecoin contract can be found on [GitHub ](https://github.com/curvefi/curve-stablecoin). Related deployments can be found [here](../deployments.md).
:::
---
## Curve Stablecoin Infrastructure Components
`crvUSD` token which is based on the [ERC-20 Token Standard](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/).
Savings crvUSD, abbreviated as `scrvUSD`, is a yield-bearing version of crvUSD built on top of a Yearn V3 Vault. It earns a portion of the yield generated by borrowers who pay interest to mint crvUSD.
The Controller is the contract the **user interacts with** to **create a loan and further manage the position**. It holds all user debt information. External liquidations are also done through it.
LLAMMA is the **market-making contract that rebalances the collateral**. As the name suggests, this contract is responsible for liquidating collateral. Every market has its own AMM (created from a blueprint contract) containing the collateral asset and crvUSD.
The Factory is used to **add new markets**, **raise or lower debt ceilings** of already existing markets, **set blueprint contracts for AMM and Controller**, and **set fee receiver**.
Monetary policy contracts are integrated into the crvUSD system and are **responsible for the interest rate** of crvUSD markets.
PegKeepers are contracts that help **stabilize the peg of crvUSD**. They are allocated a specific amount of crvUSD to secure the peg.
The AggregatorStablePrice contract is designed to **aggregate the price of crvUSD based on multiple Curve pools**. This price is mainly used as an oracle for calculating the interest rate, providing an aggregated and exponential moving average price.
Oracle contract used for collaterals in the markets.
The `FlashLender.vy` contract allows users to take out a flash loan for `crvUSD`.
---
## Controller & AMM Versions
The crvUSD mint markets use several iterations of the Controller and AMM (LLAMMA) contracts. Each version was deployed as a new **blueprint** — existing markets are not affected when a new implementation is set. Llamalend later adopted the V3 blueprint, so all Llamalend markets share the same Controller/AMM version as the latest crvUSD markets.
### Version Matrix
| Version | Vyper | Controller Blueprint | AMM Blueprint | Source |
|---------|-------|---------------------|---------------|--------|
| **V1** | 0.3.7 | `0x856...8CC` ([Etherscan](https://etherscan.io/address/0x856fF1aaff4782eEe27D2C6bbAd48781F57f88CC)) | `0x7Ec...099` ([Etherscan](https://etherscan.io/address/0x7Ec8e02b74CDD1C1c222DbF3Bf47F3256B734099)) | [58289a4](https://github.com/curvefi/curve-stablecoin/tree/58289a4283d7cc3c53aba2d3801dcac5ef124957) |
| **V2** | 0.3.9 | `0x803...737` ([Etherscan](https://etherscan.io/address/0x80333bd8791Fee04C4C3e1CA8a524CEfA7C94737)) | `0x762...260` ([Etherscan](https://etherscan.io/address/0x7624C0DD4f5D06d650DDFF25fFEC45D032501260)) | — |
| **V3** | 0.3.10 | `0xe3e...415` ([Etherscan](https://etherscan.io/address/0xe3e3Fb7E9f48d26817b7210C9bD6B22744790415)) | `0x2B7...3e9` ([Etherscan](https://etherscan.io/address/0x2B7e624bdb839975d56D8428d9f6A4cf1160D3e9)) | [b0240d8](https://github.com/curvefi/curve-stablecoin/tree/b0240d844c9e60fdab78b481a556a187ceee3721) |
:::info
V3 is the **current blueprint** set on the Controller Factory. It is also the version used by all [Llamalend](../lending/overview.md) markets.
:::
### Market → Version Mapping
| Market (Collateral) | Controller Version | Status |
|---------------------|--------------------|--------|
| sfrxETH (v1) | V1 | Deprecated (0 debt ceiling) |
| wstETH | V1 | Active |
| WBTC | V1 | Active |
| WETH | V1 | Active |
| sfrxETH (v2) | V2 | Active |
| tBTC | V2 | Active |
| weETH | V3 | Active |
| cbBTC | V3 | Active |
| LBTC | V3 | Active |
### Changelog
#### V1 → V2
- **Compiler upgrade**: Vyper 0.3.7 → 0.3.9.
- Minor internal changes; the external API is identical.
#### V2 → V3
V3 is a significant upgrade that was developed alongside Llamalend. The same blueprint is shared by crvUSD mint markets and all Llamalend lending markets.
- **Compiler upgrade**: Vyper 0.3.9 → 0.3.10.
- **Delegated loan creation**: `create_loan`, `borrow_more`, `repay`, and `liquidate` accept an optional `_for` parameter, allowing approved operators to manage loans on behalf of users. See [`approve`](./controller.md#approve).
- **Extra health buffer**: Users can set [`extra_health`](./controller.md#extra_health) via [`set_extra_health`](./controller.md#set_extra_health), adding a health buffer when entering soft liquidation.
- **Extended borrow**: New [`borrow_more_extended`](./controller.md#borrow_more_extended) function supporting callback-based leverage.
- **Arbitrary-decimal tokens**: The Controller now handles tokens with any number of decimals (not just 18), with rounding adjusted in favor of existing borrowers. This was necessary for Llamalend's flexible token support.
- **`collect_fees()` disabled for lending**: In Llamalend markets, admin fees are zero and all interest goes to vault depositors.
- **Native ETH transfers removed**: Automatic ETH wrapping is permanently disabled for safety.
- **`check_lock` / `save_rate`**: New external helpers used by the Vault contract in Llamalend.
---
## PegKeepers: Stabilizing the crvUSD Peg
:::github[GitHub]
Source code of all PegKeepers can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/tree/master/contracts/stabilizer).
A list of all contract deployments can be found [here](../../deployments.md).
:::
## General Concepts
## Stabilization Method
PegKeepers are specialized contracts **designed to maintain the stability of the crvUSD peg**. They hold a pre-minted supply of crvUSD tokens to be utilized for peg stabilization efforts. The operation of PegKeepers is **restricted to only two actions: depositing and withdrawing from liquidity pools**. As long as these pre-minted crvUSD tokens are not deposited anywhere, they can and should be counted as out-of-circulation.
These contracts are each associated with a specific liquidity pool that includes crvUSD and another fiat-redeemable USD stablecoin.
The basic idea of PegKeepers revolves around monitoring the price of crvUSD and the balances of the linked pools and taking corresponding actions. When the **price of crvUSD exceeds 1.0**, indicating a deviation towards the upside, PegKeepers **deposit their crvUSD**into their linked pool and, in exchange, receive LP tokens. This action increases the crvUSD balance within the pool, thus exerting downward pressure on its price and aiding in peg stabilization.
Conversely, should the crvUSD **price drop below 1.0**, signaling a downward peg deviation, PegKeepers are **permitted to burn their LP tokens and withdraw crvUSD**from the pool to reduce the balance within it and push the price back towards parity. This withdrawal mechanism is contingent on the PegKeeper having previously deposited crvUSD into the pool, as the **contract must have LP tokens to burn in the process**.
Moreover, the **`update` function**that deposits and withdraws crvUSD is **callable by any EOA or smart contract**. To foster engagement, callers are rewarded with a caller share as an incentive.
## Impact on crvUSD Interest Rate
PegKeepers significantly influence the interest rate of crvUSD markets. The interest rate is affected by various factors, including the DebtFraction across all PegKeepers. A higher debt accumulated by PegKeepers[^1] increases the DebtFraction, which, in turn, leads to a lower interest rate.
[^1]: PegKeeper debt is accumulated by depositing into the linked liquidity pool. If the contract deposited 100 crvUSD, debt is equal to 100.
*The DebtFraction is defined as:*
$$\text{DebtFraction} = \frac{{\text{PegKeeperDebt}}}{{\text{TotalDebt}}}$$
For a comprehensive understanding of the factors influencing the interest rate, please refer to the [MonetaryPolicy](../monetary-policy/monetary-policy.md#interest-rate-mechanics) section.
---
## PegKeeperV1
The initial version of `PegKeeper.vy` encountered two significant problems:
## Spam Attack Issue
A notable challenge in the first version of PegKeepers was its **susceptibility to spam attacks**.
This issue stemmed from the ability of an attacker to manipulate the price of crvUSD very close to 1, followed by executing the `update` function to make a minimal deposit (or withdrawal), before moving the price back. With a mandatory **15-minute cooldown** before the `update` function could be called again, an attacker could exploit this interval to periodically disrupt the PegKeepers' capacity for peg stabilization.
Although executing such an attack would entail **significant costs for the attacker**, resulting in **substantial revenue for the liquidity pool**, the potential for continuous exploitation was still present. This issue highlighted the need for a refined approach to prevent such manipulative activities and ensure the effective stabilization of the peg.
## Depegging Scenario
A more critical issue arose when a PegKeeper engaged in a deposit, essentially taking on debt by depositing crvUSD into the pool. If the coin paired with crvUSD in the pool experienced a **significant depeg**, the PegKeeper could find itself **unable to off-load its debt by withdrawing its crvUSD**. This situation would leave a quantity of unbacked crvUSD in circulation.
*These issues were addressed in the second version of the PegKeeper.*
---
## PegKeeperV2 and Regulator
:::github[GitHub]
Research regarding PegKeeperV2 can be found here: [`curve-stablecoin-researches`](https://github.com/curvefi/curve-stablecoin-researches/tree/main/peg_keeper).
:::
The transition to PegKeeperV2 marks a significant refinement in the system's architecture, introducing a **clear division of duties between two specialized contracts**. The **`PegKeeperV2`** contract is now **exclusively focused on carrying out the operational tasks** essential for maintaining the peg's stability, while the **`PegKeeperRegulator`** contract assumes a **pivotal role in oversight and regulation**.
Central to this new structure is the `PegKeeperRegulator.vy` contract, which grants the PegKeepers allowance to deposit or withdraw crvUSD based on different conditions. Additionally, the contract has the option to **pause and unpause the deposit and withdrawal actions**through its admin or emergency admin.
*Additionally, this version introduces robust solutions to previously identified issues, such as susceptibility to spam attacks and challenges in managing depeg situations:*
## Mitigating Spam Attacks with Oracle Price Verification
To address the spam attack issue in the first version of PegKeepers, an innovative solution involving the `price_oracle` and `get_p` function from stableswap pools was implemented. This approach allows the system to verify if the current AMM market prices significantly deviate from the pool's oracle's EMA price, thereby ensuring actions to stabilize the peg are only taken when the price is within an accepted deviation.
*The solution utilizes two prices from the pools:*
- `_p0` is represented by the `price_oracle` (EMA oracle).
- `_p1` by the `get_p` function representing the current price in the AMM.
A public variable, `price_deviation`, is introduced, which checks if the price is within the accepted range by employing an absolute error. Its value is upgradable, but can only be done by the admin of the Regulator contract.
```vyper
@internal
@view
def _price_in_range(_p0: uint256, _p1: uint256) -> bool:
"""
@notice Checks if the price is within the accepted range, employing absolute error for spam-attack protection.
@dev The formula used is: 0 < |p1 - p0| <= deviation * 2, where deviation is a predefined tolerance.
"""
deviation: uint256 = self.price_deviation
return unsafe_sub(unsafe_add(deviation, _p0), _p1) < deviation << 1
```
This function effectively measures if the current price (`_p1`) is within an acceptable range of the exponential moving average price (`_p0`), thereby deterring spam attacks by requiring prices to be reasonably aligned with the oracle's for updates to proceed. This mechanism prevents the attacker from being able to call the `update` function straight after manipulating the price close to one.
Further details on setting the price_deviation parameter can be found in the Curve Finance stablecoin research documentation: [Deviation Parameter Explanation](https://github.com/curvefi/curve-stablecoin-researches/tree/main/peg_keeper#deviation).
## Mitigating Depeg Issue using Absolute Deviation Error
In order to mitigate potential depged risk and therefore leaving the PegKeeper with debt, a `worst_price_threshold` variable was introduced.
```vyper
price: uint256 = max_value(uint256) # Will fail if PegKeeper is not in self.price_pairs
largest_price: uint256 = 0
debt_ratios: DynArray[uint256, MAX_LEN] = []
for info in self.peg_keepers:
price_oracle: uint256 = self._get_price_oracle(info)
if info.peg_keeper.address == _pk:
price = price_oracle
if not self._price_in_range(price, self._get_price(info)):
return 0
continue
elif largest_price < price_oracle:
largest_price = price_oracle
debt_ratios.append(self._get_ratio(info.peg_keeper))
if largest_price < unsafe_sub(price, self.worst_price_threshold):
return 0
```
The code adeptly updates the `largest_price` variable to reflect the highest `price_oracle` value found across all PegKeeper pools, explicitly excluding the pool which is being deposited into.
The critical operation is the **comparison between `largest_price` and the result of `unsafe_sub(price, self.worst_price_threshold)`**. This comparison essentially evaluates the highest price of coins paired against crvUSD in auxiliary pools against the adjusted price of the coin paired against crvUSD in the intended liquidity pool, **after accounting for a predefined worst price threshold (`worst_price_threshold`)**.
If `largest_price` is found to be lower than the difference, it **indicates a potential depegging scenario**. In response, the Regulator contract **proactively blocks any further deposits**of crvUSD to preemptively address the depeg risk.
This safeguard acts as a bulwark against significant price divergences between the highest observed price (`largest_price`) and the target price, with the `worst_price_threshold` serving as a key variable in this evaluation. Failure to align with this safeguard (i.e., when `largest_price` significantly undercuts the threshold) triggers a halt in operations, as indicated by a return value of 0. Such a mechanism is vital for mitigating risks tied to price volatility, thereby ensuring the system's stability and preserving the integrity of pegged relationships.
Additionally, to **prevent a single PegKeeper from taking on all the debt, ratio limits were implemented**. More on these limits [here](./peg-keeper-regulator.md#providing).
---
## PegKeeperRegulator
The regulator contract supervises prices and other parameters telling whether the PegKeeper are allowed to provide or withdraw crvUSD.
:::vyper[`PegKeeperRegulator.vy`]
Source code for the `PegKeeperRegulator.vy` contract is available on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/stabilizer/PegKeeperRegulator.vy). Relevant deployments can be found [here](../../deployments.md).
:::
Technically speaking, allowance is always granted but if certain checks do not pass, the Regulator will allow an amount of 0, which in return can be seen as not allowing anything to deposit or withdraw.
---
## Providing
*The Regulator will only grant allowance to the PegKeeper to provide crvUSD to the pool if the following requirements are met. If any of these conditions are not satisfied, the function will return 0, causing the transaction to ultimately revert:*
- **Providing is not paused**: This is checked using the `is_killed` method. If providing is paused, no crvUSD can be added to the pool.
- **Aggregated crvUSD price is higher than 1.0**: The crvUSD price, obtained from the `aggregator` contract, must be above 1.0 (`10**18`). If the price is equal to or below 1.0, providing crvUSD is not allowed.
- **Price consistency check**: The `get_p` (current AMM state price) and `price_oracle` (AMM EMA Price Oracle) must be within a specified deviation range (`price_deviation`). This is to prevent spam attacks by ensuring that the current price is not significantly deviating from the oracle price.
- **Depeg threshold check**: To ensure that the price of an asset has not depegged significantly, the current price is compared against a `worst_price_threshold`. This check ensures that prices across the pools with PegKeepers are within an acceptable range of deviation. If the price deviation is within the threshold, the PegKeeper is allowed to provide crvUSD.
Additionally, the system has implemented limit ratios to ensure a balanced and stable distribution of debt among the PegKeepers. The formula used to calculate the maximum allowed debt ratio, $\text{maxRatio}$, can be seen below. It dynamically adjusts the allowable debt ratio based on the aggregate debt of all PegKeepers in the system. These ratios can be plotted:
*The allowed amount to provide and the max debt ratio to provide is calculated as follows:*
$$\text{allowedToProvide} = \frac{\text{maxRatio} \times \text{total}}{10^{18}} - \text{debt}$$
$$\text{maxRatio} = \frac{(\alpha + \beta \times \frac{\text{rsum}}{10^{18}})^2}{10^{18}}$$
$$\text{rsum} = \sum_{i=n_{pk}}^{n} \sqrt{r_i \times 10^{18}}$$
Where:
- \(\text\{total\}\) is the sum of the debt and the balance of crvUSD held by the PegKeeper.
- \(\text\{debt\}\) is the current amount of crvUSD deposited into the pool by the PegKeeper.
- \( r_i \) represents the debt ratios of the other PegKeepers.
:::example[Example: Amount allowed to provide with all empty PegKeepers]
Let's take a look at the scenario when all PegKeepers are empty, therefore $r = [0, 0, 0, 0]$.
$\text{rsum} = \sqrt{0 \times 10^{18}} + \sqrt{0 \times 10^{18}} + \sqrt{0 \times 10^{18}} + \sqrt{0 \times 10^{18}} = 0$
$\text{maxRatio} = \frac{(0.5 + 0.25 \times \frac{0}{10^{18}})^2}{10^{18}} = 0.25$
*The `maxRatio` the PegKeeper can provide is 0.25. The actual crvUSD to provide is calculated as follows:*[^1]
$\text{allowedToProvide} = \frac{0.25 \times 25000000}{1} - 0 = 6250000$
[^1]: Assuming the PegKeeper has a total balance of 25m crvUSD.
---
**To have full Desmos functionality and a cleaner overview, please view the graph directly on Desmos: [https://www.desmos.com/calculator/szhqv2edsd](https://www.desmos.com/calculator/szhqv2edsd).**
:::
### `provide_allowed`
::::description[`PegKeeperRegulator.provide_allowed(_pk: address=msg.sender) -> uint256`]
Function to check how much crvUSD a PegKeeper is allowed to provide into a liquidity pool. If the PegKeeper is not permitted to provide any, the function will return 0.
:::warning
This function may return a higher amount than the actual crvUSD that can be deposited, as it does not consider the current crvUSD balance of the PegKeeper. The returned value is capped by the maximum crvUSD balance of the PegKeeper in the `_provide` function of the PegKeeper itself.
:::
| Input | Type | Description |
| ----- | --------- | --------------------------------------------------- |
| `_pk` | `address` | PegKeeper address; Defaults to `msg.sender` as the function is usually called by the PegKeeper itself |
Returns: amount of crvUSD allowed to provide (`uint256`).
```py
struct PegKeeperInfo:
peg_keeper: PegKeeper
pool: StableSwap
is_inverse: bool
include_index: bool
peg_keepers: public(DynArray[PegKeeperInfo, MAX_LEN])
@external
@view
def provide_allowed(_pk: address=msg.sender) -> uint256:
"""
@notice Allow PegKeeper to provide stablecoin into the pool
@dev Can return more amount than available
@dev Checks
1) current price in range of oracle in case of spam-attack
2) current price location among other pools in case of contrary coin depeg
3) stablecoin price is above 1
@return Amount of stablecoin allowed to provide
"""
if self.is_killed in Killed.Provide:
return 0
if self.aggregator.price() < ONE:
return 0
price: uint256 = max_value(uint256) # Will fail if PegKeeper is not in self.price_pairs
largest_price: uint256 = 0
debt_ratios: DynArray[uint256, MAX_LEN] = []
for info in self.peg_keepers:
price_oracle: uint256 = self._get_price_oracle(info)
if info.peg_keeper.address == _pk:
price = price_oracle
if not self._price_in_range(price, self._get_price(info)):
return 0
continue
elif largest_price < price_oracle:
largest_price = price_oracle
debt_ratios.append(self._get_ratio(info.peg_keeper))
if largest_price < unsafe_sub(price, self.worst_price_threshold):
return 0
debt: uint256 = PegKeeper(_pk).debt()
total: uint256 = debt + STABLECOIN.balanceOf(_pk)
return self._get_max_ratio(debt_ratios) * total / ONE - debt
@internal
@pure
def _get_price_oracle(_info: PegKeeperInfo) -> uint256:
"""
@return Price of the coin in STABLECOIN
"""
price: uint256 = 0
if _info.include_index:
price = _info.pool.price_oracle(0)
else:
price = _info.pool.price_oracle()
if _info.is_inverse:
price = 10 **36 / price
return price
@internal
@view
def _price_in_range(_p0: uint256, _p1: uint256) -> bool:
"""
@notice Check that the price is in accepted range using absolute error
@dev Needed for spam-attack protection
"""
# |p1 - p0| <= deviation
# -deviation <= p1 - p0 <= deviation
# 0 < deviation + p1 - p0 <= 2 * deviation
# can use unsafe
deviation: uint256 = self.price_deviation
return unsafe_sub(unsafe_add(deviation, _p0), _p1) < deviation << 1
@internal
@pure
def _get_price(_info: PegKeeperInfo) -> uint256:
"""
@return Price of the coin in STABLECOIN
"""
price: uint256 = 0
if _info.include_index:
price = _info.pool.get_p(0)
else:
price = _info.pool.get_p()
if _info.is_inverse:
price = 10 **36 / price
return price
@internal
@view
def _get_ratio(_peg_keeper: PegKeeper) -> uint256:
"""
@return debt ratio limited up to 1
"""
debt: uint256 = _peg_keeper.debt()
return debt * ONE / (1 + debt + STABLECOIN.balanceOf(_peg_keeper.address))
@internal
@view
def _get_max_ratio(_debt_ratios: DynArray[uint256, MAX_LEN]) -> uint256:
rsum: uint256 = 0
for r in _debt_ratios:
rsum += isqrt(r * ONE)
return (self.alpha + self.beta * rsum / ONE) **2 / ONE
```
```shell
>>> soon
```
::::
---
## Withdrawing
*The Regulator will grant allowance to the PegKeeper to withdraw crvUSD from the pool if the following requirements are met. If any of these conditions are not met, the function will return 0, causing the transaction to ultimately revert:*
- **Withdrawing is not paused**: This is checked using the `is_killed` method. If withdrawing is paused, no crvUSD can be removed from the pool.
- **Aggregated crvUSD price is less than 1.0**: The crvUSD price, obtained from the `aggregator` contract, must be above 1.0 (`10**18`). If the price is equal to or below 1.0, withdrawing crvUSD is not allowed.
- **Price consistency check**: The `get_p` (current AMM state price) and `price_oracle` (AMM EMA Price Oracle) must be within a specified deviation range (`price_deviation`). This is to prevent spam attacks by ensuring that the current price is not significantly deviating from the oracle price.
### `withdraw_allowed`
::::description[`PegKeeperRegulator.withdraw_allowed(_pk: address=msg.sender) -> uint256`]
Function to check how much crvUSD a PegKeeper is allowed to withdraw from the pool.
:::warning
If allowance to withdraw is granted, the function will always return `max_value(uint256)`. The actual value to withdraw is limited within the `_withdraw` function of the PegKeeper itself.
:::
| Input | Type | Description |
| ----- | --------- | ---------------------------------------------------- |
| `_pk` | `address` | PegKeeper address; defaults to `msg.sender` as it's usually called by the PegKeeper itself |
Returns: amount of crvUSD allowed to withdraw (`uint256`).
```vyper
struct PegKeeperInfo:
peg_keeper: PegKeeper
pool: StableSwap
is_inverse: bool
include_index: bool
peg_keeper_i: HashMap[PegKeeper, uint256] # 1 + index of peg keeper in a list
@external
@view
def withdraw_allowed(_pk: address=msg.sender) -> uint256:
"""
@notice Allow Peg Keeper to withdraw stablecoin from the pool
@dev Can return more amount than available
@dev Checks
1) current price in range of oracle in case of spam-attack
2) stablecoin price is below 1
@return Amount of stablecoin allowed to withdraw
"""
if self.is_killed in Killed.Withdraw:
return 0
if self.aggregator.price() > ONE:
return 0
i: uint256 = self.peg_keeper_i[PegKeeper(_pk)]
if i > 0:
info: PegKeeperInfo = self.peg_keepers[i - 1]
if self._price_in_range(self._get_price(info), self._get_price_oracle(info)):
return max_value(uint256)
return 0
@internal
@pure
def _get_price_oracle(_info: PegKeeperInfo) -> uint256:
"""
@return Price of the coin in STABLECOIN
"""
price: uint256 = 0
if _info.include_index:
price = _info.pool.price_oracle(0)
else:
price = _info.pool.price_oracle()
if _info.is_inverse:
price = 10 **36 / price
return price
@internal
@view
def _price_in_range(_p0: uint256, _p1: uint256) -> bool:
"""
@notice Check that the price is in accepted range using absolute error
@dev Needed for spam-attack protection
"""
# |p1 - p0| <= deviation
# -deviation <= p1 - p0 <= deviation
# 0 < deviation + p1 - p0 <= 2 * deviation
# can use unsafe
deviation: uint256 = self.price_deviation
return unsafe_sub(unsafe_add(deviation, _p0), _p1) < deviation << 1
@internal
@pure
def _get_price(_info: PegKeeperInfo) -> uint256:
"""
@return Price of the coin in STABLECOIN
"""
price: uint256 = 0
if _info.include_index:
price = _info.pool.get_p(0)
else:
price = _info.pool.get_p()
if _info.is_inverse:
price = 10 **36 / price
return price
```
```shell
>>> soon
```
::::
---
## Parameters
*The Regulator uses several parameters:*
- `worst_price_threshold` is a threshold value for the price of the pegged coin. If the threshold is exceeded, providing crvUSD will not be allowed as the pegged coin is potentially depegged (too far away from other pegged coins' prices).
- `price_deviation` represents an absolute error value and is used to check if prices (`get_p` and `price_oracle`) are within a certain range of each other in order to prevent spam attacks.
- `alpha` and `beta` are used for the calculations of the maximum debt ratio within `_get_max_ratio`.
For more details on the calculations and research behind these parameters, see [here](https://github.com/curvefi/curve-stablecoin-researches/blob/main/peg_keeper/README.md).
### `worst_price_threshold`
::::description[`PegKeeperRegulator.worst_price_threshold() -> uint256: view`]
Getter for the current worst price threshold. The value can only be changed by the `admin` calling the [`set_worst_price_threshold`](#set_worst_price_threshold) function.
Returns: worst price threshold (`uint256`).
Emits: `WorstPriceThreshold` at contract initialization
```vyper
event WorstPriceThreshold:
threshold: uint256
worst_price_threshold: public(uint256)
@external
def __init__(_stablecoin: ERC20, _agg: Aggregator, _fee_receiver: address, _admin: address, _emergency_admin: address):
...
self.worst_price_threshold = 3 * 10 **(18 - 4) # 0.0003
...
log WorstPriceThreshold(self.worst_price_threshold)
...
```
```shell
>>> PegKeeperRegulator.worst_price_threshold()
300000000000000
```
::::
### `price_deviation`
::::description[`PegKeeperRegulator.price_deviation() -> uint256: view`]
Getter for the current price deviation value. The value can only be changed by the `admin` calling the [`set_price_deviation`](#set_price_deviation) function.
Returns: price deviation (`uint256`).
Emits: `PriceDeviation` at contract initialization
```vyper
event PriceDeviation:
price_deviation: uint256
price_deviation: public(uint256)
@external
def __init__(_stablecoin: ERC20, _agg: Aggregator, _fee_receiver: address, _admin: address, _emergency_admin: address):
...
self.price_deviation = 5 * 10 **(18 - 4) # 0.0005 = 0.05%
...
log PriceDeviation(self.price_deviation)
...
```
```shell
>>> PegKeeperRegulator.price_deviation()
500000000000000
```
::::
### `alpha`
::::description[`PegKeeperRegulator.alpha() -> uint256: view`]
Getter for the alpha value, which represents the initial boundary. This value can be changed by the `admin` by calling the [`set_debt_parameters`](#set_debt_parameters) function.
Returns: alpha (`uint256`).
Emits: `DebtParameters` at contract initialization
```vyper
event DebtParameters:
alpha: uint256
beta: uint256
alpha: public(uint256) # Initial boundary
@external
def __init__(_stablecoin: ERC20, _agg: Aggregator, _fee_receiver: address, _admin: address, _emergency_admin: address):
...
self.alpha = ONE / 2 # 1/2
...
log DebtParameters(self.alpha, self.beta)
```
```shell
>>> PegKeeperRegulator.alpha()
500000000000000000
```
::::
### `beta`
::::description[`PegKeeperRegulator.beta() -> uint256: view`]
Getter for the beta value, which represents each PegKeeper's impact. This value can be changed by the `admin` by calling the [`set_debt_parameters`](#set_debt_parameters) function.
Returns: beta (`uint256`).
Emits: `DebtParameters` at contract initialization
```vyper
event DebtParameters:
alpha: uint256
beta: uint256
beta: public(uint256) # Each PegKeeper's impact
@external
def __init__(_stablecoin: ERC20, _agg: Aggregator, _fee_receiver: address, _admin: address, _emergency_admin: address):
...
self.beta = ONE / 4 # 1/4
...
log DebtParameters(self.alpha, self.beta)
```
```shell
>>> PegKeeperRegulator.beta()
250000000000000000
```
::::
### `set_worst_price_threshold`
::::description[`PegKeeperRegulator.set_worst_price_threshold(_threshold: uint256)`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to set `_threshold` as the new `worst_price_threshold` value.
Emits: `WorstPriceThreshold`
| Input | Type | Description |
| ------------ | --------- | ------------ |
| `_threshold` | `uint256` | New value for `worst_price_threshold` |
```vyper
event WorstPriceThreshold:
threshold: uint256
worst_price_threshold: public(uint256)
@external
def set_worst_price_threshold(_threshold: uint256):
"""
@notice Set threshold for the worst price that is still accepted
@param _threshold Price threshold with base 10 **18 (1.0 = 10 **18)
"""
assert msg.sender == self.admin
assert _threshold <= 10 **(18 - 2) # 0.01
self.worst_price_threshold = _threshold
log WorstPriceThreshold(_threshold)
```
```shell
>>> soon
```
::::
### `set_price_deviation`
::::description[`PegKeeperRegulator.set_price_deviation(_deviation: uint256)`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to set `_deviation` as the new `price_deviation` value.
Emits: `PriceDeviation`
| Input | Type | Description |
| ------------ | --------- | ------------ |
| `_deviation` | `uint256` | New value for `price_deviation` |
```vyper
event PriceDeviation:
price_deviation: uint256
price_deviation: public(uint256)
@external
def set_price_deviation(_deviation: uint256):
"""
@notice Set acceptable deviation of current price from oracle's
@param _deviation Deviation of price with base 10 **18 (1.0 = 10 **18)
"""
assert msg.sender == self.admin
assert _deviation <= 10 **20
self.price_deviation = _deviation
log PriceDeviation(_deviation)
```
```shell
>>> soon
```
::::
### `set_debt_parameters`
::::description[`PegKeeperRegulator.set_debt_parameters(_alpha: uint256, _beta: uint256)`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to set new parameters for `alpha` and `beta`.
Emits: `DebtParameters`
| Input | Type | Description |
| -------- | --------- | --------------------- |
| `_alpha` | `uint256` | New value for `alpha` |
| `_beta` | `uint256` | New value for `beta` |
```vyper
event DebtParameters:
alpha: uint256
beta: uint256
ONE: constant(uint256) = 10 **18
alpha: public(uint256) # Initial boundary
beta: public(uint256) # Each PegKeeper's impact
@external
def set_debt_parameters(_alpha: uint256, _beta: uint256):
"""
@notice Set parameters for calculation of debt limits
@dev 10 **18 precision
"""
assert msg.sender == self.admin
assert _alpha <= ONE
assert _beta <= ONE
self.alpha = _alpha
self.beta = _beta
log DebtParameters(_alpha, _beta)
```
```shell
>>> soon
```
::::
---
## Adding and Removing PegKeepers
PegKeepers rely on the Regulator, as it provides the contract with information on whether they are allowed to provide or withdraw crvUSD from the pool. These PegKeepers need to be added by `admin` using the [`add_peg_keepers`](#add_peg_keepers) function and are then stored within the [`peg_keepers`](#peg_keepers) variable.
PegKeepers can be removed from the Regulator contract by the `admin` using the [`remove_peg_keepers`](#remove_peg_keepers) function.
### `peg_keepers`
::::description[`PegKeeperRegulator.peg_keepers(arg0: uint256) -> PegKeeperInfo: view`]
Getter for the PegKeeper contract at index `arg0`.
Returns: `PegKeeperInfo` (`struct`) consisting of the PegKeeper (`address`), its associated pool (`address`), if it is inverse (`bool`) and wether the pool has more than two coins (`bool`).
| Input | Type | Description |
| ------ | --------- | ----------- |
| `arg0` | `address` | Index of the PegKeeper; starts at 0 |
```vyper
struct PegKeeperInfo:
peg_keeper: PegKeeper
pool: StableSwap
is_inverse: bool
include_index: bool
peg_keepers: public(DynArray[PegKeeperInfo, MAX_LEN])
```
```shell
>>> PegKeeperRegulator.peg_keepers(0)
'0x9201da0D97CaAAff53f01B2fB56767C7072dE340, 0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E, false, false'
>>> PegKeeperRegulator.peg_keepers(1)
'0xFb726F57d251aB5C731E5C64eD4F5F94351eF9F3, 0x390f3595bCa2Df7d23783dFd126427CCeb997BF4, false, false'
```
::::
### `add_peg_keepers`
::::description[`PegKeeperRegulator.add_peg_keepers(_peg_keepers: DynArray[PegKeeper, MAX_LEN])`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to add one or more `PegKeepers` to the `Regulator`. Simultaneously, the PegKeeper is added to the `peg_keepers` list and indexed in `peg_keeper_i`.
Emits: `AddPegKeeper`
| Input | Type | Description |
| -------------- | --------- | ------------ |
| `_peg_keepers` | `DynArray[PegKeeper, MAX_LEN]` | PegKeeper contracts to add |
```vyper
event AddPegKeeper:
peg_keeper: PegKeeper
pool: StableSwap
is_inverse: bool
struct PegKeeperInfo:
peg_keeper: PegKeeper
pool: StableSwap
is_inverse: bool
include_index: bool
@external
def add_peg_keepers(_peg_keepers: DynArray[PegKeeper, MAX_LEN]):
assert msg.sender == self.admin
i: uint256 = len(self.peg_keepers)
for pk in _peg_keepers:
assert self.peg_keeper_i[pk] == empty(uint256) # dev: duplicate
pool: StableSwap = pk.pool()
success: bool = raw_call(
pool.address, _abi_encode(convert(0, uint256), method_id=method_id("price_oracle(uint256)")),
revert_on_failure=False
)
info: PegKeeperInfo = PegKeeperInfo({
peg_keeper: pk,
pool: pool,
is_inverse: pk.IS_INVERSE(),
include_index: success,
})
self.peg_keepers.append(info) # dev: too many pairs
i += 1
self.peg_keeper_i[pk] = i
log AddPegKeeper(info.peg_keeper, info.pool, info.is_inverse)
```
```shell
>>> soon
```
::::
### `remove_peg_keepers`
::::description[`PegKeeperRegulator.remove_peg_keepers(_peg_keepers: DynArray[PegKeeper, MAX_LEN])`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to remove one or more `PegKeepers` from the `Regulator` contract.
Emits: `RemovePegKeeper`
| Input | Type | Description |
| -------------- | --------- | ------------ |
| `_peg_keepers` | `DynArray[PegKeeper, MAX_LEN]` | PegKeeper contracts to remove |
```vyper
event RemovePegKeeper:
peg_keeper: PegKeeper
struct PegKeeperInfo:
peg_keeper: PegKeeper
pool: StableSwap
is_inverse: bool
include_index: bool
peg_keepers: public(DynArray[PegKeeperInfo, MAX_LEN])
peg_keeper_i: HashMap[PegKeeper, uint256] # 1 + index of peg keeper in a list
@external
def remove_peg_keepers(_peg_keepers: DynArray[PegKeeper, MAX_LEN]):
"""
@dev Most gas efficient will be sort pools reversely
"""
assert msg.sender == self.admin
peg_keepers: DynArray[PegKeeperInfo, MAX_LEN] = self.peg_keepers
for pk in _peg_keepers:
i: uint256 = self.peg_keeper_i[pk] - 1 # dev: pool not found
max_n: uint256 = len(peg_keepers) - 1
if i < max_n:
peg_keepers[i] = peg_keepers[max_n]
self.peg_keeper_i[peg_keepers[i].peg_keeper] = 1 + i
peg_keepers.pop()
self.peg_keeper_i[pk] = empty(uint256)
log RemovePegKeeper(pk)
self.peg_keepers = peg_keepers
```
```shell
>>> soon
```
::::
---
## Pausing and Unpausing PegKeepers
In this context, **"killing"**refers to either **pausing or unpausing PegKeepers**. When the Regulator is "killed," it means the contract **restricts the PegKeeper from performing one or both of the following actions: providing or withdrawing crvUSD.**Both actions, providing and withdrawing, can be killed separately. For example, the Regulator can kill the permission to provide any additional crvUSD to pools but keep the withdrawing action "unkilled" so that it is still possible to unload debt.
Only the `admin` and `emergency_admin` are able to kill. The former is the Curve DAO, and the latter is the EmergencyDAO.
### `is_killed`
::::description[`PegKeeperRegulator.is_killed() -> uint256: view`]
Getter to check if the Regulator allows providing or withdrawing.
Returns: index value of the `Killed` enum (`bool`).
```vyper
enum Killed:
Provide # 1
Withdraw # 2
is_killed: public(Killed)
```
```shell
>>> PegKeeperRegulator.is_killed()
0 # nothing killed
>>> PegKeeperRegulator.is_killed()
1 # providing killed
>>> PegKeeperRegulator.is_killed()
2 # withdrawing killed
>>> PegKeeperRegulator.is_killed()
3 # providing and withdrawing killed
```
::::
### `set_killed`
::::description[`PegKeeperRegulator.set_killed(_is_killed: Killed)`]
:::guard[Guarded Method]
This function can only be called by the `admin` or `emergency` of the contract.
:::
Function to pause or unpause PegKeepers.
There are four options for pausing/unpausing, depending on the value set for the `Killed` enum:
- `0` -> provide and withdraw allowed
- `1` -> provide paused, withdraw allowed
- `2` -> provide allowed, withdraw paused
- `3` -> provide and withdraw paused
Emits: `SetKilled`
| Input | Type | Description |
| ------------ | --------- | ----------- |
| `_is_killed` | `uint256` | Value depending on the action wanted |
```vyper
event SetKilled:
is_killed: Killed
by: address
@external
def set_killed(_is_killed: Killed):
"""
@notice Pause/unpause Peg Keepers
@dev 0 unpause, 1 provide, 2 withdraw, 3 everything
"""
assert msg.sender in [self.admin, self.emergency_admin]
self.is_killed = _is_killed
log SetKilled(_is_killed, msg.sender)
```
```shell
>>> soon
```
::::
---
## Contract Ownership
The Regulator contract has two types of ownerships, the `admin` and the `emergency_admin`.
While the `admin` is able to call any guarded function from the contract, like setting new parameters and pausing/unpausing PegKeepers, etc., the `emergency_admin` is only allowed to pause and unpause pools. More on pausing pools [here](#pausing-and-unpausing-pegkeepers).
Both their ownerships can be transferred using the corresponding [`set_admin`](#set_admin) or [`set_emergency_admin`](#set_emergency_admin) functions.
### `admin`
::::description[`PegKeeperRegulator.admin() -> address: view`]
Getter for the current admin of the Regulator contract. This address can only be changed by the `admin` by calling the [`set_admin`](#set_admin) function.
Returns: current admin (`address`).
Emits: `SetAdmin` at contract initialization
```vyper
event SetAdmin:
admin: address
admin: public(address)
@external
def __init__(_stablecoin: ERC20, _agg: Aggregator, _fee_receiver: address, _admin: address, _emergency_admin: address):
...
self.admin = _admin
log SetAdmin(_admin)
...
```
```shell
>>> PegKeeperRegulator.admin()
'0x40907540d8a6C65c637785e8f8B742ae6b0b9968'
```
::::
### `set_admin`
::::description[`PegKeeperRegulator.set_admin(_admin: address)`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to set a new admin for the contract.
Emits: `SetAdmin`
| Input | Type | Description |
| -------- | --------- | ------------ |
| `_admin` | `address` | New admin address |
```vyper
event SetAdmin:
admin: address
admin: public(address)
@external
def set_admin(_admin: address):
# We are not doing commit / apply because the owner will be a voting DAO anyway
# which has vote delays
assert msg.sender == self.admin
self.admin = _admin
log SetAdmin(_admin)
```
```shell
>>> soon
```
::::
### `emergency_admin`
::::description[`PegKeeperRegulator.emergency_admin() -> address: view`]
Getter for the current emergency admin of the Regulator contract. This address can only be changed by the `admin` by calling the [`set_emergency_admin`](#set_emergency_admin) function.
Returns: emergency admin (`address`).
Emits: `SetEmergencyAdmin` at contract initialization
```vyper
event SetEmergencyAdmin:
admin: address
emergency_admin: public(address)
@external
def __init__(_stablecoin: ERC20, _agg: Aggregator, _fee_receiver: address, _admin: address, _emergency_admin: address):
...
self.emergency_admin = _emergency_admin
log SetEmergencyAdmin(_emergency_admin)
...
```
```shell
>>> PegKeeperRegulator.emergency_admin()
'0x467947EE34aF926cF1DCac093870f613C96B1E0c'
```
::::
### `set_emergency_admin`
::::description[`PegKeeperRegulator.set_emergency_admin(_admin: address)`]
Function to set a new emergency admin for the contract.
| Input | Type | Description |
| -------- | --------- | ------------ |
| `_admin` | `address` | New emergency admin address |
Emits: `SetEmergencyAdmin`
```vyper
event SetEmergencyAdmin:
admin: address
emergency_admin: public(address)
@external
def set_emergency_admin(_admin: address):
assert msg.sender == self.admin
self.emergency_admin = _admin
log SetEmergencyAdmin(_admin)
```
```shell
>>> soon
```
::::
---
## Fee Receiver and Aggregator Contract
### `fee_receiver`
::::description[`PegKeeperRegulator.fee_receiver() -> address: view`]
Getter for the fee receiver. The fee receiver can be changed via the [`set_fee_receiver`](#set_fee_receiver) function.
Returns: fee receiver (`address`).
```vyper
fee_receiver: public(address)
@external
def __init__(_stablecoin: ERC20, _agg: Aggregator, _fee_receiver: address, _admin: address, _emergency_admin: address):
...
self.fee_receiver = _fee_receiver
...
```
```shell
>>> PegKeeperRegulator.fee_receiver()
'0xeCb456EA5365865EbAb8a2661B0c503410e9B347'
```
::::
### `aggregator`
::::description[`PegKeeperRegulator.aggregator() -> address: view`]
Getter for the crvusd price aggregator contract. This address is set when intializing the contract and can be changed using [`set_aggregator`](#set_aggregator).
Returns: price aggregator contract (`address`).
```vyper
interface Aggregator:
def price() -> uint256: view
def price_w() -> uint256: nonpayable
aggregator: public(Aggregator)
@external
def __init__(_stablecoin: ERC20, _agg: Aggregator, _admin: address, _emergency_admin: address):
...
self.aggregator = _agg
...
```
```shell
>>> PegKeeperRegulator.aggregator()
'0x18672b1b0c623a30089A280Ed9256379fb0E4E62'
```
::::
### `set_fee_receiver`
::::description[`PegKeeperRegulator.set_fee_receiver(_fee_receiver: address)`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to set a new fee receiver.
Emits: `SetFeeReceiver`
| Input | Type | Description |
| --------------- | --------- | ------------------------ |
| `_fee_receiver` | `address` | New fee receiver address |
```vyper
fee_receiver: public(address)
event SetFeeReceiver:
fee_receiver: address
@external
def set_fee_receiver(_fee_receiver: address):
"""
@notice Set new PegKeeper's profit receiver
"""
assert msg.sender == self.admin
self.fee_receiver = _fee_receiver
log SetFeeReceiver(_fee_receiver)
```
```shell
>>> soon
```
::::
### `set_aggregator`
::::description[`PegKeeperRegulator.set_aggregator(_agg: Aggregator)`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to set a new aggregator contract.
Emits: `SetAggregator`
| Input | Type | Description |
| --------------- | --------- | ----------------------- |
| `_agg` | `address` | New aggregator contract |
```vyper
event SetAggregator:
aggregator: address
aggregator: public(Aggregator)
@external
def set_aggregator(_agg: Aggregator):
"""
@notice Set new crvUSD price aggregator
"""
assert msg.sender == self.admin
self.aggregator = _agg
log SetAggregator(_agg.address)
```
```shell
>>> soon
```
::::
---
## Other Methods
### `stablecoin`
::::description[`PegKeeperRegulator.stablecoin() -> address: view`]
Getter for the stablecoin the PegKeeper stabilizes, which is crvUSD. This address is set when intializing the contract and can not be changed.
Returns: stablecoin (`address`).
```vyper
interface ERC20:
def balanceOf(_owner: address) -> uint256: view
STABLECOIN: immutable(ERC20)
@external
@view
def stablecoin() -> ERC20:
return STABLECOIN
@external
def __init__(_stablecoin: ERC20, _agg: Aggregator, _admin: address, _emergency_admin: address):
STABLECOIN = _stablecoin
...
```
```shell
>>> PegKeeperRegulator.stablecoin()
'0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'
```
::::
---
## PegKeeperV1
## Concept of PegKeepers
PegKeepers are contracts that help stabilize the peg of crvUSD. Each Keeper is allocated a specific amount of crvUSD to secure the peg.
The DAO decides this balance and can be **raised or lowered** by calling `set_debt_ceiling()` in the [Factory](../factory.md).
The underlying actions of the PegKeepers can be divided into two actions, which get executed when calling [`update()`](#update):
- **crvUSD price > 1**: The PegKeeper mints and deposits crvUSD single-sidedly into the pool to which it is "linked", and receives LP tokens in exchange. This increases the balance of crvUSD in the pool and therefore decreases the price.
It is important to note that the LP tokens are not staked in the gauge (if there is one). Thus, the PegKeeper does not receive CRV emissions.
- **crvUSD price < 1**: If PegKeepers hold a balance of the corresponding LP token, they can single-sidedly withdraw crvUSD from the liquidity pool and burn it. This action reduces the supply of crvUSD in the pool and should subsequently increase its price.
:::note
PegKeepers **do not actually mint or burn crvUSD tokens**. They have a defined allocated balance of crvUSD tokens that they can use for deposits. It is important to note that **PegKeepers cannot do anything else apart from depositing and withdrawing**. Therefore, crvUSD token balances of the PegKeepers that are not deposited into a pool may not be counted as circulating supply, although technically they are.
:::
:::vyper[`PegKeeper.vy`]
Source code for this contract is available on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/stabilizer/PegKeeper.vy).
| PegKeepers | Deployment Address |
| ------------------------- | ------------------- |
|`PegKeeper for crvusd/USDC`|[0xaA346781dDD7009caa644A4980f044C50cD2ae22](https://etherscan.io/address/0xaA346781dDD7009caa644A4980f044C50cD2ae22#code)|
|`PegKeeper for crvusd/USDT`|[0xE7cd2b4EB1d98CD6a4A48B6071D46401Ac7DC5C8](https://etherscan.io/address/0xE7cd2b4EB1d98CD6a4A48B6071D46401Ac7DC5C8#code)|
|`PegKeeper for crvusd/USDP`|[0x6B765d07cf966c745B340AdCa67749fE75B5c345](https://etherscan.io/address/0x6B765d07cf966c745B340AdCa67749fE75B5c345#code)|
|`PegKeeper for crvusd/TUSD`|[0x1ef89Ed0eDd93D1EC09E4c07373f69C49f4dcCae](https://etherscan.io/address/0x1ef89Ed0eDd93D1EC09E4c07373f69C49f4dcCae#code)|
:::
## Stabilisation Method
The most important function in the PegKeeper contract is the `update()` function. When invoked, the PegKeeper either mints and single-sidedly deposits crvUSD into the StableSwap pool, or it withdraws crvUSD from the pool by redeeming the LP tokens received from previous deposits.
- **Deposit and Mint:**This mechanism is triggered when the *price of crvUSD > 1*. Minting and depositing into the pool will increase the crvUSD supply and decrease its price. The LP tokens that the PegKeeper receives when depositing crvUSD into the pool are not staked in the gauge (if the pool has one), which means the PegKeeper does not receive CRV inflation rewards.
- **Withdraw and Burn:**This mechanism is triggered when the *price of crvUSD < 1*. By withdrawing crvUSD from the pool, the supply of crvUSD decreases, which increases its price.
PegKeepers have unlimited approval for the liquidity pool, allowing them to deposit and withdraw crvUSD.
### `update`
::::description[`PegKeeper.update(_beneficiary: address = msg.sender) -> uint256`]
Function to either **mint and deposit**or **withdraw and burn**based on the balances within the pools.
A share (`caller_share`) of the generated profit will be awarded to the function's caller. By default, this is set to `msg.sender`, but there is also the possibility to input a `_beneficiary` address to which the rewards will be sent.
Returns: caller profit (`uint256`).
Emits: `Provide` or `Withdraw`
:::note
There is an `ACTION_DELAY` of 15 minutes before calling the function again.
:::
```vyper
event Provide:
amount: uint256
@external
@nonpayable
def update(_beneficiary: address = msg.sender) -> uint256:
"""
@notice Provide or withdraw coins from the pool to stabilize it
@param _beneficiary Beneficiary address
@return Amount of profit received by beneficiary
"""
if self.last_change + ACTION_DELAY > block.timestamp:
return 0
balance_pegged: uint256 = POOL.balances(I)
balance_peg: uint256 = POOL.balances(1 - I) * PEG_MUL
initial_profit: uint256 = self._calc_profit()
p_agg: uint256 = AGGREGATOR.price() # Current USD per stablecoin
# Checking the balance will ensure no-loss of the stabilizer, but to ensure stabilization
# we need to exclude "bad" p_agg, so we add an extra check for it
if balance_peg > balance_pegged:
assert p_agg >= 10**18
self._provide((balance_peg - balance_pegged) / 5) # this dumps stablecoin
else:
assert p_agg <= 10**18
self._withdraw((balance_pegged - balance_peg) / 5) # this pumps stablecoin
# Send generated profit
new_profit: uint256 = self._calc_profit()
assert new_profit >= initial_profit, "peg unprofitable"
lp_amount: uint256 = new_profit - initial_profit
caller_profit: uint256 = lp_amount * self.caller_share / SHARE_PRECISION
if caller_profit > 0:
POOL.transfer(_beneficiary, caller_profit)
return caller_profit
@internal
def _provide(_amount: uint256):
# We already have all reserves here
# ERC20(PEGGED).mint(self, _amount)
if _amount == 0:
return
amounts: uint256[2] = empty(uint256[2])
amounts[I] = _amount
POOL.add_liquidity(amounts, 0)
self.last_change = block.timestamp
self.debt += _amount
log Provide(_amount)
```
```vyper
event Withdraw:
amount: uint256
@external
@nonpayable
def update(_beneficiary: address = msg.sender) -> uint256:
"""
@notice Provide or withdraw coins from the pool to stabilize it
@param _beneficiary Beneficiary address
@return Amount of profit received by beneficiary
"""
if self.last_change + ACTION_DELAY > block.timestamp:
return 0
balance_pegged: uint256 = POOL.balances(I)
balance_peg: uint256 = POOL.balances(1 - I) * PEG_MUL
initial_profit: uint256 = self._calc_profit()
p_agg: uint256 = AGGREGATOR.price() # Current USD per stablecoin
# Checking the balance will ensure no-loss of the stabilizer, but to ensure stabilization
# we need to exclude "bad" p_agg, so we add an extra check for it
if balance_peg > balance_pegged:
assert p_agg >= 10**18
self._provide((balance_peg - balance_pegged) / 5) # this dumps stablecoin
else:
assert p_agg <= 10**18
self._withdraw((balance_pegged - balance_peg) / 5) # this pumps stablecoin
# Send generated profit
new_profit: uint256 = self._calc_profit()
assert new_profit >= initial_profit, "peg unprofitable"
lp_amount: uint256 = new_profit - initial_profit
caller_profit: uint256 = lp_amount * self.caller_share / SHARE_PRECISION
if caller_profit > 0:
POOL.transfer(_beneficiary, caller_profit)
return caller_profit
@internal
def _withdraw(_amount: uint256):
if _amount == 0:
return
debt: uint256 = self.debt
amount: uint256 = min(_amount, debt)
amounts: uint256[2] = empty(uint256[2])
amounts[I] = amount
POOL.remove_liquidity_imbalance(amounts, max_value(uint256))
self.last_change = block.timestamp
self.debt -= amount
log Withdraw(amount)
```
```shell
>>> PegKepper.update()
```
::::
### `last_change`
::::description[`PegKeeper.last_change() -> uint256: view`]
Function which retrieves the timestamp of when the balances of the PegKeeper were last altered. This variable is updated each time `update()` (`_provide` or `_withdraw`) is called. This variable is of importance for `update()`, as there is a mandatory delay of 15 * 60 seconds before the function can be called again.
Returns: timestamp (`uint256`).
```vyper
last_change: public(uint256)
```
```shell
>>> PegKepper.last_change()
1688794235
```
::::
## Calculating and Withdrawing Profits
### `calc_profit`
::::description[`PegKeeper.calc_profit() -> uint256`]
Function to calculate the generated profit in LP tokens.
Returns: generated profit (`uint256`).
```vyper
PRECISION: constant(uint256) = 10 **18
# Calculation error for profit
PROFIT_THRESHOLD: constant(uint256) = 10 **18
@internal
@view
def _calc_profit() -> uint256:
lp_balance: uint256 = POOL.balanceOf(self)
virtual_price: uint256 = POOL.get_virtual_price()
lp_debt: uint256 = self.debt * PRECISION / virtual_price + PROFIT_THRESHOLD
if lp_balance <= lp_debt:
return 0
else:
return lp_balance - lp_debt
@external
@view
def calc_profit() -> uint256:
"""
@notice Calculate generated profit in LP tokens
@return Amount of generated profit
"""
return self._calc_profit()
```
```shell
>>> PegKepper.calc_profit()
41173451286504149038
```
::::
### `estimate_caller_profit`
::::description[`PegKeeper.estimate_caller_profit() -> uint256`]
Function to estimate the profit from calling `update()`. The caller of the function will receive 20% of the total profits.
Returns: expected amount of profit going to the caller (`uint256`).
:::warning
Please note that this method provides an estimate and may not reflect the precise profit. The actual profit tends to be higher due to the increasing virtual price of the LP token.
:::
```vyper
ACTION_DELAY: constant(uint256) = 15 * 60
@external
@view
def estimate_caller_profit() -> uint256:
"""
@notice Estimate profit from calling update()
@dev This method is not precise, real profit is always more because of increasing virtual price
@return Expected amount of profit going to beneficiary
"""
if self.last_change + ACTION_DELAY > block.timestamp:
return 0
balance_pegged: uint256 = POOL.balances(I)
balance_peg: uint256 = POOL.balances(1 - I) * PEG_MUL
initial_profit: uint256 = self._calc_profit()
p_agg: uint256 = AGGREGATOR.price() # Current USD per stablecoin
# Checking the balance will ensure no-loss of the stabilizer, but to ensure stabilization
# we need to exclude "bad" p_agg, so we add an extra check for it
new_profit: uint256 = 0
if balance_peg > balance_pegged:
if p_agg < 10**18:
return 0
new_profit = self._calc_future_profit((balance_peg - balance_pegged) / 5, True) # this dumps stablecoin
else:
if p_agg > 10**18:
return 0
new_profit = self._calc_future_profit((balance_pegged - balance_peg) / 5, False) # this pumps stablecoin
if new_profit < initial_profit:
return 0
lp_amount: uint256 = new_profit - initial_profit
return lp_amount * self.caller_share / SHARE_PRECISION
```
```shell
>>> PegKepper.estimate_caller_profit()
0
```
::::
### `caller_share`
::::description[`PegKeeper.caller_share() -> uint256: view`]
Getter for the caller share which is the share of the profit generated when calling the `update()` function. The share is intended to incentivize the call of the function. The precision of the variable is set to $10^5$.
Returns: caller share (`uint256`).
```vyper
SHARE_PRECISION: constant(uint256) = 10 **5
caller_share: public(uint256)
@external
def __init__(_pool: CurvePool, _index: uint256, _receiver: address, _caller_share: uint256, _factory: address, _aggregator: StableAggregator, _admin: address):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _index Index of the pegged
@param _receiver Receiver of the profit
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _aggregator Price aggregator which shows the price of pegged in real "dollars"
@param _admin Admin account
"""
assert _index < 2
POOL = _pool
I = _index
pegged: address = _pool.coins(_index)
PEGGED = pegged
ERC20(pegged).approve(_pool.address, max_value(uint256))
ERC20(pegged).approve(_factory, max_value(uint256))
PEG_MUL = 10 **(18 - ERC20(_pool.coins(1 - _index)).decimals())
self.admin = _admin
assert _receiver != empty(address)
self.receiver = _receiver
log ApplyNewAdmin(msg.sender)
log ApplyNewReceiver(_receiver)
assert _caller_share <= SHARE_PRECISION # dev: bad part value
self.caller_share = _caller_share
log SetNewCallerShare(_caller_share)
FACTORY = _factory
AGGREGATOR = _aggregator
IS_INVERSE = (_index == 0)
```
```shell
>>> PegKepper.caller_share()
20000
```
::::
### `set_new_caller_share`
::::description[`PegKeeper.set_new_caller_share(_new_caller_share: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set the caller share to `_new_caller_share`.
| Input | Type | Description |
| ----------- | -------| ----|
| `_new_caller_share` | `uint256` | New caller share |
Emits: `SetNewCallerShare`
```vyper
event SetNewCallerShare:
caller_share: uint256
SHARE_PRECISION: constant(uint256) = 10 **5
caller_share: public(uint256)
@external
@nonpayable
def set_new_caller_share(_new_caller_share: uint256):
"""
@notice Set new update caller's part
@param _new_caller_share Part with SHARE_PRECISION
"""
assert msg.sender == self.admin # dev: only admin
assert _new_caller_share <= SHARE_PRECISION # dev: bad part value
self.caller_share = _new_caller_share
log SetNewCallerShare(_new_caller_share)
```
```shell
>>> PegKepper.set_new_caller_share(30000)
```
::::
### `withdraw_profit`
::::description[`PegKeeper.withdraw_profit() -> uint256`]
Function to withdraw the profit generated by the PegKeeper.
Returns: amount of LP tokens (`uint256`).
Emits: `Profit`
```vyper
event Profit:
lp_amount: uint256
@external
@nonpayable
def withdraw_profit() -> uint256:
"""
@notice Withdraw profit generated by Peg Keeper
@return Amount of LP Token received
"""
lp_amount: uint256 = self._calc_profit()
POOL.transfer(self.receiver, lp_amount)
log Profit(lp_amount)
return lp_amount
```
```shell
>>> PegKepper.withdraw_profit():
1222209056795882453168
```
::::
## Admin and Receiver
PegKeepers have an `admin` and a `receiver`. Both of these variables can be changed by calling the respective admin-guarded functions, but such changes must first be approved by a DAO vote.
After approval, the newly designated admin or receiver is required to apply these changes within a timeframe of `3 * 86400` seconds, which equates to a timespan of *three days*. Should there be an attempt to implement these changes after this period, the function will revert.
### `admin`
::::description[`PegKeeper.admin() -> address: view`]
Getter for the admin of the PegKeeper.
Returns: admin (`address`).
```vyper
admin: public(address)
@external
def __init__(_pool: CurvePool, _index: uint256, _receiver: address, _caller_share: uint256, _factory: address, _aggregator: StableAggregator, _admin: address):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _index Index of the pegged
@param _receiver Receiver of the profit
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _aggregator Price aggregator which shows the price of pegged in real "dollars"
@param _admin Admin account
"""
...
self.admin = _admin
...
```
```shell
>>> PegKepper.admin()
'0x40907540d8a6C65c637785e8f8B742ae6b0b9968'
```
::::
### `future_admin`
::::description[`PegKeeper.future_admin() -> address: view`]
Getter for the future admin of the PegKeeper.
Returns: future admin (`address`).
```vyper
future_admin: public(address)
```
```shell
>>> PegKepper.future_admin()
'0x0000000000000000000000000000000000000000'
```
::::
### `commit_new_admin`
::::description[`PegKeeper.commit_new_admin(_new_admin: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to commit a new admin.
| Input | Type | Description |
| ----------- | -------| ----|
| `_new_admin` | `address` | new admin address |
Emits: `CommitNewAdmin`
```vyper
event CommitNewAdmin:
admin: address
@external
@nonpayable
def commit_new_admin(_new_admin: address):
"""
@notice Commit new admin of the Peg Keeper
@param _new_admin Address of the new admin
"""
assert msg.sender == self.admin # dev: only admin
assert self.new_admin_deadline == 0 # dev: active action
deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY
self.new_admin_deadline = deadline
self.future_admin = _new_admin
log CommitNewAdmin(_new_admin)
```
```shell
>>> PegKepper.commit_new_admin("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
```
::::
### `apply_new_admin`
::::description[`PegKeeper.apply_new_admin()`]
:::guard[Guarded Method]
This function is only callable by the `future_admin` of the contract.
:::
Function to apply the new admin of the PegKeeper.
Emits: `ApplyNewAdmin`
```vyper
event ApplyNewAdmin:
admin: address
@external
@nonpayable
def apply_new_admin():
"""
@notice Apply new admin of the Peg Keeper
@dev Should be executed from new admin
"""
new_admin: address = self.future_admin
assert msg.sender == new_admin # dev: only new admin
assert block.timestamp >= self.new_admin_deadline # dev: insufficient time
assert self.new_admin_deadline != 0 # dev: no active action
self.admin = new_admin
self.new_admin_deadline = 0
log ApplyNewAdmin(new_admin)
```
```shell
>>> PegKepper.apply_new_admin()
```
::::
### `new_admin_deadline`
::::description[`PegKeeper.new_admin_deadline() -> uint256: view`]
Getter for the timestamp indicating the deadline by which the `future_admin` can apply the admin change. Once the deadline is over, the address will no longer be able to apply the changes. The deadline is set for a **timeperiod of three days**.
Returns: timestamp (`uint256`).
```vyper
new_admin_deadline: public(uint256)
```
```shell
>>> PegKepper.new_admin_deadline()
0
```
::::
### `receiver`
::::description[`PegKeeper.receiver() -> address: view`]
Getter for the receiver of the PegKeeper's profits.
Returns: receiver (`address`).
```vyper
receiver: public(address)
@external
def __init__(_pool: CurvePool, _index: uint256, _receiver: address, _caller_share: uint256, _factory: address, _aggregator: StableAggregator, _admin: address):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _index Index of the pegged
@param _receiver Receiver of the profit
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _aggregator Price aggregator which shows the price pegged in real "dollars"
@param _admin Admin account
"""
...
assert _receiver != empty(address)
self.receiver = _receiver
...
```
```shell
>>> PegKepper.receiver()
'0xeCb456EA5365865EbAb8a2661B0c503410e9B347'
```
::::
### `future_receiver`
::::description[`PegKeeper.future_receiver() -> address: view`]
Getter for the future receiver of the PegKeeper's profit.
Returns: future receiver (`address`).
```vyper
future_admin: public(address)
```
```shell
>>> PegKepper.future_receiver()
'0x0000000000000000000000000000000000000000'
```
::::
### `commit_new_receiver`
::::description[`PegKeeper.commit_new_receiver(_new_receiver: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to commit a new receiver address.
| Input | Type | Description |
| ----------- | -------| ----|
| `_new_receiver` | `address` | new receiver address |
Emits: `CommitNewReceiver`
```vyper
event CommitNewReceiver:
receiver: address
@external
@nonpayable
def commit_new_receiver(_new_receiver: address):
"""
@notice Commit new receiver of profit
@param _new_receiver Address of the new receiver
"""
assert msg.sender == self.admin # dev: only admin
assert self.new_receiver_deadline == 0 # dev: active action
deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY
self.new_receiver_deadline = deadline
self.future_receiver = _new_receiver
log CommitNewReceiver(_new_receiver)
```
```shell
>>> PegKepper.commit_new_receiver("0x0000000000000000000000000000000000000000")
```
::::
### `apply_new_receiver`
::::description[`PegKeeper.apply_new_receiver()`]
Function to apply the new receiver address of the PegKeeper's profit.
Emits: `ApplyNewReceiver`
```vyper
event ApplyNewReceiver:
receiver: address
@external
@nonpayable
def apply_new_receiver():
"""
@notice Apply new receiver of profit
"""
assert block.timestamp >= self.new_receiver_deadline # dev: insufficient time
assert self.new_receiver_deadline != 0 # dev: no active action
new_receiver: address = self.future_receiver
self.receiver = new_receiver
self.new_receiver_deadline = 0
log ApplyNewReceiver(new_receiver)
```
```shell
>>> PegKepper.apply_new_receiver():
```
::::
### `new_receiver_deadline`
::::description[`PegKeeper.new_receiver_deadline() -> uint256: view`]
Getter for the timestamp indicating the deadline by which the `future_receiver` can apply the receiver change. Once the deadline is over, the address will no longer be able to apply the changes. The deadline is set for a **timeperiod of three days**.
Returns: timestamp (`uint256`).
```vyper
new_receiver_deadline: public(uint256)
```
```shell
>>> PegKepper.new_receiver_deadline()
0
```
::::
### `revert_new_option`
::::description[`PegKeeper.revert_new_options()`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to revert admin or receiver changes. Calling this function sets the admin and receiver deadline back to 0 and emits ApplyNewAdmin and ApplyNewReceiver events to revert the changes.
Emits: `ApplyNewAdmin` and `ApplyNewReceiver`
```vyper
event ApplyNewReceiver:
receiver: address
event ApplyNewAdmin:
admin: address
@external
@nonpayable
def revert_new_options():
"""
@notice Revert new admin of the Peg Keeper or new receiver
@dev Should be executed from admin
"""
assert msg.sender == self.admin # dev: only admin
self.new_admin_deadline = 0
self.new_receiver_deadline = 0
log ApplyNewAdmin(self.admin)
log ApplyNewReceiver(self.receiver)
```
```shell
>>> PegKepper.revert_new_options():
```
::::
## Contract Info Methods
### `debt`
::::description[`PegKeeper.debt() -> uint256: view`]
Getter for the crvUSD debt of the PegKeeper. When the PegKeeper deposits crvUSD into the pool, the debt is incremented by the deposited amount. Conversely, if the PegKeeper withdraws, the debt is reduced by the withdrawn amount. `debt` is used to calculate the DebtFraction of the PegKeepers.
Returns: debt (`uint256`).
```vyper
debt: public(uint256)
```
```shell
>>> PegKepper.debt()
10569198033275719942044356
```
::::
### `FACTORY`
::::description[`PegKeeper.FACTORY() -> address: view`]
Getter for the address of the factory contract.
Returns: factory contract (`address`).
```vyper
FACTORY: immutable(address)
@external
def __init__(_pool: CurvePool, _index: uint256, _receiver: address, _caller_share: uint256, _factory: address, _aggregator: StableAggregator, _admin: address):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _index Index of the pegged
@param _receiver Receiver of the profit
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _aggregator Price aggregator which shows the price of pegged in real "dollars"
@param _admin Admin account
"""
...
FACTORY = _factory
...
```
```shell
>>> PegKepper.FACTORY()
'0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC'
```
::::
### `PEGGED`
::::description[`PegKeeper.PEGGED() -> address: view`]
Getter for the address of the pegged token (crvUSD). Pegged asset is determined by the index of the token in the corresponding `pool`. Index value is stored in `I`.
Returns: pegged token contract (`address`).
```vyper
PEGGED: immutable(address)
@external
def __init__(_pool: CurvePool, _index: uint256, _receiver: address, _caller_share: uint256, _factory: address, _aggregator: StableAggregator, _admin: address):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _index Index of the pegged
@param _receiver Receiver of the profit
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _aggregator Price aggregator which shows the price of pegged in real "dollars"
@param _admin Admin account
"""
...
PEGGED = pegged
...
```
```shell
>>> PegKepper.PEGGED()
'0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'
```
::::
### `POOL`
::::description[`PegKeeper.POOL() -> address: view`]
Getter for the pool contract address in which the PegKeeper deposits and withdraws.
Returns: pool contract (`address`).
```vyper
POOL: immutable(CurvePool)
@external
def __init__(_pool: CurvePool, _index: uint256, _receiver: address, _caller_share: uint256, _factory: address, _aggregator: StableAggregator, _admin: address):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _index Index of the pegged
@param _receiver Receiver of the profit
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _aggregator Price aggregator which shows the price of pegged in real "dollars"
@param _admin Admin account
"""
...
POOL = _pool
...
```
```shell
>>> PegKepper.POOL()
'0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E'
```
::::
### `AGGREGATOR`
::::description[`PegKeeper.AGGREGATOR() -> address: view`]
Getter for the price aggregator contract for crvUSD. This contract is used to determine the value of crvUSD.
Returns: price aggregator contract (`address`).
```vyper
AGGREGATOR: immutable(StableAggregator)
@external
def __init__(_pool: CurvePool, _index: uint256, _receiver: address, _caller_share: uint256, _factory: address, _aggregator: StableAggregator, _admin: address):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _index Index of the pegged
@param _receiver Receiver of the profit
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _aggregator Price aggregator which shows the price of pegged in real "dollars"
@param _admin Admin account
"""
...
AGGREGATOR = _aggregator
...
```
```shell
>>> PegKepper.AGGREGATOR()
'0xe5Afcf332a5457E8FafCD668BcE3dF953762Dfe7'
```
::::
---
## PegKeeperV2
:::vyper[`PegKeeperV2.vy`]
Source code for the `PegKeeperV2.vy` contract is available on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/stabilizer/PegKeeperV2.vy). Relevant deployments can be found [here](../../deployments.md).
:::
## Stabilization Method Enhancement in PegKeeperV2
The `PegKeeperV2` retains the overarching stabilization approach of its predecessor, `PegKeeperV1`, through the `update` function. This function adapts its operations based on varying conditions to take appropriate measures for maintaining stability.
A significant evolution from `PegKeeperV1` is the integration with the `PegKeeperRegulator` contract. This new contract plays a crucial role in granting allowance to the PegKeepers to deposit into or withdraw from the liquidity pool. Depositing increases the debt of a PegKeeper, while withdrawing reduces it.
For a detailed overview on the additional checks implemented, please see: [Providing](./peg-keeper-regulator.md#providing)[^1] and [Withdrawing](./peg-keeper-regulator.md#withdrawing).
[^1]: In this context, "providing" is the terminology adopted by the new PegKeeper to describe the act of depositing crvUSD into a liquidity pool, marking a shift from the conventional term "depositing."
### `update`
::::description[`PegKeeperV2.update(_beneficiary: address = msg.sender) -> uint256`]
Function to provide or withdraw coins from the pool to stabilize it. The `_beneficiary` address is awarded a share of the profits for calling the function. There is a delay determined by the `action_delay` variable before the function can be called again. If it is called prior to that, the function will return 0. The maximum amount to provide is to get the pool to a 50/50 balance. Obviously, the PegKeeper is ultimately limited by its own balance of crvUSD. It can't deposit more than it has.
If a PegKeeper is ultimately allowed to deposit or withdraw is determined by the [`PegKeeperRegulator`](./peg-keeper-regulator.md).
| Input | Type | Description |
| -------------- | --------- | ----------- |
| `_beneficiary` | `address` | Address to receive the caller profit. Defaults to `msg.sender` |
Returns: amount of profit received by the beneficiary (`uint256`).
Emits: `Provide` or `Withdraw`
```vyper
event Provide:
amount: uint256
action_delay: uint256
POOL: immutable(CurvePool)
I: immutable(uint256) # index of pegged in pool
@external
@nonpayable
def update(_beneficiary: address = msg.sender) -> uint256:
"""
@notice Provide or withdraw coins from the pool to stabilize it
@param _beneficiary Beneficiary address
@return Amount of profit received by beneficiary
"""
if self.last_change + self.action_delay > block.timestamp:
return 0
balance_pegged: uint256 = POOL.balances(I)
balance_peg: uint256 = POOL.balances(1 - I) * PEG_MUL
initial_profit: uint256 = self._calc_profit()
if balance_peg > balance_pegged:
allowed: uint256 = self.regulator.provide_allowed()
assert allowed > 0, "Regulator ban"
self._provide(min(unsafe_sub(balance_peg, balance_pegged) / 5, allowed)) # this dumps stablecoin
else:
allowed: uint256 = self.regulator.withdraw_allowed()
assert allowed > 0, "Regulator ban"
self._withdraw(min(unsafe_sub(balance_pegged, balance_peg) / 5, allowed)) # this pumps stablecoin
# Send generated profit
new_profit: uint256 = self._calc_profit()
assert new_profit > initial_profit, "peg unprofitable"
lp_amount: uint256 = new_profit - initial_profit
caller_profit: uint256 = lp_amount * self.caller_share / SHARE_PRECISION
if caller_profit > 0:
POOL.transfer(_beneficiary, caller_profit)
return caller_profit
@internal
def _provide(_amount: uint256):
"""
@notice Implementation of provide
@dev Coins should be already in the contract
"""
if _amount == 0:
return
amount: uint256 = min(_amount, PEGGED.balanceOf(self))
if IS_NG:
amounts: DynArray[uint256, 2] = [0, 0]
amounts[I] = amount
CurvePoolNG(POOL.address).add_liquidity(amounts, 0)
else:
amounts: uint256[2] = empty(uint256[2])
amounts[I] = amount
CurvePoolOld(POOL.address).add_liquidity(amounts, 0)
self.last_change = block.timestamp
self.debt += amount
log Provide(amount)
@internal
@view
def _calc_profit() -> uint256:
"""
@notice Calculate PegKeeper's profit using current values
"""
return self._calc_profit_from(POOL.balanceOf(self), POOL.get_virtual_price(), self.debt)
@internal
@pure
def _calc_profit_from(lp_balance: uint256, virtual_price: uint256, debt: uint256) -> uint256:
"""
@notice PegKeeper's profit calculation formula
"""
lp_debt: uint256 = debt * PRECISION / virtual_price
if lp_balance <= lp_debt:
return 0
else:
return lp_balance - lp_debt
```
```vyper
@external
@view
def provide_allowed(_pk: address=msg.sender) -> uint256:
"""
@notice Allow PegKeeper to provide stablecoin into the pool
@dev Can return more amount than available
@dev Checks
1) current price in range of oracle in case of spam-attack
2) current price location among other pools in case of contrary coin depeg
3) stablecoin price is above 1
@return Amount of stablecoin allowed to provide
"""
if self.is_killed in Killed.Provide:
return 0
if self.aggregator.price() < ONE:
return 0
price: uint256 = max_value(uint256) # Will fail if PegKeeper is not in self.price_pairs
largest_price: uint256 = 0
debt_ratios: DynArray[uint256, MAX_LEN] = []
for info in self.peg_keepers:
price_oracle: uint256 = self._get_price_oracle(info)
if info.peg_keeper.address == _pk:
price = price_oracle
if not self._price_in_range(price, self._get_price(info)):
return 0
continue
elif largest_price < price_oracle:
largest_price = price_oracle
debt_ratios.append(self._get_ratio(info.peg_keeper))
if largest_price < unsafe_sub(price, self.worst_price_threshold):
return 0
debt: uint256 = PegKeeper(_pk).debt()
total: uint256 = debt + STABLECOIN.balanceOf(_pk)
return self._get_max_ratio(debt_ratios) * total / ONE - debt
```
```vyper
event Withdraw:
amount: uint256
action_delay: uint256
@external
@nonpayable
def update(_beneficiary: address = msg.sender) -> uint256:
"""
@notice Provide or withdraw coins from the pool to stabilize it
@param _beneficiary Beneficiary address
@return Amount of profit received by beneficiary
"""
if self.last_change + self.action_delay > block.timestamp:
return 0
balance_pegged: uint256 = POOL.balances(I)
balance_peg: uint256 = POOL.balances(1 - I) * PEG_MUL
initial_profit: uint256 = self._calc_profit()
if balance_peg > balance_pegged:
allowed: uint256 = self.regulator.provide_allowed()
assert allowed > 0, "Regulator ban"
self._provide(min(unsafe_sub(balance_peg, balance_pegged) / 5, allowed)) # this dumps stablecoin
else:
allowed: uint256 = self.regulator.withdraw_allowed()
assert allowed > 0, "Regulator ban"
self._withdraw(min(unsafe_sub(balance_pegged, balance_peg) / 5, allowed)) # this pumps stablecoin
# Send generated profit
new_profit: uint256 = self._calc_profit()
assert new_profit > initial_profit, "peg unprofitable"
lp_amount: uint256 = new_profit - initial_profit
caller_profit: uint256 = lp_amount * self.caller_share / SHARE_PRECISION
if caller_profit > 0:
POOL.transfer(_beneficiary, caller_profit)
return caller_profit
@internal
def _withdraw(_amount: uint256):
"""
@notice Implementation of withdraw
"""
if _amount == 0:
return
debt: uint256 = self.debt
amount: uint256 = min(_amount, debt)
if IS_NG:
amounts: DynArray[uint256, 2] = [0, 0]
amounts[I] = amount
CurvePoolNG(POOL.address).remove_liquidity_imbalance(amounts, max_value(uint256))
else:
amounts: uint256[2] = empty(uint256[2])
amounts[I] = amount
CurvePoolOld(POOL.address).remove_liquidity_imbalance(amounts, max_value(uint256))
self.last_change = block.timestamp
self.debt = debt - amount
log Withdraw(amount)
@internal
@view
def _calc_profit() -> uint256:
"""
@notice Calculate PegKeeper's profit using current values
"""
return self._calc_profit_from(POOL.balanceOf(self), POOL.get_virtual_price(), self.debt)
@internal
@pure
def _calc_profit_from(lp_balance: uint256, virtual_price: uint256, debt: uint256) -> uint256:
"""
@notice PegKeeper's profit calculation formula
"""
lp_debt: uint256 = debt * PRECISION / virtual_price
if lp_balance <= lp_debt:
return 0
else:
return lp_balance - lp_debt
```
```vyper
@external
@view
def withdraw_allowed(_pk: address=msg.sender) -> uint256:
"""
@notice Allow Peg Keeper to withdraw stablecoin from the pool
@dev Can return more amount than available
@dev Checks
1) current price in range of oracle in case of spam-attack
2) stablecoin price is below 1
@return Amount of stablecoin allowed to withdraw
"""
if self.is_killed in Killed.Withdraw:
return 0
if self.aggregator.price() > ONE:
return 0
i: uint256 = self.peg_keeper_i[PegKeeper(_pk)]
if i > 0:
info: PegKeeperInfo = self.peg_keepers[i - 1]
if self._price_in_range(self._get_price(info), self._get_price_oracle(info)):
return max_value(uint256)
return 0
```
```shell
>>> soon
```
::::
### `last_change`
::::description[`PegKeeperV2.last_change() -> uint256: view`]
Getter for the last time a change in debt occurred. This variable is set to `block.timestamp` whenever the PegKeeper provides or withdraws crvUSD by calling [`update`](#update).
Returns: timestamp (`uint256`).
```vyper
last_change: public(uint256)
```
```shell
>>> PegKeeperV2.last_change()
1722174559
```
::::
---
## Calculating and Withdrawing Profits
By providing and withdrawing assets through liquidity pools, the PegKeeper generates profit.
The PegKeeper has a caller share mechanism, which incentivizes external users to call the `update` function. This mechanism ensures that the PegKeeper operates efficiently and maintains the peg by distributing a portion of the profit to the caller.
The profit generated by the PegKeeper is denominated in LP tokens. When profit is withdrawn using the [`withdraw_profit`](#withdraw_profit) function, it is transferred to the universal fee receiver specified in the [`PegKeeperRegulator`](./peg-keeper-regulator.md#fee-receiver-and-aggregator-contract) contract.
### `calc_profit`
::::description[`PegKeeperV2.calc_profit() -> uint256`]
Function to calculate the generated profit in LP tokens. This profit calculation does not include already withdrawn profit; it represents the full profit accumulated so far. The profit is calculated using the following formula:
$$\text{profit} = \max(0, B_{LP} - \frac{\text{debt} \times 10^{18}}{VP_{LP}})$$
with:
- $B_{LP}$ is the LP token balance of the PegKeeper.
- $VP_{LP}$ is the virtual price of the LP token.
- $\text{debt}$ is the current debt of the PegKeeper (denominated in crvUSD).
Returns: calculated profit in LP tokens (`uint256`).
```vyper
@external
@view
def calc_profit() -> uint256:
"""
@notice Calculate generated profit in LP tokens. Does NOT include already withdrawn profit
@return Amount of generated profit
"""
return self._calc_profit()
@internal
@view
def _calc_profit() -> uint256:
"""
@notice Calculate PegKeeper's profit using current values
"""
return self._calc_profit_from(POOL.balanceOf(self), POOL.get_virtual_price(), self.debt)
@internal
@pure
def _calc_profit_from(lp_balance: uint256, virtual_price: uint256, debt: uint256) -> uint256:
"""
@notice PegKeeper's profit calculation formula
"""
lp_debt: uint256 = debt * PRECISION / virtual_price
if lp_balance <= lp_debt:
return 0
else:
return lp_balance - lp_debt
```
```shell
>>> PegKeeperV2.calc_profit()
0
```
::::
### `estimate_caller_profit`
::::description[`PegKeeperV2.estimate_caller_profit() -> uint256`]
Function to estimate the profit that a caller would receive from calling the `update()` function.
:::warning
This estimation is not precise and tends to be conservative, as the actual profit might be higher due to the increasing virtual price over time.
:::
Returns: estimated caller profit (`uint256`).
```vyper
I: immutable(uint256) # index of pegged in pool
PEG_MUL: immutable(uint256)
@external
@view
def estimate_caller_profit() -> uint256:
"""
@notice Estimate profit from calling update()
@dev This method is not precise, real profit is always more because of increasing virtual price
@return Expected amount of profit going to beneficiary
"""
if self.last_change + self.action_delay > block.timestamp:
return 0
balance_pegged: uint256 = POOL.balances(I)
balance_peg: uint256 = POOL.balances(1 - I) * PEG_MUL
call_profit: uint256 = 0
if balance_peg > balance_pegged:
allowed: uint256 = self.regulator.provide_allowed()
call_profit = self._calc_call_profit(min((balance_peg - balance_pegged) / 5, allowed), True) # this dumps stablecoin
else:
allowed: uint256 = self.regulator.withdraw_allowed()
call_profit = self._calc_call_profit(min((balance_pegged - balance_peg) / 5, allowed), False) # this pumps stablecoin
return call_profit * self.caller_share / SHARE_PRECISION
@internal
@view
def _calc_call_profit(_amount: uint256, _is_deposit: bool) -> uint256:
"""
@notice Calculate overall profit from calling update()
"""
lp_balance: uint256 = POOL.balanceOf(self)
virtual_price: uint256 = POOL.get_virtual_price()
debt: uint256 = self.debt
initial_profit: uint256 = self._calc_profit_from(lp_balance, virtual_price, debt)
amount: uint256 = _amount
if _is_deposit:
amount = min(_amount, PEGGED.balanceOf(self))
else:
amount = min(_amount, debt)
amounts: uint256[2] = empty(uint256[2])
amounts[I] = amount
lp_balance_diff: uint256 = POOL.calc_token_amount(amounts, _is_deposit)
if _is_deposit:
lp_balance += lp_balance_diff
debt += amount
else:
lp_balance -= lp_balance_diff
debt -= amount
new_profit: uint256 = self._calc_profit_from(lp_balance, virtual_price, debt)
if new_profit <= initial_profit:
return 0
return new_profit - initial_profit
@internal
@pure
def _calc_profit_from(lp_balance: uint256, virtual_price: uint256, debt: uint256) -> uint256:
"""
@notice PegKeeper's profit calculation formula
"""
lp_debt: uint256 = debt * PRECISION / virtual_price
if lp_balance <= lp_debt:
return 0
else:
return lp_balance - lp_debt
```
```shell
>>> PegKeeperV2.estimate_caller_profit()
0
```
::::
### `caller_share`
::::description[`PegKeeperV2.caller_share() -> uint256: view`]
Getter for the caller share, which represents the share of the profit generated when the `update()` function is called. This share is designed to incentivize users to call the function. SHARE_PRECISION is set to $10^5$.
Returns: caller share (`uint256`).
```vyper
event SetNewCallerShare:
caller_share: uint256
caller_share: public(uint256)
@external
def __init__(
_pool: CurvePool, _caller_share: uint256,
_factory: address, _regulator: Regulator, _admin: address,
):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _regulator Peg Keeper Regulator
@param _admin Admin account
"""
...
assert _caller_share <= SHARE_PRECISION # dev: bad part value
self.caller_share = _caller_share
log SetNewCallerShare(_caller_share)
...
```
```shell
>>> PegKeeperV2.caller_share()
20000
```
::::
### `set_new_caller_share`
::::description[`PegKeeperV2.set_new_caller_share(_new_caller_share: uint256)`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to set a new caller share. New share need to be smaller or equal than `SHARE_PRECISION`, which is $10^5$.
Emits: `SetNewCallerShare`
| Input | Type | Description |
|---------------------|-----------|------------------|
| `_new_caller_share` | `uint256` | New caller share |
```vyper
event SetNewCallerShare:
caller_share: uint256
SHARE_PRECISION: constant(uint256) = 10 **5
caller_share: public(uint256)
admin: public(address)
@external
@nonpayable
def set_new_caller_share(_new_caller_share: uint256):
"""
@notice Set new update caller's part
@param _new_caller_share Part with SHARE_PRECISION
"""
assert msg.sender == self.admin # dev: only admin
assert _new_caller_share <= SHARE_PRECISION # dev: bad part value
self.caller_share = _new_caller_share
log SetNewCallerShare(_new_caller_share)
```
```shell
>>> soon
```
::::
### `withdraw_profit`
::::description[`PegKeeperV2.withdraw_profit() -> uint256`]
Function to withdraw the profit generated by the PegKeeper. The profit is denominated in LP tokens and is transfered to the [`fee_receiver`](./peg-keeper-regulator.md#fee_receiver) specified in the `PegKeeperRegulator` contract.
Returns: LP tokens withdrawn (`uint256`).
```vyper
interface Regulator:
def stablecoin() -> address: view
def provide_allowed(_pk: address=msg.sender) -> uint256: view
def withdraw_allowed(_pk: address=msg.sender) -> uint256: view
def fee_receiver() -> address: view
@external
@nonpayable
def withdraw_profit() -> uint256:
"""
@notice Withdraw profit generated by Peg Keeper
@return Amount of LP Token received
"""
lp_amount: uint256 = self._calc_profit()
POOL.transfer(self.regulator.fee_receiver(), lp_amount)
log Profit(lp_amount)
return lp_amount
@internal
@view
def _calc_profit() -> uint256:
"""
@notice Calculate PegKeeper's profit using current values
"""
return self._calc_profit_from(POOL.balanceOf(self), POOL.get_virtual_price(), self.debt)
@internal
@pure
def _calc_profit_from(lp_balance: uint256, virtual_price: uint256, debt: uint256) -> uint256:
"""
@notice PegKeeper's profit calculation formula
"""
lp_debt: uint256 = debt * PRECISION / virtual_price
if lp_balance <= lp_debt:
return 0
else:
return lp_balance - lp_debt
```
```shell
>>> soon
```
::::
---
## Action Delay
The `action_delay` variable determines the time delay that needs to pass before the PegKeeper can provide or withdraw liquidity again via the `update` function.
### `action_delay`
::::description[`PegKeeperV2.action_delay() -> uint256`]
:::warning
Due to the missed declaration as a public variable, the `action_delay` does not have a public getter method that returns its value. Nonetheless, this does not impact the workings of the smart contract. Although there is no public getter, a new event is emitted when the variable is changed using the `set_new_action_delay` function.
The `action_delay` was set to 12 seconds at contract initialization and an according `SetNewActionDelay` [was emitted](https://etherscan.io/tx/0xeb9d586749fe7b3ebf4e92134b088e667e3c7a882b969aab4d6429ce5022acee#eventlog#323).
:::
```py
@external
def __init__(
_pool: CurvePool, _caller_share: uint256,
_factory: address, _regulator: Regulator, _admin: address,
):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _regulator Peg Keeper Regulator
@param _admin Admin account
"""
...
self.action_delay = 12 # 1 block
log SetNewActionDelay(12)
...
# Time between providing/withdrawing coins
action_delay: uint256
```
::::
### `set_new_action_delay`
::::description[`PegKeeperV2.set_new_action_delay(_new_action_delay: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set a new `action_delay` value.
Emits: `SetNewActionDelay`
| Input | Type | Description |
| ------------------- | --------- | ---------------------- |
| `_new_action_delay` | `uint256` | New action delay value |
```py
event SetNewActionDelay:
action_delay: uint256
# Time between providing/withdrawing coins
action_delay: uint256
@external
@nonpayable
def set_new_action_delay(_new_action_delay: uint256):
"""
@notice Set new action delay
@param _new_action_delay Action delay in seconds
"""
assert msg.sender == self.admin # dev: only admin
self.action_delay = _new_action_delay
log SetNewActionDelay(_new_action_delay)
```
```shell
>>> soon
```
::::
---
## PegKeeperRegulator Contract
The main use case of the `PegKeeperRegulator` contract is to supervise prices and other parameters, and to inform the PegKeeper whether it is allowed to provide or withdraw crvUSD. All PegKeepers share the same universal Regulator contract. More details on the `PegKeeperRegulator` contract can be found [here](./peg-keeper-regulator.md).
### `regulator`
::::description[`PegKeeperV2.regulator() -> address: view`]
Getter for the `PegKeeperRegulator` contract. This contract can be changed by the `admin` via the `set_new_regulator` function.
Returns: regulator contract (`address`).
Emits: `SetNewRegulator` at contract initialization
```vyper
event SetNewRegulator:
regulator: address
regulator: public(Regulator)
@external
def __init__(
_pool: CurvePool, _caller_share: uint256,
_factory: address, _regulator: Regulator, _admin: address,
):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _regulator Peg Keeper Regulator
@param _admin Admin account
"""
...
self.regulator = _regulator
log SetNewRegulator(_regulator.address)
...
```
```shell
>>> PegKeeperV2.regulator()
'0x36a04CAffc681fa179558B2Aaba30395CDdd855f'
```
::::
### `set_new_regulator`
::::description[`PegKeeperV2.set_new_regulator(_new_regulator: Regulator)`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to set a new regulator contract.
Emits: `SetNewRegulator`
| Input | Type | Description |
| ---------------- | --------- | ------------ |
| `_new_regulator` | `address` | New regulator contract |
```vyper
event SetNewRegulator:
regulator: address
regulator: public(Regulator)
@external
@nonpayable
def set_new_regulator(_new_regulator: Regulator):
"""
@notice Set new peg keeper regulator
"""
assert msg.sender == self.admin # dev: only admin
assert _new_regulator.address != empty(address) # dev: bad regulator
self.regulator = _new_regulator
log SetNewRegulator(_new_regulator.address)
```
```shell
>>> soon
```
::::
---
## Contract Ownership
Ownership of the PegKeepers adheres to the standard procedure. The transition of ownership can only be done by the `admin`. Following this commit, the designated `future_admin`, specified at the time of commitment, is required to apply the changes to complete the change of ownership.
### `admin`
::::description[`PegKeeperV2.admin() -> address: view`]
Getter for the current admin of the PegKeeper. The admin can only be changed by the admin by via the [`commit_new_admin`](#commit_new_admin) function.
Returns: current admin (`address`).
Emits: `ApplyNewAdmin` at contract initialization
```vyper
event ApplyNewAdmin:
admin: address
admin: public(address)
@external
def __init__(
_pool: CurvePool, _caller_share: uint256,
_factory: address, _regulator: Regulator, _admin: address,
):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _regulator Peg Keeper Regulator
@param _admin Admin account
"""
...
self.admin = _admin
log ApplyNewAdmin(msg.sender)
...
```
```shell
>>> PegKeeperV2.admin()
'0x40907540d8a6C65c637785e8f8B742ae6b0b9968'
```
::::
### `future_admin`
::::description[`PegKeeperV2.future_admin() -> address: view`]
Getter for the future admin of the PegKeeper. This variable is set when commiting a new admin.
Returns: future admin (`address`).
```vyper
future_admin: public(address)
```
```shell
>>> PegKeeperV2.future_admin()
'0x0000000000000000000000000000000000000000'
```
::::
### `commit_new_admin`
::::description[`PegKeeperV2.commit_new_admin(_new_admin: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to commit `_new_admin` as the new admin of the PegKeeper. For the admin to change, the future admin need to apply the changes via `apply_new_admin`.
Emits: `CommitNewAdmin`
| Input | Type | Description |
| ------------ | --------- | ----------- |
| `_new_admin` | `address` | New admin |
```vyper
event CommitNewAdmin:
admin: address
admin: public(address)
future_admin: public(address)
@external
@nonpayable
def commit_new_admin(_new_admin: address):
"""
@notice Commit new admin of the Peg Keeper
@dev In order to revert, commit_new_admin(current_admin) may be called
@param _new_admin Address of the new admin
"""
assert msg.sender == self.admin # dev: only admin
assert _new_admin != empty(address) # dev: bad admin
self.new_admin_deadline = block.timestamp + ADMIN_ACTIONS_DELAY
self.future_admin = _new_admin
log CommitNewAdmin(_new_admin)
```
```shell
>>> soon
```
::::
### `apply_new_admin`
::::description[`PegKeeperV2.apply_new_admin()`]
:::guard[Guarded Method]
This function is only callable by the `future_admin` of the contract.
:::
Function to apply the new admin. This method sets the `future_admin` set in `commit_new_admin` as the new `admin`. Additionally, there is a delay of three days (`ADMIN_ACTIONS_DELAY`) starting with the `commit_new_admin` call. Only after the delay has passed can the new admin be applied.
Emits: `ApplyNewAdmin`
```vyper
event ApplyNewAdmin:
admin: address
ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400
admin: public(address)
future_admin: public(address)
@external
@nonpayable
def apply_new_admin():
"""
@notice Apply new admin of the Peg Keeper
@dev Should be executed from new admin
"""
new_admin: address = self.future_admin
new_admin_deadline: uint256 = self.new_admin_deadline
assert msg.sender == new_admin # dev: only new admin
assert block.timestamp >= new_admin_deadline # dev: insufficient time
assert new_admin_deadline != 0 # dev: no active action
self.admin = new_admin
self.new_admin_deadline = 0
log ApplyNewAdmin(new_admin)
```
```shell
>>> soon
```
::::
### `new_admin_deadline`
::::description[`PegKeeperV2.new_admin_deadline() -> uint256: view`]
Getter for the admin deadline. When commiting a new admin, there is a delay of three days (`ADMIN_ACTIONS_DELAY`) before the change of ownership can be applied. Otherwise the call will revert.
Returns: timestamp after which the new admin can be applied (`uint256`).
```vyper
new_admin_deadline: public(uint256)
```
```shell
>>> PegKeeperV2.new_admin_deadline()
0
```
::::
---
## Contract Info Methods
### `debt`
::::description[`PegKeeperV2.debt() -> uint256: view`]
Getter for the current debt of the PegKeeper. Debt increases when crvUSD is provided to the liquidity pool and decreases when crvUSD is withdrawn again. The debt is denominated in crvUSD tokens.
Returns: current debt (`uint256`).
```vyper
debt: public(uint256)
```
```shell
>>> PegKeeperV2.debt()
0
```
::::
### `pool`
::::description[`PegKeeperV2.pool() -> address: view`]
Getter for the pool that the PegKeeper provides to or withdraws from.
Returns: liquidity pool (`address`).
```vyper
POOL: immutable(CurvePool)
@pure
@external
def pool() -> CurvePool:
"""
@return StableSwap pool being used
"""
return POOL
@external
def __init__(
_pool: CurvePool, _receiver: address, _caller_share: uint256,
_factory: address, _regulator: Regulator, _admin: address,
):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _receiver Receiver of the profit
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _regulator Peg Keeper Regulator
@param _admin Admin account
"""
POOL = _pool
...
```
```shell
>>> PegKeeperV2.pool()
'0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E'
```
::::
### `FACTORY`
::::description[`PegKeeperV2.FACTORY() -> address: view`]
Getter for the Factory contract. This address is able to take coins away in the case of reducing the debt limit of a PegKeeper. Due to this, maximum approval is granted to this address when initializing the contract.
Returns: Factory (`address`).
```vyper
FACTORY: immutable(address)
@external
def __init__(
_pool: CurvePool, _receiver: address, _caller_share: uint256,
_factory: address, _regulator: Regulator, _admin: address,
):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _receiver Receiver of the profit
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _regulator Peg Keeper Regulator
@param _admin Admin account
"""
...
pegged.approve(_factory, max_value(uint256))
...
FACTORY = _factory
```
```shell
>>> PegKeeperV2.FACTORY()
'0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC'
```
::::
### `pegged`
::::description[`PegKeeperV2.pegged() -> address: view`]
Getter for the pegged coin, which is crvUSD.
Returns: pegged coin (`address`).
```vyper
PEGGED: immutable(ERC20)
@external
def __init__(
_pool: CurvePool, _receiver: address, _caller_share: uint256,
_factory: address, _regulator: Regulator, _admin: address,
):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _receiver Receiver of the profit
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _regulator Peg Keeper Regulator
@param _admin Admin account
"""
...
pegged: ERC20 = ERC20(_regulator.stablecoin())
PEGGED = pegged
pegged.approve(_pool.address, max_value(uint256))
pegged.approve(_factory, max_value(uint256))
...
```
```shell
>>> PegKeeperV2.pegged()
'0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'
```
::::
### `IS_INVERSE`
::::description[`PegKeeperV2.IS_INVERSE() -> bool: view`]
Getter to check if crvUSD token index in the pool is inverse. This variable is set when initializing the contract. If crvUSD is coin[0] in the liquidity pool, `IS_INVERSE` will be set to `true`. This variable is not directly relevant in the PegKeeper contract, but it is of great importance in the `PegKeeperRegulator` regarding calculations with oracles.
Returns: true or false (`bool`).
```vyper
IS_INVERSE: public(immutable(bool))
@external
def __init__(
_pool: CurvePool, _receiver: address, _caller_share: uint256,
_factory: address, _regulator: Regulator, _admin: address,
):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _receiver Receiver of the profit
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _regulator Peg Keeper Regulator
@param _admin Admin account
"""
...
for i in range(2):
if coins[i] == pegged:
I = i
IS_INVERSE = (i == 0)
else:
PEG_MUL = 10 **(18 - coins[i].decimals())
```
```shell
>>> PegKeeperV2.IS_INVERSE()
'False'
```
::::
### `IS_NG`
::::description[`PegKeeperV2.IS_NG() -> bool: view`]
Getter to check if the pool associated with the PegKeeper is a new generation (NG) pool. This is important when adding and removing liquidity, as the interface of NG pools is slightly different from the prior ones.
Returns: true or false (`bool`).
```vyper
IS_NG: public(immutable(bool)) # Interface for CurveStableSwapNG
@external
def __init__(
_pool: CurvePool, _caller_share: uint256,
_factory: address, _regulator: Regulator, _admin: address,
):
"""
@notice Contract constructor
@param _pool Contract pool address
@param _caller_share Caller's share of profit
@param _factory Factory which should be able to take coins away
@param _regulator Peg Keeper Regulator
@param _admin Admin account
"""
...
IS_NG = raw_call(
_pool.address, _abi_encode(convert(0, uint256), method_id=method_id("price_oracle(uint256)")),
revert_on_failure=False
)
...
```
```shell
>>> PegKeeperV2.IS_NG()
'False'
```
::::
---
## Curve DAO Token (CRV)
The Curve DAO Token (CRV) is the protocol's governance token. It is based on the ERC-20 token standard as defined at [EIP-20](https://eips.ethereum.org/EIPS/eip-20).
:::vyper[`CRV.vy`]
The source code for the `CRV.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-dao-contracts/blob/567927551903f71ce5a73049e077be87111963cc/contracts/ERC20CRV.vy). The contract is written using [Vyper](https://vyper.readthedocs.io) version `0.2.4`.
The token is deployed on :logos-ethereum: Ethereum at [`0xD533a949740bb3306d119CC777fa900bA034cd52`](https://etherscan.io/address/0xD533a949740bb3306d119CC777fa900bA034cd52).
```json
[{"name":"Transfer","inputs":[{"type":"address","name":"_from","indexed":true},{"type":"address","name":"_to","indexed":true},{"type":"uint256","name":"_value","indexed":false}],"anonymous":false,"type":"event"},{"name":"Approval","inputs":[{"type":"address","name":"_owner","indexed":true},{"type":"address","name":"_spender","indexed":true},{"type":"uint256","name":"_value","indexed":false}],"anonymous":false,"type":"event"},{"name":"UpdateMiningParameters","inputs":[{"type":"uint256","name":"time","indexed":false},{"type":"uint256","name":"rate","indexed":false},{"type":"uint256","name":"supply","indexed":false}],"anonymous":false,"type":"event"},{"name":"SetMinter","inputs":[{"type":"address","name":"minter","indexed":false}],"anonymous":false,"type":"event"},{"name":"SetAdmin","inputs":[{"type":"address","name":"admin","indexed":false}],"anonymous":false,"type":"event"},{"outputs":[],"inputs":[{"type":"string","name":"_name"},{"type":"string","name":"_symbol"},{"type":"uint256","name":"_decimals"}],"stateMutability":"nonpayable","type":"constructor"},{"name":"update_mining_parameters","outputs":[],"inputs":[],"stateMutability":"nonpayable","type":"function","gas":148748},{"name":"start_epoch_time_write","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"nonpayable","type":"function","gas":149603},{"name":"future_epoch_time_write","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"nonpayable","type":"function","gas":149806},{"name":"available_supply","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":4018},{"name":"mintable_in_timeframe","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"uint256","name":"start"},{"type":"uint256","name":"end"}],"stateMutability":"view","type":"function","gas":2216141},{"name":"set_minter","outputs":[],"inputs":[{"type":"address","name":"_minter"}],"stateMutability":"nonpayable","type":"function","gas":38698},{"name":"set_admin","outputs":[],"inputs":[{"type":"address","name":"_admin"}],"stateMutability":"nonpayable","type":"function","gas":37837},{"name":"totalSupply","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":1421},{"name":"allowance","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"_owner"},{"type":"address","name":"_spender"}],"stateMutability":"view","type":"function","gas":1759},{"name":"transfer","outputs":[{"type":"bool","name":""}],"inputs":[{"type":"address","name":"_to"},{"type":"uint256","name":"_value"}],"stateMutability":"nonpayable","type":"function","gas":75139},{"name":"transferFrom","outputs":[{"type":"bool","name":""}],"inputs":[{"type":"address","name":"_from"},{"type":"address","name":"_to"},{"type":"uint256","name":"_value"}],"stateMutability":"nonpayable","type":"function","gas":111433},{"name":"approve","outputs":[{"type":"bool","name":""}],"inputs":[{"type":"address","name":"_spender"},{"type":"uint256","name":"_value"}],"stateMutability":"nonpayable","type":"function","gas":39288},{"name":"mint","outputs":[{"type":"bool","name":""}],"inputs":[{"type":"address","name":"_to"},{"type":"uint256","name":"_value"}],"stateMutability":"nonpayable","type":"function","gas":228030},{"name":"burn","outputs":[{"type":"bool","name":""}],"inputs":[{"type":"uint256","name":"_value"}],"stateMutability":"nonpayable","type":"function","gas":74999},{"name":"set_name","outputs":[],"inputs":[{"type":"string","name":"_name"},{"type":"string","name":"_symbol"}],"stateMutability":"nonpayable","type":"function","gas":178270},{"name":"name","outputs":[{"type":"string","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":8063},{"name":"symbol","outputs":[{"type":"string","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":7116},{"name":"decimals","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":1721},{"name":"balanceOf","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"arg0"}],"stateMutability":"view","type":"function","gas":1905},{"name":"minter","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":1781},{"name":"admin","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":1811},{"name":"mining_epoch","outputs":[{"type":"int128","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":1841},{"name":"start_epoch_time","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":1871},{"name":"rate","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":1901}]
```
:::
For a broader understanding of the use case of the CRV token, check out [Understanding CRV](https://resources.curve.fi/crv-token/overview/).
---
## Transfer and Allowance
### `approve`
::::description[`CRV.approve(_spender: address, _value: uint256) -> bool`]
:::warning
Approval may only be from `zero -> nonzero` or from `nonzero -> zero` in order to mitigate the potential race condition described here: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
:::
Function to approve `_spender` to transfer `_value` tokens on behalf of `msg.sender`.
| Input | Type | Description |
| ---------- | --------- | ----------------- |
| `_spender` | `address` | Spender address |
| `_value` | `uint256` | Amount to approve |
Returns: true (`bool`).
Emits: `Approval` event.
```vyper
event Approval:
_owner: indexed(address)
_spender: indexed(address)
_value: uint256
allowances: HashMap[address, HashMap[address, uint256]]
@external
def approve(_spender : address, _value : uint256) -> bool:
"""
@notice Approve `_spender` to transfer `_value` tokens on behalf of `msg.sender`
@dev Approval may only be from zero -> nonzero or from nonzero -> zero in order
to mitigate the potential race condition described here:
https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
@param _spender The address which will spend the funds
@param _value The amount of tokens to be spent
@return bool success
"""
assert _value == 0 or self.allowances[msg.sender][_spender] == 0
self.allowances[msg.sender][_spender] = _value
log Approval(msg.sender, _spender, _value)
return True
```
This example approves `0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045` to transfer 1 CRV token on behalf of `msg.sender`.
```shell
>>> CRV.approve('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 1000000000000000000)
'True'
```
::::
### `allowance`
::::description[`CRV.allowance(_owner: address, _spender: address) -> uint256: view`]
Getter method to check the amount of tokens that `_owner` has allowed `_spender` to use.
| Input | Type | Description |
| ---------- | --------- | --------------- |
| `_owner` | `address` | Owner address |
| `_spender` | `address` | Spender address |
Returns: amount of tokens (`uint256`) that `_owner` has allowed `_spender` to use.
```vyper
allowances: HashMap[address, HashMap[address, uint256]]
@external
@view
def allowance(_owner : address, _spender : address) -> uint256:
"""
@notice Check the amount of tokens that an owner allowed to a spender
@param _owner The address which owns the funds
@param _spender The address which will spend the funds
@return uint256 specifying the amount of tokens still available for the spender
"""
return self.allowances[_owner][_spender]
```
This example returns the amount of CRV tokens that a spender is allowed to spend on behalf of an owner. Enter addresses and click **Query** to fetch the value live from the blockchain.
::::
### `transfer`
::::description[`CRV.transfer(_to: address, _value: uint256) -> bool`]
:::warning
Vyper does not allow underflows; thus, any subtraction in this function will revert if there is an insufficient balance.
Additionally, transfers to `ZERO_ADDRESS` are not allowed.
:::
Function to transfer `_value` tokens from `msg.sender` to `_to`.
| Input | Type | Description |
| -------- | --------- | ------------------------------ |
| `_to` | `address` | Receiver address of the tokens |
| `_value` | `uint256` | Amount of tokens to transfer |
Returns: true (`bool`).
Emits: `Transfer` event.
```vyper
event Transfer:
_from: indexed(address)
_to: indexed(address)
_value: uint256
balanceOf: public(HashMap[address, uint256])
@external
def transfer(_to : address, _value : uint256) -> bool:
"""
@notice Transfer `_value` tokens from `msg.sender` to `_to`
@dev Vyper does not allow underflows, so the subtraction in
this function will revert on an insufficient balance
@param _to The address to transfer to
@param _value The amount to be transferred
@return bool success
"""
assert _to != ZERO_ADDRESS # dev: transfers to 0x0 are not allowed
self.balanceOf[msg.sender] -= _value
self.balanceOf[_to] += _value
log Transfer(msg.sender, _to, _value)
return True
```
This example transfers 1 CRV token from `msg.sender` to `0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045`.
```shell
>>> CRV.transfer('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 1000000000000000000)
'True'
```
::::
### `transferFrom`
::::description[`CRV.transferFrom(_from: address, _to: address, _value: uint256) -> bool`]
:::warning
Vyper does not allow underflows; thus, any subtraction in this function will revert if there is an insufficient balance.
Additionally, transfers to `ZERO_ADDRESS` are not allowed.
:::
Function to transfer `_value` tokens from `_from` to `_to`.
| Input | Type | Description |
| -------- | --------- | ------------------------------ |
| `_from` | `address` | Address to send tokens from |
| `_to` | `address` | Receiver address of the tokens |
| `_value` | `uint256` | Amount of tokens to transfer |
Returns: true (`bool`).
Emits: `Transfer` event.
```vyper
event Transfer:
_from: indexed(address)
_to: indexed(address)
_value: uint256
balanceOf: public(HashMap[address, uint256])
allowances: HashMap[address, HashMap[address, uint256]]
@external
def transferFrom(_from : address, _to : address, _value : uint256) -> bool:
"""
@notice Transfer `_value` tokens from `_from` to `_to`
@param _from address The address which you want to send tokens from
@param _to address The address which you want to transfer to
@param _value uint256 the amount of tokens to be transferred
@return bool success
"""
assert _to != ZERO_ADDRESS # dev: transfers to 0x0 are not allowed
# NOTE: vyper does not allow underflows
# so the following subtraction would revert on insufficient balance
self.balanceOf[_from] -= _value
self.balanceOf[_to] += _value
self.allowances[_from][msg.sender] -= _value
log Transfer(_from, _to, _value)
return True
```
This example transfers 1 CRV token from `0x7a16fF8270133F063aAb6C9977183D9e72835428` to `0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045`.
```shell
>>> CRV.transferFrom('0x7a16fF8270133F063aAb6C9977183D9e72835428', '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 1000000000000000000)
'True'
```
::::
---
## Emissions, Minting and Burning
Curve has a strict minting mechanism of new CRV tokens. New tokens are minted based on the gauge weights of an epoch. For more information, see [Gauge Weight Voting](../gauges/overview.md#gauge-weight-voting)
:::info[Minting New CRV]
New `CRV` tokens can only be minted by the `minter` contract.
:::
Mining parameters are used to determine token emissions, which are based on epochs (one year). With each passing epoch, the `rate` will be reduced, consequently decreasing the overall CRV emissions.
The rate can be adjusted by invoking the `update_mining_parameters()` function. Although this function is accessible to anyone, attempts to call it will be reverted if a year has not elapsed since the last update. When successfully executed, the `mining_epoch` increments by 1, and the `start_epoch_time` updates to the timestamp of the function call. Furthermore, the `update_mining_parameters()` function will automatically trigger if someone attempts to mint CRV before a scheduled rate reduction.
*Effectively, each rate reduction decreases CRV inflation by approximately 15.9%. The future rate is calculated as follows:*
$$\text{rate}_\text{future} = \text{rate}_\text{current} \cdot \frac{10^{18}}{2^{\frac{1}{4}} \cdot 10^{18}}$$
with $\text{rate}_\text{current}$ fetched from the [`rate()`](#rate) function.
---
### `minter`
::::description[`CRV.minter() -> address: view`]
Getter for the `Minter` contract address. The minter address can only be set once (at deployment) and not altered after.
Returns: `Minter` contract (`address`).
```vyper
minter: public(address)
```
This example returns the minter contract address. The value is fetched live from the blockchain.
::::
### `mint`
::::description[`CRV.mint(_to: address, _value: uint256) -> bool`]
:::guard[Guarded Method]
This function is only callable by the `Minter` contract.
:::
Function to mint `_value` tokens and assign them to `_to`.
| Input | Type | Description |
| -------- | --------- | ----------------------------- |
| `_to` | `address` | Receiver of the minted tokens |
| `_value` | `uint256` | Amount to mint |
Returns: true (`bool`).
Emits: `Transfer` event.
```vyper
event Transfer:
_from: indexed(address)
_to: indexed(address)
_value: uint256
minter: public(address)
balanceOf: public(HashMap[address, uint256])
total_supply: uint256
@external
def mint(_to: address, _value: uint256) -> bool:
"""
@notice Mint `_value` tokens and assign them to `_to`
@dev Emits a Transfer event originating from 0x00
@param _to The account that will receive the created tokens
@param _value The amount that will be created
@return bool success
"""
assert msg.sender == self.minter # dev: minter only
assert _to != ZERO_ADDRESS # dev: zero address
if block.timestamp >= self.start_epoch_time + RATE_REDUCTION_TIME:
self._update_mining_parameters()
_total_supply: uint256 = self.total_supply + _value
assert _total_supply <= self._available_supply() # dev: exceeds allowable mint amount
self.total_supply = _total_supply
self.balanceOf[_to] += _value
log Transfer(ZERO_ADDRESS, _to, _value)
return True
```
This example mints 1 CRV token to `0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045`.
```shell
>>> CRV.mint('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 1000000000000000000)
'True'
```
::::
### `mintable_in_timeframe`
::::description[`CRV.mintable_in_timeframe(start: uint256, end: uint256) -> uint256: view`]
Getter for the mintable supply between `start` and `end` timestamps. The value is dependent on the current emission `rate` of the token.
| Input | Type | Description |
| ------- | --------- | --------------- |
| `start` | `uint256` | Start timestamp |
| `end` | `uint256` | End timestamp |
Returns: mintable tokens (`uint256`).
```vyper
@external
@view
def mintable_in_timeframe(start: uint256, end: uint256) -> uint256:
"""
@notice How much supply is mintable from start timestamp till end timestamp
@param start Start of the time interval (timestamp)
@param end End of the time interval (timestamp)
@return Tokens mintable from `start` till `end`
"""
assert start <= end # dev: start > end
to_mint: uint256 = 0
current_epoch_time: uint256 = self.start_epoch_time
current_rate: uint256 = self.rate
# Special case if end is in future (not yet minted) epoch
if end > current_epoch_time + RATE_REDUCTION_TIME:
current_epoch_time += RATE_REDUCTION_TIME
current_rate = current_rate * RATE_DENOMINATOR / RATE_REDUCTION_COEFFICIENT
assert end <= current_epoch_time + RATE_REDUCTION_TIME # dev: too far in future
for i in range(999): # Curve will not work in 1000 years. Darn!
if end >= current_epoch_time:
current_end: uint256 = end
if current_end > current_epoch_time + RATE_REDUCTION_TIME:
current_end = current_epoch_time + RATE_REDUCTION_TIME
current_start: uint256 = start
if current_start >= current_epoch_time + RATE_REDUCTION_TIME:
break # We should never get here but what if...
elif current_start < current_epoch_time:
current_start = current_epoch_time
to_mint += current_rate * (current_end - current_start)
if start >= current_epoch_time:
break
current_epoch_time -= RATE_REDUCTION_TIME
current_rate = current_rate * RATE_REDUCTION_COEFFICIENT / RATE_DENOMINATOR # double-division with rounding made rate a bit less => good
assert current_rate <= INITIAL_RATE # This should never happen
return to_mint
```
This example returns the mintable CRV supply between two timestamps. Enter start and end timestamps and click **Query** to fetch the value live from the blockchain.
::::
### `burn`
::::description[`CRV.burn(_value: uint256) -> bool`]
Function to burn `_value` tokens of the function caller by sending them to `ZERO_ADDRESS`.
| Input | Type | Description |
| -------- | --------- | ------------------------ |
| `_value` | `uint256` | Amount of tokens to burn |
Returns: true (`bool`).
Emits: `Transfer` event.
```vyper
event Transfer:
_from: indexed(address)
_to: indexed(address)
_value: uint256
balanceOf: public(HashMap[address, uint256])
total_supply: uint256
@external
def burn(_value: uint256) -> bool:
"""
@notice Burn `_value` tokens belonging to `msg.sender`
@dev Emits a Transfer event with a destination of 0x00
@param _value The amount that will be burned
@return bool success
"""
self.balanceOf[msg.sender] -= _value
self.total_supply -= _value
log Transfer(msg.sender, ZERO_ADDRESS, _value)
return True
```
This example burns 1 CRV token belonging to `msg.sender`.
```shell
>>> CRV.burn(1000000000000000000)
'True'
```
::::
### `mining_epoch`
::::description[`CRV.mining_epoch() -> int128: view`]
Getter for the current mining epoch. The mining epoch is incremented by 1 every time [`update_mining_parameters()`](#update_mining_parameters) is successfully called. At deployment, `mining_epoch` was set to -1.
Returns: mining epoch (`int128`).
```vyper
mining_epoch: public(int128)
@external
def __init__(_name: String[64], _symbol: String[32], _decimals: uint256):
"""
@notice Contract constructor
@param _name Token full name
@param _symbol Token symbol
@param _decimals Number of decimals for token
"""
...
self.mining_epoch = -1
...
@internal
def _update_mining_parameters():
"""
@dev Update mining rate and supply at the start of the epoch
Any modifying mining call must also call this
"""
_rate: uint256 = self.rate
_start_epoch_supply: uint256 = self.start_epoch_supply
self.start_epoch_time += RATE_REDUCTION_TIME
self.mining_epoch += 1
if _rate == 0:
_rate = INITIAL_RATE
else:
_start_epoch_supply += _rate * RATE_REDUCTION_TIME
self.start_epoch_supply = _start_epoch_supply
_rate = _rate * RATE_DENOMINATOR / RATE_REDUCTION_COEFFICIENT
self.rate = _rate
log UpdateMiningParameters(block.timestamp, _rate, _start_epoch_supply)
```
This example returns the current mining epoch. The value is fetched live from the blockchain.
::::
### `start_epoch_time`
::::description[`CRV.start_epoch_time() -> uint256: view`]
Getter for the start timestamp of the current mining epoch.
Returns: timestamp (`uint256`).
```vyper
start_epoch_time: public(uint256)
@external
def __init__(_name: String[64], _symbol: String[32], _decimals: uint256):
"""
@notice Contract constructor
@param _name Token full name
@param _symbol Token symbol
@param _decimals Number of decimals for token
"""
...
self.start_epoch_time = block.timestamp + INFLATION_DELAY - RATE_REDUCTION_TIME
...
```
This example returns the start timestamp of the current mining epoch. The value is fetched live from the blockchain.
::::
### `rate`
::::description[`CRV.rate() -> uint256: view`]
Getter for the current inflation rate of the CRV token emission. The rate is denominated in emissions per second and has a base of 1e18.
To calculate the CRV emission per day:
$$\text{daily\_emission} = \text{rate} \times 86400$$
$$\text{weekly\_emission} = \text{rate} \times 86400 \times 7$$
$$\text{yearly\_emission} = \text{rate} \times 86400 \times 365$$
Returns: current inflation rate (`uint256`).
```vyper
rate: public(uint256)
```
This example returns the current CRV emission rate per second. The value is fetched live from the blockchain.
::::
### `update_mining_parameters`
::::description[`CRV.update_mining_parameters()`]
Function to update the mining parameters for the token. By updating, the newly decreased inflation rate is applied. This function is callable by anyone. However, the call will revert if `block.timestamp` is less than or equal to `start_epoch_time` + `RATE_REDUCTION_TIME`, indicating that one year has not yet passed and therefore the rate cannot be updated yet.
Emits: `UpdateMiningParameters` event.
```vyper
event UpdateMiningParameters:
time: uint256
rate: uint256
supply: uint256
YEAR: constant(uint256) = 86400 * 365
# Supply parameters
INITIAL_SUPPLY: constant(uint256) = 1_303_030_303
INITIAL_RATE: constant(uint256) = 274_815_283 * 10 **18 / YEAR # leading to 43% premine
RATE_REDUCTION_TIME: constant(uint256) = YEAR
RATE_REDUCTION_COEFFICIENT: constant(uint256) = 1189207115002721024 # 2 **(1/4) * 1e18
RATE_DENOMINATOR: constant(uint256) = 10 **18
INFLATION_DELAY: constant(uint256) = 86400
# Supply variables
mining_epoch: public(int128)
start_epoch_time: public(uint256)
rate: public(uint256)
start_epoch_supply: uint256
@external
def update_mining_parameters():
"""
@notice Update mining rate and supply at the start of the epoch
@dev Callable by any address, but only once per epoch
Total supply becomes slightly larger if this function is called late
"""
assert block.timestamp >= self.start_epoch_time + RATE_REDUCTION_TIME # dev: too soon!
self._update_mining_parameters()
@internal
def _update_mining_parameters():
"""
@dev Update mining rate and supply at the start of the epoch
Any modifying mining call must also call this
"""
_rate: uint256 = self.rate
_start_epoch_supply: uint256 = self.start_epoch_supply
self.start_epoch_time += RATE_REDUCTION_TIME
self.mining_epoch += 1
if _rate == 0:
_rate = INITIAL_RATE
else:
_start_epoch_supply += _rate * RATE_REDUCTION_TIME
self.start_epoch_supply = _start_epoch_supply
_rate = _rate * RATE_DENOMINATOR / RATE_REDUCTION_COEFFICIENT
self.rate = _rate
log UpdateMiningParameters(block.timestamp, _rate, _start_epoch_supply)
```
This example updates the mining parameters for the CRV token, applying the newly decreased inflation rate.
```shell
>>> CRV.update_mining_parameters()
```
::::
### `start_epoch_time_write`
::::description[`CRV.start_epoch_time_write() -> uint256`]
Function to get the current mining epoch start while simultaneously updating mining parameters if possible. If updating is not possible, the function will only return the start timestamp of the current epoch.
Returns: start timestamp of the epoch (`uint256`).
```vyper
start_epoch_time: public(uint256)
@external
def start_epoch_time_write() -> uint256:
"""
@notice Get timestamp of the current mining epoch start
while simultaneously updating mining parameters
@return Timestamp of the epoch
"""
_start_epoch_time: uint256 = self.start_epoch_time
if block.timestamp >= _start_epoch_time + RATE_REDUCTION_TIME:
self._update_mining_parameters()
return self.start_epoch_time
else:
return _start_epoch_time
```
This example returns the current epoch start timestamp while updating mining parameters if applicable.
```shell
>>> CRV.start_epoch_time_write()
1691625600
```
::::
### `future_epoch_time_write`
::::description[`CRV.future_epoch_time_write() -> uint256`]
Function to get the next mining epoch start timestamp while simultaneously updating mining parameters if possible. If updating is not possible, the function will only return the start timestamp of the future epoch.
Returns: start timestamp of the future epoch (`uint256`).
```vyper
@external
def future_epoch_time_write() -> uint256:
"""
@notice Get timestamp of the next mining epoch start
while simultaneously updating mining parameters
@return Timestamp of the next epoch
"""
_start_epoch_time: uint256 = self.start_epoch_time
if block.timestamp >= _start_epoch_time + RATE_REDUCTION_TIME:
self._update_mining_parameters()
return self.start_epoch_time + RATE_REDUCTION_TIME
else:
return _start_epoch_time + RATE_REDUCTION_TIME
```
This example returns the next epoch start timestamp while updating mining parameters if applicable.
```shell
>>> CRV.future_epoch_time_write()
1723161600
```
::::
---
## Admin Controls and Other Methods
The controls over the Curve DAO Token are strictly limited. The `admin` of the contract can only modify the `name`, `admin`, or `minter`[^1].
Since the [`CurveOwnershipAgent`](https://etherscan.io/address/0x40907540d8a6C65c637785e8f8B742ae6b0b9968) is the current admin of the contract, any changes to these parameters would require a successfully passed DAO vote.
[^1]: Although `set_minter` is technically an admin-guarded function, there is **no actual way to change the minter address** because the code checks if the current minter is set to `ZERO_ADDRESS`, which was only true when the contract was initially deployed.
### `admin`
::::description[`CRV.admin() -> address: view`]
Getter for the current admin of the contract.
Returns: admin (`address`).
```vyper
admin: public(address)
@external
def __init__(_name: String[64], _symbol: String[32], _decimals: uint256):
"""
@notice Contract constructor
@param _name Token full name
@param _symbol Token symbol
@param _decimals Number of decimals for token
"""
...
self.admin = msg.sender
...
```
This example returns the current admin of the CRV token contract. The value is fetched live from the blockchain.
::::
### `set_admin`
::::description[`CRV.set_admin(_admin: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to change the admin of the contract.
| Input | Type | Description |
| -------- | --------- | ----------------- |
| `_admin` | `address` | New admin address |
Emits: `SetAdmin` event.
```vyper
event SetAdmin:
admin: address
admin: public(address)
@external
def set_admin(_admin: address):
"""
@notice Set the new admin.
@dev After all is set up, admin only can change the token name
@param _admin New admin address
"""
assert msg.sender == self.admin # dev: admin only
self.admin = _admin
log SetAdmin(_admin)
```
This example sets a new admin for the CRV token contract.
```shell
>>> CRV.set_admin("0x0000000000000000000000000000000000000000")
```
::::
### `name`
::::description[`CRV.name() -> String[64]: view`]
Getter for the name of the token. Name of the token can be changed by calling the **`set_name`** function.
Returns: token name (`String[64]`).
```vyper
name: public(String[64])
@external
def __init__(_name: String[64], _symbol: String[32], _decimals: uint256):
"""
@notice Contract constructor
@param _name Token full name
@param _symbol Token symbol
@param _decimals Number of decimals for token
"""
init_supply: uint256 = INITIAL_SUPPLY * 10 **_decimals
self.name = _name
...
```
This example returns the name of the CRV token. The value is fetched live from the blockchain.
::::
### `symbol`
::::description[`CRV.symbol() -> String[32]: view`]
Getter of the token symbol. Symbol of the token can be changed by calling the **`set_name`** function.
Returns: token symbol (`String[32]`).
```vyper
symbol: public(String[32])
@external
def __init__(_name: String[64], _symbol: String[32], _decimals: uint256):
"""
@notice Contract constructor
@param _name Token full name
@param _symbol Token symbol
@param _decimals Number of decimals for token
"""
...
self.symbol = _symbol
...
```
This example returns the symbol of the CRV token. The value is fetched live from the blockchain.
::::
### `set_name`
::::description[`CRV.set_name(_name: String[64], _symbol: String[32])`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to change the token name and symbol.
| Input | Type | Description |
| --------- | ------------ | ----------------- |
| `_name` | `String[64]` | New token name. |
| `_symbol` | `String[32]` | New token symbol. |
```vyper
name: public(String[64])
symbol: public(String[32])
@external
def set_name(_name: String[64], _symbol: String[32]):
"""
@notice Change the token name and symbol to `_name` and `_symbol`
@dev Only callable by the admin account
@param _name New token name
@param _symbol New token symbol
"""
assert msg.sender == self.admin, "Only admin is allowed to change name"
self.name = _name
self.symbol = _symbol
```
This example changes the name and symbol of the CRV token.
```shell
>>> CRV.set_name("New Name", "New Symbol")
```
::::
### `set_minter`
::::description[`CRV.set_minter(_minter: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
:::warning[Changing the `minter` contract is not possible anymore!]
This function was only utilized during the initial deployment of the Curve DAO Token. The code permits setting the `minter` exclusively when the current minter is `ZERO_ADDRESS`, a condition met solely at the time of deployment. Consequently, the `minter` variable could only be set once and cannot be changed thereafter.
:::
Function to set the minter contract for the token.
| Input | Type | Description |
| --------- | --------- | ----------------------- |
| `_minter` | `address` | Minter contract address |
Emits: `SetMinter` event.
```vyper
event SetMinter:
minter: address
minter: public(address)
@external
def set_minter(_minter: address):
"""
@notice Set the minter address
@dev Only callable once, when minter has not yet been set
@param _minter Address of the minter
"""
assert msg.sender == self.admin # dev: admin only
assert self.minter == ZERO_ADDRESS # dev: can set the minter only once, at creation
self.minter = _minter
log SetMinter(_minter)
```
This example attempts to set the minter contract address. Since the minter is already set, this call will revert.
```shell
>>> CRV.set_minter("0x0000000000000000000000000000000000000000")
```
::::
### `available_supply`
::::description[`CRV.available_supply() -> uint256: view`]
Getter for the current number of CRV tokens - claimed or unclaimed - in existence.
Returns: currently existing tokens (`uint256`).
```vyper
@internal
@view
def _available_supply() -> uint256:
return self.start_epoch_supply + (block.timestamp - self.start_epoch_time) * self.rate
@external
@view
def available_supply() -> uint256:
"""
@notice Current number of tokens in existence (claimed or unclaimed)
"""
return self._available_supply()
```
This example returns the current available supply of CRV tokens. The value is fetched live from the blockchain.
::::
### `totalSupply`
::::description[`CRV.totalSupply() -> uint256: view`]
Getter for the total number of tokens in existence.
Returns: total supply (`uint256`).
```vyper
total_supply: uint256
@external
@view
def totalSupply() -> uint256:
"""
@notice Total number of tokens in existence.
"""
return self.total_supply
```
This example returns the total supply of CRV tokens. The value is fetched live from the blockchain.
::::
### `decimals`
::::description[`CRV.decimals() -> uint256: view`]
Getter of the decimals of the token.
Returns: decimals (`uint256`).
```vyper
decimals: public(uint256)
@external
def __init__(_name: String[64], _symbol: String[32], _decimals: uint256):
"""
@notice Contract constructor
@param _name Token full name
@param _symbol Token symbol
@param _decimals Number of decimals for token
"""
...
self.decimals = _decimals
...
```
This example returns the number of decimals of the CRV token. The value is fetched live from the blockchain.
::::
### `balanceOf`
::::description[`CRV.balanceOf(arg0: address) -> uint256: view`]
Getter for the CRV token balance of a specific address.
| Input | Type | Description |
| ------ | --------- | ------------------------------- |
| `arg0` | `address` | Wallet to check CRV balance for |
Returns: balance (`uint256`).
```vyper
balanceOf: public(HashMap[address, uint256])
```
This example returns the CRV token balance for a given address. Enter an address and click **Query** to fetch the value live from the blockchain.
::::
---
## Governance and Voting
Curve governance and voting is a integral part of the protocol as all relevant contract are in full control of the DAO. Curve uses [Aragon](https://aragon.org/) for governance and control of the protocol admin functionality. Interaction with Aragon occurs through a [modified implementation](https://github.com/curvefi/curve-aragon-voting) of the Aragon [Voting App](https://github.com/aragon/aragon-apps/tree/master/apps/voting).
Curve's governance system is fully controlled by the DAO and extends to all deployed chains. Not only the Ethereum mainnet, but also all EVM sidechains. Due to the limitation of veCRV being on Ethereum only, all votes (also crosschain ones) are voted on Ethereum.
:::info[Curve Voting Library]
Curve developed a Python toolkit to help create and simulate governance votes. For more information, see the [Voting Library](voting-library.md).
:::
---
## Aragon
Curve has **two different voting agents**, a ownership and parameter agent. While the ownership agent is used for critical changes such as changing the DAO's voting address, the parameter agent is/was mainly used for changing pool parameters. Recently, usage of the parameter agent decreased as parameter changes are now mainly also handled by the ownership agent.
- Voting Ownership: [`0xe478de485ad2fe566d49342cbd03e49ed7db3356`](https://etherscan.io/address/0xe478de485ad2fe566d49342cbd03e49ed7db3356)
- Voting Parameter: [`0xbcff8b0b9419b9a88c44546519b1e909cf330399`](https://etherscan.io/address/0xbcff8b0b9419b9a88c44546519b1e909cf330399)
These contracts are the entry points for creating new votes. Both votes have different quorum and support requirements (more down below).
- Ownership Agent: [`0x40907540d8a6C65c637785e8f8B742ae6b0b9968`](https://etherscan.io/address/0x40907540d8a6C65c637785e8f8B742ae6b0b9968)
- Parameter Agent: [`0x4EEb3bA4f221cA16ed4A0cC7254E2E32DF948c5f`](https://etherscan.io/address/0x4EEb3bA4f221cA16ed4A0cC7254E2E32DF948c5f)
- Emergency Agent: [`0x467947EE34aF926cF1DCac093870f613C96B1E0c`](https://etherscan.io/address/0x467947EE34aF926cF1DCac093870f613C96B1E0c)
---
## Quorum and Support Requirements
Quorum and support requirements are different for each vote type:
- Ownership Votes: 30% quorum, 51% support
- Parameter Votes: 15% quorum, 60% support
Generally, these values can be fetched from the `OwnershipVoting` and `ParameterVoting` contracts:
```python
VotingContract.supportRequiredPct() # minimum support
510000000000000000 # 51%
VotingContract.minAcceptQuorumPct() # minimum quorum
300000000000000000 # 30%
```
---
## Creating a Vote
The VotingOwnership and VotingParameter contracts are the entry points for creating new votes. New votes are created by calling the `newVote` function and passing in the `executionScript` and `metadata` as arguments. New votes can only be created by wallets with at least 2500 veCRV. Voting duration, regardless of the vote type, is 7 days. Once created, proposals can not be deleted or altered.
:::colab[Notebook for Creating a Vote]
To simplify this process, a simple notebook is avaliable to create a new vote. It uses a GoogleColab with Titanoboa (which allows for the connection of a wallet like Rabby or MetaMask) which not only allows the simulation, but also the creation of the vote. One simply needs to modify the script accordingly and run it.
https://colab.research.google.com/drive/1SEmqdBgY3Pcg7q4XWGIoQOc1q5GEVGR6?usp=sharing
:::
---
## Voting on Proposals
Any user with a veCRV balance can vote on a proposal. Vote duration is always 7 days and a vote can not be changed once it has been conducted.
Voting power **starts decaying halfway through the voting period**. If a user starts with 1000 veCRV, and the voting period is 7 days, they will still have a voting power of 1000 veCRV after 3.5 days but its starting to decay linearly until the end of the voting period. So, after another 1.75 days, the user will have a voting power of 500 veCRV, etc. This precausion taken to avoid whales from manipulating votes voting at the last minute.
---
## Executing a Vote
After a vote has reached support and quorum, it can be executed. Execution of votes is fully permissionless (anyone can execute a vote; no minimum veCRV required; only some ETH for gas required) and once done, the `executionScript` passed in when creating the vote is executed.
Execution can either be done through the [Curve UI](https://dao.curve.fi/#/ethereum) or directly from the according Voting Contract.
```python
VotingContract.executeVote()
```
---
## Curve Voting Library
The `curve-voting-lib` is a Python toolkit for creating and simulating Curve DAO governance votes. Built on [titanoboa](https://github.com/vyperlang/titanoboa) (a Vyper/EVM execution framework), it replaces the deprecated `curve-dao` package.
:::github[GitHub]
Source code for `curve-voting-lib` can be found on [GitHub](https://github.com/curvefi/curve-voting-lib).
:::
---
## Overview
The library uses a **context manager** pattern (`vote()`) that automatically captures contract interactions and constructs [Aragon EVM scripts](https://hack.aragon.org/docs/aragonos-ref#evmscripts). Every vote is **simulation-first**: before going live, the library creates the vote using the Convex voter proxy (the largest veCRV holder), votes yes, time-travels past the voting period, and executes — verifying the entire lifecycle on a forked mainnet.
Key features:
- **Context manager voting** — `vote()` monkey-patches titanoboa's `prepare_calldata` to capture contract calls
- **Automatic simulation** — every vote is simulated before going live
- **IPFS integration** — vote descriptions are pinned via Pinata with local caching
- **Cross-chain support** — `xvote()` context manager for cross-chain governance across 20+ chains
---
## Installation
```bash
git clone https://github.com/curvefi/curve-voting-lib.git
cd curve-voting-lib
uv sync
uv run python -m pip install -e .
```
### Environment Variables
| Variable | Description |
| ------------ | ---------------------------------------- |
| `RPC_URL` | Ethereum RPC endpoint URL |
| `PINATA_JWT` | Pinata API JWT token for IPFS pinning |
---
## DAO Types
Curve has two DAO types with different quorum and support requirements:
| DAO Type | Quorum | Support | Use Case |
| ------------- | ------ | ------- | --------------------------------------------- |
| `OWNERSHIP` | 30% | 51% | Critical changes (ownership transfers, etc.) |
| `PARAMETER` | 15% | 60% | Parameter adjustments (pool fees, etc.) |
Each DAO type is a `DAOParameters` dataclass containing the agent, voting contract, token, and quorum:
```python
from voting import OWNERSHIP, PARAMETER
# OWNERSHIP
# agent: 0x40907540d8a6C65c637785e8f8B742ae6b0b9968
# voting: 0xE478de485ad2fe566d49342Cbd03E49ed7DB3356
# token: 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2
# quorum: 30
# PARAMETER
# agent: 0x4eeb3ba4f221ca16ed4a0cc7254e2e32df948c5f
# voting: 0xbcff8b0b9419b9a88c44546519b1e909cf330399
# token: 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2
# quorum: 15
```
---
## Core API
### `vote()` Context Manager
The primary interface. Any mutable contract call made inside the `with vote(...)` block is automatically captured and bundled into an Aragon EVM script.
```python
from voting import vote, OWNERSHIP
with vote(
OWNERSHIP,
"Description of what this vote does",
live_env=None, # None = simulation only, BrowserEnv() or CustomEnv() for live
):
# All contract calls here are captured as vote actions
contract.do_something(value)
assert contract.something() == value # assertions work too
```
The context manager also acts as a **prank** (the caller is set to the DAO agent) and an **anchor** (state changes are reverted after the block). On exit, it:
1. Prepares the EVM script from captured actions
2. Creates the vote using the Convex voter proxy (`0x989AEB4D175E16225E39E87D0D97A3360524AD80`)
3. Votes yes, time-travels past the voting period, and executes
4. If `live_env` is set, pins the description to IPFS and creates the vote on-chain
### `vote_test()` Context Manager
Use `vote_test()` inside a `vote()` block to run assertions without capturing them as vote actions:
```python
with vote(OWNERSHIP, "Set pool implementation"):
factory.set_pool_implementation(new_impl, impl_hash)
with vote_test():
# These calls are NOT captured as vote actions
assert factory.pool_implementations(impl_hash) == new_impl
```
### Live Voting Environments
For creating actual on-chain votes (not just simulations):
```python
from voting import vote, OWNERSHIP, BrowserEnv, CustomEnv
# Using a browser wallet (Rabby, MetaMask, etc.)
with vote(OWNERSHIP, "Description", live_env=BrowserEnv()):
contract.do_something()
# Using a custom account
from eth_account import Account
env = CustomEnv(rpc="https://ethereum-rpc.publicnode.com/", account=Account.from_key("0x..."))
with vote(OWNERSHIP, "Description", live_env=env):
contract.do_something()
```
---
## Usage Example
This example sets a new pool implementation on the TwoCrypto-NG factory:
```python
from voting import vote, abi, OWNERSHIP, BrowserEnv
from eth_utils import keccak
RPC_URL = os.getenv("RPC_URL")
boa.fork(RPC_URL)
factory = abi.twocrypto_ng_mainnet_factory.at("0x98EE851a00abeE0d95D08cF4CA2BdCE32aeaAF7F")
with vote(
OWNERSHIP,
"[twocrypto] Add implementations for donations-enabled pools",
live=BrowserEnv(),
):
factory.set_pool_implementation(
donations_pool := "0xbab4CA419DF4e9ED96435823990C64deAD976a9F",
donations_hash := int.from_bytes(keccak(b"donations"), "big"),
)
assert factory.pool_implementations(donations_hash) == donations_pool
```
Scripts can be run using:
```bash
uv run scripts/gauges/add_gauge.py
uv run scripts/twocrypto-ng/set_implementation.py
```
---
## Cross-chain Governance (`xvote()`)
The `xvote()` context manager nests inside `vote()` to create cross-chain governance proposals. It forks the target chain, captures messages, and broadcasts them through the appropriate broadcaster contract.
```python
from voting import vote, xvote, OWNERSHIP
from voting.xgov.chains import FRAXTAL
with vote(OWNERSHIP, description="[Fraxtal] Set things"):
with xvote(FRAXTAL, "https://rpc.frax.com"):
things.set()
```
### Supported Chains
The library supports 20+ chains including Arbitrum, Optimism, Base, Fraxtal, Mantle, Polygon, Gnosis, Avalanche, BSC, Fantom, Sonic, Kava, Celo, Aurora, Moonbeam, X Layer, Taiko, Ink, Etherlink, XDC, Corn, Plume, Hyperliquid, and TAC.
Each chain uses one of several broadcaster types:
| Broadcaster Type | Chains |
| ------------------ | ------------------------------------- |
| Arbitrum | Arbitrum |
| Optimism | Optimism, Fraxtal, Base, Mantle |
| Polygon zkEVM | X Layer |
| Taiko | Taiko |
| Storage Proofs | All other chains (Gnosis, Polygon, BSC, Avalanche, Fantom, Sonic, Kava, Celo, etc.) |
For more details on the cross-chain governance infrastructure, see the [Cross-chain Governance](x-gov/overview.md) section.
---
## Pre-built ABIs
The `abi` module provides pre-configured contract interfaces for common Curve contracts:
```python
from voting import abi
# Available ABIs include:
abi.aragon_agent # Aragon Agent contract
abi.voting # Aragon Voting contract
abi.twocrypto_ng_mainnet_factory # TwoCrypto-NG Factory
# ... and more
```
These are loaded using `boa.loads_abi()` and can be attached to deployed addresses with `.at()`.
---
## L2 Agents
The `Agent` contracts acts as a proxy for the agents on Ethereum. Relayed votes are directly executed from the `Relayer` contract itself. Execution of messages is done automatically by the `Agent` contract.
:::vyper[`Agent.vy`]
The source code of the `Agent.vy` contract can be found on [GitHub ](https://github.com/curvefi/curve-xgov/blob/master/contracts/Agent.vy).
A comprehensive list of all deployed contracts is available [here ↗](../../../deployments.md).
:::
Just like on Ethereum, there are three different agent contracts: The `Ownership Agent`, the `Parameter Agent`, and the `Emergency Agent`, each one with their own specific roles.
---
### `execute`
::::description[`Agent.execute(_messages: DynArray[Message, MAX_MESSAGES])`]
:::guard[Guarded Method]
This function can only be called by the `RELAYER` of the contract.
:::
Function to execute a sequence of relayed messages. Calling this function directly from the `Agent` contract will result in a reverted transaction, as it can only be called directly from the `Relayer` contract. Execution happens automatically after a message has been relayed.
| Input | Type | Description |
| ----------- | ---------------------------------- | ------------ |
| `_messages` | `DynArray[Message, MAX_MESSAGES]` | Message to execute. |
```vyper
struct Message:
target: address
data: Bytes[MAX_BYTES]
MAX_BYTES: constant(uint256) = 1024
MAX_MESSAGES: constant(uint256) = 8
RELAYER: public(immutable(address))
@external
def execute(_messages: DynArray[Message, MAX_MESSAGES]):
"""
@notice Execute a sequence of messages.
@param _messages An array of messages to be executed.
"""
assert msg.sender == RELAYER
for message in _messages:
raw_call(message.target, message.data)
```
::::
### `RELAYER`
::::description[`Agent.RELAYER() -> address: view`]
Getter for the relayer contract, which relays the messages to the according agent.
Returns: relayer contract (`address`).
```vyper
RELAYER: public(immutable(address))
@external
def __init__():
RELAYER = msg.sender
```
This example returns the `Relayer` contracts for the Arbitrum and Optimism chains.
```shell
>>> Agent.RELAYER() # arbitrum
'0xb7b0FF38E0A01D798B5cd395BbA6Ddb56A323830'
>>> Agent.RELAYER() # optimism
'0x8e1e5001C7B8920196c7E3EdF2BCf47B2B6153ff'
```
::::
---
## Broadcaster
Once a governance vote on Ethereum is successfully passed and executed, a corresponding sequence of messages needs to be communicated to other chains. The `Broadcaster` contract is responsible for broadcasting messages from Ethereum to the `Relayer` contract on other chains.
:::vyper[`Broadcaster.vy`]
Because L2's provide different infrastructures to broadcast messages, the individual broadcaster contracts might slightly vary in their source code and vyper version.
*The following is a list of the individual broadcaster contracts:*
- [ `ArbitrumBroadcaster.vy`](https://github.com/curvefi/curve-xgov/blob/master/contracts/arbitrum/ArbitrumBroadcaster.vy) for Arbitrum
- [ `OptimismBroadcaster.vy`](https://github.com/curvefi/curve-xgov/blob/master/contracts/optimism/OptimismBroadcaster.vy) for Optimism and Optimistic Rollups
- [ `GnosisBroadcaster.vy`](https://github.com/curvefi/curve-xgov/blob/master/contracts/gnosis/GnosisBroadcaster.vy) for Gnosis
- [ `XYZBroadcaster.vy`](https://github.com/curvefi/curve-xgov/blob/master/contracts/xyz/XYZBroadcaster.vy) for all other chains
A comprehensive list of all deployed contracts is available [here ↗](../../../deployments.md).
:::
The `Broadcaster` contracts are managed by the following three admins, which are controlled by the DAO:
- Ownership Admin: [`0x40907540d8a6C65c637785e8f8B742ae6b0b9968`](https://etherscan.io/address/0x40907540d8a6C65c637785e8f8B742ae6b0b9968)
- Parameter Admin: [`0x4EEb3bA4f221cA16ed4A0cC7254E2E32DF948c5f`](https://etherscan.io/address/0x4EEb3bA4f221cA16ed4A0cC7254E2E32DF948c5f)
- Emergency Admin: [`0x467947EE34aF926cF1DCac093870f613C96B1E0c`](https://etherscan.io/address/0x467947EE34aF926cF1DCac093870f613C96B1E0c)
:::warning[Upgradable Ownership]
The admins of the `Broadcaster` contracts are upgradable via a [commit-apply](../../../resources/curve-practices.md#commit--apply) process after a governance vote has passed.
:::
---
## Optimism and Optimistic Rollups
### `broadcast`
::::description[`OptimismBroadcaster.broadcast(_messages: DynArray[Message, MAX_MESSAGES], _gas_limit: uint32 = 0)`]
:::guard[Guarded Method]
This function is only callable by one of the agents (`ownership`, `parameter` or `emergency`).
:::
Function to broadcast a sequence of messages to the `Relayer` contract on a L2.
| Input | Type | Description |
| ------------ | --------------------------------- | --------------------------------- |
| `_messages` | `DynArray[Message, MAX_MESSAGES]` | Sequence of messages to broadcast |
| `_gas_limit` | `uint32` | Gas limit for execution on L2; defaults to `0` |
```vyper
enum Agent:
OWNERSHIP
PARAMETER
EMERGENCY
struct Message:
target: address
data: Bytes[MAX_BYTES]
MAX_BYTES: constant(uint256) = 1024
MAX_MESSAGES: constant(uint256) = 8
agent: HashMap[address, Agent]
ovm_chain: public(address) # CanonicalTransactionChain
ovm_messenger: public(address) # CrossDomainMessenger
@external
def broadcast(_messages: DynArray[Message, MAX_MESSAGES], _gas_limit: uint32 = 0):
"""
@notice Broadcast a sequence of messeages.
@param _messages The sequence of messages to broadcast.
@param _gas_limit The L2 gas limit required to execute the sequence of messages.
"""
agent: Agent = self.agent[msg.sender]
assert agent != empty(Agent)
# https://community.optimism.io/docs/developers/bridge/messaging/#for-l1-%E2%87%92-l2-transactions
gas_limit: uint32 = _gas_limit
if gas_limit == 0:
gas_limit = OVMChain(self.ovm_chain).enqueueL2GasPrepaid()
raw_call(
self.ovm_messenger,
_abi_encode( # sendMessage(address,bytes,uint32)
self,
_abi_encode( # relay(uint256,(address,bytes)[])
agent,
_messages,
method_id=method_id("relay(uint256,(address,bytes)[])"),
),
gas_limit,
method_id=method_id("sendMessage(address,bytes,uint32)"),
),
)
```
::::
### `ovm_chain`
::::description[`OptimismBroadcaster.ovm_chain() -> address: view`]
Getter for the OVM Canonical Transaction Chain contract. This contract can be changed using the [`set_ovm_chain`](#set_ovm_chain) function.
```vyper
interface OVMChain:
def enqueueL2GasPrepaid() -> uint32: view
ovm_chain: public(address) # CanonicalTransactionChain
```
```shell
>>> OptimismBroadcaster.ovm_chain()
'0x5E4e65926BA27467555EB562121fac00D24E9dD2'
```
::::
### `ovm_messenger`
::::description[`OptimismBroadcaster.ovm_messenger() -> address: view`]
Getter for the OVM Cross Domain Messenger contract. This contract can be changed using the [`set_ovm_messenger`](#set_ovm_messenger) function.
```vyper
ovm_messenger: public(address) # CrossDomainMessenger
```
```shell
>>> OptimismBroadcaster.ovm_messenger()
'0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1'
```
::::
### `set_ovm_chain`
::::description[`OptimismBroadcaster.set_ovm_chain(_ovm_chain: address)`]
:::guard[Guarded Method]
This function can only be called by the `ownership admin`.
:::
Function to set a new OVM Canonical Transaction Chain contract.
Emits: `SetOVMChain` event.
| Input | Type | Description |
| ------------ | --------- | ---------------------------- |
| `_ovm_chain` | `address` | New ovm chain address |
```py
event SetOVMChain:
ovm_chain: address
struct AdminSet:
ownership: address
parameter: address
emergency: address
admins: public(AdminSet)
@external
def set_ovm_chain(_ovm_chain: address):
"""
@notice Set the OVM Canonical Transaction Chain storage variable.
"""
assert msg.sender == self.admins.ownership
self.ovm_chain = _ovm_chain
log SetOVMChain(_ovm_chain)
```
This example sets the `ovm_chain` from `ZERO_ADDRESS` to `0x5E4e65926BA27467555EB562121fac00D24E9dD2`.
```shell
>>> OptimismBroadcaster.ovm_chain()
'0x0000000000000000000000000000000000000000'
>>> OptimismBroadcaster.set_ovm_chain(0x5E4e65926BA27467555EB562121fac00D24E9dD2)
>>> OptimismBroadcaster.ovm_chain()
'0x5E4e65926BA27467555EB562121fac00D24E9dD2'
```
::::
### `set_ovm_messenger`
::::description[`OptimismBroadcaster.set_ovm_messenger(_ovm_messenger: address)`]
:::guard[Guarded Method]
This function can only be called by the `ownership admin`.
:::
Function to set a new OVM Cross Domain messenger contract.
Emits: `SetOVMMessenger` event.
| Input | Type | Description |
| ---------------- | --------- | ---------------------------- |
| `_ovm_messenger` | `address` | New ovm messenger address |
```py
event SetOVMMessenger:
ovm_messenger: address
struct AdminSet:
ownership: address
parameter: address
emergency: address
admins: public(AdminSet)
@external
def set_ovm_messenger(_ovm_messenger: address):
"""
@notice Set the OVM Cross Domain Messenger storage variable.
"""
assert msg.sender == self.admins.ownership
self.ovm_messenger = _ovm_messenger
log SetOVMMessenger(_ovm_messenger)
```
This example sets the `ovm_messenger` from `ZERO_ADDRESS` to `0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1`.
```shell
>>> OptimismBroadcaster.ovm_messenger()
'0x0000000000000000000000000000000000000000'
>>> OptimismBroadcaster.set_ovm_messenger(0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1)
>>> OptimismBroadcaster.ovm_messenger()
'0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1'
```
::::
---
## Arbitrum
More on how L1 to L2 messaging on Arbitrum works can be found on the [official Arbitrum documentation](https://docs.arbitrum.io/arbos/l1-to-l2-messaging).
### `broadcast`
::::description[`ArbitrumBroadcaster.broadcast(_messages: DynArray[Message, MAX_MESSAGES], _gas_limit: uint256, _max_fee_per_gas: uint256)`]
:::guard[Guarded Method]
This function is only callable by one of the agents (`ownership`, `parameter` or `emergency`).
:::
Function to broadcast a sequence of messages to the `Relayer` contract on a L2.
| Input | Type | Description |
| ------------------ | --------------------------------- | --------------------------------------------- |
| `_messages` | `DynArray[Message, MAX_MESSAGES]` | Sequence of messages to broadcast |
| `_gas_limit` | `uint256` | Gas limit for execution on L2 |
| `_max_fee_per_gas` | `uint256` | Maximum gas price bid for the execution on L2 |
```py
agent: HashMap[address, Agent]
arb_inbox: public(address)
arb_refund: public(address)
@external
def broadcast(_messages: DynArray[Message, MAX_MESSAGES], _gas_limit: uint256, _max_fee_per_gas: uint256):
"""
@notice Broadcast a sequence of messeages.
@param _messages The sequence of messages to broadcast.
@param _gas_limit The gas limit for the execution on L2.
@param _max_fee_per_gas The maximum gas price bid for the execution on L2.
"""
agent: Agent = self.agent[msg.sender]
assert agent != empty(Agent)
# define all variables here before expanding memory enormously
arb_inbox: address = self.arb_inbox
arb_refund: address = self.arb_refund
submission_cost: uint256 = 0
data: Bytes[MAXSIZE] = _abi_encode(
agent,
_messages,
method_id=method_id("relay(uint256,(address,bytes)[])"),
)
submission_cost = IArbInbox(arb_inbox).calculateRetryableSubmissionFee(len(data), block.basefee)
# NOTE: using `unsafeCreateRetryableTicket` so that refund address is not aliased
raw_call(
arb_inbox,
_abi_encode(
self, # to
empty(uint256), # l2CallValue
submission_cost, # maxSubmissionCost
arb_refund, # excessFeeRefundAddress
arb_refund, # callValueRefundAddress
_gas_limit,
_max_fee_per_gas,
data,
method_id=method_id("unsafeCreateRetryableTicket(address,uint256,uint256,address,address,uint256,uint256,bytes)"),
),
value=submission_cost + _gas_limit * _max_fee_per_gas,
)
```
::::
### `arb_inbox`
::::description[`Broadcaster.arb_inbox() -> address: view`]
Getter for the Arbitrum Delayed Inbox contract.
```vyper
arb_inbox: public(address)
```
```shell
>>> Broadcaster.arb_inbox()
'0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f'
```
::::
### `arb_refund`
::::description[`Broadcaster.arb_refund() -> address: view`]
Getter for the refund address, which is the L2 Vault.
```vyper
arb_refund: public(address)
```
```shell
>>> Broadcaster.arb_refund()
'0x25877b9413Cc7832A6d142891b50bd53935feF82'
```
::::
### `set_arb_inbox`
::::description[`Broadcaster.set_arb_inbox(_arb_inbox: address)`]
:::guard[Guarded Method]
This function is only callable by the `ownership admin`.
:::
Function to set a new Arbitrum Inbox contract.
Emits: `SetArbInbox`
| Input | Type | Description |
| ------------ | --------- | ---------------------------- |
| `_arb_inbox` | `address` | New Arbitrum inbox address |
```vyper
event SetArbInbox:
arb_inbox: address
@external
def set_arb_inbox(_arb_inbox: address):
assert msg.sender == self.admins.ownership
self.arb_inbox = _arb_inbox
log SetArbInbox(_arb_inbox)
```
::::
### `set_arb_refund`
::::description[`Broadcaster.set_arb_refund(_arb_refund: address)`]
:::guard[Guarded Method]
This function is only callable by the `ownership admin`.
:::
Function to set a new refund address.
Emits: `SetArbRefund`
| Input | Type | Description |
| -------------- | --------- | ---------------------------- |
| `_arb_refund` | `address` | New refund address |
```vyper
event SetArbRefund:
arb_refund: address
@external
def set_arb_refund(_arb_refund: address):
assert msg.sender == self.admins.ownership
self.arb_refund = _arb_refund
log SetArbRefund(_arb_refund)
```
::::
---
## Other Chains
Outside of Arbitrum, Optimism, and Optimistic Rollups, Curves cross-chain infrastructure uses a single [`XYZBroadcaster.vy`](https://github.com/curvefi/curve-xgov/blob/master/contracts/xyz/XYZBroadcaster.vy) contract deployed at [`0x5786696bB5bE7fCDb9997E7f89355d9e97FF8d89`](https://etherscan.io/address/0x5786696bB5bE7fCDb9997E7f89355d9e97FF8d89).
This contract is responsible for broadcasting messages across several blockchains including [`Avalanche`](https://www.avax.network/), [`Fantom`](https://fantom.foundation/), [`BinanceSmartChain`](https://www.bnbchain.org/en/bnb-smart-chain), [`Kava`](https://www.kava.io/), and [`Polygon`](https://polygon.technology/).
### `broadcast`
::::description[`XYZBroadcaster.broadcast(_chain_id: uint256, _messages: DynArray[Message, MAX_MESSAGES])`]
:::guard[Guarded Method]
This function is only callable by one of the agents (`ownership`, `parameter` or `emergency`).
:::
Function to broadcast a sequence of messages to the `Relayer` contract on a L2.
| Input | Type | Description |
| ------------ | --------------------------------- | ---------------------------- |
| `_chain_id` | `uint256` | Chain ID to broadcast to |
| `_messages` | `DynArray[Message, MAX_MESSAGES]` | Sequence of messages to broadcast |
```vyper
event Broadcast:
agent: Agent
chain_id: uint256
nonce: uint256
digest: bytes32
enum Agent:
OWNERSHIP
PARAMETER
EMERGENCY
admins: public(AdminSet)
future_admins: public(AdminSet)
agent: HashMap[address, Agent]
nonce: public(HashMap[Agent, HashMap[uint256, uint256]]) # agent -> chainId -> nonce
digest: public(HashMap[Agent, HashMap[uint256, HashMap[uint256, bytes32]]]) # agent -> chainId -> nonce -> messageDigest
@external
def broadcast(_chain_id: uint256, _messages: DynArray[Message, MAX_MESSAGES]):
"""
@notice Broadcast a sequence of messeages.
@param _chain_id The chain id to have messages executed on.
@param _messages The sequence of messages to broadcast.
"""
agent: Agent = self.agent[msg.sender]
assert agent != empty(Agent)
digest: bytes32 = keccak256(_abi_encode(_messages))
nonce: uint256 = self.nonce[agent][_chain_id]
self.digest[agent][_chain_id][nonce] = digest
self.nonce[agent][_chain_id] = nonce + 1
log Broadcast(agent, _chain_id, nonce, digest)
```
::::
---
## L1/L2 Governance
For being able to handle and execute governance activities not only on `Ethereum` but also on other networks such as `Arbitrum`, `Optimism`, `Base`, and many more, Curve has developed a cross-chain governance system called labeled as `x-gov`.
:::github[GitHub]
Source code of the `x-gov` repository can be found on [ GitHub](https://github.com/curvefi/curve-xgov).
A comprehensive list of all deployed components on different chains is available [here](../../../deployments.md).
:::
---
## Architecture & Smart Contracts
Voting for governance proposals is exclusively conducted on the Ethereum mainnet. Following a successful vote, the to be executed actions are broadcasted to a L2 network via a `Broadcaster` contract followed by the execution of the intended actions by `Agents` on the respective network via a `Relayer` contract.
The `Broadcaster` contract is responsible for broadcasting governance actions from Ethereum to the sidechain for execution.
The `Relayer` contract facilitates the relaying of governance actions from Ethereum to `Agents` on the sidechain for execution.
On each sidechain and Layer 2 network, the `Agent` contract assumes three distinct roles: `ownership`, `parameter`, and `emergency`, mirroring the structure found on the Ethereum mainnet for controlled actions.
The `Vault` serves as a contract to hold various assets, controlled by the `OwnershipAgent`.
---
## Creating a Cross-Chain Vote
:::colab[Notebook for Creating a Cross-Chain Vote]
Creating a cross-chain vote is very similar to creating a vote on Ethereum. The only difference is that the `executionScript` is a bit more complex. Here is an example of a cross-chain vote which modifes the minimum and maximum interest rate for lending markets on Arbitrum.
https://colab.research.google.com/drive/1SEmqdBgY3Pcg7q4XWGIoQOc1q5GEVGR6?usp=sharing
The notebook can easily be modified to create votes for any other chain.
:::
---
## Example: Claiming $OP Airdrop on L2
Best way to showcase how a system works is to show an example. Shortly after the launch of Optimism, a airdrop of `OP` tokens to projects that built infrastructure on Optimism was conducted. As Curve deployed their market-making infrastructure there, they were allocated `500,000 OP` tokens. The airdrop could be claimed by simply calling the `transferFrom` on the `OP` token contract as the Vault contract was granted allowance.
Due to Curve's voting mechanism being only on Ethereum, a [proposal to claim the 500,000+ OP tokens](https://dao.curve.fi/#/ethereum/proposals/522-OWNERSHIP) was made.
The proposal initiated a call to the `broadcast` function of the [`Optimism Broadcaster`](https://etherscan.io/address/0x8e1e5001C7B8920196c7E3EdF2BCf47B2B6153ff) with the following calldata:
```sh
Call via agent (0x40907540d8a6C65c637785e8f8B742ae6b0b9968):
├─ To: 0x8e1e5001C7B8920196c7E3EdF2BCf47B2B6153ff
├─ Function: broadcast
└─ Inputs: [('(address,bytes)[]', '_messages', (('0x4200000000000000000000000000000000000042', '23b872dd00000000000000000000000019793c7824be70ec58bb673ca42d2779d12581be000000000000000000000000d166eedf272b860e991d331b71041799379185d5000000000000000000000000000000000000000000006ae6c7dd0a9fb2700000'),))]
```
*[Decoding](https://tools.deth.net/calldata-decoder) the calldata[^1] results in the following:*
[^1]: Calldata in our case is `23b872dd00000000000000000000000019793c7824be70ec58bb673ca42d2779d12581be000000000000000000000000d166eedf272b860e991d331b71041799379185d5000000000000000000000000000000000000000000006ae6c7dd0a9fb2700000`
```sh
function: transferFrom
├─ from: 0x19793c7824Be70ec58BB673CA42D2779d12581BE
├─ to: 0xD166EEdf272B860E991d331B71041799379185D5
└─ amount: 504828000000000000000000
```
Conclusion: Once the vote on the Ethereum Mainnet was successfully passed and [executed](https://etherscan.io/tx/0x31a99a3fbbaf93d2a19861bc8b307ee8806a54c4c5d55580362a6cc41e59a8c0)[^2], the `Optimism Broadcaster` contract relayed the message to the `Relayer` on L2. Subsequently, the `OwnershipAgent` executed the specified calldata, resulting in the transfer of `504,828 OP` tokens from `0x19793c7824Be70ec58BB673CA42D2779d12581BE` to `0xD166EEdf272B860E991d331B71041799379185D5`.
[^2]: Executing passed votes is fully permissionless. Anyone can do it.
---
## L2 Relayer
The `Relayer` contract acts as a middleman, receiving messages from the `Broadcaster` and relaying them to the according `Agent` (`ownership`, `parameter`, or `emergency`).
:::vyper[`Relayer.vy`]
The source code for the `Relayer.vy` contract slightly differ depending on the chain its deployed to.
- [ `ArbitrumRelayer.vy`](https://github.com/curvefi/curve-xgov/blob/master/contracts/arbitrum/ArbitrumRelayer.vy) for Arbitrum
- [ `OptimismRelayer.vy`](https://github.com/curvefi/curve-xgov/blob/master/contracts/optimism/OptimismRelayer.vy) for Optimism and Optimistic Rollups
- [ `XYZRelayer.vy`](https://github.com/curvefi/curve-xgov/blob/master/contracts/xyz/XYZRelayer.vy) for all other chains
A comprehensive list of all deployed contracts is available [here ↗](../../../deployments.md).
:::
The `Relayer` receives the broadcasted message and forwards the message to the appropriate agent. The `Agents` are then responsible for executing the `calldata` of the message.
:::warning[Upgradability of Agents]
A Relayer's agent addresses cannot be altered. Once choosen, there is no way back. In the case of any issues, a new `Relayer` contract has to be deployed.
:::
---
## Relaying Messages
The actual structure of the `relay` function may vary slightly depending on the chain-specific `Relayer` used. However, the general concept remains consistent:
A message is broadcast through the `Broadcaster` contract from Ethereum to the L2, where the `Relayer` relays the message and executes it via the corresponding `Agent`.
### `relay`
::::description[`Relayer.relay(_agent: Agent, _messages: DynArray[Message, MAX_MESSAGES])`]
Function to receive a message from the `Broadcaster` and relay the message to the according agent. This function is automatically called by the `MESSENGER` contract of the according chain. There is no need to manually call this function, which would actually revert as it is a guarded function.
| Input | Type | Description |
| ----------- | --------------------------------- | --------------------------------- |
| `_agent` | `Agent` | Agent to relay the message to |
| `_messages` | `DynArray[Message, MAX_MESSAGES]` | Sequence of messages to relay |
```vyper title="ArbitrumRelayer.vy"
@external
def relay(_agent: Agent, _messages: DynArray[Message, MAX_MESSAGES]):
"""
@notice Receive messages for an agent and relay them.
@param _agent The agent to relay messages to.
@param _messages The sequence of messages to relay.
"""
assert IArbSys(ARBSYS).wasMyCallersAddressAliased()
assert IArbSys(ARBSYS).myCallersAddressWithoutAliasing() == self
IAgent(self.agent[_agent]).execute(_messages)
```
```vyper title="OptimismRelayer.vy"
@external
def relay(_agent: Agent, _messages: DynArray[Message, MAX_MESSAGES]):
"""
@notice Receive messages for an agent and relay them.
@param _agent The agent to relay messages to.
@param _messages The sequence of messages to relay.
"""
assert msg.sender == MESSENGER
assert IMessenger(MESSENGER).xDomainMessageSender() == self
IAgent(self.agent[_agent]).execute(_messages)
```
```vyper title="XYZRelayer.vy"
@external
def relay(_agent: Agent, _messages: DynArray[Message, MAX_MESSAGES]):
"""
@notice Receive messages for an agent and relay them.
@param _agent The agent to relay messages to.
@param _messages The sequence of messages to relay.
"""
assert msg.sender == self.messenger
IAgent(self.agent[_agent]).execute(_messages)
```
::::
---
## Agents
The contract contains the addresses of the `Agents` that are responsible for executing the messages.
### `OWNERSHIP_AGENT`
::::description[`Relayer.OWNERSHIP_AGENT() -> address: view`]
Getter for the ownership agent.
Returns: ownership agent (`address`).
```vyper
OWNERSHIP_AGENT: public(immutable(address))
```
This examples returns the ownership agents for the Arbitrum and Optimism chains.
```shell
>>> ArbitrumRelayer.OWNERSHIP_AGENT()
'0x452030a5D962d37D97A9D65487663cD5fd9C2B32' # arbitrum
>>> OptimismRelayer.OWNERSHIP_AGENT()
'0x28c4A1Fa47EEE9226F8dE7D6AF0a41C62Ca98267' # optimism
```
::::
### `PARAMETER_AGENT`
::::description[`Relayer.PARAMETER_AGENT() -> address: view`]
Getter for the parameter agent.
Returns: parameter agent (`address`).
```vyper
PARAMETER_AGENT: public(immutable(address))
```
This examples returns the parameter agents for the Arbitrum and Optimism chains.
```shell
>>> ArbitrumRelayer.PARAMETER_AGENT()
'0x5ccbB27FB594c5cF6aC0670bbcb360c0072F6839' # arbitrum
>>> OptimismRelayer.PARAMETER_AGENT()
'0xE7F2B72E94d1c2497150c24EA8D65aFFf1027b9b' # optimism
```
::::
### `EMERGENCY_AGENT`
::::description[`Relayer.EMERGENCY_AGENT() -> address: view`]
Getter for the emergency agent.
Returns: emergency agent (`address`).
```vyper
EMERGENCY_AGENT: public(immutable(address))
```
This examples returns the emergency agents for the Arbitrum and Optimism chains.
```shell
>>> ArbitrumRelayer.EMERGENCY_AGENT()
'0x2CB6E1Adf22Af1A38d7C3370441743a123991EC3' # arbitrum
>>> OptimismRelayer.EMERGENCY_AGENT()
'0x9fF1ddE4BE9BbD891836863d227248047B3D881b' # optimism
```
::::
---
## L2 Vault
The `Vault` is a simple smart contract designed to enable the DAO to manage chain-native assets and ERC-20 tokens across chains other than Ethereum.
:::vyper[`Vault.vy`]
The source code of the `Vault.vy` contract can be found on [GitHub ](https://github.com/curvefi/curve-xgov/blob/master/contracts/Vault.vy).
A comprehensive list of all deployed contracts is available [here ↗](../../../deployments.md).
:::
This contract is directly controlled by its `owner`, which is the `OwnershipAgent` of the respective chain.
---
## Transferring Assets
The contract features a transfer function that allows the `owner` to transfer tokens out of the Vault to a specified receiver address.
### `transfer`
::::description[`Vault.transfer(_token: address, _to: address, _value: uint256)`]
:::guard[Guarded Method]
This function can only be called by the `owner` of the contract, which is the respective chain's `OwnershipAgent`.
:::
Function to transfer a specific amount of tokens from the vault to another address.
| Input | Type | Description |
| --------- | ---------- | ---------------------------- |
| `_token` | `address` | Token to transfer |
| `_to` | `address` | Destination of the asset |
| `_value` | `uint256` | Amount of assets to transfer |
```vyper
NATIVE: constant(address) = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
owner: public(address)
@external
def transfer(_token: address, _to: address, _value: uint256):
"""
@notice Transfer an asset
@param _token The token to transfer, or NATIVE if transferring the chain native asset
@param _to The destination of the asset
@param _value The amount of the asset to transfer
"""
assert msg.sender == self.owner
if _token == NATIVE:
send(_to, _value)
else:
assert ERC20(_token).transfer(_to, _value, default_return_value=True)
```
This example transfers 1 ARB token from the vault to `0x0000000000000000000000000000000000000000` on Arbitrum.
```shell
>>> Vault.transfer('0x912CE59144191C1204E64559FE8253a0e49E6548', '0x0000000000000000000000000000000000000000', 1000000000000000000)
```
::::
---
## Contract Ownership
Ownership of the Vault contract follows the classic model of contract ownership. It includes an `owner` address, which can be updated by first committing a future owner and then applying the changes. More on transfering ownership can be found [here](../../../resources/curve-practices.md#commit--apply).
---
## L2 veCRV Delegation
The `L2veCRVDelegation` contract enables users to delegate their veCRV voting power to different addresses on other networks (Layer 2s or sidechains). This is essential for veCRV-utility activities like veCRV boosting on chains where their original address may not be available or convenient. The contract supports both user-initiated delegation and DAO-administered delegation for special cases (e.g., lost keys or non-reachable addresses). The contract also includes mechanisms to allow or revoke delegation to an address and to prevent frontrunning attacks.
:::vyper[`L2veCRVDelegation.vy`]
The source code for the `L2veCRVDelegation.vy` contract is available on [GitHub](https://github.com/curvefi/storage-proofs/blob/main/contracts/vecrv/VecrvDelegate.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.4.0`.
The `L2veCRVDelegation` on :logos-ethereum: Ethereum is deployed at [`0xde1e6A7E8297076f070E857130E593107A0E0cF5`](https://etherscan.io/address/0xde1e6A7E8297076f070E857130E593107A0E0cF5) and contract version is `0.0.1`.
```json
[{"anonymous":false,"inputs":[{"indexed":true,"name":"_chain_id","type":"uint256"},{"indexed":true,"name":"_to","type":"address"}],"name":"AllowDelegation","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_chain_id","type":"uint256"},{"indexed":true,"name":"_from","type":"address"},{"indexed":false,"name":"_to","type":"address"}],"name":"Delegate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previous_owner","type":"address"},{"indexed":true,"name":"new_owner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"inputs":[{"name":"new_owner","type":"address"}],"name":"transfer_ownership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_chain_id","type":"uint256"},{"name":"_from","type":"address"}],"name":"delegated","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_chain_id","type":"uint256"},{"name":"_to","type":"address"}],"name":"delegator","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_chain_id","type":"uint256"},{"name":"_to","type":"address"}],"name":"delegation_allowed","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_chain_id","type":"uint256"},{"name":"_to","type":"address"}],"name":"delegate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_chain_id","type":"uint256"}],"name":"allow_delegation","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_chain_id","type":"uint256"},{"name":"_allow","type":"bool"}],"name":"allow_delegation","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_chain_id","type":"uint256"},{"name":"_from","type":"address"},{"name":"_to","type":"address"}],"name":"delegate_from","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"version","outputs":[{"name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_owner","type":"address"}],"outputs":[],"stateMutability":"nonpayable","type":"constructor"}]
```
:::
---
## Delegation
The delegation system in `L2veCRVDelegation` is designed to be flexible and secure. Users can delegate their veCRV voting power to another address on a specific chain, revoke delegation, or allow others to delegate to them. The contract also provides DAO-level controls for exceptional cases, ensuring that delegation can be managed even if a user is unable to interact directly. The following functions describe the available delegation mechanisms and their intended use cases.
### `delegate`
::::description[`L2veCRVDelegation.delegate(_chain_id: uint256, _to: address)`]
Function to delegate veCRV to another address on a specific chain. Only addresses that have explicitly allowed delegation (via `allow_delegation`) can be delegated to. To revoke delegation, delegate to your own address.
Emits: `Delegate` event.
| Input | Type | Description |
| ----------- | --------- | ------------------------------------ |
| `_chain_id` | `uint256` | Chain ID where to set the delegation |
| `_to` | `address` | Address to delegate to |
```vyper title="L2veCRVDelegation.vy"
event Delegate:
_chain_id: indexed(uint256)
_from: indexed(address)
_to: address
# [chain id][address from][address to]
delegation_from: HashMap[uint256, HashMap[address, address]]
delegation_to: HashMap[uint256, HashMap[address, address]]
@external
def delegate(_chain_id: uint256, _to: address):
"""
@notice Delegate veCRV balance to another address
@dev To revoke delegation set delegation to yourself
@param _chain_id Chain ID where to set
@param _to Address to delegate to
"""
assert self.delegation_to[_chain_id][_to] == self, "Not allowed"
self._delegate(_chain_id, msg.sender, _to)
def _delegate(_chain_id: uint256, _from: address, _to: address):
# Clean previous delegation
prev_to: address = self.delegation_from[_chain_id][_from]
if prev_to not in [empty(address), self]:
self.delegation_to[_chain_id][prev_to] = empty(address)
self.delegation_from[_chain_id][_from] = _to
self.delegation_to[_chain_id][_to] = _from
log Delegate(_chain_id, _from, _to)
```
This example delegates the caller's veCRV balance to `0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683` on chain `146`.
```shell
>>> L2veCRVDelegation.delegate(146, '0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683')
```
::::
### `delegate_from`
::::description[`L2veCRVDelegation.delegate_from(_chain_id: uint256, _from: address, _to: address)`]
:::guard[Guarded Method by [Snekmate 🐍](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
DAO-only function to set delegation for addresses that cannot interact directly (e.g., lost keys or non-reachable addresses). Only callable by the contract owner.
Emits: `Delegate` event.
| Input | Type | Description |
| ----------- | --------- | ------------------------------------ |
| `_chain_id` | `uint256` | Chain ID where to set the delegation |
| `_from` | `address` | Address that delegates |
| `_to` | `address` | Address balance being delegated to |
```vyper
from snekmate.auth import ownable
event Delegate:
_chain_id: indexed(uint256)
_from: indexed(address)
_to: address
# [chain id][address from][address to]
delegation_from: HashMap[uint256, HashMap[address, address]]
delegation_to: HashMap[uint256, HashMap[address, address]]
@external
def delegate_from(_chain_id: uint256, _from: address, _to: address):
"""
@notice DAO-owned method to set delegation for non-reachable addresses
@param _chain_id Chain ID where to set
@param _from Address that delegates
@param _to Address balance being delegated to
"""
ownable._check_owner()
self._delegate(_chain_id, _from, _to)
def _delegate(_chain_id: uint256, _from: address, _to: address):
# Clean previous delegation
prev_to: address = self.delegation_from[_chain_id][_from]
if prev_to not in [empty(address), self]:
self.delegation_to[_chain_id][prev_to] = empty(address)
self.delegation_from[_chain_id][_from] = _to
self.delegation_to[_chain_id][_to] = _from
log Delegate(_chain_id, _from, _to)
```
```vyper
owner: public(address)
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
In this example, the DAO delegates the veCRV balance from `0x5802ad5D5B1c63b3FC7DE97B55e6db19e5d36462` to `0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683` on chain `146`.
```shell
# DAO sets delegation for a non-reachable address
>>> L2veCRVDelegation.delegate_from(146, '0x5802ad5D5B1c63b3FC7DE97B55e6db19e5d36462', '0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683')
```
::::
### `allow_delegation`
::::description[`L2veCRVDelegation.allow_delegation(_chain_id: uint256, _allow: bool = True)`]
Allows or revokes permission for others to delegate veCRV to your address on a specific chain. This must be called before anyone can delegate to you, and is required to prevent frontrunning attacks.
Emits: `AllowDelegation` or `Delegate` event.
| Input | Type | Description |
| ----------- | --------- | ----------------------------------------------------------------- |
| `_chain_id` | `uint256` | Chain ID |
| `_allow` | `bool` | `true` to allow delegation, `false` to remove; defaults to `true` |
```vyper title="L2veCRVDelegation.vy"
event AllowDelegation:
_chain_id: indexed(uint256)
_to: indexed(address)
event Delegate:
_chain_id: indexed(uint256)
_from: indexed(address)
_to: address
# [chain id][address from][address to]
delegation_from: HashMap[uint256, HashMap[address, address]]
delegation_to: HashMap[uint256, HashMap[address, address]]
@external
def allow_delegation(_chain_id: uint256, _allow: bool = True):
"""
@notice Allow delegation to your address
@dev Needed to deal with frontrun
@param _chain_id Chaind ID to allow for
@param _allow True(default) if allow, and False to remove delegation
"""
# Clean current delegation
_from: address = self.delegation_to[_chain_id][msg.sender]
if _from not in [empty(address), self]:
self.delegation_from[_chain_id][_from] = empty(address)
log Delegate(_chain_id, _from, empty(address))
if _allow:
self.delegation_to[_chain_id][msg.sender] = self
log AllowDelegation(_chain_id, msg.sender)
else:
self.delegation_to[_chain_id][msg.sender] = empty(address)
```
This example shows how to allow and revoke delegation.
```shell
>>> L2veCRVDelegation.allow_delegation(146, True) # Allow delegation to your address on chain 146
>>> L2veCRVDelegation.allow_delegation(146, False) # Revoke delegation
```
::::
### `delegation_allowed`
::::description[`L2veCRVDelegation.delegation_allowed(_chain_id: uint256, _to: address) -> bool: view`]
Getter method to check whether delegation to a given address is currently allowed on a specific chain.
Returns: `true` if delegation is allowed, `false` otherwise (`bool`).
| Input | Type | Description |
| ----------- | --------- | -------------------- |
| `_chain_id` | `uint256` | Chain ID |
| `_to` | `address` | Address to check for |
```vyper title="L2veCRVDelegation.vy"
delegation_to: HashMap[uint256, HashMap[address, address]]
@external
@view
def delegation_allowed(_chain_id: uint256, _to: address) -> bool:
"""
@notice Check whether delegation to this address is allowed
@param _chain_id Chain ID to check for
@param _to Address to check for
@return True if allowed to delegate
"""
return self.delegation_to[_chain_id][_to] == self
```
This example checks if delegation for a given address is allowed on a specific chain. Enter a chain ID and address, then click **Query** to fetch the value live from the blockchain.
::::
### `delegated`
::::description[`L2veCRVDelegation.delegated(_chain_id: uint256, _from: address) -> address: view`]
Returns the address to which a given user's veCRV balance is delegated on a specific chain. If no delegation is set, returns the original address.
Returns: delegation destination (`address`).
| Input | Type | Description |
| ----------- | --------- | ----------- |
| `_chain_id` | `uint256` | Chain ID |
| `_from` | `address` | Delegator |
```vyper title="L2veCRVDelegation.vy"
delegation_from: HashMap[uint256, HashMap[address, address]]
@external
@view
def delegated(_chain_id: uint256, _from: address) -> address:
"""
@notice Get contract balance being delegated to
@param _chain_id Chain ID to check for
@param _from Address of delegator
@return Destination address of delegation
"""
addr: address = self.delegation_from[_chain_id][_from]
if addr == empty(address):
addr = _from
return addr
```
This example returns the address to which a given user's veCRV balance is delegated on a specific chain. Enter a chain ID and delegator address, then click **Query** to fetch the value live from the blockchain.
::::
### `delegator`
::::description[`L2veCRVDelegation.delegator(_chain_id: uint256, _to: address) -> address: view`]
Getter for the address that delegated their veCRV balance to a given address on a specific chain. If no delegator is set, returns the `_to` address itself.
Returns: delegator (`address`).
| Input | Type | Description |
| ----------- | --------- | ------------------------------------ |
| `_chain_id` | `uint256` | Chain ID to check for |
| `_to` | `address` | Delegatee |
```vyper title="L2veCRVDelegation.vy"
delegation_to: HashMap[uint256, HashMap[address, address]]
@external
@view
def delegator(_chain_id: uint256, _to: address) -> address:
"""
@notice Get contract delegating balance to `_to`
@param _chain_id Chain ID to check for
@param _to Address of delegated to
@return Address of delegator
"""
addr: address = self.delegation_to[_chain_id][_to]
if addr in [empty(address), self]:
return _to
return addr
```
This example returns the address that delegated their veCRV balance to a given address on a specific chain. Enter a chain ID and delegatee address, then click **Query** to fetch the value live from the blockchain.
::::
---
## Contract Ownership
Ownership of the contract is managed using the [`ownable.vy`](https://github.com/pcaversaccio/snekmate/blob/main/src/snekmate/auth/ownable.vy) module from 🐍 [Snekmate](https://github.com/pcaversaccio/snekmate) which implements a basic access control mechanism, where there is an `owner` that can be granted exclusive access to specific functions.
### `owner`
::::description[`L2veCRVDelegation.owner() -> address: view`]
Getter for the owner of the contract. This is the address that can call restricted functions like `transfer_ownership` and `delegate_from`.
Returns: contract owner (`address`).
```vyper
from snekmate.auth import ownable
initializes: ownable
exports: (
ownable.transfer_ownership,
ownable.owner,
)
@deploy
def __init__(_owner: address):
"""
@notice Contract constructor
@param _owner Owner address
"""
ownable.__init__()
ownable._transfer_ownership(_owner)
```
```vyper
owner: public(address)
@deploy
@payable
def __init__():
"""
@dev To omit the opcodes for checking the `msg.value`
in the creation-time EVM bytecode, the constructor
is declared as `payable`.
@notice The `owner` role will be assigned to
the `msg.sender`.
"""
self._transfer_ownership(msg.sender)
```
::::
### `transfer_ownership`
::::description[`L2veCRVDelegation.transfer_ownership(new_owner: address)`]
:::guard[Guarded Method by [Snekmate 🐍](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Function to transfer the ownership of the contract to a new address.
Emits: `OwnershipTransferred`
| Input | Type | Description |
| ----------- | --------- | -------------------------- |
| `new_owner` | `address` | New owner of the contract |
```vyper
from snekmate.auth import ownable
initializes: ownable
exports: (
ownable.transfer_ownership,
ownable.owner,
)
@deploy
def __init__(_owner: address):
"""
@notice Contract constructor
@param _owner Owner address
"""
ownable.__init__()
ownable._transfer_ownership(_owner)
```
```vyper
owner: public(address)
event OwnershipTransferred:
previous_owner: indexed(address)
new_owner: indexed(address)
@external
def transfer_ownership(new_owner: address):
"""
@dev Transfers the ownership of the contract
to a new account `new_owner`.
@notice Note that this function can only be
called by the current `owner`. Also,
the `new_owner` cannot be the zero address.
@param new_owner The 20-byte address of the new owner.
"""
self._check_owner()
assert new_owner != empty(address), "ownable: new owner is the zero address"
self._transfer_ownership(new_owner)
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
@internal
def _transfer_ownership(new_owner: address):
"""
@dev Transfers the ownership of the contract
to a new account `new_owner`.
@notice This is an `internal` function without
access restriction.
@param new_owner The 20-byte address of the new owner.
"""
old_owner: address = self.owner
self.owner = new_owner
log OwnershipTransferred(old_owner, new_owner)
```
In this example, the ownership of the contract is transferred to a new address.
```shell
>>> L2veCRVDelegation.transfer_ownership("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
```
::::
---
## Other Methods
### `version`
::::description[`L2veCRVDelegation.version() -> String: view`]
Getter for the contract version.
Returns: contract version (`String`).
```vyper title="L2veCRVDelegation.vy"
version: public(constant(String[8])) = "0.0.1"
```
::::
---
## L2 VotingEscrow Oracle
The `L2VotingEscrowOracle` contract is used to fetch information from the `VotingEscrow` from Ethereum. This data can then be used to calculate boost rates for providing liquidity.
:::vyper[`L2VotingEscrowOracle.vy`]
The source code for the `L2VotingEscrowOracle.vy` contract is available on [GitHub](https://github.com/curvefi/storage-proofs/blob/main/contracts/vecrv/VecrvOracle.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.4.0`.
The `VotingEscrow` on :logos-ethereum: Ethereum is deployed at [`0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2`](https://etherscan.io/address/0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2).
The `L2VotingEscrowOracle` contract is deployed at the following addresses and is version `1.0.0`:
- :logos-optimism: Optimism: [`0xf1946d4879646e0fcd8f5bb32a5636ed8055176d`](https://optimistic.etherscan.io/address/0xf1946d4879646e0fcd8f5bb32a5636ed8055176d)
- :logos-arbitrum: Arbitrum: [`0x4D1AF9911e4c19f64Be36c36EF39Fd026Bc9bb61`](https://arbiscan.io/address/0x4D1AF9911e4c19f64Be36c36EF39Fd026Bc9bb61)
- :logos-fraxtal: Fraxtal: [`0xF3daD3Ca2eF135b248128Ab1Ed984FB6F2185CBf`](https://fraxscan.com/address/0xF3daD3Ca2eF135b248128Ab1Ed984FB6F2185CBf)
- :logos-sonic: Sonic: [`0x361aa6D20fbf6185490eB2ddf1DD1D3F301C201d`](https://sonicscan.org/address/0x361aa6D20fbf6185490eB2ddf1DD1D3F301C201d)
- :logos-mantle: Mantle: [`0x852F32c22C5035EA12566EDFB4415625776D75d5`](https://mantlescan.xyz/address/0x852F32c22C5035EA12566EDFB4415625776D75d5)
- :logos-base: Base: [`0xeB896fB7D1AaE921d586B0E5a037496aFd3E2412`](https://basescan.org/address/0xeB896fB7D1AaE921d586B0E5a037496aFd3E2412)
- :logos-taiko: Taiko: [`0x5C57BdcFF69B4F1D894EA70c0470D39C8FA0ee30`](https://taikoscan.io/address/0x5C57BdcFF69B4F1D894EA70c0470D39C8FA0ee30)
```json
[{"anonymous":false,"inputs":[{"indexed":false,"name":"_epoch","type":"uint256"}],"name":"UpdateTotal","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_user","type":"address"},{"indexed":false,"name":"_user_point_epoch","type":"uint256"}],"name":"UpdateBalance","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_from","type":"address"},{"indexed":false,"name":"_to","type":"address"}],"name":"Delegate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"role","type":"bytes32"},{"indexed":true,"name":"account","type":"address"},{"indexed":true,"name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"role","type":"bytes32"},{"indexed":true,"name":"previousAdminRole","type":"bytes32"},{"indexed":true,"name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"role","type":"bytes32"},{"indexed":true,"name":"account","type":"address"},{"indexed":true,"name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"inputs":[{"name":"interface_id","type":"bytes4"}],"name":"supportsInterface","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"bytes32"},{"name":"arg1","type":"address"}],"name":"hasRole","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"role","type":"bytes32"},{"name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"role","type":"bytes32"},{"name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_from","type":"address"}],"name":"delegated","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_to","type":"address"}],"name":"delegator","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_user","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_user","type":"address"},{"name":"_timestamp","type":"uint256"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_timestamp","type":"uint256"}],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_addr","type":"address"}],"name":"get_last_user_slope","outputs":[{"name":"","type":"int128"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_addr","type":"address"}],"name":"locked__end","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_user","type":"address"},{"name":"_user_point_epoch","type":"uint256"},{"components":[{"name":"bias","type":"int128"},{"name":"slope","type":"int128"},{"name":"ts","type":"uint256"},{"name":"blk","type":"uint256"}],"name":"_user_point_history","type":"tuple"},{"components":[{"name":"amount","type":"int128"},{"name":"end","type":"uint256"}],"name":"_locked","type":"tuple"},{"name":"_block_number","type":"uint256"}],"name":"update_balance","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_epoch","type":"uint256"},{"components":[{"name":"bias","type":"int128"},{"name":"slope","type":"int128"},{"name":"ts","type":"uint256"},{"name":"blk","type":"uint256"}],"name":"_point_history","type":"tuple"},{"name":"_slope_changes","type":"int128[]"},{"name":"_block_number","type":"uint256"}],"name":"update_total","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_block_number","type":"uint256"}],"name":"update_delegation","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"BALANCE_VERIFIER","outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TOTAL_VERIFIER","outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DELEGATION_VERIFIER","outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"epoch","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint256"}],"name":"point_history","outputs":[{"components":[{"name":"bias","type":"int128"},{"name":"slope","type":"int128"},{"name":"ts","type":"uint256"},{"name":"blk","type":"uint256"}],"name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"}],"name":"user_point_epoch","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"},{"name":"arg1","type":"uint256"}],"name":"user_point_history","outputs":[{"components":[{"name":"bias","type":"int128"},{"name":"slope","type":"int128"},{"name":"ts","type":"uint256"},{"name":"blk","type":"uint256"}],"name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"}],"name":"locked","outputs":[{"components":[{"name":"amount","type":"int128"},{"name":"end","type":"uint256"}],"name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint256"}],"name":"slope_changes","outputs":[{"name":"","type":"int128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"last_block_number","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"version","outputs":[{"name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"outputs":[],"stateMutability":"nonpayable","type":"constructor"}]
```
:::
---
## Updating the Oracle
The "Updating the Oracle" section describes the privileged functions that allow authorized verifiers to update the state of the L2VotingEscrowOracle contract. These include updating individual user veCRV balances (`update_balance`) and the global voting power state (`update_total`). Each function is protected by role-based access control, ensuring only designated accounts (with roles like `BALANCE_VERIFIER` or `TOTAL_VERIFIER`) can perform updates. Updates are linearized using a block number check to prevent outdated data from overwriting newer state. This mechanism ensures the oracle remains synchronized with the canonical VotingEscrow contract on Ethereum, providing accurate and secure off-chain voting power data for L2 environments.
### `update_balance`
::::description[`L2VotingEscrowOracle.update_balance(_user: address, _user_point_epoch: uint256, _user_point_history: Point, _locked: LockedBalance, _block_number: uint256):`]
:::guard[Guarded Method by [Snekmate 🐍](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. Calling the `update_balance()` function can only be done by the address holding the `BALANCE_VERIFIER` role.
:::
Function to update the user's veCRV balance.
Emits: `UpdateBalance`
| Input | Type | Description |
| ------- | --------- | ----------- |
| `_user` | `address` | Address of the user to update the balance |
| `_user_point_epoch` | `uint256` | Last `_user`'s checkpointed epoch |
| `_user_point_history` | `Point` | Last `_user`'s point history |
| `_locked` | `LockedBalance` | `_user`'s locked balance |
| `_block_number` | `uint256` | Block number |
```vyper
event UpdateBalance:
_user: address
_user_point_epoch: uint256
user_point_epoch: public(HashMap[address, uint256])
user_point_history: public(HashMap[address, HashMap[uint256, Point]])
locked: public(HashMap[address, LockedBalance])
@external
def update_balance(
_user: address,
_user_point_epoch: uint256,
_user_point_history: Point,
_locked: LockedBalance,
_block_number: uint256,
):
"""
@notice Update user balance
@param _user Address of the user to verify for
@param _user_point_epoch Last `_user`s checkpointed epoch
@param _user_point_history Last `_user`s point history
@param _locked `_user`s locked balance
"""
access_control._check_role(BALANCE_VERIFIER, msg.sender)
assert self.last_block_number <= _block_number, "Outdated update"
# assert (
# self.user_point_epoch[_user] <= _user_point_epoch
# and self.user_point_history[_user][_user_point_epoch].ts <= _user_point_history.ts
# ), "Outdated update"
self.user_point_epoch[_user] = _user_point_epoch
self.user_point_history[_user][_user_point_epoch] = _user_point_history
self.locked[_user] = _locked
log UpdateBalance(_user, _user_point_epoch)
self.last_block_number = _block_number
```
```vyper
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
@param role The 32-byte role definition.
@param account The 20-byte address of the account.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
```shell
>>> soon
```
::::
### `balanceOf`
::::description[`L2VotingEscrowOracle.balanceOf(_user: address, _timestamp: uint256 = block.timestamp) -> uint256: view`]
Returns the veCRV balance of a user at a given timestamp, accounting for delegation.
Returns: veCRV balance of the user at a specific timestamp (`uint256`).
| Input | Type | Description |
| ---------- | --------- | ------------------------------------------- |
| `_user` | `address` | Address of the user |
| `_timestamp` | `uint256` | Timestamp for balance check; defaults to current ts |
```vyper title="L2VotingEscrowOracle.vy"
epoch: public(uint256)
point_history: public(HashMap[uint256, Point])
@view
@external
def balanceOf(_user: address, _timestamp: uint256 = block.timestamp) -> uint256:
"""
@notice Get veCRV balance of user
@param _user Address of the user
@param _timestamp Timestamp for the balance check
@return Balance of user
"""
user: address = self._get_user_after_delegation(_user)
if user == empty(address):
return 0
return self._balanceOf(user, _timestamp)
@view
def _balanceOf(user: address, timestamp: uint256) -> uint256:
epoch: uint256 = self.user_point_epoch[user]
if epoch == 0:
return 0
last_point: Point = self.user_point_history[user][epoch]
last_point.bias -= last_point.slope * convert(timestamp - last_point.ts, int128)
if last_point.bias < 0:
return 0
return convert(last_point.bias, uint256)
```
This example returns the veCRV balance of `0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683` on Optimism.
```shell
>>> L2VotingEscrowOracle.balanceOf("0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683")
2036475234652423013212
```
::::
### `update_total`
::::description[`L2VotingEscrowOracle.update_total(_epoch: uint256, _point_history: Point, _slope_changes: DynArray[int128, SLOPE_CHANGES_CNT], _block_number: uint256):`]
:::guard[Guarded Method by [Snekmate 🐍](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. Calling the `update_total()` function can only be done by the address holding the `TOTAL_VERIFIER` role.
:::
Updates the global `VotingEscrow` values, including epoch, point history, and slope changes.
Emits: `UpdateTotal`
| Input | Type | Description |
| --------------- | ------------------------------------ | ------------------------------------------- |
| `_epoch` | `uint256` | Current epoch in VotingEscrow contract |
| `_point_history`| `Point` | Last epoch point history |
| `_slope_changes`| `DynArray[int128, SLOPE_CHANGES_CNT]`| Slope changes for upcoming epochs |
| `_block_number` | `uint256` | Block number for update linearization |
```vyper
event UpdateTotal:
_epoch: uint256
epoch: public(uint256)
point_history: public(HashMap[uint256, Point])
@external
def update_total(
_epoch: uint256,
_point_history: Point,
_slope_changes: DynArray[int128, SLOPE_CHANGES_CNT],
_block_number: uint256,
):
"""
@notice Update VotingEscrow global values
@param _epoch Current epoch in VotingEscrow contract
@param _point_history Last epoch point history
@param _slope_changes Slope changes for upcoming epochs
"""
access_control._check_role(TOTAL_VERIFIER, msg.sender)
assert self.last_block_number <= _block_number, "Outdated update"
# assert (
# self.epoch <= _epoch and self.point_history[_epoch].ts <= _point_history.ts
# ), "Outdated update"
self.epoch = _epoch
self.point_history[_epoch] = _point_history
start_time: uint256 = WEEK + (_point_history.ts // WEEK) * WEEK
for i: uint256 in range(len(_slope_changes), bound=SLOPE_CHANGES_CNT):
self.slope_changes[start_time + WEEK * i] = _slope_changes[i]
log UpdateTotal(_epoch)
self.last_block_number = _block_number
```
```vyper
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
@param role The 32-byte role definition.
@param account The 20-byte address of the account.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
```shell
>>> soon
```
::::
### `totalSupply`
::::description[`L2VotingEscrowOracle.totalSupply(_timestamp: uint256 = block.timestamp) -> uint256: view`]
Getter for the total veCRV voting power at a given timestamp.
Returns: total veCRV supply at a specific timestamp (`uint256`).
| Input | Type | Description |
| ---------- | --------- | ------------------------------------------- |
| `_timestamp` | `uint256` | Timestamp for total supply check; defaults to current ts |
```vyper title="L2VotingEscrowOracle.vy"
point_history: public(HashMap[uint256, Point])
slope_changes: public(HashMap[uint256, int128])
@view
@external
def totalSupply(_timestamp: uint256 = block.timestamp) -> uint256:
"""
@notice Calculate total voting power
@param _timestamp Timestamp at which to check totalSupply
@return Total supply
"""
last_point: Point = self.point_history[self.epoch]
t_i: uint256 = (last_point.ts // WEEK) * WEEK
for i: uint256 in range(256):
t_i += WEEK
d_slope: int128 = 0
if t_i > _timestamp:
t_i = _timestamp
else:
d_slope = self.slope_changes[t_i]
last_point.bias -= last_point.slope * convert(t_i - last_point.ts, int128)
if t_i == _timestamp or d_slope == 0:
break
last_point.slope += d_slope
last_point.ts = t_i
if last_point.bias < 0:
return 0
return convert(last_point.bias, uint256)
```
This example returns the total veCRV supply on a specific Layer 2 network.
```shell
>>> L2VotingEscrowOracle.totalSupply(1751872783)
799701502604227430381519403
```
::::
---
## Delegations
The `L2VotingEscrowOracle` contract supports delegation of veCRV balances, allowing one address to delegate its voting power to another. This is managed through the `delegated` and `delegator` view functions, which let users query the current delegation relationships. When a user delegates, their veCRV balance is effectively counted towards the delegatee, enabling boosting strategies. Delegation updates are controlled by the `DELEGATION_VERIFIER` role, ensuring only authorized entities can modify delegation mappings.
### `delegated`
::::description[`L2VotingEscrowOracle.delegated(_from: address) -> address: view`]
Getter for the address to which the veCRV balance of `_from` is delegated. If not delegated, returns `_from` itself.
Returns: address receiving the delegation (`address`).
| Input | Type | Description |
| ---------- | --------- | ------------------------ |
| `_from` | `address` | Address of the delegator |
```vyper title="L2VotingEscrowOracle.vy"
# [address from][address to]
delegation_from: HashMap[address, address]
@external
@view
def delegated(_from: address) -> address:
"""
@notice Get contract balance being delegated to
@param _from Address of delegator
@return Destination address of delegation
"""
addr: address = self.delegation_from[_from]
if addr == empty(address):
addr = _from
return addr
```
This example shows the delegation of `0x5802ad5D5B1c63b3FC7DE97B55e6db19e5d36462` to `0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683`.
```shell
>>> L2VotingEscrowOracle.delegated("0x5802ad5D5B1c63b3FC7DE97B55e6db19e5d36462")
"0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683"
```
::::
### `delegator`
::::description[`L2VotingEscrowOracle.delegator(_to: address) -> address: view`]
Getter for the address that delegated its veCRV balance to `_to`. If not delegated, returns `_to` itself.
Returns: address of the delegator (`address`).
| Input | Type | Description |
| ---------- | --------- | ------------------------------------------- |
| `_to` | `address` | Address of the delegatee |
```vyper title="L2VotingEscrowOracle.vy"
delegation_to: HashMap[address, address]
@external
@view
def delegator(_to: address) -> address:
"""
@notice Get contract delegating balance to `_to`
@param _to Address of delegated to
@return Address of delegator
"""
addr: address = self.delegation_to[_to]
if addr == empty(address):
addr = _to
return addr
```
This example returns the delegator of `0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683`.
```shell
>>> L2VotingEscrowOracle.delegator("0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683")
"0x5802ad5D5B1c63b3FC7DE97B55e6db19e5d36462"
```
::::
### `update_delegation`
::::description[`L2VotingEscrowOracle.update_delegation(_from: address, _to: address, _block_number: uint256):`]
:::guard[Guarded Method by [Snekmate 🐍](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. Calling the `update_delegation()` function can only be done by the address holding the `DELEGATION_VERIFIER` role.
:::
Function to update the delegation of veCRV balance from `_from` to `_to`.
Emits: `Delegate`
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------- |
| `_from` | `address` | Address being delegated |
| `_to` | `address` | Address delegated to |
| `_block_number`| `uint256`| Block number at which delegation holds true |
```vyper
# [address from][address to]
delegation_from: HashMap[address, address]
delegation_to: HashMap[address, address]
last_delegation: HashMap[address, uint256]
last_block_number: public(uint256)
@external
def update_delegation(_from: address, _to: address, _block_number: uint256):
"""
@notice Update veCRV balance delegation
@dev Block number is used to linearize updates
@param _from Address being delegated
@param _to Address delegated to
@param _block_number Block number at which delegation holds true
"""
access_control._check_role(DELEGATION_VERIFIER, msg.sender)
assert self.last_block_number <= _block_number, "Outdated update"
delegated: address = self.delegation_from[_from]
if delegated != empty(address): # revoke delegation
self.delegation_to[delegated] = empty(address)
self.delegation_from[_from] = _to
if _to != empty(address):
self.delegation_to[_to] = _from
log Delegate(_from, _to)
self.last_block_number = _block_number
```
```vyper
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
@param role The 32-byte role definition.
@param account The 20-byte address of the account.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
```shell
>>> soon
```
::::
---
## Roles and Ownership Management
The `L2VotingEscrowOracle` contract uses a role-based access control system, implemented via the Snekmate `access_control` module, to manage permissions for sensitive operations. Roles such as `BALANCE_VERIFIER`, `TOTAL_VERIFIER`, and `DELEGATION_VERIFIER` restrict who can update user balances, total supply, and delegation mappings, respectively. The `DEFAULT_ADMIN_ROLE` acts as the admin for all roles, and only accounts with the appropriate admin role can grant or revoke roles. This structure ensures that only authorized entities can perform privileged actions, providing robust security and flexibility for contract management.
### `BALANCE_VERIFIER`
::::description[`L2VotingEscrowOracle.BALANCE_VERIFIER() -> bytes32: view`]
The role identifier for accounts allowed to update user balances.
Returns: role hash (`bytes32`).
```vyper title="L2VotingEscrowOracle.vy"
BALANCE_VERIFIER: public(constant(bytes32)) = keccak256("BALANCE_VERIFIER")
@deploy
def __init__():
access_control.__init__()
access_control._set_role_admin(BALANCE_VERIFIER, access_control.DEFAULT_ADMIN_ROLE)
access_control._set_role_admin(TOTAL_VERIFIER, access_control.DEFAULT_ADMIN_ROLE)
access_control._set_role_admin(DELEGATION_VERIFIER, access_control.DEFAULT_ADMIN_ROLE)
```
This example returns the `BALANCE_VERIFIER` role as `bytes32` of the veCRV Oracle on Optimism.
```shell
>>> L2VotingEscrowOracle.BALANCE_VERIFIER()
'0x91ecbab409000ca436e362529d6a0ee19bfacafc306d0b7328e4b31a37513d1c'
```
::::
### `TOTAL_VERIFIER`
::::description[`L2VotingEscrowOracle.TOTAL_VERIFIER() -> bytes32: view`]
The role identifier for accounts allowed to update total supply.
Returns: role hash (`bytes32`).
```vyper title="L2VotingEscrowOracle.vy"
TOTAL_VERIFIER: public(constant(bytes32)) = keccak256("TOTAL_VERIFIER")
@deploy
def __init__():
access_control.__init__()
access_control._set_role_admin(BALANCE_VERIFIER, access_control.DEFAULT_ADMIN_ROLE)
access_control._set_role_admin(TOTAL_VERIFIER, access_control.DEFAULT_ADMIN_ROLE)
access_control._set_role_admin(DELEGATION_VERIFIER, access_control.DEFAULT_ADMIN_ROLE)
```
This example returns the `TOTAL_VERIFIER` role as `bytes32` of the veCRV Oracle on Optimism.
```shell
>>> L2VotingEscrowOracle.TOTAL_VERIFIER()
'0x91bab4a1f219aaf3591b80c219b7a6eda6e5ddcadf2001c395591dcc40ecfbb7'
```
::::
### `DELEGATION_VERIFIER`
::::description[`L2VotingEscrowOracle.DELEGATION_VERIFIER() -> bytes32: view`]
The role identifier for accounts allowed to update delegation.
Returns: role hash (`bytes32`).
```vyper title="L2VotingEscrowOracle.vy"
DELEGATION_VERIFIER: public(constant(bytes32)) = keccak256("DELEGATION_VERIFIER")
@deploy
def __init__():
access_control.__init__()
access_control._set_role_admin(BALANCE_VERIFIER, access_control.DEFAULT_ADMIN_ROLE)
access_control._set_role_admin(TOTAL_VERIFIER, access_control.DEFAULT_ADMIN_ROLE)
access_control._set_role_admin(DELEGATION_VERIFIER, access_control.DEFAULT_ADMIN_ROLE)
```
This example returns the `DELEGATION_VERIFIER` role as `bytes32` of the veCRV Oracle on Optimism.
```shell
>>> L2VotingEscrowOracle.DELEGATION_VERIFIER()
'0xe887cc0717dab2ad628f68695129fefff34ee397bdd39e44a259e2cae80f49b7'
```
::::
### `DEFAULT_ADMIN_ROLE`
::::description[`L2VotingEscrowOracle.DEFAULT_ADMIN_ROLE() -> bytes32: view`]
Getter for the default admin role.
Returns: default admin (`bytes32`).
```vyper
from snekmate.auth import access_control
initializes: access_control
exports: (
access_control.supportsInterface,
access_control.hasRole,
access_control.DEFAULT_ADMIN_ROLE,
access_control.grantRole,
access_control.revokeRole,
)
```
```vyper
# @dev The default 32-byte admin role.
DEFAULT_ADMIN_ROLE: public(constant(bytes32)) = empty(bytes32)
```
This example returns the `DEFAULT_ADMIN_ROLE` role as `bytes32` of the veCRV Oracle on Optimism.
```shell
>>> L2VotingEscrowOracle.DEFAULT_ADMIN_ROLE()
'0x0000000000000000000000000000000000000000000000000000000000000000'
```
::::
### `grantRole`
::::description[`L2VotingEscrowOracle.grantRole(role: bytes32, account: address):`]
:::guard[Guarded Method by [Snekmate 🐍](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. Granting a role is only callable by an account with the admin role for the given role.
:::
Function to grant a role to an account.
Emits: `RoleGranted`
| Input | Type | Description |
| ---------- | --------- | ------------------------------------------- |
| `role` | `bytes32` | Role identifier |
| `account` | `address` | Address to grant the role to |
```vyper
from snekmate.auth import access_control
initializes: access_control
exports: (
access_control.supportsInterface,
access_control.hasRole,
access_control.DEFAULT_ADMIN_ROLE,
access_control.grantRole,
access_control.revokeRole,
)
```
```vyper
@external
def grantRole(role: bytes32, account: address):
"""
@dev Grants `role` to `account`.
@notice If `account` had not been already
granted `role`, emits a `RoleGranted`
event. Note that the caller must have
`role`'s admin role.
@param role The 32-byte role definition.
@param account The 20-byte address of the account.
"""
self._check_role(self.getRoleAdmin[role], msg.sender)
self._grant_role(role, account)
@internal
def _grant_role(role: bytes32, account: address):
"""
@dev Grants `role` to `account`.
@notice This is an `internal` function without
access restriction.
@param role The 32-byte role definition.
@param account The 20-byte address of the account.
"""
if (not(self.hasRole[role][account])):
self.hasRole[role][account] = True
log IAccessControl.RoleGranted(role, account, msg.sender)
```
::::
### `revokeRole`
::::description[`L2VotingEscrowOracle.revokeRole(role: bytes32, account: address):`]
:::guard[Guarded Method by [Snekmate 🐍](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. Revoking a role is only callable by an account with the admin role for the given role.
:::
Function to revoke a role from an account.
Emits: `RoleRevoked`
| Input | Type | Description |
| ---------- | --------- | ------------------------------------------- |
| `role` | `bytes32` | Role identifier |
| `account` | `address` | Address to revoke the role from |
```vyper
from snekmate.auth import access_control
initializes: access_control
exports: (
access_control.supportsInterface,
access_control.hasRole,
access_control.DEFAULT_ADMIN_ROLE,
access_control.grantRole,
access_control.revokeRole,
)
```
```vyper
@external
def revokeRole(role: bytes32, account: address):
"""
@dev Revokes `role` from `account`.
@notice If `account` had been granted `role`,
emits a `RoleRevoked` event. Note that
the caller must have `role`'s admin role.
@param role The 32-byte role definition.
@param account The 20-byte address of the account.
"""
self._check_role(self.getRoleAdmin[role], msg.sender)
self._revoke_role(role, account)
@internal
def _revoke_role(role: bytes32, account: address):
"""
@dev Revokes `role` from `account`.
@notice This is an `internal` function without
access restriction.
@param role The 32-byte role definition.
@param account The 20-byte address of the account.
"""
if (self.hasRole[role][account]):
self.hasRole[role][account] = False
log IAccessControl.RoleRevoked(role, account, msg.sender)
```
::::
### `supportsInterface`
::::description[`L2VotingEscrowOracle.supportsInterface(interface_id: bytes4) -> bool: view`]
Getter to check if the contract implements a specific interface ID.
Returns: true or false (`bool`).
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------- |
| `interface_id` | `bytes4` | Interface identifier |
```vyper
from snekmate.auth import access_control
initializes: access_control
exports: (
access_control.supportsInterface,
access_control.hasRole,
access_control.DEFAULT_ADMIN_ROLE,
access_control.grantRole,
access_control.revokeRole,
)
```
```vyper
_SUPPORTED_INTERFACES: constant(bytes4[2]) = [
0x01FFC9A7, # The ERC-165 identifier for ERC-165.
0x7965DB0B, # The ERC-165 identifier for `IAccessControl`.
]
@external
@view
def supportsInterface(interface_id: bytes4) -> bool:
"""
@dev Returns `True` if this contract implements the
interface defined by `interface_id`.
@param interface_id The 4-byte interface identifier.
@return bool The verification whether the contract
implements the interface or not.
"""
return interface_id in _SUPPORTED_INTERFACES
```
```shell
>>> L2VotingEscrowOracle.supportsInterface("0x01FFC9A7")
'true'
```
::::
### `hasRole`
::::description[`L2VotingEscrowOracle.hasRole(arg0: bytes32, arg1: address) -> bool: view`]
Getter to check if an address has a specified role.
Returns: true or false (`bool`).
| Input | Type | Description |
| ---------- | --------- | ------------------------------------------- |
| `role` | `bytes32` | Role identifier |
| `account` | `address` | Address to check |
```vyper
from snekmate.auth import access_control
initializes: access_control
exports: (
access_control.supportsInterface,
access_control.hasRole,
access_control.DEFAULT_ADMIN_ROLE,
access_control.grantRole,
access_control.revokeRole,
)
```
```vyper
# @dev Returns `True` if `account` has been granted `role`.
hasRole: public(HashMap[bytes32, HashMap[address, bool]])
```
```shell
>>> L2VotingEscrowOracle.hasRole('0xe887cc0717dab2ad628f68695129fefff34ee397bdd39e44a259e2cae80f49b7', '0x1d04Fcb6293690D75E9262A89Ac3B816772E6841')
'true'
```
::::
---
## User Info
The contract provides getter functions that allow querying detailed information about user voting power, lock status, and historical checkpoints in the `L2VotingEscrowOracle` contract. These functions enable users and integrators to track veCRV balances, lock expirations, voting power decay (slope), and historical states for any address.
- If a user address is not found or has no history, functions like `balanceOf`, `locked__end`, and `get_last_user_slope` will return `0`
- If a delegation is not set, `delegated(_from)` and `delegator(_to)` will return the address itself
- If a user delegates out but is not delegated to, some getters may return `0` to indicate no effective balance
### `get_last_user_slope`
::::description[`L2VotingEscrowOracle.get_last_user_slope(_addr: address) -> int128: view`]
Returns the most recently recorded rate of voting power decrease (slope) for a user.
Returns: last user slope (`int128`).
| Input | Type | Description |
| ---------- | --------- | ------------------------------------------- |
| `_addr` | `address` | Address of the user |
```vyper title="L2VotingEscrowOracle.vy"
user_point_epoch: public(HashMap[address, uint256])
user_point_history: public(HashMap[address, HashMap[uint256, Point]])
# [address from][address to]
delegation_from: HashMap[address, address]
delegation_to: HashMap[address, address]
@external
@view
def get_last_user_slope(_addr: address) -> int128:
"""
@notice Get the most recently recorded rate of voting power decrease for `addr`
@param _addr Address of the user wallet
@return Value of the slope
"""
user: address = self._get_user_after_delegation(_addr)
if user == empty(address):
return 0
uepoch: uint256 = self.user_point_epoch[user]
return self.user_point_history[user][uepoch].slope
@view
def _get_user_after_delegation(_user: address) -> address:
user: address = self.delegation_to[_user]
if user == empty(address):
if self.delegation_from[_user] not in [empty(address), _user]: # only delegation out
return empty(address)
user = _user
return user
```
```shell
>>> L2VotingEscrowOracle.get_last_user_slope("0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683")
31709791983764
```
::::
### `locked__end`
::::description[`L2VotingEscrowOracle.locked__end(_addr: address) -> uint256: view`]
Getter for the timestamp when a user's lock finishes.
Returns: ts when the lock ends (`uint256`).
| Input | Type | Description |
| ---------- | --------- | ------------------------------------------- |
| `_addr` | `address` | Address of the user |
```vyper title="L2VotingEscrowOracle.vy"
locked: public(HashMap[address, LockedBalance])
# [address from][address to]
delegation_from: HashMap[address, address]
delegation_to: HashMap[address, address]
@external
@view
def locked__end(_addr: address) -> uint256:
"""
@notice Get timestamp when `_addr`'s lock finishes
@param _addr User wallet
@return Epoch time of the lock end
"""
user: address = self._get_user_after_delegation(_addr)
if user == empty(address):
return 0
return self.locked[user].end
@view
def _get_user_after_delegation(_user: address) -> address:
user: address = self.delegation_to[_user]
if user == empty(address):
if self.delegation_from[_user] not in [empty(address), _user]: # only delegation out
return empty(address)
user = _user
return user
```
This example returns the timestamp when the user's veCRV lock ends.
```shell
>>> L2VotingEscrowOracle.locked__end("0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683")
1816214400
```
::::
### `epoch`
::::description[`L2VotingEscrowOracle.epoch() -> uint256: view`]
The current epoch of the `VotingEscrow` contract.
Returns: current epoch (`uint256`).
```vyper title="L2VotingEscrowOracle.vy"
epoch: public(uint256)
```
```shell
>>> L2VotingEscrowOracle.epoch()
58959
```
::::
### `point_history`
::::description[`L2VotingEscrowOracle.point_history(arg0: uint256) -> bias: int128, slope: int128, ts: uint256, blk: uint256: view`]
Getter for the point history of point `arg0`.
Returns: bias (`int128`), slope (`int128`), ts (`uint256`) and blk (`uint256`).
| Input | Type | Description |
| ------ | --------- | ----------- |
| `arg0` | `uint256` | value of the point to check |
```vyper title="L2VotingEscrowOracle.vy"
struct Point:
bias: int128
slope: int128
ts: uint256
blk: uint256
point_history: public(HashMap[uint256, Point])
```
This example returns the point history of an epoch.
```shell
>>> L2VotingEscrowOracle.point_history(58959)
800073125242408678972029063, 6788868070537972059, 1751818043, 22861249
```
::::
### `user_point_epoch`
::::description[`L2VotingEscrowOracle.user_point_epoch(_addr: address) -> uint256: view`]
Getter for the last checkpointed epoch for a user.
Returns: last checkpointed epoch (`uint256`).
| Input | Type | Description |
| ---------- | --------- | --------------------- |
| `_addr` | `address` | Address of the user |
```vyper title="L2VotingEscrowOracle.vy"
struct Point:
bias: int128
slope: int128
ts: uint256
blk: uint256
user_point_history: public(HashMap[address, HashMap[uint256, Point]])
```
This example returns the user point epoch of an address.
```shell
>>> L2VotingEscrowOracle.user_point_epoch("0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683")
5
```
::::
### `user_point_history`
::::description[`L2VotingEscrowOracle.user_point_history(arg0: address, arg1: uint256) -> Point: view`]
Getter for the point history for a user at a given index.
Returns: `Point` struct containing bias (`int128`), slope (`int128`), ts (`uint256`) and blk (`uint256`).
| Input | Type | Description |
| ------ | --------- | ------------------- |
| `arg0` | `address` | Address of the user |
| `arg1` | `uint256` | Epoch index |
```vyper title="L2VotingEscrowOracle.vy"
struct Point:
bias: int128
slope: int128
ts: uint256
blk: uint256
user_point_history: public(HashMap[address, HashMap[uint256, Point]])
```
This example returns the user point history of the address `0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683` at epoch `5`.
```shell
>>> L2VotingEscrowOracle.user_point_history("0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683", 5)
3740249651192218998932, 1709791983764, 1712172287, 19577313
```
::::
### `locked`
::::description[`L2VotingEscrowOracle.locked(arg0: address) -> LockedBalance: view`]
Returns the locked balance struct for a user.
Returns: `LockedBalance` struct containing amount (`int128`) and end timestamp (`uint256`) of locked CRV.
| Input | Type | Description |
| ---------- | --------- | ------------------------------------------- |
| `arg0` | `address` | Address of the user |
```vyper title="L2VotingEscrowOracle.vy"
struct LockedBalance:
amount: int128
end: uint256
locked: public(HashMap[address, LockedBalance])
```
This example returns the total amount of CRV tokens locked, along with the timestamp when the lock ends.
```shell
>>> L2VotingEscrowOracle.locked("0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683")
4000000000000000000000, 1830124800
```
::::
### `slope_changes`
::::description[`L2VotingEscrowOracle.slope_changes(arg0: uint256) -> int128: view`]
Getter for the slope change at a given future timestamp.
Returns: slope change (`int128`).
| Input | Type | Description |
| ------ | --------- | ----------- |
| `arg0` | `uint256` | Timestamp |
```vyper title="L2VotingEscrowOracle.vy"
slope_changes: public(HashMap[uint256, int128])
```
```shell
>>> L2VotingEscrowOracle.slope_changes(1760971290)
0
```
::::
### `last_block_number`
::::description[`L2VotingEscrowOracle.last_block_number() -> uint256: view`]
Getter for the last ETH block number at which an update was made.
Returns: block number (`uint256`).
```vyper title="L2VotingEscrowOracle.vy"
last_block_number: public(uint256)
```
This example returns the last Ethereum mainnet block at which an update to either the total supply or a user's veCRV balance was made.
```shell
>>> L2VotingEscrowOracle.last_block_number()
22861295
```
::::
---
## Other Methods
### `version`
::::description[`L2VotingEscrowOracle.version() -> String[8]: view`]
Getter for the contract version.
Returns: version string (`String[8]`).
```vyper title="L2VotingEscrowOracle.vy"
version: public(constant(String[8])) = "1.0.0"
```
```shell
>>> L2VotingEscrowOracle.version()
'1.0.0'
```
::::
### `name`
::::description[`L2VotingEscrowOracle.name() -> String[64]: view`]
Getter for the token name.
Returns: token name (`String[64]`).
```vyper title="L2VotingEscrowOracle.vy"
name: public(constant(String[64])) = "Vote-escrowed CRV"
```
```shell
>>> L2VotingEscrowOracle.name()
'Vote-escrowed CRV'
```
::::
### `symbol`
::::description[`L2VotingEscrowOracle.symbol() -> String[32]: view`]
Getter for the token symbol.
Returns: token symbol (`String[32]`).
```vyper title="L2VotingEscrowOracle.vy"
symbol: public(constant(String[32])) = "veCRV"
```
```shell
>>> L2VotingEscrowOracle.symbol()
'veCRV'
```
::::
### `decimals`
::::description[`L2VotingEscrowOracle.decimals() -> uint256: view`]
Getter for the token decimals.
Returns: decimals (`uint256`).
```vyper title="L2VotingEscrowOracle.vy"
decimals: public(constant(uint256)) = 18
```
```shell
>>> L2VotingEscrowOracle.decimals()
18
```
::::
---
## L2 veCRV Verifiers
L2 verifier contracts are used to securely synchronize veCRV and related state from Ethereum mainnet (L1) to supported L2s. They validate Merkle proofs and block data from L1, allowing trust-minimized updates of veCRV balances, total supply, and delegation state on L2.
---
## veCRV Verifier
The `VecrvVerifier` contract is used to verify and update the total supply and individual balances of veCRV on L2s by validating state proofs from L1. It enables trust-minimized synchronization of veCRV state by accepting Merkle proofs and block data, and updating the canonical veCRV oracle with supply and balance changes. This contract is typically called by relayers or bridges to reflect L1 veCRV state on L2.
:::solidity[`VecrvVerifier.sol`]
The source code for the `VecrvVerifier` contract is available on [GitHub](https://github.com/curvefi/storage-proofs/blob/main/contracts/vecrv/verifiers/VecrvVerifier.sol). The contract is written in [Solidity](https://soliditylang.org/) version `0.8.18`.
The `VecrvVerifier` contract is deployed at the following addresses:
- :logos-optimism: Optimism: [`0x4eee0D7F5C84EF30AEd22137EED4188ac778f97f`](https://optimistic.etherscan.io/address/0x4eee0D7F5C84EF30AEd22137EED4188ac778f97f)
- :logos-arbitrum: Arbitrum: [`0x852F32c22C5035EA12566EDFB4415625776D75d5`](https://arbiscan.io/address/0x852F32c22C5035EA12566EDFB4415625776D75d5)
- :logos-fraxtal: Fraxtal: [`0x4D1AF9911e4c19f64Be36c36EF39Fd026Bc9bb61`](https://fraxscan.com/address/0x4D1AF9911e4c19f64Be36c36EF39Fd026Bc9bb61)
- :logos-sonic: Sonic: [`0x38334e319D257d8f580f66393d25A6CD647A6AbC`](https://sonicscan.org/address/0x38334e319D257d8f580f66393d25A6CD647A6AbC)
- :logos-mantle: Mantle: [`0x820945D1E5759a57874846371F22b56b73c6AE85`](https://mantlescan.xyz/address/0x820945D1E5759a57874846371F22b56b73c6AE85)
- :logos-base: Base: [`0x3fE593E651Cd0B383AD36b75F4159f30BB0631A6`](https://basescan.org/address/0x3fE593E651Cd0B383AD36b75F4159f30BB0631A6)
- :logos-taiko: Taiko: [`0x3B519ae13D7CeB72CC922815f5dAaD741aD5087B`](https://taikoscan.io/address/0x3B519ae13D7CeB72CC922815f5dAaD741aD5087B)
```json
[{"inputs":[{"internalType":"address","name":"_block_hash_oracle","type":"address"},{"internalType":"address","name":"_vecrv_oracle","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"BLOCK_HASH_ORACLE","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MIN_SLOPE_CHANGES_CNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VE_ORACLE","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_user","type":"address"},{"internalType":"bytes","name":"_block_header_rlp","type":"bytes"},{"internalType":"bytes","name":"_proof_rlp","type":"bytes"}],"name":"verifyBalanceByBlockHash","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_user","type":"address"},{"internalType":"uint256","name":"_block_number","type":"uint256"},{"internalType":"bytes","name":"_proof_rlp","type":"bytes"}],"name":"verifyBalanceByStateRoot","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"_block_header_rlp","type":"bytes"},{"internalType":"bytes","name":"_proof_rlp","type":"bytes"}],"name":"verifyTotalByBlockHash","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_block_number","type":"uint256"},{"internalType":"bytes","name":"_proof_rlp","type":"bytes"}],"name":"verifyTotalByStateRoot","outputs":[],"stateMutability":"nonpayable","type":"function"}]
```
:::
### `BLOCK_HASH_ORACLE`
::::description[`VecrvVerifier.BLOCK_HASH_ORACLE() -> address: view`]
Getter for the block hash oracle contract, which is used to retrieve block hashes and state roots for verification.
Returns: block hash oracle (`address`).
```solidity title="VecrvVerifier.sol"
address public immutable BLOCK_HASH_ORACLE;
```
This example returns the block hash oracle on Optimism.
```shell
>>> VecrvVerifier.BLOCK_HASH_ORACLE()
'0xeB896fB7D1AaE921d586B0E5a037496aFd3E2412'
```
::::
### `MIN_SLOPE_CHANGES_CNT`
::::description[`VecrvVerifier.MIN_SLOPE_CHANGES_CNT() -> uint256: view`]
Returns the minimum number of slope changes required for a valid proof. This is set to 4, corresponding to 1 month (assuming 1 week per slope change).
Returns: minimum slope changes count (`uint256`).
```solidity title="VecrvVerifierCore.sol"
uint256 public constant MIN_SLOPE_CHANGES_CNT = 4; // 1 month
```
This example returns the minimum slope change count.
```shell
>>> VecrvVerifier.MIN_SLOPE_CHANGES_CNT()
4
```
::::
### `VE_ORACLE`
::::description[`VecrvVerifier.VE_ORACLE() -> address: view`]
Getter for the veCRV oracle contract, which is called to update the total supply and user balances after verification.
Returns: veCRV oracle (`address`).
```solidity title="VecrvVerifierCore.sol"
address public immutable VE_ORACLE;
constructor(address _ve_oracle) {
VE_ORACLE = _ve_oracle;
}
```
This example returns the VE_ORACLE address on Optimism.
```shell
>>> VecrvVerifier.VE_ORACLE()
'0xF1946D4879646e0FCD8F5bb32a5636ED8055176D'
```
::::
### `verifyBalanceByBlockHash`
::::description[`VecrvVerifier.verifyBalanceByBlockHash(address _user, bytes memory _block_header_rlp, bytes memory _proof_rlp) external`]
Verifies a user's veCRV balance and updates the total veCRV supply using a block hash. This function is intended for use with RLP-encoded block headers and state proofs.
| Input | Type | Description |
| ------------------- | --------- | ------------------------------ |
| `_user` | `address` | User to verify the balance for |
| `_block_header_rlp` | `bytes` | RLP-encoded block header |
| `_proof_rlp` | `bytes` | State proof of the parameters |
```solidity
/// @param _user User to verify balance for
/// @param _block_header_rlp The RLP-encoded block header
/// @param _proof_rlp The state proof of the parameters
function verifyBalanceByBlockHash(
address _user,
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external {
RLPReader.RLPItem[] memory proofs = _proof_rlp.toRlpItem().toList();
require(proofs.length >= 1, "Invalid number of proofs");
(bytes32 storage_root, uint256 block_number) = _extractAccountStorageRoot(_block_header_rlp, proofs[0]);
_updateTotal(storage_root, block_number, proofs[1].toList());
_updateBalance(_user, storage_root, block_number, proofs[2].toList());
}
function _extractAccountStorageRoot(
bytes memory _block_header_rlp,
RLPReader.RLPItem memory account_proof
) internal returns (bytes32, uint256) {
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
require(block_header.hash != bytes32(0), "Invalid blockhash");
require(
block_header.hash == IBlockHashOracle(BLOCK_HASH_ORACLE).get_block_hash(block_header.number),
"Blockhash mismatch"
);
return (_extractAccountStorageRoot(block_header.stateRootHash, account_proof), block_header.number);
}
```
```solidity
function _extractAccountStorageRoot(
bytes32 state_root_hash,
RLPReader.RLPItem memory account_proof
) internal returns (bytes32) {
Verifier.Account memory account = Verifier.extractAccountFromProof(
VOTING_ESCROW_HASH,
state_root_hash,
account_proof.toList()
);
require(account.exists, "VotingEscrow account does not exist");
return account.storageRoot;
}
/// @dev Update total parameters with proofs
function _updateTotal(
bytes32 storageRoot,
uint256 block_number,
RLPReader.RLPItem[] memory proofs
) internal {
require(proofs.length >= SLOPE_CHANGES_PROOF_I + MIN_SLOPE_CHANGES_CNT, "Invalid number of total proofs");
// Extract slot values
uint256 epoch = Verifier.extractSlotValueFromProof(
keccak256(abi.encode(EPOCH_SLOT)),
storageRoot,
proofs[EPOCH_PROOF_I].toList()
).value;
IVecrvOracle.Point memory point_history = _extract_point(
POINT_HISTORY_PROOF_I,
keccak256(abi.encode(uint256(keccak256(abi.encode(POINT_HISTORY_SLOT))) + epoch)),
storageRoot,
proofs
);
uint256 start = WEEK + point_history.ts / WEEK * WEEK;
int128[] memory slope_changes = new int128[](proofs.length - SLOPE_CHANGES_PROOF_I);
for (uint256 i = 0; i < proofs.length - SLOPE_CHANGES_PROOF_I; ++i) {
slope_changes[i] = int128(int256(Verifier.extractSlotValueFromProof(
keccak256(abi.encode(keccak256(abi.encode(SLOPE_CHANGES_SLOT, start + i * WEEK)))),
storageRoot,
proofs[SLOPE_CHANGES_PROOF_I + i].toList()
).value));
}
return IVecrvOracle(VE_ORACLE).update_total(
epoch,
point_history,
slope_changes,
block_number
);
}
/// @dev Update user's balance with proofs
function _updateBalance(
address user,
bytes32 storageRoot,
uint256 block_number,
RLPReader.RLPItem[] memory proofs
) internal {
require(proofs.length == LOCKED_BALANCE_PROOF_I + 2, "Invalid number of balance proofs");
// Extract slot values
uint256 user_point_epoch = Verifier.extractSlotValueFromProof(
keccak256(
abi.encode(keccak256(abi.encode(USER_POINT_EPOCH_SLOT, user)))
),
storageRoot,
proofs[USER_POINT_EPOCH_PROOF_I].toList()
).value;
IVecrvOracle.Point memory user_point_history = _extract_point(
USER_POINT_HISTORY_PROOF_I,
keccak256(abi.encode(uint256(keccak256(abi.encode(keccak256(abi.encode(USER_POINT_HISTORY_SLOT, user))))) + user_point_epoch)),
storageRoot,
proofs
);
IVecrvOracle.LockedBalance memory locked = _extract_locked_balance(
LOCKED_BALANCE_PROOF_I,
keccak256(abi.encode(keccak256(abi.encode(LOCKED_BALANCE_SLOT, user)))),
storageRoot,
proofs
);
return IVecrvOracle(VE_ORACLE).update_balance(
user,
user_point_epoch,
user_point_history,
locked,
block_number
);
}
```
```shell
>>> soon
```
::::
### `verifyBalanceByStateRoot`
::::description[`VecrvVerifier.verifyBalanceByStateRoot(address _user, uint256 _block_number, bytes memory _proof_rlp) external`]
Verifies a user's veCRV balance and updates the total veCRV supply using a state root obtained from the block hash oracle.
| Input | Type | Description |
| ------------------- | --------- | ------------------------------ |
| `_user` | `address` | User to verify the balance for |
| `_block_number` | `uint256` | Block number to use state root |
| `_proof_rlp` | `bytes` | State proof of the parameters |
```solidity
/// @param _user User to verify balance for
/// @param _block_number Number of the block to use state root hash
/// @param _proof_rlp The state proof of the parameters
function verifyBalanceByStateRoot(
address _user,
uint256 _block_number,
bytes memory _proof_rlp
) external {
RLPReader.RLPItem[] memory proofs = _proof_rlp.toRlpItem().toList();
require(proofs.length >= 1, "Invalid number of proofs");
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
bytes32 storage_root = _extractAccountStorageRoot(state_root, proofs[0]);
_updateTotal(storage_root, _block_number, proofs[1].toList());
_updateBalance(_user, storage_root, _block_number, proofs[2].toList());
}
```
```solidity
function _extractAccountStorageRoot(
bytes32 state_root_hash,
RLPReader.RLPItem memory account_proof
) internal returns (bytes32) {
Verifier.Account memory account = Verifier.extractAccountFromProof(
VOTING_ESCROW_HASH,
state_root_hash,
account_proof.toList()
);
require(account.exists, "VotingEscrow account does not exist");
return account.storageRoot;
}
```
```shell
>>> soon
```
::::
### `verifyTotalByBlockHash`
::::description[`VecrvVerifier.verifyTotalByBlockHash(bytes memory _block_header_rlp, bytes memory _proof_rlp) external`]
Verifies and updates the total veCRV supply using a block hash and state proof. Intended for use with RLP-encoded block headers and state proofs. The proofs must be constructed off-chain and provided as input.
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------- |
| `_block_header_rlp` | `bytes` | RLP-encoded block header from L1 |
| `_proof_rlp` | `bytes` | State proof of the parameters |
```solidity title="VecrvVerifier.sol"
/// @param _block_header_rlp The RLP-encoded block header
/// @param _proof_rlp The state proof of the parameters
function verifyTotalByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external {
RLPReader.RLPItem[] memory proofs = _proof_rlp.toRlpItem().toList();
require(proofs.length >= 1, "Invalid number of proofs");
(bytes32 storage_root, uint256 block_number) = _extractAccountStorageRoot(_block_header_rlp, proofs[0]);
_updateTotal(storage_root, block_number, proofs[1].toList());
}
```
```shell
>>> soon
```
::::
### `verifyTotalByStateRoot`
::::description[`VecrvVerifier.verifyTotalByStateRoot(uint256 _block_number, bytes memory _proof_rlp) external`]
Verifies and updates the total veCRV supply using a state root obtained from the block hash oracle. The proofs must be constructed off-chain and provided as input.
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------- |
| `_block_number` | `uint256` | Block number to use state root |
| `_proof_rlp` | `bytes` | State proof of the parameters |
```solidity title="VecrvVerifier.sol"
/// @param _block_number Number of the block to use state root hash
/// @param _proof_rlp The state proof of the parameters
function verifyTotalByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external {
RLPReader.RLPItem[] memory proofs = _proof_rlp.toRlpItem().toList();
require(proofs.length >= 1, "Invalid number of proofs");
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
bytes32 storage_root = _extractAccountStorageRoot(state_root, proofs[0]);
_updateTotal(storage_root, _block_number, proofs[1].toList());
}
```
```shell
>>> soon
```
::::
## Delegation Verifier
The `DelegationVerifier` contract is used to verify and update veCRV delegation state on L2s by validating state proofs from L1. It enables trust-minimized synchronization of delegated veCRV balances by accepting Merkle proofs and block data, and updating the canonical veCRV oracle with delegation changes. This contract is typically called by relayers or bridges to reflect L1 delegation state on L2.
:::solidity[`DelegationVerifier.sol`]
The source code for the `DelegationVerifier` contract is available on [GitHub](https://github.com/curvefi/storage-proofs/blob/main/contracts/vecrv/verifiers/DelegationVerifier.sol). The contract is written in [Solidity](https://soliditylang.org/) version `0.8.18`.
The `DelegationVerifier` contract is deployed at the following addresses:
- :logos-optimism: Optimism: [`0x1d04Fcb6293690D75E9262A89Ac3B816772E6841`](https://optimistic.etherscan.io/address/0x1d04Fcb6293690D75E9262A89Ac3B816772E6841)
- :logos-arbitrum: Arbitrum: [`0x820945D1E5759a57874846371F22b56b73c6AE85`](https://arbiscan.io/address/0x820945D1E5759a57874846371F22b56b73c6AE85)
- :logos-fraxtal: Fraxtal: [`0x852F32c22C5035EA12566EDFB4415625776D75d5`](https://fraxscan.com/address/0x852F32c22C5035EA12566EDFB4415625776D75d5)
- :logos-sonic: Sonic: [`0xC29229b477582CE810e8C261b2869b9d8c82F4a7`](https://sonicscan.org/address/0xC29229b477582CE810e8C261b2869b9d8c82F4a7)
- :logos-mantle: Mantle: [`0x1df9cEeE7aB8804749B795D64307A3CFE0e84905`](https://mantlescan.xyz/address/0x1df9cEeE7aB8804749B795D64307A3CFE0e84905)
- :logos-base: Base: [`0xAeB976BB02b5c36DcD57372a3B18326BfA4983C8`](https://basescan.org/address/0xAeB976BB02b5c36DcD57372a3B18326BfA4983C8)
- :logos-taiko: Taiko: [`0xD41f7CcB1e72e282b50b0f331944f8ea7D4CACB6`](https://taikoscan.io/address/0xD41f7CcB1e72e282b50b0f331944f8ea7D4CACB6)
```json
[{"inputs":[{"internalType":"address","name":"_block_hash_oracle","type":"address"},{"internalType":"address","name":"_vecrv_oracle","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"BLOCK_HASH_ORACLE","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VE_ORACLE","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"bytes","name":"_block_header_rlp","type":"bytes"},{"internalType":"bytes","name":"_proof_rlp","type":"bytes"}],"name":"verifyDelegationByBlockHash","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"uint256","name":"_block_number","type":"uint256"},{"internalType":"bytes","name":"_proof_rlp","type":"bytes"}],"name":"verifyDelegationByStateRoot","outputs":[],"stateMutability":"nonpayable","type":"function"}]
```
:::
### `BLOCK_HASH_ORACLE`
::::description[`DelegationVerifier.BLOCK_HASH_ORACLE() -> address: view`]
Getter for the block hash oracle contract, which is used to retrieve block hashes and state roots for verification.
Returns: block hash oracle (`address`).
```solidity title="DelegationVerifier.sol"
address public immutable BLOCK_HASH_ORACLE;
constructor(address _block_hash_oracle, address _vecrv_oracle)
{
BLOCK_HASH_ORACLE = _block_hash_oracle;
VE_ORACLE = _vecrv_oracle;
}
```
```shell
>>> DelegationVerifier.BLOCK_HASH_ORACLE()
'0xeB896fB7D1AaE921d586B0E5a037496aFd3E2412'
```
::::
### `VE_ORACLE`
::::description[`DelegationVerifier.VE_ORACLE() -> address: view`]
Getter for the veCRV oracle contract, which is called to update delegation state after verification.
Returns: veCRV oracle (`address`).
```solidity title="DelegationVerifier.sol"
address public immutable VE_ORACLE;
constructor(address _block_hash_oracle, address _vecrv_oracle)
{
BLOCK_HASH_ORACLE = _block_hash_oracle;
VE_ORACLE = _vecrv_oracle;
}
```
```shell
>>> DelegationVerifier.VE_ORACLE()
'0xF1946D4879646e0FCD8F5bb32a5636ED8055176D'
```
::::
### `verifyDelegationByBlockHash`
::::description[`DelegationVerifier.verifyDelegationByBlockHash(address _from, bytes memory _block_header_rlp, bytes memory _proof_rlp) external`]
Verifies and updates the delegation of veCRV balance from `_from` to the delegated address using a block hash. This function is intended for use with RLP-encoded block headers and state proofs.
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------- |
| `_from` | `address` | Address from which balance is delegated |
| `_block_header_rlp` | `bytes` | RLP-encoded block header from L1 |
| `_proof_rlp` | `bytes` | State proof of the parameters |
```solidity title="DelegationVerifier.sol"
interface IBlockHashOracle {
function get_block_hash(uint256 _number) external view returns (bytes32);
function get_state_root(uint256 _number) external view returns (bytes32);
}
interface IVecrvOracle {
function update_delegation(
address from,
address to,
uint256 block_number
) external;
}
function verifyDelegationByBlockHash(
address _from,
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external {
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
require(block_header.hash != bytes32(0), "Invalid blockhash");
require(
block_header.hash == IBlockHashOracle(BLOCK_HASH_ORACLE).get_block_hash(block_header.number),
"Blockhash mismatch"
);
return _updateDelegation(_from, block_header.number, block_header.stateRootHash, _proof_rlp);
}
/// @dev Update delegation using proof. `blockNumber` is used for updates linearization
function _updateDelegation(
address from,
uint256 blockNumber,
bytes32 stateRoot,
bytes memory proofRlp
) internal {
RLPReader.RLPItem[] memory proofs = proofRlp.toRlpItem().toList();
require(proofs.length == 2, "Invalid number of proofs");
// Extract account proof
Verifier.Account memory account = Verifier.extractAccountFromProof(
VE_DELEGATE_HASH,
stateRoot,
proofs[0].toList()
);
require(account.exists, "Delegate account does not exist");
// Extract slot values
address to = address(uint160(Verifier.extractSlotValueFromProof(
keccak256(abi.encode(
keccak256(abi.encode(
keccak256(abi.encode(1, block.chainid)), // slot of delegation_from[chain.id][]
from
))
)),
account.storageRoot,
proofs[1].toList()
).value));
require(to != VE_DELEGATE, "Delegate not set");
return IVecrvOracle(VE_ORACLE).update_delegation(from, to, blockNumber);
}
```
```shell
>>> soon
```
::::
### `verifyDelegationByStateRoot`
::::description[`DelegationVerifier.verifyDelegationByStateRoot(address _from, uint256 _block_number, bytes memory _proof_rlp) external`]
Verifies and updates the delegation of veCRV balance from `_from` to the delegated address using a state root obtained from the block hash oracle.
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------- |
| `_from` | `address` | Address from which balance is delegated |
| `_block_number` | `uint256` | Block number to use state root |
| `_proof_rlp` | `bytes` | State proof of the parameters |
```solidity title="DelegationVerifier.sol"
interface IBlockHashOracle {
function get_block_hash(uint256 _number) external view returns (bytes32);
function get_state_root(uint256 _number) external view returns (bytes32);
}
interface IVecrvOracle {
function update_delegation(
address from,
address to,
uint256 block_number
) external;
}
/// @param _from Address from which balance is delegated
/// @param _block_number Number of the block to use state root hash
/// @param _proof_rlp The state proof of the parameters
function verifyDelegationByStateRoot(
address _from,
uint256 _block_number,
bytes memory _proof_rlp
) external {
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
return _updateDelegation(_from, _block_number, state_root, _proof_rlp);
}
/// @dev Update delegation using proof. `blockNumber` is used for updates linearization
function _updateDelegation(
address from,
uint256 blockNumber,
bytes32 stateRoot,
bytes memory proofRlp
) internal {
RLPReader.RLPItem[] memory proofs = proofRlp.toRlpItem().toList();
require(proofs.length == 2, "Invalid number of proofs");
// Extract account proof
Verifier.Account memory account = Verifier.extractAccountFromProof(
VE_DELEGATE_HASH,
stateRoot,
proofs[0].toList()
);
require(account.exists, "Delegate account does not exist");
// Extract slot values
address to = address(uint160(Verifier.extractSlotValueFromProof(
keccak256(abi.encode(
keccak256(abi.encode(
keccak256(abi.encode(1, block.chainid)), // slot of delegation_from[chain.id][]
from
))
)),
account.storageRoot,
proofs[1].toList()
).value));
require(to != VE_DELEGATE, "Delegate not set");
return IVecrvOracle(VE_ORACLE).update_delegation(from, to, blockNumber);
}
```
```shell
>>> soon
```
::::
---
## Voting Escrow (veCRV)
Participating in Curve DAO governance requires that an account have a balance of vote-escrowed CRV (veCRV). **veCRV is a non-standard ERC-20 implementation, used within the Aragon DAO to determine each account's voting power.**
:::vyper[`VotingEscrow.vy`]
The source code for the `VotingEscrow.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/VotingEscrow.vy). The contract is written using [Vyper](https://vyper.readthedocs.io) version `0.2.4`.
The contract is deployed on :logos-ethereum: Ethereum at [`0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2`](https://etherscan.io/address/0x5f3b5dfeb7b28cdbd7faba78963ee202a494e2a2).
```json
[{"name":"CommitOwnership","inputs":[{"type":"address","name":"admin","indexed":false}],"anonymous":false,"type":"event"},{"name":"ApplyOwnership","inputs":[{"type":"address","name":"admin","indexed":false}],"anonymous":false,"type":"event"},{"name":"Deposit","inputs":[{"type":"address","name":"provider","indexed":true},{"type":"uint256","name":"value","indexed":false},{"type":"uint256","name":"locktime","indexed":true},{"type":"int128","name":"type","indexed":false},{"type":"uint256","name":"ts","indexed":false}],"anonymous":false,"type":"event"},{"name":"Withdraw","inputs":[{"type":"address","name":"provider","indexed":true},{"type":"uint256","name":"value","indexed":false},{"type":"uint256","name":"ts","indexed":false}],"anonymous":false,"type":"event"},{"name":"Supply","inputs":[{"type":"uint256","name":"prevSupply","indexed":false},{"type":"uint256","name":"supply","indexed":false}],"anonymous":false,"type":"event"},{"outputs":[],"inputs":[{"type":"address","name":"token_addr"},{"type":"string","name":"_name"},{"type":"string","name":"_symbol"},{"type":"string","name":"_version"}],"stateMutability":"nonpayable","type":"constructor"},{"name":"commit_transfer_ownership","outputs":[],"inputs":[{"type":"address","name":"addr"}],"stateMutability":"nonpayable","type":"function"},{"name":"apply_transfer_ownership","outputs":[],"inputs":[],"stateMutability":"nonpayable","type":"function"},{"name":"commit_smart_wallet_checker","outputs":[],"inputs":[{"type":"address","name":"addr"}],"stateMutability":"nonpayable","type":"function"},{"name":"apply_smart_wallet_checker","outputs":[],"inputs":[],"stateMutability":"nonpayable","type":"function"},{"name":"create_lock","outputs":[],"inputs":[{"type":"uint256","name":"_value"},{"type":"uint256","name":"_unlock_time"}],"stateMutability":"nonpayable","type":"function"},{"name":"increase_amount","outputs":[],"inputs":[{"type":"uint256","name":"_value"}],"stateMutability":"nonpayable","type":"function"},{"name":"increase_unlock_time","outputs":[],"inputs":[{"type":"uint256","name":"_unlock_time"}],"stateMutability":"nonpayable","type":"function"},{"name":"deposit_for","outputs":[],"inputs":[{"type":"address","name":"_addr"},{"type":"uint256","name":"_value"}],"stateMutability":"nonpayable","type":"function"},{"name":"withdraw","outputs":[],"inputs":[],"stateMutability":"nonpayable","type":"function"},{"name":"checkpoint","outputs":[],"inputs":[],"stateMutability":"nonpayable","type":"function"},{"name":"changeController","outputs":[],"inputs":[{"type":"address","name":"_newController"}],"stateMutability":"nonpayable","type":"function"},{"name":"get_last_user_slope","outputs":[{"type":"int128","name":""}],"inputs":[{"type":"address","name":"addr"}],"stateMutability":"view","type":"function"},{"name":"user_point_history__ts","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"_addr"},{"type":"uint256","name":"_idx"}],"stateMutability":"view","type":"function"},{"name":"locked__end","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"_addr"}],"stateMutability":"view","type":"function"},{"name":"balanceOf","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"addr"}],"stateMutability":"view","type":"function"},{"name":"balanceOf","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"addr"},{"type":"uint256","name":"_t"}],"stateMutability":"view","type":"function"},{"name":"balanceOfAt","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"addr"},{"type":"uint256","name":"_block"}],"stateMutability":"view","type":"function"},{"name":"totalSupply","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"totalSupply","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"uint256","name":"t"}],"stateMutability":"view","type":"function"},{"name":"totalSupplyAt","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"uint256","name":"_block"}],"stateMutability":"view","type":"function"},{"name":"token","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"supply","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"locked","outputs":[{"type":"int128","name":"amount"},{"type":"uint256","name":"end"}],"inputs":[{"type":"address","name":"arg0"}],"stateMutability":"view","type":"function"},{"name":"epoch","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"point_history","outputs":[{"type":"int128","name":"bias"},{"type":"int128","name":"slope"},{"type":"uint256","name":"ts"},{"type":"uint256","name":"blk"}],"inputs":[{"type":"uint256","name":"arg0"}],"stateMutability":"view","type":"function"},{"name":"user_point_history","outputs":[{"type":"int128","name":"bias"},{"type":"int128","name":"slope"},{"type":"uint256","name":"ts"},{"type":"uint256","name":"blk"}],"inputs":[{"type":"address","name":"arg0"},{"type":"uint256","name":"arg1"}],"stateMutability":"view","type":"function"},{"name":"user_point_epoch","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"arg0"}],"stateMutability":"view","type":"function"},{"name":"slope_changes","outputs":[{"type":"int128","name":""}],"inputs":[{"type":"uint256","name":"arg0"}],"stateMutability":"view","type":"function"},{"name":"controller","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"transfersEnabled","outputs":[{"type":"bool","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"name","outputs":[{"type":"string","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"symbol","outputs":[{"type":"string","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"version","outputs":[{"type":"string","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"decimals","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"future_smart_wallet_checker","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"smart_wallet_checker","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"admin","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function"},{"name":"future_admin","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function"}]
```
:::
`locktime` is denominated in years. The *maximum lock duration is four years* and the *minimum is one week*.
| CRV | veCRV | Locktime |
| :--: | :-----: | :------: |
| 1 | 1 | 4 years |
| 1 | 0.75 | 3 years |
| 1 | 0.5 | 2 years |
| 1 | 0.25 | 1 year |
| x | x * n/4 | n |
:::warning
When a user locks their CRV tokens for voting, they will receive veCRV based on the lock duration and the amount locked. Locking is **not reversible** and veCRV tokens are **non-transferable**. If a user decides to vote-lock their CRV tokens, they will only be able to **reclaim the CRV tokens after the lock duration has ended**.
Additionally, a user **cannot have multiple locks** with different expiry dates. However, a lock can be extended, or additional CRV can be added to it at any time.
:::
---
## Implementation Details
User voting power $w_{i}$ is linearly decreasing since the moment of lock. So does the total voting power $W$. In order to avoid periodic check-ins, every time the user deposits, or withdraws, or changes the locktime, we record user's slope and bias for the linear function $w_{i}(t)$ in the public mapping `user_point_history`. We also change slope and bias for the total voting power $W(t)$ and record it in `point_history`. In addition, when a user's lock is scheduled to end, we schedule change of slopes of $W(t)$ in the future in `slope_changes`. Every change involves increasing the `epoch` by 1.
This way we don't have to iterate over all users to figure out how much $W(t)$ should change by, neither do we require users to check in periodically. However, we limit the end of user locks to times rounded off by whole weeks.
Slopes and biases change both when a user deposits and locks governance tokens, and when the locktime expires. All the possible expiration times are rounded to whole weeks to make the number of reads from blockchain proportional to the number of missed weeks at most, not the number of users (which is potentially large).
---
## Lock Management
### `create_lock`
::::description[`VotingEscrow.create_lock(_value: uint256, _unlock_time: uint256)`]
Function to deposit `_value` CRV into the VotingEscrow and create a new lock until `_unlock_time`. The unlock time is rounded down to whole weeks.
| Input | Type | Description |
| -------------- | --------- | ---------------------------------- |
| `_value` | `uint256` | Amount of CRV to deposit |
| `_unlock_time` | `uint256` | Timestamp of the unlock time |
Emits: `Deposit` and `Supply` events.
```vyper
struct LockedBalance:
amount: int128
end: uint256
locked: public(HashMap[address, LockedBalance])
WEEK: constant(uint256) = 7 * 86400 # all future times are rounded by week
MAXTIME: constant(uint256) = 4 * 365 * 86400 # 4 years
@external
@nonreentrant('lock')
def create_lock(_value: uint256, _unlock_time: uint256):
"""
@notice Deposit `_value` tokens for `msg.sender` and lock until `_unlock_time`
@param _value Amount to deposit
@param _unlock_time Epoch time when tokens unlock, rounded down to whole weeks
"""
self.assert_not_contract(msg.sender)
unlock_time: uint256 = (_unlock_time / WEEK) * WEEK # Locktime is rounded down to weeks
_locked: LockedBalance = self.locked[msg.sender]
assert _value > 0 # dev: need non-zero value
assert _locked.amount == 0, "Withdraw old tokens first"
assert unlock_time > block.timestamp, "Can only lock until time in the future"
assert unlock_time <= block.timestamp + MAXTIME, "Voting lock can be 4 years max"
self._deposit_for(msg.sender, _value, unlock_time, _locked, CREATE_LOCK_TYPE)
```
This example creates a new lock of 100 CRV tokens until a specified unlock timestamp.
```shell
>>> VotingEscrow.create_lock(100000000000000000000, 1694003759)
```
::::
### `increase_amount`
::::description[`VotingEscrow.increase_amount(_value: uint256)`]
Function to deposit `_value` additional CRV tokens to an existing lock without modifying the unlock time.
| Input | Type | Description |
| -------- | --------- | --------------------------------- |
| `_value` | `uint256` | Amount of CRV to additionally lock |
Emits: `Deposit` and `Supply` events.
```vyper
struct LockedBalance:
amount: int128
end: uint256
locked: public(HashMap[address, LockedBalance])
@external
@nonreentrant('lock')
def increase_amount(_value: uint256):
"""
@notice Deposit `_value` additional tokens for `msg.sender`
without modifying the unlock time
@param _value Amount of tokens to deposit and add to the lock
"""
self.assert_not_contract(msg.sender)
_locked: LockedBalance = self.locked[msg.sender]
assert _value > 0 # dev: need non-zero value
assert _locked.amount > 0, "No existing lock found"
assert _locked.end > block.timestamp, "Cannot add to expired lock. Withdraw"
self._deposit_for(msg.sender, _value, 0, _locked, INCREASE_LOCK_AMOUNT)
```
This example adds 100 CRV tokens to an existing lock.
```shell
>>> VotingEscrow.increase_amount(100000000000000000000)
```
::::
### `increase_unlock_time`
::::description[`VotingEscrow.increase_unlock_time(_unlock_time: uint256)`]
Function to extend the unlock time on an already existing lock until `_unlock_time`. The unlock time is rounded down to whole weeks.
| Input | Type | Description |
| -------------- | --------- | ---------------------- |
| `_unlock_time` | `uint256` | New unlock timestamp |
Emits: `Deposit` and `Supply` events.
```vyper
struct LockedBalance:
amount: int128
end: uint256
locked: public(HashMap[address, LockedBalance])
WEEK: constant(uint256) = 7 * 86400 # all future times are rounded by week
MAXTIME: constant(uint256) = 4 * 365 * 86400 # 4 years
@external
@nonreentrant('lock')
def increase_unlock_time(_unlock_time: uint256):
"""
@notice Extend the unlock time for `msg.sender` to `_unlock_time`
@param _unlock_time New epoch time for unlocking
"""
self.assert_not_contract(msg.sender)
_locked: LockedBalance = self.locked[msg.sender]
unlock_time: uint256 = (_unlock_time / WEEK) * WEEK # Locktime is rounded down to weeks
assert _locked.end > block.timestamp, "Lock expired"
assert _locked.amount > 0, "Nothing is locked"
assert unlock_time > _locked.end, "Can only increase lock duration"
assert unlock_time <= block.timestamp + MAXTIME, "Voting lock can be 4 years max"
self._deposit_for(msg.sender, 0, unlock_time, _locked, INCREASE_UNLOCK_TIME)
```
This example extends the unlock time of an existing lock to a new timestamp.
```shell
>>> VotingEscrow.increase_unlock_time(1694003759)
```
::::
### `deposit_for`
::::description[`VotingEscrow.deposit_for(_addr: address, _value: uint256)`]
Function to deposit `_value` tokens for `_addr` and add them to an existing lock. Anyone (even a smart contract) can deposit for someone else, but cannot extend their locktime or deposit for a brand new user.
| Input | Type | Description |
| -------- | --------- | ----------------------------- |
| `_addr` | `address` | Address to deposit for |
| `_value` | `uint256` | Amount of tokens to lock |
Emits: `Deposit` and `Supply` events.
```vyper
struct LockedBalance:
amount: int128
end: uint256
locked: public(HashMap[address, LockedBalance])
@external
@nonreentrant('lock')
def deposit_for(_addr: address, _value: uint256):
"""
@notice Deposit `_value` tokens for `_addr` and add to the lock
@dev Anyone (even a smart contract) can deposit for someone else, but
cannot extend their locktime and deposit for a brand new user
@param _addr User's wallet address
@param _value Amount to add to user's lock
"""
_locked: LockedBalance = self.locked[_addr]
assert _value > 0 # dev: need non-zero value
assert _locked.amount > 0, "No existing lock found"
assert _locked.end > block.timestamp, "Cannot add to expired lock. Withdraw"
self._deposit_for(_addr, _value, 0, self.locked[_addr], DEPOSIT_FOR_TYPE)
```
This example deposits 100 CRV tokens into an existing lock owned by another address.
```shell
>>> VotingEscrow.deposit_for("0x7a16fF8270133F063aAb6C9977183D9e72835428", 100000000000000000000)
```
::::
### `withdraw`
::::description[`VotingEscrow.withdraw()`]
Function to withdraw all deposited CRV tokens once a lock has expired.
Emits: `Withdraw` and `Supply` events.
```vyper
struct LockedBalance:
amount: int128
end: uint256
locked: public(HashMap[address, LockedBalance])
supply: public(uint256)
token: public(address)
@external
@nonreentrant('lock')
def withdraw():
"""
@notice Withdraw all tokens for `msg.sender`
@dev Only possible if the lock has expired
"""
_locked: LockedBalance = self.locked[msg.sender]
assert block.timestamp >= _locked.end, "The lock didn't expire"
value: uint256 = convert(_locked.amount, uint256)
old_locked: LockedBalance = _locked
_locked.end = 0
_locked.amount = 0
self.locked[msg.sender] = _locked
supply_before: uint256 = self.supply
self.supply = supply_before - value
# old_locked can have either expired <= timestamp or zero end
# _locked has only 0 end
# Both can have >= 0 amount
self._checkpoint(msg.sender, old_locked, _locked)
assert ERC20(self.token).transfer(msg.sender, value)
log Withdraw(msg.sender, value, block.timestamp)
log Supply(supply_before, supply_before - value)
```
This example withdraws all CRV tokens after the lock has expired.
```shell
>>> VotingEscrow.withdraw()
```
::::
### `checkpoint`
::::description[`VotingEscrow.checkpoint()`]
Function to record global data to a checkpoint. This updates the global `point_history` and `epoch`. Can be called by anyone.
```vyper
ZERO_ADDRESS: constant(address) = 0x0000000000000000000000000000000000000000
struct LockedBalance:
amount: int128
end: uint256
@external
def checkpoint():
"""
@notice Record global data to checkpoint
"""
self._checkpoint(ZERO_ADDRESS, empty(LockedBalance), empty(LockedBalance))
```
This example records a global data checkpoint, updating the `point_history` and `epoch`.
```shell
>>> VotingEscrow.checkpoint()
```
::::
---
## Voting Power & Balances
### `balanceOf`
::::description[`VotingEscrow.balanceOf(addr: address, _t: uint256 = block.timestamp) -> uint256: view`]
Getter for the current veCRV balance (= voting power) of `addr` at timestamp `_t`. Defaults to `block.timestamp`.
:::note
These are not real ERC-20 balances. They measure voting weights that decay linearly over time.
:::
| Input | Type | Description |
| ------ | --------- | ---------------------------------------------- |
| `addr` | `address` | User wallet address |
| `_t` | `uint256` | Timestamp; defaults to `block.timestamp` |
Returns: voting power (`uint256`).
```vyper
struct Point:
bias: int128
slope: int128 # - dweight / dt
ts: uint256
blk: uint256 # block
user_point_epoch: public(HashMap[address, uint256])
user_point_history: public(HashMap[address, Point[1000000000]]) # user -> Point[user_epoch]
@external
@view
def balanceOf(addr: address, _t: uint256 = block.timestamp) -> uint256:
"""
@notice Get the current voting power for `msg.sender`
@dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility
@param addr User wallet address
@param _t Epoch time to return voting power at
@return User voting power
"""
_epoch: uint256 = self.user_point_epoch[addr]
if _epoch == 0:
return 0
else:
last_point: Point = self.user_point_history[addr][_epoch]
last_point.bias -= last_point.slope * convert(_t - last_point.ts, int128)
if last_point.bias < 0:
last_point.bias = 0
return convert(last_point.bias, uint256)
```
This example returns the current veCRV balance (voting power) of an address. Enter an address and click **Query** to fetch the value live from the blockchain.
::::
### `balanceOfAt`
::::description[`VotingEscrow.balanceOfAt(addr: address, _block: uint256) -> uint256: view`]
Getter for the veCRV balance (= voting power) of `addr` at block height `_block`.
| Input | Type | Description |
| -------- | --------- | ------------------------ |
| `addr` | `address` | User wallet address |
| `_block` | `uint256` | Block height |
Returns: voting power (`uint256`) at a specific block.
```vyper
struct Point:
bias: int128
slope: int128 # - dweight / dt
ts: uint256
blk: uint256 # block
epoch: public(uint256)
point_history: public(Point[100000000000000000000000000000]) # epoch -> unsigned point
user_point_epoch: public(HashMap[address, uint256])
user_point_history: public(HashMap[address, Point[1000000000]]) # user -> Point[user_epoch]
@external
@view
def balanceOfAt(addr: address, _block: uint256) -> uint256:
"""
@notice Measure voting power of `addr` at block height `_block`
@dev Adheres to MiniMe `balanceOfAt` interface: https://github.com/Giveth/minime
@param addr User's wallet address
@param _block Block to calculate the voting power at
@return Voting power
"""
assert _block <= block.number
# Binary search
_min: uint256 = 0
_max: uint256 = self.user_point_epoch[addr]
for i in range(128): # Will be always enough for 128-bit numbers
if _min >= _max:
break
_mid: uint256 = (_min + _max + 1) / 2
if self.user_point_history[addr][_mid].blk <= _block:
_min = _mid
else:
_max = _mid - 1
upoint: Point = self.user_point_history[addr][_min]
max_epoch: uint256 = self.epoch
_epoch: uint256 = self.find_block_epoch(_block, max_epoch)
point_0: Point = self.point_history[_epoch]
d_block: uint256 = 0
d_t: uint256 = 0
if _epoch < max_epoch:
point_1: Point = self.point_history[_epoch + 1]
d_block = point_1.blk - point_0.blk
d_t = point_1.ts - point_0.ts
else:
d_block = block.number - point_0.blk
d_t = block.timestamp - point_0.ts
block_time: uint256 = point_0.ts
if d_block != 0:
block_time += d_t * (_block - point_0.blk) / d_block
upoint.bias -= upoint.slope * convert(block_time - upoint.ts, int128)
if upoint.bias >= 0:
return convert(upoint.bias, uint256)
else:
return 0
```
This example returns the veCRV balance (voting power) of an address at a specific block height.
::::
### `totalSupply`
::::description[`VotingEscrow.totalSupply(t: uint256 = block.timestamp) -> uint256: view`]
Getter for the current total supply of veCRV (= total voting power) at timestamp `t`. Defaults to `block.timestamp`.
| Input | Type | Description |
| ----- | --------- | ---------------------------------------- |
| `t` | `uint256` | Timestamp; defaults to `block.timestamp` |
Returns: total voting power (`uint256`).
```vyper
struct Point:
bias: int128
slope: int128 # - dweight / dt
ts: uint256
blk: uint256 # block
epoch: public(uint256)
point_history: public(Point[100000000000000000000000000000]) # epoch -> unsigned point
@external
@view
def totalSupply(t: uint256 = block.timestamp) -> uint256:
"""
@notice Calculate total voting power
@dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility
@return Total voting power
"""
_epoch: uint256 = self.epoch
last_point: Point = self.point_history[_epoch]
return self.supply_at(last_point, t)
```
This example returns the current total veCRV voting power. The value is fetched live from the blockchain.
::::
### `totalSupplyAt`
::::description[`VotingEscrow.totalSupplyAt(_block: uint256) -> uint256: view`]
Getter for the total supply of veCRV (= total voting power) at block height `_block`.
| Input | Type | Description |
| -------- | --------- | ------------ |
| `_block` | `uint256` | Block height |
Returns: total voting power (`uint256`) at a specific block.
```vyper
struct Point:
bias: int128
slope: int128 # - dweight / dt
ts: uint256
blk: uint256 # block
epoch: public(uint256)
point_history: public(Point[100000000000000000000000000000]) # epoch -> unsigned point
@external
@view
def totalSupplyAt(_block: uint256) -> uint256:
"""
@notice Calculate total voting power at some point in the past
@param _block Block to calculate the total voting power at
@return Total voting power at `_block`
"""
assert _block <= block.number
_epoch: uint256 = self.epoch
target_epoch: uint256 = self.find_block_epoch(_block, _epoch)
point: Point = self.point_history[target_epoch]
dt: uint256 = 0
if target_epoch < _epoch:
point_next: Point = self.point_history[target_epoch + 1]
if point.blk != point_next.blk:
dt = (_block - point.blk) * (point_next.ts - point.ts) / (point_next.blk - point.blk)
else:
if point.blk != block.number:
dt = (_block - point.blk) * (block.timestamp - point.ts) / (block.number - point.blk)
# Now dt contains info on how far are we beyond point
return self.supply_at(point, point.ts + dt)
```
This example returns the total veCRV voting power at a specific block height.
::::
### `supply`
::::description[`VotingEscrow.supply() -> uint256: view`]
Getter for the total amount of CRV tokens locked in the contract.
Returns: locked CRV amount (`uint256`).
```vyper
supply: public(uint256)
```
This example returns the total amount of CRV tokens locked in the contract. The value is fetched live from the blockchain.
::::
### `locked`
::::description[`VotingEscrow.locked(arg0: address) -> amount: int128, end: uint256: view`]
Getter for the locked balance of address `arg0`. Returns the `LockedBalance` struct containing the locked amount and the unlock timestamp.
| Input | Type | Description |
| ------ | --------- | -------------- |
| `arg0` | `address` | User address |
Returns: amount (`int128`) and unlock time (`uint256`).
```vyper
struct LockedBalance:
amount: int128
end: uint256
locked: public(HashMap[address, LockedBalance])
```
This example returns the locked CRV amount and unlock timestamp for a given address.
::::
### `locked__end`
::::description[`VotingEscrow.locked__end(_addr: address) -> uint256: view`]
Getter for the timestamp when `_addr`'s lock finishes.
| Input | Type | Description |
| ------- | --------- | ------------ |
| `_addr` | `address` | User address |
Returns: unlock timestamp (`uint256`).
```vyper
struct LockedBalance:
amount: int128
end: uint256
locked: public(HashMap[address, LockedBalance])
@external
@view
def locked__end(_addr: address) -> uint256:
"""
@notice Get timestamp when `_addr`'s lock finishes
@param _addr User wallet
@return Epoch time of the lock end
"""
return self.locked[_addr].end
```
This example returns the unlock timestamp for a given address.
::::
### `get_last_user_slope`
::::description[`VotingEscrow.get_last_user_slope(addr: address) -> int128: view`]
Getter for the most recently recorded rate of voting power decrease for `addr`.
| Input | Type | Description |
| ------ | --------- | ------------ |
| `addr` | `address` | User address |
Returns: slope value (`int128`).
```vyper
struct Point:
bias: int128
slope: int128 # - dweight / dt
ts: uint256
blk: uint256 # block
user_point_epoch: public(HashMap[address, uint256])
user_point_history: public(HashMap[address, Point[1000000000]]) # user -> Point[user_epoch]
@external
@view
def get_last_user_slope(addr: address) -> int128:
"""
@notice Get the most recently recorded rate of voting power decrease for `addr`
@param addr Address of the user wallet
@return Value of the slope
"""
uepoch: uint256 = self.user_point_epoch[addr]
return self.user_point_history[addr][uepoch].slope
```
This example returns the most recent rate of voting power decrease for a given address.
::::
---
## Checkpoints & History
### `epoch`
::::description[`VotingEscrow.epoch() -> uint256: view`]
Getter for the current global epoch. The epoch is incremented by 1 every time a checkpoint is recorded.
Returns: current epoch (`uint256`).
```vyper
epoch: public(uint256)
```
This example returns the current global epoch. The value is fetched live from the blockchain.
::::
### `point_history`
::::description[`VotingEscrow.point_history(arg0: uint256) -> bias: int128, slope: int128, ts: uint256, blk: uint256: view`]
Getter for the global point history at epoch `arg0`. Each point records the aggregate bias, slope, timestamp, and block number.
| Input | Type | Description |
| ------ | --------- | ------------ |
| `arg0` | `uint256` | Epoch number |
Returns: bias (`int128`), slope (`int128`), ts (`uint256`) and blk (`uint256`).
```vyper
struct Point:
bias: int128
slope: int128 # - dweight / dt
ts: uint256
blk: uint256 # block
point_history: public(Point[100000000000000000000000000000]) # epoch -> unsigned point
```
This example returns the global point history at epoch 3 (bias, slope, timestamp, block number).
::::
### `user_point_epoch`
::::description[`VotingEscrow.user_point_epoch(arg0: address) -> uint256: view`]
Getter for the current checkpoint epoch for a specific user. This is incremented each time the user's lock state changes (create, increase amount, increase time, withdraw).
| Input | Type | Description |
| ------ | --------- | ------------ |
| `arg0` | `address` | User address |
Returns: user epoch (`uint256`).
```vyper
user_point_epoch: public(HashMap[address, uint256])
```
This example returns the current checkpoint epoch for a specific user.
::::
### `user_point_history`
::::description[`VotingEscrow.user_point_history(arg0: address, arg1: uint256) -> bias: int128, slope: int128, ts: uint256, blk: uint256: view`]
Getter for a user's point history at a specific user epoch. Each point records the user's bias, slope, timestamp, and block number at that checkpoint.
| Input | Type | Description |
| ------ | --------- | ----------------- |
| `arg0` | `address` | User address |
| `arg1` | `uint256` | User epoch number |
Returns: bias (`int128`), slope (`int128`), ts (`uint256`) and blk (`uint256`).
```vyper
user_point_history: public(HashMap[address, Point[1000000000]]) # user -> Point[user_epoch]
```
This example returns the point history for a user at their first checkpoint (bias, slope, timestamp, block number).
::::
### `user_point_history__ts`
::::description[`VotingEscrow.user_point_history__ts(_addr: address, _idx: uint256) -> uint256: view`]
Convenience getter for the timestamp of checkpoint `_idx` for `_addr`.
| Input | Type | Description |
| ------- | --------- | ----------------- |
| `_addr` | `address` | User address |
| `_idx` | `uint256` | User epoch number |
Returns: timestamp (`uint256`).
```vyper
struct Point:
bias: int128
slope: int128 # - dweight / dt
ts: uint256
blk: uint256 # block
user_point_history: public(HashMap[address, Point[1000000000]]) # user -> Point[user_epoch]
@external
@view
def user_point_history__ts(_addr: address, _idx: uint256) -> uint256:
"""
@notice Get the timestamp for checkpoint `_idx` for `_addr`
@param _addr User wallet address
@param _idx User epoch number
@return Epoch time of the checkpoint
"""
return self.user_point_history[_addr][_idx].ts
```
This example returns the timestamp of the first checkpoint for a given address.
::::
### `slope_changes`
::::description[`VotingEscrow.slope_changes(arg0: uint256) -> int128: view`]
Getter for scheduled slope changes at a future timestamp. When a lock expires, the global slope decreases. These changes are pre-scheduled so the contract doesn't need to iterate over all users.
| Input | Type | Description |
| ------ | --------- | ------------------------- |
| `arg0` | `uint256` | Timestamp (rounded to week) |
Returns: signed slope change (`int128`).
```vyper
slope_changes: public(HashMap[uint256, int128]) # time -> signed slope change
```
This example returns the scheduled slope change at a specific future timestamp.
::::
---
## SmartWalletChecker
The `SmartWalletChecker` is an external contract referenced by VotingEscrow to determine whether smart contracts are allowed to lock CRV. The internal `assert_not_contract` function checks callers against this contract whenever `create_lock`, `increase_amount`, or `increase_unlock_time` is called.
:::info[SmartWalletChecker Upgrade]
The SmartWalletChecker was originally used to **restrict which smart contracts could lock CRV** — contracts needed to be explicitly whitelisted via an `approveWallet` function, and access could be revoked via `revokeWallet`. This was in place to prevent tokenizing the escrow.
The checker has since been upgraded to a simple Vyper contract that **returns `True` for any input**, effectively removing all smart contract restrictions. Any smart contract can now lock CRV without needing prior approval.
```vyper
# pragma version 0.4.1
# @title Smart Wallet Whitelist
# @notice Dummy contract bypassing smart wallet check for veCRV.
# Returns 'true' for any input.
@view
@external
def check(_wallet: address) -> bool:
return True
```
:::
### `smart_wallet_checker`
::::description[`VotingEscrow.smart_wallet_checker() -> address: view`]
Getter for the current SmartWalletChecker contract address.
Returns: SmartWalletChecker contract (`address`).
```vyper
smart_wallet_checker: public(address)
```
This example returns the current SmartWalletChecker contract address.
::::
### `future_smart_wallet_checker`
::::description[`VotingEscrow.future_smart_wallet_checker() -> address: view`]
Getter for the future SmartWalletChecker contract address. Set via [`commit_smart_wallet_checker`](#commit_smart_wallet_checker) and applied via [`apply_smart_wallet_checker`](#apply_smart_wallet_checker).
Returns: future SmartWalletChecker contract (`address`).
```vyper
future_smart_wallet_checker: public(address)
```
This example returns the future SmartWalletChecker contract address (zero address means no pending change).
::::
### `commit_smart_wallet_checker`
::::description[`VotingEscrow.commit_smart_wallet_checker(addr: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to commit a new SmartWalletChecker contract address. Changes need to be applied via [`apply_smart_wallet_checker`](#apply_smart_wallet_checker).
| Input | Type | Description |
| ------ | --------- | ------------------------------- |
| `addr` | `address` | New SmartWalletChecker contract |
```vyper
future_smart_wallet_checker: public(address)
@external
def commit_smart_wallet_checker(addr: address):
"""
@notice Set an external contract to check for approved smart contract wallets
@param addr Address of Smart contract checker
"""
assert msg.sender == self.admin
self.future_smart_wallet_checker = addr
```
This example commits a new SmartWalletChecker contract address.
```shell
>>> VotingEscrow.commit_smart_wallet_checker("0x1234567890abcdef1234567890abcdef12345678")
```
::::
### `apply_smart_wallet_checker`
::::description[`VotingEscrow.apply_smart_wallet_checker()`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to apply the previously committed SmartWalletChecker address.
```vyper
smart_wallet_checker: public(address)
future_smart_wallet_checker: public(address)
@external
def apply_smart_wallet_checker():
"""
@notice Apply setting external contract to check approved smart contract wallets
"""
assert msg.sender == self.admin
self.smart_wallet_checker = self.future_smart_wallet_checker
```
This example applies the previously committed SmartWalletChecker address.
```shell
>>> VotingEscrow.apply_smart_wallet_checker()
```
::::
---
## Contract Info
### `token`
::::description[`VotingEscrow.token() -> address: view`]
Getter for the CRV token address that is locked in the contract.
Returns: CRV token address (`address`).
```vyper
token: public(address)
@external
def __init__(token_addr: address, _name: String[64], _symbol: String[32], _version: String[32]):
...
self.token = token_addr
...
```
This example returns the CRV token address locked in the contract.
::::
### `name`
::::description[`VotingEscrow.name() -> String[64]: view`]
Getter for the name of the token.
Returns: name (`String[64]`).
```vyper
name: public(String[64])
```
This example returns the name of the token.
::::
### `symbol`
::::description[`VotingEscrow.symbol() -> String[32]: view`]
Getter for the symbol of the token.
Returns: symbol (`String[32]`).
```vyper
symbol: public(String[32])
```
This example returns the symbol of the token.
::::
### `version`
::::description[`VotingEscrow.version() -> String[32]: view`]
Getter for the version of the contract.
Returns: version (`String[32]`).
```vyper
version: public(String[32])
```
This example returns the version of the contract.
::::
### `decimals`
::::description[`VotingEscrow.decimals() -> uint256: view`]
Getter for the decimals of the token.
Returns: decimals (`uint256`).
```vyper
decimals: public(uint256)
```
This example returns the number of decimals of the token.
::::
### `controller`
::::description[`VotingEscrow.controller() -> address: view`]
Getter for the Aragon controller of the contract.
Returns: controller address (`address`).
```vyper
controller: public(address)
```
This example returns the Aragon controller address.
::::
### `changeController`
::::description[`VotingEscrow.changeController(_newController: address)`]
:::guard[Guarded Method]
This function is only callable by the `controller` of the contract.
:::
Dummy method required for Aragon compatibility.
| Input | Type | Description |
| ----------------- | --------- | ---------------------- |
| `_newController` | `address` | New controller address |
```vyper
controller: public(address)
@external
def changeController(_newController: address):
"""
@dev Dummy method required for Aragon compatibility
"""
assert msg.sender == self.controller
self.controller = _newController
```
This example changes the Aragon controller address.
```shell
>>> VotingEscrow.changeController("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
```
::::
### `transfersEnabled`
::::description[`VotingEscrow.transfersEnabled() -> bool: view`]
View method required for Aragon compatibility. Always returns `True`.
Returns: transfers enabled (`bool`).
```vyper
transfersEnabled: public(bool)
@external
def __init__(token_addr: address, _name: String[64], _symbol: String[32], _version: String[32]):
...
self.transfersEnabled = True
...
```
This example returns whether transfers are enabled (always `True` for Aragon compatibility).
::::
---
## Admin Controls
The **CurveOwnershipAgent** ([`0x40907540d8a6C65c637785e8f8B742ae6b0b9968`](https://etherscan.io/address/0x40907540d8a6C65c637785e8f8B742ae6b0b9968)) is the current `admin` of the VotingEscrow. Any changes to admin-controlled parameters require a successfully passed DAO vote.
### `admin`
::::description[`VotingEscrow.admin() -> address: view`]
Getter for the current admin of the contract.
Returns: admin (`address`).
```vyper
admin: public(address) # Can and will be a smart contract
```
This example returns the current admin of the contract (CurveOwnershipAgent).
::::
### `future_admin`
::::description[`VotingEscrow.future_admin() -> address: view`]
Getter for the future admin of the contract. Set via [`commit_transfer_ownership`](#commit_transfer_ownership) and applied via [`apply_transfer_ownership`](#apply_transfer_ownership).
Returns: future admin (`address`).
```vyper
future_admin: public(address)
```
This example returns the future admin address (zero address means no pending transfer).
::::
### `commit_transfer_ownership`
::::description[`VotingEscrow.commit_transfer_ownership(addr: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to commit the transfer of ownership to `addr`. Changes need to be applied via [`apply_transfer_ownership`](#apply_transfer_ownership).
| Input | Type | Description |
| ------ | --------- | ----------------- |
| `addr` | `address` | New admin address |
Emits: `CommitOwnership` event.
```vyper
event CommitOwnership:
admin: address
future_admin: public(address)
@external
def commit_transfer_ownership(addr: address):
"""
@notice Transfer ownership of VotingEscrow contract to `addr`
@param addr Address to have ownership transferred to
"""
assert msg.sender == self.admin # dev: admin only
self.future_admin = addr
log CommitOwnership(addr)
```
This example commits a new admin address for the ownership transfer.
```shell
>>> VotingEscrow.commit_transfer_ownership("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
```
::::
### `apply_transfer_ownership`
::::description[`VotingEscrow.apply_transfer_ownership()`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to apply the previously committed ownership transfer.
Emits: `ApplyOwnership` event.
```vyper
event ApplyOwnership:
admin: address
admin: public(address) # Can and will be a smart contract
future_admin: public(address)
@external
def apply_transfer_ownership():
"""
@notice Apply ownership transfer
"""
assert msg.sender == self.admin # dev: admin only
_admin: address = self.future_admin
assert _admin != ZERO_ADDRESS # dev: admin not set
self.admin = _admin
log ApplyOwnership(_admin)
```
This example applies the previously committed ownership transfer.
```shell
>>> VotingEscrow.apply_transfer_ownership()
```
::::
---
## Contract Deployments
---
## Documentation Overview
Curve is a **decentralized exchange (DEX) and automated market maker (AMM) on Ethereum and EVM-compatible sidechains/L2s**, designed for the **efficient trading of stablecoins and volatile assets**.
Additionally, Curve has launched its own stablecoin, **crvUSD**, and **Curve Lending**, both featuring a **unique liquidation mechanism** known as **LLAMMA**.
This documentation outlines the technical implementation of the core Curve protocol and related smart contracts. It may be useful for contributors to the Curve codebase, third-party integrators, or technically proficient users of the protocol.
:::tip[Resources for Non-Technical Users]
Non-technical users might prefer the **[User Docs](/user/introduction)** site as it offers more general insights and information.
:::
---------
Core smart contracts include the Curve DAO Token, governance infrastructure governed by vote-escrowed CRV, mechanisms for fee collection and distribution, gauges, and many other components.
System of liquidity gauges and the `GaugeController` that directs CRV inflation to liquidity providers based on veCRV votes.
Implementation of StableSwap and CryptoSwap algorithms into on-chain exchange contracts, including Stableswap-NG, Twocrypto-NG, Tricrypto-NG, pool factories and routers.
Over-collateralized USD stablecoin powered by a unique liquidating algorithm ([LLAMMA](./crvusd/amm.md)), which progressively converts the put-up collateral token into crvUSD when the loan health decreases to certain thresholds.
Savings version of crvUSD. An ERC-4626 compliant Vault that earns yield from crvUSD interest fees, with cross-chain oracle support.
Permissionless lending markets to borrow or lend crvUSD against any asset with a proper oracle. Powered by Curve's unique liquidation algorithm, [LLAMMA](./crvusd/amm.md).
Fee collection, distribution, and burning architecture including the `FeeCollector`, `FeeSplitter`, `FeeDistributor`, and `CowSwapBurner`.
Section targeted at integrators covering contracts like `AddressProvider`, `MetaRegistry`, and the public Curve API.
---
## FastBridgeL2
The FastBridgeL2 contract serves as the Layer 2 coordinator for the FastBridge system, handling the initiation of both native and fast bridge transactions. This contract is deployed on each supported L2 network (Arbitrum, Optimism, Fraxtal) and manages the user-facing interface for bridging crvUSD tokens to Ethereum mainnet.
The contract implements a dual-bridge mechanism that simultaneously initiates both the slow native bridge and the fast LayerZero messaging system. It enforces rate limits per 42-hour interval, minimum amounts, and manages native token fees to ensure the system operates efficiently while maintaining security and economic sustainability.
:::vyper[`FastBridgeL2.vy`]
The source code for the `FastBridgeL2.vy` contract can be found on [GitHub](https://github.com/curvefi/fast-bridge/blob/main/contracts/FastBridgeL2.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.4.3` and utilizes a [Snekmate module](https://github.com/pcaversaccio/snekmate/blob/main/src/snekmate/auth/ownable.vy) to handle contract ownership.
The source code was audited by [:logos-chainsecurity: ChainSecurity](https://www.chainsecurity.com/). The full audit report can be found [here](/pdf/audits/ChainSecurity_Curve_Fast_Bridge_audit.pdf).
The contract is deployed on the following L2 networks:
- :logos-arbitrum: Arbitrum: [`0x1f2af270029d028400265ce1dd0919ba8780dae1`](https://arbiscan.io/address/0x1f2af270029d028400265ce1dd0919ba8780dae1)
- :logos-optimism: Optimism: [`0xd16d5ec345dd86fb63c6a9c43c517210f1027914`](https://optimistic.etherscan.io/address/0xd16d5ec345dd86fb63c6a9c43c517210f1027914)
- :logos-fraxtal: Fraxtal: [`0x3fe593e651cd0b383ad36b75f4159f30bb0631a6`](https://fraxscan.com/address/0x3fe593e651cd0b383ad36b75f4159f30bb0631a6)
:::
---
## Core Functions
The FastBridgeL2 contract provides essential functions for initiating bridge transactions, checking available amounts, and calculating costs. These functions work together to provide users with a seamless bridging experience while maintaining system security and efficiency.
### `bridge`
::::description[`FastBridgeL2.bridge(_token: IERC20, _to: address, _amount: uint256, _min_amount: uint256=0) -> uint256`]
Function to initiate a fast bridge transaction for crvUSD tokens from L2 to mainnet. This function handles both the native bridge (slow) and fast bridge (immediate) mechanisms. Users must provide native tokens to cover bridge and messaging fees. The function enforces rate limits per 42-hour interval and minimum amounts.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_token` | `IERC20` | The token to bridge (only crvUSD is supported) |
| `_to` | `address` | The receiver on destination chain |
| `_amount` | `uint256` | The amount of crvUSD to bridge; 2^256-1 for the whole available balance |
| `_min_amount` | `uint256` | Minimum amount to bridge; defaults to 0 |
Returns: The actual amount of crvUSD that was bridged (`uint256`).
Emits: `Bridge` event.
```vyper
interface IMessenger:
def initiate_fast_bridge(_to: address, _amount: uint256, _lz_fee_refund: address): payable
def quote_message_fee() -> uint256: view
CRVUSD: public(immutable(IERC20))
VAULT: public(immutable(address))
INTERVAL: constant(uint256) = 86400 * 7 // 4 # 42 hours
min_amount: public(uint256) # Minimum amount to initiate bridge. Might be costy to claim on Ethereum
limit: public(uint256) # Maximum amount to bridge in an INTERVAL, so there's no queue to resolve to claim on Ethereum
bridged: public(HashMap[uint256, uint256]) # Amounts of bridge coins per INTERVAL
bridger: public(IBridger)
messenger: public(IMessenger)
@external
@payable
def bridge(_token: IERC20, _to: address, _amount: uint256, _min_amount: uint256=0) -> uint256:
"""
@notice Bridge crvUSD
@param _token The token to bridge (only crvUSD is supported)
@param _to The receiver on destination chain
@param _amount The amount of crvUSD to deposit, 2^256-1 for the whole available balance
@param _min_amount Minimum amount to bridge
@return Bridged amount
"""
assert _token == CRVUSD, "Not supported"
assert _to != empty(address), "Bad receiver"
amount: uint256 = _amount
if amount == max_value(uint256):
amount = min(staticcall CRVUSD.balanceOf(msg.sender), staticcall CRVUSD.allowance(msg.sender, self))
# Apply daily limit
available: uint256 = self._get_available()
amount = min(amount, available)
assert amount >= _min_amount
assert extcall CRVUSD.transferFrom(msg.sender, self, amount, default_return_value=True)
self.bridged[block.timestamp // INTERVAL] += amount
bridger_cost: uint256 = self.bridger_cost()
messaging_cost: uint256 = self.messaging_cost()
assert msg.value >= bridger_cost + messaging_cost, "Insufficient msg.value"
# Initiate bridge transaction using native bridge
extcall self.bridger.bridge(CRVUSD, VAULT, amount, self.min_amount, value=bridger_cost)
# Message for VAULT to release amount while waiting
extcall self.messenger.initiate_fast_bridge(_to, amount, msg.sender, value=messaging_cost)
# Refund the rest of the msg.value
if msg.value > bridger_cost + messaging_cost:
send(msg.sender, msg.value - bridger_cost - messaging_cost)
log IBridger.Bridge(token=_token, sender=msg.sender, receiver=_to, amount=amount)
return amount
```
```shell
>>> FastBridgeL2.bridge(crvusd, '0x1234567890abcdef1234567890abcdef12345678', 10000 * 10**18)
10000000000000000000000
```
::::
### `allowed_to_bridge`
::::description[`FastBridgeL2.allowed_to_bridge(_ts: uint256=block.timestamp) -> (uint256, uint256): view`]
Checks how much crvUSD can be bridged at a specific timestamp, considering the rate limit and minimum requirements. Returns both the minimum and maximum amounts that can be bridged in the current 42-hour interval.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_ts` | `uint256` | Timestamp at which to check (default: current block timestamp) |
Returns: A tuple of (minimum_amount, maximum_amount) that can be bridged (`(uint256, uint256)`).
```vyper
INTERVAL: constant(uint256) = 86400 * 7 // 4 # 42 hours
min_amount: public(uint256) # Minimum amount to initiate bridge. Might be costly to claim on Ethereum
limit: public(uint256) # Maximum amount to bridge in an INTERVAL, so there's no queue to resolve to claim on Ethereum
bridged: public(HashMap[uint256, uint256]) # Amounts of bridge coins per INTERVAL
@external
@view
def allowed_to_bridge(_ts: uint256=block.timestamp) -> (uint256, uint256):
"""
@notice Get interval of allowed amounts to bridge
@param _ts Timestamp at which to check (current by default)
@return (minimum, maximum) amounts allowed to bridge
"""
if _ts < block.timestamp: # outdated
return (0, 0)
available: uint256 = self._get_available(_ts)
# Funds transferred to the contract are lost :(
min_amount: uint256 = self.min_amount
if available < min_amount: # Not enough for bridge initiation
return (0, 0)
return (min_amount, available)
@view
def _get_available(ts: uint256=block.timestamp) -> uint256:
limit: uint256 = self.limit
bridged: uint256 = self.bridged[ts // INTERVAL]
return limit - min(bridged, limit)
```
```shell
>>> FastBridgeL2.allowed_to_bridge()
(1000000000000000000, 500000000000000000000000)
```
::::
### `cost`
::::description[`FastBridgeL2.cost() -> uint256: view`]
Calculates the total native token cost required for a bridge transaction. This includes both the native bridge fee and the fast messaging fee. Users must send this amount as `msg.value` when calling the `bridge()` function.
Returns: Total native token amount needed for the bridge transaction (`uint256`).
```vyper
implements: IBridger
interface IMessenger:
def initiate_fast_bridge(_to: address, _amount: uint256, _lz_fee_refund: address): payable
def quote_message_fee() -> uint256: view
bridger: public(IBridger)
messenger: public(IMessenger)
@external
@view
def cost() -> uint256:
"""
@notice Quote messaging fee in native token. This value has to be provided
as msg.value when calling bridge(). This is not fee in crvUSD that is paid to the vault!
@return Native token amount needed for bridge tx
"""
return self.messaging_cost() + self.bridger_cost()
@internal
@view
def messaging_cost() -> uint256:
"""
Messaging cost to pass message to VAULT (Fast Bridge)
@return Native token amount needed for messenger
"""
return staticcall self.messenger.quote_message_fee()
@internal
@view
def bridger_cost() -> uint256:
"""
Bridger cost to bridge crvUSD to VAULT (Native Bridge)
@return Native token amount needed for bridger
"""
return staticcall self.bridger.cost()
```
```shell
>>> FastBridgeL2.cost()
234567890000000
```
::::
---
## Variables
The FastBridgeL2 contract maintains several important state variables that control its operation, track bridged amounts, manage limits, and store contract addresses. These variables work together to ensure proper functioning of the bridge system while maintaining security and economic sustainability.
### `min_amount`
::::description[`FastBridgeL2.min_amount() -> uint256: view`]
The minimum amount of crvUSD required to initiate a bridge transaction. This threshold exists because claiming small amounts on Ethereum can be expensive due to gas costs. Can be changed using the [`set_min_amount`](#set_min_amount) function.
Returns: Minimum crvUSD amount required for bridging (`uint256`).
```vyper
min_amount: public(uint256) # Minimum amount to initiate bridge. Might be costly to claim on Ethereum
```
```shell
>>> FastBridgeL2.min_amount()
1000000000000000000
```
::::
### `limit`
::::description[`FastBridgeL2.limit() -> uint256: view`]
The maximum amount of crvUSD that can be bridged within a 42-hour interval. This limit prevents overwhelming the Ethereum claim queue and ensures smooth processing of bridge transactions. Can be changed using the [`set_limit`](#set_limit) function.
Returns: Maximum crvUSD amount that can be bridged per interval (`uint256`).
```vyper
limit: public(uint256) # Maximum amount to bridge in an INTERVAL, so there's no queue to resolve to claim on Ethereum
```
```shell
>>> FastBridgeL2.limit()
500000000000000000000000
```
::::
### `bridged`
::::description[`FastBridgeL2.bridged(arg0: uint256) -> uint256: view`]
Tracks the total amount of crvUSD that has been bridged in each 42-hour interval. The key is the timestamp divided by the interval (151,200 seconds), and the value is the cumulative amount bridged.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `arg0` | `uint256` | Time interval key (timestamp // 151200) |
Returns: Total crvUSD amount bridged in the specified time interval (`uint256`).
```vyper
bridged: public(HashMap[uint256, uint256]) # Amounts of bridge coins per INTERVAL
```
```shell
>>> FastBridgeL2.bridged(block.timestamp // 151200)
125000000000000000000000
```
::::
### `bridger`
::::description[`FastBridgeL2.bridger() -> IBridger: view`]
The contract responsible for handling the native bridge transaction that actually moves crvUSD from L2 to mainnet. This is the slower but reliable bridge mechanism. Can be changed using the [`set_bridger`](#set_bridger) function.
Returns: Address of the bridger contract (`IBridger`).
```vyper
bridger: public(IBridger)
```
```shell
>>> FastBridgeL2.bridger()
'0x8a5a5299f35614ac558aa290c2d5856edec1b5ad'
```
::::
### `messenger`
::::description[`FastBridgeL2.messenger() -> IMessenger: view`]
The contract responsible for sending fast messages to the mainnet vault, enabling immediate access to bridged funds while the native bridge transaction is still pending. Can be changed using the [`set_messenger`](#set_messenger) function.
Returns: Address of the messenger contract (`IMessenger`).
```vyper
messenger: public(IMessenger)
```
```shell
>>> FastBridgeL2.messenger()
'0x14e11c1b8f04a7de306a7b5bf21bbca0d5cf79ff'
```
::::
### `CRVUSD`
::::description[`FastBridgeL2.CRVUSD() -> IERC20: view`]
The crvUSD token contract address on the L2 network. This is the token that gets bridged from L2 to mainnet. The address is set during deployment and cannot be changed.
Returns: crvUSD token contract address (`IERC20`).
```vyper
CRVUSD: public(immutable(IERC20))
```
```shell
>>> FastBridgeL2.CRVUSD()
'0xe5AfcF332a5457E8FafCD668BcE3dF953762Dfe7'
```
::::
### `VAULT`
::::description[`FastBridgeL2.VAULT() -> address: view`]
The mainnet vault contract address where bridged crvUSD tokens are sent. This is the destination for both the native bridge and the fast bridge mechanisms. The address is set during deployment and cannot be changed.
Returns: Mainnet vault contract address (`address`).
```vyper
VAULT: public(immutable(address))
```
```shell
>>> FastBridgeL2.VAULT()
'0xadB10d2d5A95e58Ddb1A0744a0d2D7B55Db7843D'
```
::::
### `version`
::::description[`FastBridgeL2.version() -> String[8]: view`]
The version identifier for this contract implementation. This helps track which version of the contract is deployed and can be used for upgrade compatibility checks.
Returns: Contract version string (`String[8]`).
```vyper
version: public(constant(String[8])) = "0.0.1"
```
```shell
>>> FastBridgeL2.version()
'0.0.1'
```
::::
---
## Owner Functions
The FastBridgeL2 contract includes several administrative functions that allow the contract owner to manage system parameters, update contract addresses, and configure operational settings. These functions are protected by ownership checks to ensure only authorized personnel can make critical changes to the system.
### `set_min_amount`
::::description[`FastBridgeL2.set_min_amount(_min_amount: uint256)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to handle ownership. This specific function is only callable by the `owner` of the contract.
:::
Updates the minimum amount of crvUSD required to initiate a bridge transaction. Only the contract owner can call this function. This helps prevent users from bridging amounts that would be uneconomical to claim on mainnet.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_min_amount` | `uint256` | New minimum amount required for bridging |
Emits: `SetMinAmount` event.
```vyper
min_amount: public(uint256) # Minimum amount to initiate bridge. Might be costly to claim on Ethereum
@external
def set_min_amount(_min_amount: uint256):
"""
@notice Set minimum amount allowed to bridge
@param _min_amount Minimum amount
"""
ownable._check_owner()
self.min_amount = _min_amount
log SetMinAmount(min_amount=_min_amount)
```
```vyper
owner: public(address)
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> FastBridgeL2.set_min_amount(5000 * 10**18)
```
::::
### `set_limit`
::::description[`FastBridgeL2.set_limit(_limit: uint256)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to handle ownership. This specific function is only callable by the `owner` of the contract.
:::
Updates the rate limit for crvUSD bridging per 42-hour interval. Only the contract owner can call this function. This limit prevents overwhelming the Ethereum claim queue and ensures smooth processing of bridge transactions.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_limit` | `uint256` | New limit for crvUSD bridging per interval |
Emits: `SetLimit` event.
```vyper
limit: public(uint256) # Maximum amount to bridge in an INTERVAL, so there's no queue to resolve to claim on Ethereum
@external
def set_limit(_limit: uint256):
"""
@notice Set new limit
@param _limit Limit on bridging per INTERVAL
"""
ownable._check_owner()
self.limit = _limit
log SetLimit(limit=_limit)
```
```vyper
owner: public(address)
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> FastBridgeL2.set_limit(1000000 * 10**18)
```
::::
### `set_bridger`
::::description[`FastBridgeL2.set_bridger(_bridger: IBridger)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to handle ownership. This specific function is only callable by the `owner` of the contract.
:::
Updates the bridger contract that handles the native bridge transaction. Only the contract owner can call this function. The function also updates the crvUSD token approval to the new bridger contract.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_bridger` | `IBridger` | New bridger contract address |
Emits: `SetBridger` event.
```vyper
bridger: public(IBridger)
@external
def set_bridger(_bridger: IBridger):
"""
@notice Set new bridger
@param _bridger Contract initiating actual bridge transaction
"""
ownable._check_owner()
assert _bridger != empty(IBridger), "Bad bridger value"
assert extcall CRVUSD.approve(self.bridger.address, 0, default_return_value=True)
assert extcall CRVUSD.approve(_bridger.address, max_value(uint256), default_return_value=True)
self.bridger = _bridger
log SetBridger(bridger=_bridger)
```
```vyper
owner: public(address)
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> FastBridgeL2.set_bridger('0x8a5a5299f35614ac558aa290c2d5856edec1b5ad')
```
::::
### `set_messenger`
::::description[`FastBridgeL2.set_messenger(_messenger: IMessenger)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to handle ownership. This specific function is only callable by the `owner` of the contract.
:::
Updates the messenger contract that handles fast message delivery to the mainnet vault. Only the contract owner can call this function. This allows for upgrading the fast bridge mechanism without changing the main contract.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_messenger` | `IMessenger` | New messenger contract address |
Emits: `SetMessenger` event.
```vyper
messenger: public(IMessenger)
@external
def set_messenger(_messenger: IMessenger):
"""
@notice Set new messenger
@param _messenger Contract passing bridge tx fast
"""
ownable._check_owner()
assert _messenger != empty(IMessenger), "Bad messenger value"
self.messenger = _messenger
log SetMessenger(messenger=_messenger)
```
```vyper
owner: public(address)
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> FastBridgeL2.set_messenger('0x14e11c1b8f04a7de306a7b5bf21bbca0d5cf79ff')
```
::::
---
## FastBridgeVault
The FastBridgeVault is the Ethereum mainnet component of the FastBridge system, responsible for managing pre-minted crvUSD tokens and handling the fast bridge mechanism. This contract serves as the central vault that holds crvUSD tokens that can be immediately released to users while their native bridge transactions are still pending.
The vault operates as a secure intermediary that bridges the gap between the slow but reliable native bridge mechanism and the immediate access requirements of users. It implements sophisticated risk management through debt ceilings, fee collection, and emergency controls to ensure system stability and user fund safety.
:::vyper[`FastBridgeVault.vy`]
The source code for the `FastBridgeVault.vy` contract can be found on [GitHub](https://github.com/curvefi/fast-bridge/blob/main/contracts/FastBridgeVault.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.4.3` and utilizes a [Snekmate module](https://github.com/pcaversaccio/snekmate/blob/main/src/snekmate/auth/access_control.vy) to handle contract ownership.
The source code was audited by [:logos-chainsecurity: ChainSecurity](https://www.chainsecurity.com/). The full audit report can be found [here](/pdf/audits/ChainSecurity_Curve_Fast_Bridge_audit.pdf).
The contract is deployed on :logos-ethereum: Ethereum for the following L2 routes:
- :logos-arbitrum: Arbitrum: [`0xadB10d2d5A95e58Ddb1A0744a0d2D7B55Db7843D`](https://etherscan.io/address/0xadB10d2d5A95e58Ddb1A0744a0d2D7B55Db7843D)
- :logos-optimism: Optimism: [`0x97d024859B68394122B3d0bb407dD7299cC8E937`](https://etherscan.io/address/0x97d024859B68394122B3d0bb407dD7299cC8E937)
- :logos-fraxtal: Fraxtal: [`0x5EF620631AA46e7d2F6f963B6bE4F6823521B9eC`](https://etherscan.io/address/0x5EF620631AA46e7d2F6f963B6bE4F6823521B9eC)
```json
[{"anonymous":false,"inputs":[{"indexed":true,"name":"receiver","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"Minted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"status","type":"bool"}],"name":"RugScheduled","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"fee","type":"uint256"}],"name":"SetFee","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"fee_receiver","type":"address"}],"name":"SetFeeReceiver","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"actor","type":"address"},{"indexed":false,"name":"killed","type":"bool"}],"name":"SetKilled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"token","type":"address"},{"indexed":false,"name":"receiver","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"Recovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"role","type":"bytes32"},{"indexed":true,"name":"account","type":"address"},{"indexed":true,"name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"role","type":"bytes32"},{"indexed":true,"name":"account","type":"address"},{"indexed":true,"name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"role","type":"bytes32"},{"indexed":true,"name":"previousAdminRole","type":"bytes32"},{"indexed":true,"name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"inputs":[{"name":"arg0","type":"bytes32"},{"name":"arg1","type":"address"}],"name":"hasRole","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"role","type":"bytes32"},{"name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"role","type":"bytes32"},{"name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"role","type":"bytes32"},{"name":"account","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"role","type":"bytes32"},{"name":"admin_role","type":"bytes32"}],"name":"set_role_admin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"schedule_rug","outputs":[{"name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_receiver","type":"address"},{"name":"_amount","type":"uint256"}],"name":"mint","outputs":[{"name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_status","type":"bool"}],"name":"set_killed","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_status","type":"bool"},{"name":"_who","type":"address"}],"name":"set_killed","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_new_fee","type":"uint256"}],"name":"set_fee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_new_fee_receiver","type":"address"}],"name":"set_fee_receiver","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"name":"coin","type":"address"},{"name":"amount","type":"uint256"}],"name":"_recovers","type":"tuple[]"},{"name":"_receiver","type":"address"}],"name":"recover","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"version","outputs":[{"name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MINTER_ROLE","outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"KILLER_ROLE","outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"CRVUSD","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MINTER","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fee_receiver","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"rug_scheduled","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"}],"name":"is_killed","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_ownership","type":"address"},{"name":"_emergency","type":"address"},{"name":"_minters","type":"address[]"}],"outputs":[],"stateMutability":"nonpayable","type":"constructor"}]
```
:::
---
## Core Functions
The FastBridgeVault provides essential functions for managing the fast bridge process, including token minting, fee collection, and emergency controls. These functions work together to provide immediate access to bridged funds while maintaining the security and economic sustainability of the system.
### `mint`
::::description[`FastBridgeVault.mint(_receiver: address, _amount: uint256) -> uint256`]
Releases pre-minted crvUSD from the vault's balance to the receiver. For callers with `MINTER_ROLE`, the function can additionally increase the receiver's claimable balance by `_amount`. The operation applies fees and respects kill switches for emergency situations. The vault's fast releases are economically backed by the incoming native-bridge transfers and debt-ceiling rug mechanism; it does not increase total crvUSD supply beyond what is backed by the slow bridge path.
If the vault does not have enough crvUSD at the time of the call (e.g. the native bridge transfer has not yet arrived), the receiver's pending balance is recorded in `balanceOf[_receiver]`. The receiver (or anyone) can later call `mint(_receiver, 0)` to claim the pending amount once the vault has been replenished.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_receiver` | `address` | Receiver of crvUSD tokens |
| `_amount` | `uint256` | Amount of crvUSD to mint (0 if not minter); use `0` to claim a pending balance |
Returns: Amount of crvUSD actually transferred to the receiver (`uint256`).
Emits: `Minted` event.
```vyper
@external
@nonreentrant
def mint(_receiver: address, _amount: uint256) -> uint256:
"""
@notice Receive bridged crvUSD
@param _receiver Receiver of crvUSD
@param _amount Amount of crvUSD to mint (0 if not minter)
@return Amount of crvUSD minted to receiver
"""
assert not (self.is_killed[empty(address)] or self.is_killed[msg.sender])
amount: uint256 = self.balanceOf[_receiver]
if access_control.hasRole[MINTER_ROLE][msg.sender]:
amount += _amount
# Apply fee
fee: uint256 = _amount * self.fee // 10 ** 18
fee_receiver: address = self.fee_receiver
if _receiver != fee_receiver:
self.balanceOf[fee_receiver] += fee
amount -= fee
available: uint256 = min(self._get_balance(), amount)
if available != 0:
assert extcall CRVUSD.transfer(_receiver, available, default_return_value=True)
self.balanceOf[_receiver] = amount - available
log Minted(receiver=_receiver, amount=available)
return available
```
```vyper
# @dev Returns `True` if `account` has been granted `role`.
hasRole: public(HashMap[bytes32, HashMap[address, bool]])
```
```shell
>>> FastBridgeVault.mint('0x1234...', 10000 * 10**18)
10000000000000000000000
```
::::
### `schedule_rug`
::::description[`FastBridgeVault.schedule_rug() -> bool`]
Checks if the vault needs to rug (reduce) its debt ceiling due to changes in the minter's debt ceiling. This function can be called by anyone and schedules the rugging process if necessary.
Returns: Boolean indicating whether rugging is needed (`bool`).
Emits: `RugScheduled` event.
```vyper
@external
def schedule_rug() -> bool:
"""
@notice Schedule rugging debt ceiling if necessary. Callable by anyone
@return Boolean whether need to rug or not
"""
rug_scheduled: bool = self._need_to_rug()
self.rug_scheduled = rug_scheduled
log RugScheduled(status=rug_scheduled)
return rug_scheduled
```
```shell
>>> FastBridgeVault.schedule_rug()
False
```
::::
## Variables
The FastBridgeVault contract maintains several important state variables that control its operation, track balances, manage fees, and implement security mechanisms. These variables work together to ensure proper functioning of the fast bridge system while maintaining security and economic sustainability.
### `balanceOf`
::::description[`FastBridgeVault.balanceOf(address) -> uint256: view`]
Tracks the pending crvUSD balance for each address. This represents tokens that have been bridged but not yet claimed by the recipient.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `arg0` | `address` | Address to check pending balance for |
Returns: Pending crvUSD balance for the given address (`uint256`).
```vyper
balanceOf: public(HashMap[address, uint256])
```
This example returns the pending crvUSD balance for a given address on the Arbitrum route vault. Enter an address and click **Query** to fetch the value live from the blockchain.
::::
### `fee`
::::description[`FastBridgeVault.fee() -> uint256: view`]
The fee rate applied to bridge transactions, expressed with 10^18 precision (e.g., 1% = 10^16).
Returns: Fee rate with 10^18 precision (`uint256`).
```vyper
fee: public(uint256) # 10^18 precision
```
This example returns the current fee rate on the Arbitrum route vault. The value is fetched live from the blockchain.
::::
### `fee_receiver`
::::description[`FastBridgeVault.fee_receiver() -> address: view`]
The address that receives the fees collected from bridge transactions.
Returns: Fee receiver address (`address`).
```vyper
fee_receiver: public(address)
```
This example returns the current fee receiver address on the Arbitrum route vault. The value is fetched live from the blockchain.
::::
### `rug_scheduled`
::::description[`FastBridgeVault.rug_scheduled() -> bool: view`]
Indicates whether a debt ceiling rugging operation has been scheduled. This happens when the minter's debt ceiling has been reduced.
Returns: Boolean indicating if rugging is scheduled (`bool`).
```vyper
rug_scheduled: public(bool)
```
This example returns whether a rug operation is currently scheduled on the Arbitrum route vault. The value is fetched live from the blockchain.
::::
### `is_killed`
::::description[`FastBridgeVault.is_killed(address) -> bool: view`]
Emergency kill switch that can disable specific minters or all minting operations. When killed, the specified address cannot mint tokens.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `address` | `address` | Address to check kill status for |
Returns: Boolean indicating if the address is killed (`bool`).
```vyper
is_killed: public(HashMap[address, bool])
```
This example checks the kill status for the zero address (global kill switch) on the Arbitrum route vault. Enter an address and click **Query** to fetch the value live from the blockchain.
::::
### `version`
::::description[`FastBridgeVault.version() -> String[8]: view`]
The version identifier for this contract implementation.
Returns: Contract version string (`String[8]`).
```vyper
version: public(constant(String[8])) = "0.0.1"
```
This example returns the contract version on the Arbitrum route vault. The value is fetched live from the blockchain.
::::
### `MINTER_ROLE`
::::description[`FastBridgeVault.MINTER_ROLE() -> bytes32: view`]
The role identifier for addresses that can mint crvUSD from the vault.
Returns: Role identifier for minters (`bytes32`).
```vyper
MINTER_ROLE: public(constant(bytes32)) = keccak256("MINTER")
```
This example returns the MINTER_ROLE identifier on the Arbitrum route vault. The value is fetched live from the blockchain.
::::
### `KILLER_ROLE`
::::description[`FastBridgeVault.KILLER_ROLE() -> bytes32: view`]
The role identifier for addresses that can kill/unkill minters in emergency situations.
Returns: Role identifier for emergency operators (`bytes32`).
```vyper
KILLER_ROLE: public(constant(bytes32)) = keccak256("KILLER")
```
This example returns the KILLER_ROLE identifier on the Arbitrum route vault. The value is fetched live from the blockchain.
::::
### `CRVUSD`
::::description[`FastBridgeVault.CRVUSD() -> IERC20: view`]
The crvUSD token contract address on mainnet. This is the token that gets minted from the vault.
Returns: crvUSD token contract address (`IERC20`).
```vyper
CRVUSD: public(constant(IERC20)) = IERC20(0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E)
```
This example returns the crvUSD token address on the Arbitrum route vault. The value is fetched live from the blockchain.
::::
### `MINTER`
::::description[`FastBridgeVault.MINTER() -> IMinter: view`]
The minter contract (ControllerFactory) that manages debt ceilings and can rug debt when necessary.
Returns: Minter contract address (`IMinter`).
```vyper
MINTER: public(constant(IMinter)) = IMinter(0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC)
```
This example returns the MINTER (ControllerFactory) contract address on the Arbitrum route vault. The value is fetched live from the blockchain.
::::
---
## Admin Functions
The FastBridgeVault includes several administrative functions that allow authorized parties to manage the system's operation, configure parameters, and respond to emergency situations. These functions are protected by role-based access control to ensure only authorized personnel can make critical changes to the system.
### `set_killed`
::::description[`FastBridgeVault.set_killed(_status: bool, _who: address=empty(address))`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the `KILLER_ROLE` role.
:::
Emergency function to kill or unkill specific minters or all minting operations. Only addresses with KILLER_ROLE can call this function.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_status` | `bool` | Boolean whether to stop minter from working |
| `_who` | `address` | Minter to kill/unkill, empty address to kill all receiving (default: empty address) |
Emits: `SetKilled` event.
```vyper
@external
def set_killed(_status: bool, _who: address=empty(address)):
"""
@notice Emergency method to kill minter
@param _status Boolean whether to stop minter from working
@param _who Minter to kill/unkill, empty address to kill all receiving
"""
access_control._check_role(KILLER_ROLE, msg.sender)
self.is_killed[_who] = _status
log SetKilled(actor=_who, killed=_status)
```
```vyper
# @dev Returns `True` if `account` has been granted `role`.
hasRole: public(HashMap[bytes32, HashMap[address, bool]])
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
@param role The 32-byte role definition.
@param account The 20-byte address of the account.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
```shell
>>> FastBridgeVault.set_killed(True, '0x1234...')
```
::::
### `set_fee`
::::description[`FastBridgeVault.set_fee(_new_fee: uint256)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the `DEFAULT_ADMIN_ROLE` role.
:::
Updates the fee rate applied to bridge transactions. Only the admin can call this function. The fee cannot exceed 100% (10^18).
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_new_fee` | `uint256` | New fee rate with 10^18 precision (max: 10^18) |
Emits: `SetFee` event.
```vyper
@external
def set_fee(_new_fee: uint256):
"""
@notice Set fee on bridge transactions
@param _new_fee Fee with 10^18 precision
"""
access_control._check_role(access_control.DEFAULT_ADMIN_ROLE, msg.sender)
assert _new_fee <= 10 ** 18
self.fee = _new_fee
log SetFee(fee=_new_fee)
```
```vyper
# @dev The default 32-byte admin role.
DEFAULT_ADMIN_ROLE: public(constant(bytes32)) = empty(bytes32)
# @dev Returns `True` if `account` has been granted `role`.
hasRole: public(HashMap[bytes32, HashMap[address, bool]])
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
@param role The 32-byte role definition.
@param account The 20-byte address of the account.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
```shell
>>> FastBridgeVault.set_fee(0)
```
::::
### `set_fee_receiver`
::::description[`FastBridgeVault.set_fee_receiver(_new_fee_receiver: address)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the `DEFAULT_ADMIN_ROLE` role.
:::
Updates the address that receives fees from bridge transactions. Only the admin can call this function.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_new_fee_receiver` | `address` | New fee receiver address |
Emits: `SetFeeReceiver` event.
```vyper
@external
def set_fee_receiver(_new_fee_receiver: address):
"""
@notice Set new fee receiver
@param _new_fee_receiver Fee receiver address
"""
access_control._check_role(access_control.DEFAULT_ADMIN_ROLE, msg.sender)
assert _new_fee_receiver != empty(address)
self.fee_receiver = _new_fee_receiver
log SetFeeReceiver(fee_receiver=_new_fee_receiver)
```
```vyper
# @dev The default 32-byte admin role.
DEFAULT_ADMIN_ROLE: public(constant(bytes32)) = empty(bytes32)
# @dev Returns `True` if `account` has been granted `role`.
hasRole: public(HashMap[bytes32, HashMap[address, bool]])
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
@param role The 32-byte role definition.
@param account The 20-byte address of the account.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
```shell
>>> FastBridgeVault.set_fee_receiver('0xa2Bcd...')
```
::::
### `recover`
::::description[`FastBridgeVault.recover(_recovers: DynArray[RecoverInput, 32], _receiver: address)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the `DEFAULT_ADMIN_ROLE` role.
:::
Emergency function to recover ERC20 tokens from the contract. This is needed in case of minter malfunction or other emergencies. Only the admin can call this function.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_recovers` | `DynArray[RecoverInput, 32]` | Array of (token, amount) pairs to recover |
| `_receiver` | `address` | Address to receive the recovered tokens |
Emits: `Recovered` event.
```vyper
@external
def recover(_recovers: DynArray[RecoverInput, 32], _receiver: address):
"""
@notice Recover ERC20 tokens from this contract. Needed in case of minter malfunction.
@dev Callable only by owner
@param _recovers (Token, amount) to recover
@param _receiver Receiver of coins
"""
access_control._check_role(access_control.DEFAULT_ADMIN_ROLE, msg.sender)
for input: RecoverInput in _recovers:
amount: uint256 = input.amount
if amount == max_value(uint256):
amount = staticcall input.coin.balanceOf(self)
extcall input.coin.transfer(_receiver, amount, default_return_value=True) # do not need safe transfer
log Recovered(token=input.coin, receiver=_receiver, amount=amount)
```
```vyper
# @dev The default 32-byte admin role.
DEFAULT_ADMIN_ROLE: public(constant(bytes32)) = empty(bytes32)
# @dev Returns `True` if `account` has been granted `role`.
hasRole: public(HashMap[bytes32, HashMap[address, bool]])
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
@param role The 32-byte role definition.
@param account The 20-byte address of the account.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
```shell
>>> FastBridgeVault.recover([('0xf939...', 1000 * 10**18)], '0x1234...')
```
::::
---
## L2MessengerLZ
The L2MessengerLZ contract serves as the LayerZero messaging component for the FastBridge system, handling the fast message delivery from L2 networks to the Ethereum mainnet vault. This contract is deployed on each supported L2 network (Arbitrum, Optimism, Fraxtal) and manages the LayerZero cross-chain messaging that enables immediate access to bridged funds.
The contract implements LayerZero's OApp (Omnichain Application) standard to provide secure and efficient cross-chain communication. It works in conjunction with the FastBridgeL2 contract to send fast bridge messages that trigger immediate crvUSD minting on the mainnet vault while the native bridge transaction is still pending.
:::vyper[`L2MessengerLZ.vy`]
The source code for the `L2MessengerLZ.vy` contract can be found on [GitHub](https://github.com/curvefi/fast-bridge/blob/main/contracts/messengers/L2MessengerLZ.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.4.3` and utilizes [LayerZero OApp](https://docs.layerzero.network/v2/concepts/applications/oapp-standard) modules for cross-chain messaging.
The source code was audited by [:logos-chainsecurity: ChainSecurity](https://www.chainsecurity.com/). The full audit report can be found [here](/pdf/audits/ChainSecurity_Curve_Fast_Bridge_audit.pdf).
The contract is deployed on the following L2 networks:
- :logos-arbitrum: Arbitrum: [`0x14e11c1b8f04a7de306a7b5bf21bbca0d5cf79ff`](https://arbiscan.io/address/0x14e11c1b8f04a7de306a7b5bf21bbca0d5cf79ff)
- :logos-optimism: Optimism: [`0x7a1f2f99b65f6c3b2413648c86c0326cff8d8837`](https://optimistic.etherscan.io/address/0x7a1f2f99b65f6c3b2413648c86c0326cff8d8837)
- :logos-fraxtal: Fraxtal: [`0x672c38258729060bf443ba28faef4f2db154c6fc`](https://fraxscan.com/address/0x672c38258729060bf443ba28faef4f2db154c6fc)
:::
---
## Core Functions
The L2MessengerLZ contract provides essential functions for initiating fast bridge messages, quoting message fees, and managing LayerZero messaging parameters. These functions work together to provide secure and efficient cross-chain communication for the fast bridge mechanism.
### `initiate_fast_bridge`
::::description[`L2MessengerLZ.initiate_fast_bridge(_to: address, _amount: uint256, _lz_fee_refund: address)`]
:::guard[Guarded Method]
This function is only callable by the `FastBridgeL2.vy` contract.
:::
Initiates a fast bridge by sending a message to the peer contract on the main chain. This function is only callable by the FastBridgeL2 contract and triggers immediate crvUSD minting on the mainnet vault while the native bridge transaction is still pending.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_to` | `address` | Address to mint crvUSD to on mainnet |
| `_amount` | `uint256` | Amount of crvUSD to mint |
| `_lz_fee_refund` | `address` | Address to receive excess LayerZero fees |
Emits: `Initiated` event.
```vyper
event Initiated:
to: address
amount: uint256
lz_fee_refund: address
@external
@payable
def initiate_fast_bridge(_to: address, _amount: uint256, _lz_fee_refund: address):
"""
@notice Initiate fast bridge by sending (to, amount) to peer on main chain
Only callable by FastBridgeL2
@param _to Address to mint to
@param _amount Amount to mint
@param _lz_fee_refund Address to deposit excess fees from transaction
"""
assert msg.sender == self.fast_bridge_l2, "Only FastBridgeL2!"
# step 1: convert message to bytes
encoded_message: Bytes[OApp.MAX_MESSAGE_SIZE] = abi_encode(_to, _amount)
# step 2: create options using OptionsBuilder module
options: Bytes[OptionsBuilder.MAX_OPTIONS_TOTAL_SIZE] = OptionsBuilder.newOptions()
options = OptionsBuilder.addExecutorLzReceiveOption(options, self.gas_limit, 0)
# step 3: send message
fees: OApp.MessagingFee = OApp.MessagingFee(nativeFee=msg.value, lzTokenFee=0)
OApp._lzSend(self.vault_eid, encoded_message, options, fees, _lz_fee_refund)
log Initiated(to=_to, amount=_amount, lz_fee_refund=_lz_fee_refund)
```
```shell
>>> L2MessengerLZ.initiate_fast_bridge('0x1234...', 10000 * 10**18, '0x5678...')
```
::::
### `quote_message_fee`
::::description[`L2MessengerLZ.quote_message_fee() -> uint256: view`]
Quotes the LayerZero message fee in native tokens required to send a fast bridge message to the mainnet vault. This function helps users and the FastBridgeL2 contract calculate the exact amount of native tokens needed for the messaging operation.
Returns: Native token amount needed for the LayerZero message (`uint256`).
```vyper
@external
@view
def quote_message_fee() -> uint256:
"""
@notice Quote message fee in native token
@return Native token amount needed for message
"""
# step 1: mock message
encoded_message: Bytes[OApp.MAX_MESSAGE_SIZE] = abi_encode(self, empty(uint256))
# step 2: mock options
options: Bytes[OptionsBuilder.MAX_OPTIONS_TOTAL_SIZE] = OptionsBuilder.newOptions()
options = OptionsBuilder.addExecutorLzReceiveOption(options, self.gas_limit, 0)
# step 3: quote fee
return OApp._quote(self.vault_eid, encoded_message, options, False).nativeFee
```
```shell
>>> L2MessengerLZ.quote_message_fee()
45678900000000
```
::::
---
## Variables
The L2MessengerLZ contract maintains several important state variables that control its operation, store contract addresses, and manage LayerZero messaging parameters. These variables work together to ensure proper functioning of the cross-chain messaging system while maintaining security and efficiency.
### `vault_eid`
::::description[`L2MessengerLZ.vault_eid() -> uint32: view`]
The LayerZero endpoint ID (EID) of the mainnet vault contract. This identifies the destination chain and contract for fast bridge messages. Can be changed using the [`set_vault_eid`](#set_vault_eid) function.
Returns: Vault chain endpoint ID (`uint32`).
```vyper
vault_eid: public(uint32)
```
```shell
>>> L2MessengerLZ.vault_eid()
30101
```
::::
### `fast_bridge_l2`
::::description[`L2MessengerLZ.fast_bridge_l2() -> address: view`]
The address of the FastBridgeL2 contract that is authorized to call the `initiate_fast_bridge` function. This ensures only the legitimate bridge contract can send fast bridge messages. Can be changed using the [`set_fast_bridge_l2`](#set_fast_bridge_l2) function.
Returns: FastBridgeL2 contract address (`address`).
```vyper
fast_bridge_l2: public(address)
```
```shell
>>> L2MessengerLZ.fast_bridge_l2()
'0x1f2af270029d028400265ce1dd0919ba8780dae1'
```
::::
### `gas_limit`
::::description[`L2MessengerLZ.gas_limit() -> uint128: view`]
The gas limit for LayerZero message execution on the destination chain. This parameter ensures sufficient gas is provided for the message processing on mainnet. Can be changed using the [`set_gas_limit`](#set_gas_limit) function.
Returns: Gas limit for destination chain execution (`uint128`).
```vyper
gas_limit: public(uint128)
```
```shell
>>> L2MessengerLZ.gas_limit()
200000
```
::::
---
## Owner Functions
The L2MessengerLZ contract includes several administrative functions that allow the contract owner to manage system parameters, update contract addresses, and configure LayerZero messaging settings. These functions are protected by ownership checks to ensure only authorized personnel can make critical changes to the system.
### `set_fast_bridge_l2`
::::description[`L2MessengerLZ.set_fast_bridge_l2(_fast_bridge_l2: address)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to handle ownership. This specific function is only callable by the `owner` of the contract.
:::
Updates the address of the FastBridgeL2 contract that is authorized to initiate fast bridge messages. Only the contract owner can call this function. This allows for updating the authorized caller if the FastBridgeL2 contract is upgraded or replaced.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_fast_bridge_l2` | `address` | New FastBridgeL2 contract address |
Emits: `SetFastBridgeL2` event.
```vyper
@external
def set_fast_bridge_l2(_fast_bridge_l2: address):
"""
@notice Set fast bridge l2 address
@param _fast_bridge_l2 FastBridgeL2 address
"""
ownable._check_owner()
assert _fast_bridge_l2 != empty(address), "Bad value"
self.fast_bridge_l2 = _fast_bridge_l2
log SetFastBridgeL2(fast_bridge_l2=_fast_bridge_l2)
```
```vyper
owner: public(address)
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> L2MessengerLZ.set_fast_bridge_l2('0x1f2af270029d028400265ce1dd0919ba8780dae1')
```
::::
### `set_vault_eid`
::::description[`L2MessengerLZ.set_vault_eid(_vault_eid: uint32)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to handle ownership. This specific function is only callable by the `owner` of the contract.
:::
Updates the LayerZero endpoint ID of the mainnet vault contract. Only the contract owner can call this function. This allows for updating the destination chain and contract if the vault is moved or the endpoint ID changes.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_vault_eid` | `uint32` | New vault endpoint ID |
Emits: `SetVaultEid` event.
```vyper
@external
def set_vault_eid(_vault_eid: uint32):
"""
@notice Set vault EID
@param _vault_eid Vault EID
"""
ownable._check_owner()
assert _vault_eid != empty(uint32), "Bad eid"
self.vault_eid = _vault_eid
log SetVaultEid(vault_eid=_vault_eid)
```
```vyper
owner: public(address)
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> L2MessengerLZ.set_vault_eid(30101)
```
::::
### `set_gas_limit`
::::description[`L2MessengerLZ.set_gas_limit(_gas_limit: uint128)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to handle ownership. This specific function is only callable by the `owner` of the contract.
:::
Updates the gas limit for LayerZero message execution on the destination chain. Only the contract owner can call this function. This allows for optimizing gas usage based on network conditions and contract requirements.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_gas_limit` | `uint128` | New gas limit for destination chain execution |
Emits: `SetGasLimit` event.
```vyper
@external
def set_gas_limit(_gas_limit: uint128):
"""
@notice Set gas limit for LZ message on destination chain
@param _gas_limit Gas limit
"""
ownable._check_owner()
self.gas_limit = _gas_limit
log SetGasLimit(gas_limit=_gas_limit)
```
```vyper
owner: public(address)
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> L2MessengerLZ.set_gas_limit(200000)
```
::::
---
## FastBridge Overview
FastBridge is a workaround solution for the 7-day delay from L2 native bridges by pre-minting crvUSD. This system enables fast cross-chain transfers of crvUSD from Layer 2 networks (Arbitrum, Optimism, Fraxtal) to Ethereum mainnet.
The traditional approach to bridging assets from Layer 2 networks to Ethereum mainnet involves a significant 7-day waiting period, which creates friction for users and limits the utility of crvUSD across different networks. FastBridge addresses this challenge by implementing a dual-bridge mechanism that provides immediate access to funds while maintaining the security and reliability of the underlying bridge infrastructure.
FastBridge implements a dual-bridge mechanism that combines:
1. **Native Bridge** (slow (7d) but reliable): Direct crvUSD transfer from L2 to Ethereum
2. **Fast Bridge** (immediate): LayerZero messaging + pre-minted crvUSD release

---
## System Components
The FastBridge system consists of several key components that work together to enable fast cross-chain transfers. These components are distributed across Layer 2 networks and Ethereum mainnet, each serving a specific role in the bridging process.
Primary coordinator on each L2 network (Arbitrum, Optimism, Fraxtal). Initiates both native and fast bridge transactions while enforcing rate limits and minimum amounts. Manages native token fees and tracks bridged amounts per 42-hour interval.
Handles LayerZero messaging from L2 to Ethereum mainnet. Encodes bridge requests and sends them through the LayerZero network to the corresponding messenger contract on Ethereum mainnet.
Holds pre-minted crvUSD on Ethereum mainnet which can be released for fast bridge operations. Manages debt ceilings and fee collection, implements emergency kill switches, and handles token recovery.
Receives LayerZero messages from L2s on Ethereum mainnet, triggers crvUSD minting in the vault, and manages the fast bridge message flow.
---
## Deployments
Live monitoring of FastBridge activity is available at the [FastBridge Monitor](https://curvefi.github.io/fast-bridge/).
### Ethereum Mainnet
| Network Route | Contract | Address |
|---------|----------|---------|
| :logos-arbitrum: Arbitrum | FastBridgeVault | [`0xadB10d2d5A95e58Ddb1A0744a0d2D7B55Db7843D`](https://etherscan.io/address/0xadB10d2d5A95e58Ddb1A0744a0d2D7B55Db7843D) |
| :logos-optimism: Optimism | FastBridgeVault | [`0x97d024859B68394122B3d0bb407dD7299cC8E937`](https://etherscan.io/address/0x97d024859B68394122B3d0bb407dD7299cC8E937) |
| :logos-fraxtal: Fraxtal | FastBridgeVault | [`0x5EF620631AA46e7d2F6f963B6bE4F6823521B9eC`](https://etherscan.io/address/0x5EF620631AA46e7d2F6f963B6bE4F6823521B9eC) |
| :logos-arbitrum: Arbitrum | VaultMessengerLZ | [`0x15945526b5c32d963391343e9bc080838fe3e6d9`](https://etherscan.io/address/0x15945526b5c32d963391343e9bc080838fe3e6d9) |
| :logos-optimism: Optimism | VaultMessengerLZ | [`0x4a10d0ff9e394f3a3dcdb297973db40ce304b44f`](https://etherscan.io/address/0x4a10d0ff9e394f3a3dcdb297973db40ce304b44f) |
| :logos-fraxtal: Fraxtal | VaultMessengerLZ | [`0xec0e1c5cc900d87b1fa44584310c43f82f75870f`](https://etherscan.io/address/0xec0e1c5cc900d87b1fa44584310c43f82f75870f) |
### Layer 2 Networks
| Network | Contract | Address |
|---------|----------|---------|
| :logos-arbitrum: Arbitrum | FastBridgeL2 | [`0x1f2af270029d028400265ce1dd0919ba8780dae1`](https://arbiscan.io/address/0x1f2af270029d028400265ce1dd0919ba8780dae1) |
| :logos-optimism: Optimism | FastBridgeL2 | [`0xd16d5ec345dd86fb63c6a9c43c517210f1027914`](https://optimistic.etherscan.io/address/0xd16d5ec345dd86fb63c6a9c43c517210f1027914) |
| :logos-fraxtal: Fraxtal | FastBridgeL2 | [`0x3fe593e651cd0b383ad36b75f4159f30bb0631a6`](https://fraxscan.com/address/0x3fe593e651cd0b383ad36b75f4159f30bb0631a6) |
| :logos-arbitrum: Arbitrum | L2MessengerLZ | [`0x14e11c1b8f04a7de306a7b5bf21bbca0d5cf79ff`](https://arbiscan.io/address/0x14e11c1b8f04a7de306a7b5bf21bbca0d5cf79ff) |
| :logos-optimism: Optimism | L2MessengerLZ | [`0x7a1f2f99b65f6c3b2413648c86c0326cff8d8837`](https://optimistic.etherscan.io/address/0x7a1f2f99b65f6c3b2413648c86c0326cff8d8837) |
| :logos-fraxtal: Fraxtal | L2MessengerLZ | [`0x672c38258729060bf443ba28faef4f2db154c6fc`](https://fraxscan.com/address/0x672c38258729060bf443ba28faef4f2db154c6fc) |
---
## How It Works
The FastBridge process involves a carefully orchestrated sequence of events that ensures both immediate access to funds and long-term security. The system operates through four main phases that work in parallel to provide users with the best possible experience.
1. User Initiates Bridge
When a user wants to bridge crvUSD from L2 to Ethereum, they call `bridge()` on the `FastBridgeL2` contract. The contract transfers crvUSD from the user to itself and initiates both bridge paths simultaneously: native and fast.
2. Native Bridge Path (Slow)
The `FastBridgeL2` calls the native bridge adapter, transferring crvUSD through the L2's native bridge. After 7 days, the crvUSD arrives at FastBridgeVault on Ethereum, where the vault holds the tokens for final settlement.
3. Fast Bridge Path (Immediate)
The `FastBridgeL2` sends a message via `L2MessengerLZ`, which travels through the LayerZero network. The `VaultMessengerLZ` receives the message on Ethereum, and the vault immediately mints crvUSD to the user, allowing them to use the tokens while the native bridge is pending.
4. Settlement
The pre-minted crvUSD is backed by the incoming native bridge transaction. When the native bridge completes, it replenishes the vault's balance mechanism.
---
## Security Model
The FastBridge system implements a comprehensive security model that addresses both technical and economic risks. The security architecture is built around multiple layers of verification, emergency controls, and risk management mechanisms that ensure the system's integrity and user fund safety.
**LayerZero Verification**
Messages are proven by 2/2 DVNs (Decentralized Verifier Networks). The LayerZero team serves as the primary verifier, while Curve core developers (SwissStake) act as the secondary verifier with maximum conservative setup.
**Emergency Controls**
The system includes a DAO Emergency Stop that can halt any mints immediately, along with kill switches that can disable specific minters or all operations. Role-based access control provides different roles for different administrative functions.
## Debt Ceilings and Limits
The FastBridge system implements multiple layers of limits to manage risk. These limits control how much crvUSD can be bridged and when.
Risk management is a critical aspect of the FastBridge system, as it involves pre-minting crvUSD tokens that are backed by pending bridge transactions. The system employs a sophisticated limit structure that balances user convenience with protocol safety, ensuring that the system can handle various market conditions while protecting against potential risks.
---
**Bridge Limits**
Each L2 network has a rate limit on how much crvUSD can be bridged within a 42-hour interval:
| Limit Type | Description | Purpose |
|------------|-------------|---------|
| **Interval Limit** | Maximum crvUSD that can be bridged per 42-hour interval | Prevents overwhelming the Ethereum claim queue |
| **Interval Tracking** | 42-hour periods (151,200 seconds) | Ensures smooth processing of bridge transactions |
| **Reset Mechanism** | Limits reset every 42 hours | Allows continuous bridging while maintaining caps |
Limits are enforced by the `FastBridgeL2` contract, where each bridge transaction reduces the available limit for the current interval. Limits are tracked using `block.timestamp // INTERVAL` where `INTERVAL = 86400 * 7 // 4` (151,200 seconds), and users can check available amounts using `allowed_to_bridge()`.
**Minimum Bridge Amounts**
To prevent uneconomical transactions, the system enforces minimum bridge amounts to prevent gas-inefficient small transactions as claiming small amounts can be expensive on Ethereum (high relative fee).
The minimum amount can be adjusted by the DAO.
---
## Emergency Controls
In case of emergencies, the system includes additional controls. The Kill Switch can stop all minting operations (KILLER_ROLE), Individual Kills can stop specific minters (KILLER_ROLE), Limit Adjustment can modify interval limits (DEFAULT_ADMIN_ROLE), and Debt Ceiling Updates can modify debt ceilings (Governance).
---
## Fee Structure
The FastBridge system implements a fee structure that balances user accessibility with operational sustainability. The vault fee is currently set to zero but is adjustable by the admin to cover operational expenses if needed.
- Native token fees on L2: Callers of `FastBridgeL2.bridge()` must provide `msg.value` covering both the native bridge fee and the LayerZero messaging fee. Any excess `msg.value` is refunded to the caller.
- Vault fee on mainnet: The vault may take a fee (with 10^18 precision) from amounts released via fast bridge. The fee is sent to `fee_receiver` and is adjustable by admin within a hard cap of 100%.
- Pre-minted release: The vault releases pre-minted crvUSD immediately upon fast message arrival; this is economically backed by the pending native bridge inflow and managed via the vault's debt-ceiling rug mechanism.
---
## VaultMessengerLZ
The VaultMessengerLZ contract serves as the Ethereum mainnet receiver for LayerZero messages in the FastBridge system. This contract receives fast bridge messages from L2 networks and triggers immediate crvUSD minting in the FastBridgeVault, enabling users to access their bridged funds instantly while the native bridge transaction is still pending.
The contract implements LayerZero's OApp (Omnichain Application) standard to provide secure and efficient cross-chain communication. It works in conjunction with the L2MessengerLZ contracts on L2 networks to complete the fast bridge message flow and enable immediate access to bridged funds.
:::vyper[`VaultMessengerLZ.vy`]
The source code for the `VaultMessengerLZ.vy` contract can be found on [GitHub](https://github.com/curvefi/fast-bridge/blob/main/contracts/messengers/VaultMessengerLZ.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.4.3` and utilizes [LayerZero OApp](https://docs.layerzero.network/v2/concepts/applications/oapp-standard) modules for cross-chain messaging.
The source code was audited by [:logos-chainsecurity: ChainSecurity](https://www.chainsecurity.com/). The full audit report can be found [here](/pdf/audits/ChainSecurity_Curve_Fast_Bridge_audit.pdf).
The contract is deployed on :logos-ethereum: Ethereum for the following L2 routes:
- :logos-arbitrum: Arbitrum: [`0x15945526b5c32d963391343e9bc080838fe3e6d9`](https://etherscan.io/address/0x15945526b5c32d963391343e9bc080838fe3e6d9)
- :logos-optimism: Optimism: [`0x4a10d0ff9e394f3a3dcdb297973db40ce304b44f`](https://etherscan.io/address/0x4a10d0ff9e394f3a3dcdb297973db40ce304b44f)
- :logos-fraxtal: Fraxtal: [`0xec0e1c5cc900d87b1fa44584310c43f82f75870f`](https://etherscan.io/address/0xec0e1c5cc900d87b1fa44584310c43f82f75870f)
```json
[{"anonymous":false,"inputs":[{"components":[{"name":"srcEid","type":"uint32"},{"name":"sender","type":"bytes32"},{"name":"nonce","type":"uint64"}],"indexed":false,"name":"origin","type":"tuple"},{"indexed":false,"name":"guid","type":"bytes32"},{"indexed":false,"name":"message","type":"bytes"}],"name":"Receive","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"vault","type":"address"}],"name":"SetVault","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previous_owner","type":"address"},{"indexed":true,"name":"new_owner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"eid","type":"uint32"},{"indexed":false,"name":"peer","type":"bytes32"}],"name":"PeerSet","type":"event"},{"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"new_owner","type":"address"}],"name":"transfer_ownership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounce_ownership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"endpoint","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint32"}],"name":"peers","outputs":[{"name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_eid","type":"uint32"},{"name":"_peer","type":"bytes32"}],"name":"setPeer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_delegate","type":"address"}],"name":"setDelegate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"name":"srcEid","type":"uint32"},{"name":"sender","type":"bytes32"},{"name":"nonce","type":"uint64"}],"name":"_origin","type":"tuple"},{"name":"_message","type":"bytes"},{"name":"_sender","type":"address"}],"name":"isComposeMsgSender","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"name":"srcEid","type":"uint32"},{"name":"sender","type":"bytes32"},{"name":"nonce","type":"uint64"}],"name":"_origin","type":"tuple"}],"name":"allowInitializePath","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_srcEid","type":"uint32"},{"name":"_sender","type":"bytes32"}],"name":"nextNonce","outputs":[{"name":"","type":"uint64"}],"stateMutability":"pure","type":"function"},{"inputs":[{"name":"_vault","type":"address"}],"name":"set_vault","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"name":"srcEid","type":"uint32"},{"name":"sender","type":"bytes32"},{"name":"nonce","type":"uint64"}],"name":"_origin","type":"tuple"},{"name":"_guid","type":"bytes32"},{"name":"_message","type":"bytes"},{"name":"_executor","type":"address"},{"name":"_extraData","type":"bytes"}],"name":"lzReceive","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"vault","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_endpoint","type":"address"}],"outputs":[],"stateMutability":"nonpayable","type":"constructor"}]
```
:::
---
## Core Functions
The VaultMessengerLZ contract provides essential functions for receiving LayerZero messages, decoding bridge requests, and triggering crvUSD minting in the vault. These functions work together to complete the fast bridge mechanism and provide immediate access to bridged funds.
### `lzReceive`
::::description[`VaultMessengerLZ.lzReceive(_origin: OApp.Origin, _guid: bytes32, _message: Bytes[OApp.MAX_MESSAGE_SIZE], _executor: address, _extraData: Bytes[OApp.MAX_EXTRA_DATA_SIZE])`]
Receives LayerZero messages originating from L2 networks and processes fast bridge requests. This function decodes the message payload and triggers crvUSD minting in the FastBridgeVault, providing immediate access to bridged funds.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_origin` | `OApp.Origin` | Origin information containing srcEid, sender, and nonce |
| `_guid` | `bytes32` | Global unique identifier for the message |
| `_message` | `Bytes[OApp.MAX_MESSAGE_SIZE]` | The encoded message payload containing to and amount |
| `_executor` | `address` | Address of the executor for the message |
| `_extraData` | `Bytes[OApp.MAX_EXTRA_DATA_SIZE]` | Additional data passed by the executor |
Emits: `Receive` event.
```vyper
interface IVault:
def mint(_receiver: address, _amount: uint256) -> uint256: nonpayable
event Receive:
origin: OApp.Origin
guid: bytes32
message: Bytes[OApp.MAX_MESSAGE_SIZE]
vault: public(IVault)
@payable
@external
def lzReceive(
_origin: OApp.Origin,
_guid: bytes32,
_message: Bytes[OApp.MAX_MESSAGE_SIZE],
_executor: address,
_extraData: Bytes[OApp.MAX_EXTRA_DATA_SIZE],
):
"""
@notice Receive message from main chain
@param _origin Origin information containing srcEid, sender, and nonce
@param _guid Global unique identifier for the message
@param _message The encoded message payload containing to and amount
@param _executor Address of the executor for the message
@param _extraData Additional data passed by the executor
"""
# Verify message source
OApp._lzReceive(_origin, _guid, _message, _executor, _extraData)
# Decode message
to: address = empty(address)
amount: uint256 = empty(uint256)
to, amount = abi_decode(_message, (address, uint256))
# Pass mint command to vault
extcall self.vault.mint(to, amount)
log Receive(origin=_origin, guid=_guid, message=_message)
```
```shell
>>> VaultMessengerLZ.lzReceive(origin, guid, message, executor, extra_data)
```
::::
---
## Variables
The VaultMessengerLZ contract maintains important state variables that control its operation and store contract addresses. These variables work together to ensure proper functioning of the cross-chain messaging system while maintaining security and efficiency.
### `vault`
::::description[`VaultMessengerLZ.vault() -> IVault: view`]
The address of the FastBridgeVault contract that receives mint commands. This contract holds pre-minted crvUSD tokens and can immediately release them to users upon receiving fast bridge messages.
Returns: FastBridgeVault contract address (`IVault`).
```vyper
vault: public(IVault)
```
This example returns the vault address on the Arbitrum route messenger. The value is fetched live from the blockchain.
::::
---
## Owner Functions
The VaultMessengerLZ contract includes administrative functions that allow the contract owner to manage system parameters and update contract addresses. These functions are protected by ownership checks to ensure only authorized personnel can make critical changes to the system.
### `set_vault`
::::description[`VaultMessengerLZ.set_vault(_vault: IVault)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to handle ownership. This specific function is only callable by the `owner` of the contract.
:::
Updates the address of the FastBridgeVault contract that receives mint commands. Only the contract owner can call this function. This allows for updating the vault address if the vault contract is upgraded or replaced.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `_vault` | `IVault` | New FastBridgeVault contract address |
Emits: `SetVault` event.
```vyper
@external
def set_vault(_vault: IVault):
"""
@notice Set vault address
@param _vault new vault address
"""
ownable._check_owner()
assert _vault != empty(IVault), "Bad vault"
self.vault = _vault
log SetVault(vault=_vault)
```
```vyper
owner: public(address)
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> VaultMessengerLZ.set_vault('0xadB10d2d5A95e58Ddb1A0744a0d2D7B55Db7843D')
```
::::
---
## CowSwapBurner
The `CowSwapBurner` is an essential component of the fee burning architecture, designed to facilitate the efficient and automated exchange of admin fees using [conditional orders](https://docs.cow.fi/cow-protocol/concepts/order-types/programmatic-orders) of the CoWSwap protocol.
:::vyper[`CowSwapBurner.vy`]
The source code for the `CowSwapBurner.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-burners/blob/main/contracts/burners/CowSwapBurner.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
The `CowSwapBurner` is only deployed on Ethereum and Gnosis so far, as CowSwap is only deployed on these chains.[^1]
- :logos-ethereum: Ethereum at [`0xC0fC3dDfec95ca45A0D2393F518D3EA1ccF44f8b`](https://etherscan.io/address/0xC0fC3dDfec95ca45A0D2393F518D3EA1ccF44f8b)
- :logos-gnosis: Gnosis at [`0x566b9F24200A9B51b76792D4e81B569AF27eda83`](https://gnosisscan.io/address/0x566b9F24200A9B51b76792D4e81B569AF27eda83)
[^1]: CowSwap recently deployed on Arbitrum. In the future, a new burner contract will be deployed on Arbitrum as well.
```json
[{"stateMutability":"nonpayable","type":"constructor","inputs":[{"name":"_fee_collector","type":"address"},{"name":"_composable_cow","type":"address"},{"name":"_vault_relayer","type":"address"},{"name":"_target_threshold","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"burn","inputs":[{"name":"_coins","type":"address[]"},{"name":"_receiver","type":"address"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"get_current_order","inputs":[],"outputs":[{"name":"","type":"tuple","components":[{"name":"sellToken","type":"address"},{"name":"buyToken","type":"address"},{"name":"receiver","type":"address"},{"name":"sellAmount","type":"uint256"},{"name":"buyAmount","type":"uint256"},{"name":"validTo","type":"uint32"},{"name":"appData","type":"bytes32"},{"name":"feeAmount","type":"uint256"},{"name":"kind","type":"bytes32"},{"name":"partiallyFillable","type":"bool"},{"name":"sellTokenBalance","type":"bytes32"},{"name":"buyTokenBalance","type":"bytes32"}]}]},{"stateMutability":"view","type":"function","name":"get_current_order","inputs":[{"name":"sell_token","type":"address"}],"outputs":[{"name":"","type":"tuple","components":[{"name":"sellToken","type":"address"},{"name":"buyToken","type":"address"},{"name":"receiver","type":"address"},{"name":"sellAmount","type":"uint256"},{"name":"buyAmount","type":"uint256"},{"name":"validTo","type":"uint32"},{"name":"appData","type":"bytes32"},{"name":"feeAmount","type":"uint256"},{"name":"kind","type":"bytes32"},{"name":"partiallyFillable","type":"bool"},{"name":"sellTokenBalance","type":"bytes32"},{"name":"buyTokenBalance","type":"bytes32"}]}]},{"stateMutability":"view","type":"function","name":"getTradeableOrder","inputs":[{"name":"_owner","type":"address"},{"name":"_sender","type":"address"},{"name":"_ctx","type":"bytes32"},{"name":"_static_input","type":"bytes"},{"name":"_offchain_input","type":"bytes"}],"outputs":[{"name":"","type":"tuple","components":[{"name":"sellToken","type":"address"},{"name":"buyToken","type":"address"},{"name":"receiver","type":"address"},{"name":"sellAmount","type":"uint256"},{"name":"buyAmount","type":"uint256"},{"name":"validTo","type":"uint32"},{"name":"appData","type":"bytes32"},{"name":"feeAmount","type":"uint256"},{"name":"kind","type":"bytes32"},{"name":"partiallyFillable","type":"bool"},{"name":"sellTokenBalance","type":"bytes32"},{"name":"buyTokenBalance","type":"bytes32"}]}]},{"stateMutability":"view","type":"function","name":"verify","inputs":[{"name":"_owner","type":"address"},{"name":"_sender","type":"address"},{"name":"_hash","type":"bytes32"},{"name":"_domain_separator","type":"bytes32"},{"name":"_ctx","type":"bytes32"},{"name":"_static_input","type":"bytes"},{"name":"_offchain_input","type":"bytes"},{"name":"_order","type":"tuple","components":[{"name":"sellToken","type":"address"},{"name":"buyToken","type":"address"},{"name":"receiver","type":"address"},{"name":"sellAmount","type":"uint256"},{"name":"buyAmount","type":"uint256"},{"name":"validTo","type":"uint32"},{"name":"appData","type":"bytes32"},{"name":"feeAmount","type":"uint256"},{"name":"kind","type":"bytes32"},{"name":"partiallyFillable","type":"bool"},{"name":"sellTokenBalance","type":"bytes32"},{"name":"buyTokenBalance","type":"bytes32"}]}],"outputs":[]},{"stateMutability":"view","type":"function","name":"isValidSignature","inputs":[{"name":"_hash","type":"bytes32"},{"name":"signature","type":"bytes"}],"outputs":[{"name":"","type":"bytes4"}]},{"stateMutability":"nonpayable","type":"function","name":"push_target","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"pure","type":"function","name":"supportsInterface","inputs":[{"name":"_interface_id","type":"bytes4"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"set_target_threshold","inputs":[{"name":"_target_threshold","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"recover","inputs":[{"name":"_coins","type":"address[]"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"fee_collector","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"vault_relayer","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"composable_cow","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"ADD_DATA","inputs":[],"outputs":[{"name":"","type":"bytes32"}]},{"stateMutability":"view","type":"function","name":"VERSION","inputs":[],"outputs":[{"name":"","type":"string"}]},{"stateMutability":"view","type":"function","name":"created","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"target_threshold","inputs":[],"outputs":[{"name":"","type":"uint256"}]}]
```
:::
This system simplifies fee burning by requiring only a single burner contract. A simple function call can create an order that sells an accrued fee token into the target token.
The old system used various kinds of burners with hardcoded routes, which often did not result in the most efficient fee burning mechanism, thereby "losing" fees that could be distributed among veCRV holders.
*To learn more about the CoW-Protocol, make sure to check out their [official documentation](https://docs.cow.fi/).*
---
## Conditional Orders
Conditional CowSwap orders are automatically created for each token to be burned using the `burn` function. This function is not directly externally callable by users through this contract; instead, it is called when the `collect` function within the `FeeCollector` contract is invoked. Additionally, there is a caller fee to incentivize this contract call.
```vyper
struct ConditionalOrderParams:
# The contract implementing the conditional order logic
handler: address # self
# Allows for multiple conditional orders of the same type and data
salt: bytes32 # Not used for now
# Data available to ALL discrete orders created by the conditional order
staticData: Bytes[STATIC_DATA_LEN] # Using coin address
composable_cow.create(ConditionalOrderParams({
handler: self,
salt: empty(bytes32),
staticData: concat(b"", convert(coin.address, bytes20)),
}), True)
```
### `created`
::::description[`CowSwapBurner.created(arg0: address) -> bool: view`]
Getter method to check if a conditional order for coin `arg0` has been created. If there is not an existing order, a new order will be created when the `burn` function is called.[^2]
[^2]: The `burn` function can only be called indirectly by the `fee_receiver` via the `collect` function.
| Input | Type | Description |
| ------ | --------- | ------------------------ |
| `arg0` | `address` | Address of coin to check |
Returns: true or false (`bool`).
```vyper
created: public(HashMap[ERC20, bool])
@external
def burn(_coins: DynArray[ERC20, MAX_COINS_LEN], _receiver: address):
"""
@notice Post hook after collect to register coins for burn
@dev Registers new orders in ComposableCow
@param _coins Which coins to burn
@param _receiver Receiver of profit
"""
assert msg.sender == fee_collector.address, "Only FeeCollector"
fee: uint256 = fee_collector.fee(Epoch.COLLECT)
fee_payouts: DynArray[Transfer, MAX_COINS_LEN] = []
self_transfers: DynArray[Transfer, MAX_COINS_LEN] = []
for coin in _coins:
if not self.created[coin]:
composable_cow.create(ConditionalOrderParams({
handler: self,
salt: empty(bytes32),
staticData: concat(b"", convert(coin.address, bytes20)),
}), True)
coin.approve(vault_relayer, max_value(uint256))
self.created[coin] = True
amount: uint256 = coin.balanceOf(fee_collector.address) * fee / ONE
fee_payouts.append(Transfer({coin: coin, to: _receiver, amount: amount}))
self_transfers.append(Transfer({coin: coin, to: self, amount: max_value(uint256)}))
fee_collector.transfer(fee_payouts)
fee_collector.transfer(self_transfers)
```
::::
### `get_current_order`
::::description[`CowSwapBurner.get_current_order(sell_token: address=empty(address)) -> GPv2Order_Data: view`]
Getter for the current order parameters of a token.
| Input | Type | Description |
| ------------ | --------- | ------------------------------------- |
| `sell_token` | `address` | Token address to check parameters for |
Returns: GPv2Order_Data consisting of:
- sellToken: `ERC20`
- buyToken: `ERC20`
- receiver: `address`
- sellAmount: `uint256`
- buyAmount: `uint256`
- validTo: `uint32`
- appData: `bytes32`
- feeAmount: `uint256`
- kind: `bytes32`
- partiallyFillable: `bool`
- sellTokenBalance: `bytes32`
- buyTokenBalance: `bytes32`
```vyper
@view
@external
def get_current_order(sell_token: address=empty(address)) -> GPv2Order_Data:
"""
@notice Get current order parameters
@param sell_token Address of possible sell token
@return Order parameters
"""
return self._get_order(ERC20(sell_token))
@view
@internal
def _get_order(sell_token: ERC20) -> GPv2Order_Data:
buy_token: ERC20 = fee_collector.target()
return GPv2Order_Data({
sellToken: sell_token, # token to sell
buyToken: buy_token, # token to buy
receiver: fee_collector.address, # receiver of the token to buy
sellAmount: 0, # Set later
buyAmount: self.target_threshold,
validTo: convert(fee_collector.epoch_time_frame(Epoch.EXCHANGE)[1], uint32), # timestamp until order is valid
appData: ADD_DATA, # extra info about the order
feeAmount: 0, # amount of fees in sellToken
kind: SELL_KIND, # buy or sell
partiallyFillable: True, # partially fillable (True) or fill-or-kill (False)
sellTokenBalance: TOKEN_BALANCE, # From where the sellToken balance is withdrawn
buyTokenBalance: TOKEN_BALANCE, # Where the buyToken is deposited
})
```
```shell
>>> CowSwapBurner.get_current_order('0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83')
0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83, 0xaBEf652195F98A91E490f047A5006B71c85f058d, 0xBb7404F9965487a9DdE721B3A5F0F3CcfA9aa4C5, 0, 1000000000000000000, 1718755200, 0x058315b749613051abcbf50cf2d605b4fa4a41554ec35d73fd058fc530da559f, 0,0xf3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775, true, 0x5a28e9363bb942b639270062aa6bb295f434bcdfc42c97267bf003f272060dc9, 0x5a28e9363bb942b639270062aa6bb295f434bcdfc42c97267bf003f272060dc9
```
::::
### `burn`
::::description[`CowSwapBurner.burn(_coins: DynArray[ERC20, MAX_COINS_LEN], _receiver: address)`]
:::guard[Guarded Method]
This function is only callable by the `FeeCollector` contract via the `collect` function.
:::
Function to create a conditional CowSwap order for coins.
| Input | Type | Description |
| ----------- | -------------------------------- | -------------------------------------------------------------------------------------- |
| `_coins` | `DynArray[ERC20, MAX_COINS_LEN]` | Coins to burn |
| `_receiver` | `address` | Receiver of the keeper fee specified in when calling `collect` within the FeeCollector |
```vyper
interface FeeCollector:
def fee(_epoch: Epoch=empty(Epoch), _ts: uint256=block.timestamp) -> uint256: view
def target() -> ERC20: view
def owner() -> address: view
def emergency_owner() -> address: view
def epoch_time_frame(epoch: Epoch, ts: uint256=block.timestamp) -> (uint256, uint256): view
def can_exchange(_coins: DynArray[ERC20, MAX_COINS_LEN]) -> bool: view
def transfer(_transfers: DynArray[Transfer, MAX_COINS_LEN]): nonpayable
struct ConditionalOrderParams:
# The contract implementing the conditional order logic
handler: address # self
# Allows for multiple conditional orders of the same type and data
salt: bytes32 # Not used for now
# Data available to ALL discrete orders created by the conditional order
staticData: Bytes[STATIC_DATA_LEN] # Using coin address
interface ComposableCow:
def create(params: ConditionalOrderParams, dispatch: bool): nonpayable
def domainSeparator() -> bytes32: view
def isValidSafeSignature(
safe: address, sender: address, _hash: bytes32, _domainSeparator: bytes32, typeHash: bytes32,
encodeData: Bytes[15 * 32],
payload: Bytes[(32 + 3 + 1 + 8) * 32],
) -> bytes4: view
@external
def burn(_coins: DynArray[ERC20, MAX_COINS_LEN], _receiver: address):
"""
@notice Post hook after collect to register coins for burn
@dev Registers new orders in ComposableCow
@param _coins Which coins to burn
@param _receiver Receiver of profit
"""
assert msg.sender == fee_collector.address, "Only FeeCollector"
fee: uint256 = fee_collector.fee(Epoch.COLLECT)
fee_payouts: DynArray[Transfer, MAX_COINS_LEN] = []
self_transfers: DynArray[Transfer, MAX_COINS_LEN] = []
for coin in _coins:
if not self.created[coin]:
composable_cow.create(ConditionalOrderParams({
handler: self,
salt: empty(bytes32),
staticData: concat(b"", convert(coin.address, bytes20)),
}), True)
coin.approve(vault_relayer, max_value(uint256))
self.created[coin] = True
amount: uint256 = coin.balanceOf(fee_collector.address) * fee / ONE
fee_payouts.append(Transfer({coin: coin, to: _receiver, amount: amount}))
self_transfers.append(Transfer({coin: coin, to: self, amount: max_value(uint256)}))
fee_collector.transfer(fee_payouts)
fee_collector.transfer(self_transfers)
```
::::
### `getTradableOrder`
::::description[`CowSwapBurner.getTradeableOrder(_owner: address, _sender: address, _ctx: bytes32, _static_input: Bytes[STATIC_DATA_LEN], _offchain_input: Bytes[OFFCHAIN_DATA_LEN]) -> GPv2Order_Data: view`]
Function to generate an order for the WatchTower.
| Input | Type | Description |
| ----------------- | -------------------------- | ----------------------------------------------- |
| `_owner` | `address` | Owner of the order |
| `_sender` | `address` | `msg.sender` context calling `isValidSignature` |
| `_ctx` | `bytes32` | Execution context |
| `_static_input` | `Bytes[STATIC_DATA_LEN]` | `sellToken` encoded as `bytes(Bytes[20])` |
| `_offchain_input` | `Bytes[OFFCHAIN_DATA_LEN]` | Not used, zero-length bytes |
Returns: order parameters (`GPv2Order_Data`).
```vyper
struct GPv2Order_Data:
sellToken: ERC20 # token to sell
buyToken: ERC20 # token to buy
receiver: address # receiver of the token to buy
sellAmount: uint256
buyAmount: uint256
validTo: uint32 # timestamp until order is valid
appData: bytes32 # extra info about the order
feeAmount: uint256 # amount of fees in sellToken
kind: bytes32 # buy or sell
partiallyFillable: bool # partially fillable (True) or fill-or-kill (False)
sellTokenBalance: bytes32 # From where the sellToken balance is withdrawn
buyTokenBalance: bytes32 # Where the buyToken is deposited
STATIC_DATA_LEN: constant(uint256) = 20
OFFCHAIN_DATA_LEN: constant(uint256) = 1
@view
@external
def getTradeableOrder(_owner: address, _sender: address, _ctx: bytes32, _static_input: Bytes[STATIC_DATA_LEN], _offchain_input: Bytes[OFFCHAIN_DATA_LEN]) -> GPv2Order_Data:
"""
@notice Generate order for WatchTower
@dev _owner, _sender, _ctx, _offchain_input are ignored
@param _owner Owner of order (self)
@param _sender `msg.sender` context calling `isValidSignature`
@param _ctx Execution context
@param _static_input sellToken encoded as bytes(Bytes[20])
@param _offchain_input Not used, zero-length bytes
@return Order parameters
"""
sell_token: ERC20 = ERC20(convert(convert(_static_input, bytes20), address))
order: GPv2Order_Data = self._get_order(sell_token)
order.sellAmount = sell_token.balanceOf(self)
if order.sellAmount == 0 or not fee_collector.can_exchange([sell_token]):
start: uint256 = 0
end: uint256 = 0
start, end = fee_collector.epoch_time_frame(Epoch.EXCHANGE)
if block.timestamp >= start:
start, end = fee_collector.epoch_time_frame(Epoch.EXCHANGE, block.timestamp + 7 * 24 * 3600)
reason: String[11] = "ZeroBalance"
if order.sellAmount != 0: # FeeCollector reject
reason = "NotAllowed"
raw_revert(_abi_encode(start, reason, method_id=method_id("PollTryAtEpoch(uint256,string)")))
return order
@view
@internal
def _get_order(sell_token: ERC20) -> GPv2Order_Data:
buy_token: ERC20 = fee_collector.target()
return GPv2Order_Data({
sellToken: sell_token, # token to sell
buyToken: buy_token, # token to buy
receiver: fee_collector.address, # receiver of the token to buy
sellAmount: 0, # Set later
buyAmount: self.target_threshold,
validTo: convert(fee_collector.epoch_time_frame(Epoch.EXCHANGE)[1], uint32), # timestamp until order is valid
appData: ADD_DATA, # extra info about the order
feeAmount: 0, # amount of fees in sellToken
kind: SELL_KIND, # buy or sell
partiallyFillable: True, # partially fillable (True) or fill-or-kill (False)
sellTokenBalance: TOKEN_BALANCE, # From where the sellToken balance is withdrawn
buyTokenBalance: TOKEN_BALANCE, # Where the buyToken is deposited
})
```
::::
### `verify`
::::description[`CowSwapBurner.verify(_owner: address, _sender: address, _hash: bytes32, _domain_separator: bytes32, _ctx: bytes32, _static_input: Bytes[STATIC_DATA_LEN], _offchain_input: Bytes[OFFCHAIN_DATA_LEN], _order: GPv2Order_Data): view`]
Function to verify CowSwap orders to ensure that the order adheres to the conditions set by the contract and can be executed properly.
| Input | Type | Description |
| ------------------- | -------------------------- | ------------------------------------------------------ |
| `_owner` | `address` | Owner of conditional order (self) |
| `_sender` | `address` | `msg.sender` context calling `isValidSignature` |
| `_hash` | `bytes32` | `EIP-712` order digest |
| `_domain_separator` | `bytes32` | `EIP-712` domain separator |
| `_ctx` | `bytes32` | Execution context |
| `_static_input` | `Bytes[STATIC_DATA_LEN]` | ConditionalOrder's staticData (coin address) |
| `_offchain_input` | `Bytes[OFFCHAIN_DATA_LEN]` | Conditional order type-specific data NOT known at time of creation for a specific discrete order (or zero-length bytes if not applicable). |
| `_order` | `GPv2Order_Data` | The proposed discrete order's `GPv2Order.Data` struct |
```vyper
struct GPv2Order_Data:
sellToken: ERC20 # token to sell
buyToken: ERC20 # token to buy
receiver: address # receiver of the token to buy
sellAmount: uint256
buyAmount: uint256
validTo: uint32 # timestamp until order is valid
appData: bytes32 # extra info about the order
feeAmount: uint256 # amount of fees in sellToken
kind: bytes32 # buy or sell
partiallyFillable: bool # partially fillable (True) or fill-or-kill (False)
sellTokenBalance: bytes32 # From where the sellToken balance is withdrawn
buyTokenBalance: bytes32 # Where the buyToken is deposited
@view
@external
def verify(
_owner: address,
_sender: address,
_hash: bytes32,
_domain_separator: bytes32,
_ctx: bytes32,
_static_input: Bytes[STATIC_DATA_LEN],
_offchain_input: Bytes[OFFCHAIN_DATA_LEN],
_order: GPv2Order_Data,
):
"""
@notice Verify order
@dev Called from ComposableCow. _owner, _sender, _hash, _domain_separator, _ctx are ignored.
@param _owner Owner of conditional order (self)
@param _sender `msg.sender` context calling `isValidSignature`
@param _hash `EIP-712` order digest
@param _domain_separator `EIP-712` domain separator
@param _ctx Execution context
@param _static_input ConditionalOrder's staticData (coin address)
@param _offchain_input Conditional order type-specific data NOT known at time of creation for a specific discrete order (or zero-length bytes if not applicable)
@param _order The proposed discrete order's `GPv2Order.Data` struct
"""
sell_token: ERC20 = ERC20(convert(convert(_static_input, bytes20), address))
if not fee_collector.can_exchange([sell_token]):
raw_revert(_abi_encode("NotAllowed", method_id=method_id("OrderNotValid(string)")))
if _offchain_input != b"":
raw_revert(_abi_encode("NonZeroOffchainInput", method_id=method_id("OrderNotValid(string)")))
order: GPv2Order_Data = self._get_order(sell_token)
order.sellAmount = _order.sellAmount # Any amount allowed
order.buyAmount = max(_order.buyAmount, order.buyAmount) # Price is discovered within CowSwap competition
if _abi_encode(order) != _abi_encode(_order):
raw_revert(_abi_encode("BadOrder", method_id=method_id("OrderNotValid(string)")))
```
::::
### `isValidSignature`
::::description[`CowSwapBurner.isValidSignature(_hash: bytes32, signature: Bytes[1792]) -> bytes4: view`]
Function to verify a ERC-1271 signature for a given hash.
| Input | Type | Description |
| ----------- | ------------- | ---------------------------------------------------------------------- |
| `_hash` | `bytes32` | Hash of a signed data |
| `signature` | `Bytes[1792]` | Signature for the object. (GPv2Order.Data, PayloadStruct) in this case |
Returns: `ERC1271_MAGIC_VALUE` if signature is OK (`bytes4`).
```vyper
ERC1271_MAGIC_VALUE: constant(bytes4) = 0x1626ba7e
@view
@external
def isValidSignature(_hash: bytes32, signature: Bytes[1792]) -> bytes4:
"""
@notice ERC1271 signature verifier method
@dev Forwards query to ComposableCow
@param _hash Hash of signed object. Ignored here
@param signature Signature for the object. (GPv2Order.Data, PayloadStruct) here
@return `ERC1271_MAGIC_VALUE` if signature is OK
"""
order: GPv2Order_Data = empty(GPv2Order_Data)
payload: PayloadStruct = empty(PayloadStruct)
order, payload = _abi_decode(signature, (GPv2Order_Data, PayloadStruct))
return composable_cow.isValidSafeSignature(self, msg.sender, _hash, composable_cow.domainSeparator(), empty(bytes32),
_abi_encode(order),
_abi_encode(payload),
)
```
::::
### `target_threshold`
::::description[`CowSwapBurner.target_threshold() -> uint256: view`]
Getter for the minimum amount of target token to be bought in an order. This value ensure that each executed order meets a certain minimum value. This variable can be changed by the `owner` of the `FeeCollector` using the [`set_target_threshold`](#set_target_threshold) function. Due to the gas efficiency of L2's, the value can be set much lower e.g. on Gnosis than on Ethereum.[^3]
[^3]: The minimum target threshold value on Gnosis is `1 (1e18)`, on Ethereum `50 (50 * 1e18)`.
Returns: target threshold (`uint256`).
```vyper
target_threshold: public(uint256) # min amount to exchange
@external
def __init__(_fee_collector: FeeCollector,
_composable_cow: ComposableCow, _vault_relayer: address, _target_threshold: uint256):
"""
@notice Contract constructor
@param _fee_collector FeeCollector to anchor to
@param _composable_cow Address of ComposableCow contract
@param _vault_relayer CowSwap's VaultRelayer contract address, all approves go there
@param _target_threshold Minimum amount of target to buy per order
"""
...
assert _target_threshold > 0, "Bad target threshold"
self.target_threshold = _target_threshold
```
::::
### `set_target_threshold`
::::description[`CowSwapBurner.set_target_threshold(_target_threshold: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the `FeeCollector` contract.
:::
Function to set a new `target_threshold` value.
| Input | Type | Description |
| ------------------- | --------- | -------------------------- |
| `_target_threshold` | `uint256` | New target threshold value |
```vyper
target_threshold: public(uint256) # min amount to exchange
@external
def set_target_threshold(_target_threshold: uint256):
"""
@dev Callable only by owner
@param _target_threshold Minimum amount of target to receive, with base=10**18
"""
assert msg.sender == fee_collector.owner(), "Only owner"
assert _target_threshold > 0, "Bad target threshold"
self.target_threshold = _target_threshold
```
::::
---
## Pushing and Recovering Coins
The `push_target` function is used to transfer any leftover target coins from the burner to the `FeeCollector`.
Additionally, there is a recover function which lets the `owner` or `emergency_owner` of the `FeeCollector` to recover ERC20 or ETH.
### `push_target`
::::description[`CowSwapBurner.push_target() -> uint256`]
Function to push the entire balance of the target coin to the `FeeCollector`. This function can be called externally, but is also called directly by the `FeeCollector` before the target coins are forwarded to the hooker contract using the `forward` function.
Returns: amount of target coins pushed (`uint256`).
```vyper
@external
def push_target() -> uint256:
"""
@notice In case target coin is left in contract can be pushed to forward
@return Amount of coin pushed further
"""
target: ERC20 = fee_collector.target()
amount: uint256 = target.balanceOf(self)
if amount > 0:
target.transfer(fee_collector.address, amount)
return amount
```
::::
### `recover`
::::description[`CowSwapBurner.recover(_coins: DynArray[ERC20, MAX_COINS_LEN])`]
:::guard[Guarded Method]
This function is only callable by the `owner` or `emergency_owner` of the `FeeCollector.vy` contract.
:::
Function to recover ERC20 tokens or ETH from this contract. Calling this function will transfer `_coins` to the `FeeCollector`.
| Input | Type | Description |
| -------- | -------------------------------- | ----------------------------------------------- |
| `_coins` | `DynArray[ERC20, MAX_COINS_LEN]` | Dynamic array of the token addresses to recover |
```vyper
@external
def recover(_coins: DynArray[ERC20, MAX_COINS_LEN]):
"""
@notice Recover ERC20 tokens or Ether from this contract
@dev Callable only by owner and emergency owner
@param _coins Token addresses
"""
assert msg.sender in [fee_collector.owner(), fee_collector.emergency_owner()], "Only owner"
for coin in _coins:
if coin.address == ETH_ADDRESS:
raw_call(fee_collector.address, b"", value=self.balance)
else:
coin.transfer(fee_collector.address, coin.balanceOf(self)) # do not need safe transfer
```
::::
---
## Valid Interface a la ERC-165
In order for the burner contract to be fully compatible with the `FeeCollector`, a specific interface needs to hold up as per [ERC-165](https://eips.ethereum.org/EIPS/eip-165):
```vyper
SUPPORTED_INTERFACES: constant(bytes4[4]) = [
# ERC165: method_id("supportsInterface(bytes4)") == 0x01ffc9a7
0x01ffc9a7,
# Burner:
# method_id("burn(address[],address)") == 0x72a436a8
# method_id("push_target()") == 0x2eb078cd
# method_id("VERSION()") == 0xffa1ad74
0xa3b5e311,
# Interface corresponding to IConditionalOrderGenerator:
# method_id("getTradeableOrder(address,address,bytes32,bytes,bytes)") == 0xb8296fc4
0xb8296fc4,
# ERC1271 interface:
# method_id("isValidSignature(bytes32,bytes)") == 0x1626ba7e
ERC1271_MAGIC_VALUE,
]
```
### `supportsInterface`
::::description[`CowSwapBurner.supportsInterface(_interface_id: bytes4) -> bool: pure`]
Function to check if the burner supports the correct interface, as specified by the [ERC-165](https://eips.ethereum.org/EIPS/eip-165) standard. This method makes sure the contract is compatible with the `FeeCollector` contract.
| Input | Type | Description |
| --------------- | -------- | ------------------- |
| `_interface_id` | `bytes4` | ID of the interface |
Returns: true or false (`bool`).
```vyper
SIGNATURE_VERIFIER_MUXER_INTERFACE: constant(bytes4) = 0x62af8dc2
ERC1271_MAGIC_VALUE: constant(bytes4) = 0x1626ba7e
SUPPORTED_INTERFACES: constant(bytes4[4]) = [
# ERC165: method_id("supportsInterface(bytes4)") == 0x01ffc9a7
0x01ffc9a7,
# Burner:
# method_id("burn(address[],address)") == 0x72a436a8
# method_id("push_target()") == 0x2eb078cd
# method_id("VERSION()") == 0xffa1ad74
0xa3b5e311,
# Interface corresponding to IConditionalOrderGenerator:
# method_id("getTradeableOrder(address,address,bytes32,bytes,bytes)") == 0xb8296fc4
0xb8296fc4,
# ERC1271 interface:
# method_id("isValidSignature(bytes32,bytes)") == 0x1626ba7e
ERC1271_MAGIC_VALUE,
]
@pure
@external
def supportsInterface(_interface_id: bytes4) -> bool:
"""
@dev Interface identification is specified in ERC-165.
Fails on SignatureVerifierMuxer for compatability with ComposableCow.
@param _interface_id Id of the interface
"""
assert _interface_id != SIGNATURE_VERIFIER_MUXER_INTERFACE
return _interface_id in SUPPORTED_INTERFACES
```
::::
---
## Contract Info Methods
### `fee_collector`
::::description[`CowSwapBurner.fee_collector() -> address: view`]
Getter for the Fee Collector address to anchor to.
Returns: fee collector (`address`).
```vyper
interface FeeCollector:
def fee(_epoch: Epoch=empty(Epoch), _ts: uint256=block.timestamp) -> uint256: view
def target() -> ERC20: view
def owner() -> address: view
def emergency_owner() -> address: view
def epoch_time_frame(epoch: Epoch, ts: uint256=block.timestamp) -> (uint256, uint256): view
def can_exchange(_coins: DynArray[ERC20, MAX_COINS_LEN]) -> bool: view
def transfer(_transfers: DynArray[Transfer, MAX_COINS_LEN]): nonpayable
fee_collector: public(immutable(FeeCollector))
@external
def __init__(_fee_collector: FeeCollector,
_composable_cow: ComposableCow, _vault_relayer: address, _target_threshold: uint256):
"""
@notice Contract constructor
@param _fee_collector FeeCollector to anchor to
@param _composable_cow Address of ComposableCow contract
@param _vault_relayer CowSwap's VaultRelayer contract address, all approves go there
@param _target_threshold Minimum amount of target to buy per order
"""
fee_collector = _fee_collector
...
```
::::
### `composable_cow`
::::description[`CowSwapBurner.composable_cow() -> address: view`]
Getter for the ComposableCow contract. ComposableCow is a framework for smoothing developer experience when building conditional orders on the CoW Protocol. For the official documentation, see [here](https://docs.cow.fi/cow-protocol/reference/contracts/periphery/composable-cow).
Returns: ComposableCow contract (`address`).
```vyper
interface ComposableCow:
def create(params: ConditionalOrderParams, dispatch: bool): nonpayable
def domainSeparator() -> bytes32: view
def isValidSafeSignature(
safe: address, sender: address, _hash: bytes32, _domainSeparator: bytes32, typeHash: bytes32,
encodeData: Bytes[15 * 32],
payload: Bytes[(32 + 3 + 1 + 8) * 32],
) -> bytes4: view
composable_cow: public(immutable(ComposableCow))
@external
def __init__(_fee_collector: FeeCollector,
_composable_cow: ComposableCow, _vault_relayer: address, _target_threshold: uint256):
"""
@notice Contract constructor
@param _fee_collector FeeCollector to anchor to
@param _composable_cow Address of ComposableCow contract
@param _vault_relayer CowSwap's VaultRelayer contract address, all approves go there
@param _target_threshold Minimum amount of target to buy per order
"""
...
composable_cow = _composable_cow
...
```
::::
### `vault_relayer`
::::description[`CowSwapBurner.vault_relayer() -> address: view`]
Getter for CoW Protocols Vault Relayer contract. This is the contract where all approvals go to. For the official documentation, see [here](https://docs.cow.fi/cow-protocol/reference/contracts/core/vault-relayer).
Returns: Vault Relayer (`address`).
```vyper
vault_relayer: public(immutable(address))
@external
def __init__(_fee_collector: FeeCollector,
_composable_cow: ComposableCow, _vault_relayer: address, _target_threshold: uint256):
"""
@notice Contract constructor
@param _fee_collector FeeCollector to anchor to
@param _composable_cow Address of ComposableCow contract
@param _vault_relayer CowSwap's VaultRelayer contract address, all approves go there
@param _target_threshold Minimum amount of target to buy per order
"""
...
vault_relayer = _vault_relayer
...
```
::::
### `ADD_DATA`
::::description[`CowSwapBurner.ADD_DATA() -> bytes32: view`]
Getter for the additional data applied in the internal `_get_order` function. The data is shown as metadata on the [CowSwap explorer](https://explorer.cow.fi/) and allows distinguishing Curve orders (e.g., see this [transaction](https://explorer.cow.fi/gc/orders/0x2bd5604e60cda24f80da9a0a5b2e69598620e383c1a074b234a86489fe1856b1566b9f24200a9b51b76792d4e81b569af27eda8366721f80?tab=overview)).
Returns: additional data (`bytes32`).
```vyper
ADD_DATA: public(constant(bytes32)) = 0x058315b749613051abcbf50cf2d605b4fa4a41554ec35d73fd058fc530da559f
```
::::
### `VERSION`
::::description[`CowSwapBurner.VERSION() -> String[20]: view`]
Getter for the burner version.
Returns: version (`String[20]`).
```vyper
VERSION: public(constant(String[20])) = "CowSwap"
```
::::
---
## FeeAllocator
The `FeeAllocator` is a contract that allocates protocol fees between different receivers based on configurable weights. It sits between the [`Hooker`](hooker.md) and the [`FeeDistributor`](fee-distributor.md), splitting the accumulated crvUSD fees among a set of receivers before forwarding the remainder to the `FeeDistributor` for distribution to veCRV holders.
Receivers can be assigned weights in basis points (bps), with a maximum total weight of 5,000 bps (50%). The remaining portion (at least 50%) always flows to the `FeeDistributor`.
:::vyper[`FeeAllocator.vy`]
The source code for the `FeeAllocator.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-burners/blob/main/contracts/FeeAllocator.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.4.1` and utilizes a [Snekmate module](https://github.com/pcaversaccio/snekmate/blob/main/src/snekmate/auth/ownable.vy) to handle contract ownership.
The contract is deployed on :logos-ethereum: Ethereum at [`0x22530d384cd9915e096ead2db7f82ee81f8eb468`](https://etherscan.io/address/0x22530d384cd9915e096ead2db7f82ee81f8eb468).
```json
[{"anonymous":false,"inputs":[{"indexed":true,"name":"receiver","type":"address"},{"indexed":false,"name":"old_weight","type":"uint256"},{"indexed":false,"name":"new_weight","type":"uint256"}],"name":"ReceiverSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"receiver","type":"address"}],"name":"ReceiverRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"total_amount","type":"uint256"},{"indexed":false,"name":"distributor_share","type":"uint256"}],"name":"FeesDistributed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previous_owner","type":"address"},{"indexed":true,"name":"new_owner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"inputs":[{"name":"new_owner","type":"address"}],"name":"transfer_ownership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_receiver","type":"address"},{"name":"_weight","type":"uint256"}],"name":"set_receiver","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"name":"receiver","type":"address"},{"name":"weight","type":"uint256"}],"name":"_configs","type":"tuple[]"}],"name":"set_multiple_receivers","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_receiver","type":"address"}],"name":"remove_receiver","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"distribute_fees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"n_receivers","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"distributor_weight","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_RECEIVERS","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_TOTAL_WEIGHT","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fee_distributor","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fee_collector","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fee_token","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"}],"name":"receiver_weights","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint256"}],"name":"receivers","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"total_weight","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VERSION","outputs":[{"name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_fee_distributor","type":"address"},{"name":"_fee_collector","type":"address"},{"name":"_owner","type":"address"}],"outputs":[],"stateMutability":"nonpayable","type":"constructor"}]
```
:::
---
## Distributing Fees
### `distribute_fees`
::::description[`FeeAllocator.distribute_fees()`]
:::guard[Guarded Method]
This function can only be called by the `hooker` address of the [`FeeCollector`](fee-collector.md) contract. The `hooker` is the contract responsible for orchestrating the fee forwarding process.
:::
Function to distribute accumulated crvUSD fees to receivers based on their weights. The function first transfers the fee token balance from the caller (the `hooker`) to this contract, then distributes proportional amounts to each receiver based on their weight. The remaining balance is forwarded to the [`FeeDistributor`](fee-distributor.md) for distribution to veCRV holders.
Emits: `FeesDistributed` event.
```vyper
@external
@nonreentrant
def distribute_fees():
"""
@notice Distribute accumulated crvUSD fees to receivers based on their weights
"""
assert (msg.sender == staticcall fee_collector.hooker()), "distribute: hooker only"
amount_receivable: uint256 = staticcall fee_token.balanceOf(msg.sender)
extcall fee_token.transferFrom(msg.sender, self, amount_receivable)
balance: uint256 = staticcall fee_token.balanceOf(self)
assert balance > 0, "receivers: no fees to distribute"
remaining_balance: uint256 = balance
for receiver: address in self.receivers:
weight: uint256 = self.receiver_weights[receiver]
amount: uint256 = balance * weight // MAX_BPS
if amount > 0:
extcall fee_token.transfer(receiver, amount, default_return_value=True)
remaining_balance -= amount
extcall fee_distributor.burn(fee_token.address)
log FeesDistributed(total_amount=balance, distributor_share=remaining_balance)
```
```shell
>>> FeeAllocator.distribute_fees()
```
::::
---
## Managing Receivers
### `set_receiver`
::::description[`FeeAllocator.set_receiver(_receiver: address, _weight: uint256)`]
:::guard[Guarded Method by [Snekmate 🐍](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Function to add a new receiver or update the weight of an existing receiver. Weights are specified in basis points (bps) where 10,000 = 100%. The total weight of all receivers cannot exceed `MAX_TOTAL_WEIGHT` (5,000 bps = 50%). To remove a receiver, use [`remove_receiver`](#remove_receiver) instead.
| Input | Type | Description |
| ----------- | --------- | ------------------------------------------------- |
| `_receiver` | `address` | Address of the receiver |
| `_weight` | `uint256` | Weight assigned to the receiver in basis points |
Emits: `ReceiverSet` event.
```vyper
@internal
def _set_receiver(_receiver: address, _weight: uint256):
"""
@notice Add or update a receiver with a specified weight
@param _receiver The address of the receiver
@param _weight The weight assigned to the receiver
"""
ownable._check_owner()
assert _receiver != empty(address), "zeroaddr: receiver"
assert _weight > 0, "receivers: invalid weight, use remove_receiver"
old_weight: uint256 = self.receiver_weights[_receiver]
new_total_weight: uint256 = self.total_weight
if old_weight > 0:
new_total_weight = new_total_weight - old_weight + _weight
else:
assert (len(self.receivers) < MAX_RECEIVERS), "receivers: max limit reached"
new_total_weight += _weight
assert (new_total_weight <= MAX_TOTAL_WEIGHT), "receivers: exceeds max total weight"
if old_weight == 0:
self.receiver_indices[_receiver] = (
len(self.receivers) + 1
) # offset by 1, 0 is for deleted receivers
self.receivers.append(_receiver)
self.receiver_weights[_receiver] = _weight
self.total_weight = new_total_weight
log ReceiverSet(receiver=_receiver, old_weight=old_weight, new_weight=_weight)
@external
def set_receiver(_receiver: address, _weight: uint256):
"""
@notice Add or update a receiver with a specified weight
@param _receiver The address of the receiver
@param _weight The weight assigned to the receiver
"""
self._set_receiver(_receiver, _weight)
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> FeeAllocator.set_receiver("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", 1000)
```
::::
### `set_multiple_receivers`
::::description[`FeeAllocator.set_multiple_receivers(_configs: DynArray[ReceiverConfig, MAX_RECEIVERS])`]
:::guard[Guarded Method by [Snekmate 🐍](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Function to add or update multiple receivers at once. Each configuration in the array specifies a receiver address and its weight. When adding new receivers whose combined weight might temporarily exceed `MAX_TOTAL_WEIGHT`, place receivers being updated with lower weights first in the array.
| Input | Type | Description |
| ---------- | ----------------------------------------- | -------------------------------------------------- |
| `_configs` | `DynArray[ReceiverConfig, MAX_RECEIVERS]` | Array of receiver configurations (address, weight) |
Emits: `ReceiverSet` event for each receiver.
```vyper
@external
def set_multiple_receivers(_configs: DynArray[ReceiverConfig, MAX_RECEIVERS]):
"""
@notice Add or update multiple receivers with specified weights
@param _configs Array of receiver configurations (address, weight)
@dev When adding new receivers, if total weight might exceed MAX_TOTAL_WEIGHT,
place receivers being updated with lower weights first in the array
"""
assert len(_configs) > 0, "receivers: empty array"
for i: uint256 in range(MAX_RECEIVERS):
if i >= len(_configs):
break
config: ReceiverConfig = _configs[i]
self._set_receiver(config.receiver, config.weight)
@internal
def _set_receiver(_receiver: address, _weight: uint256):
"""
@notice Add or update a receiver with a specified weight
@param _receiver The address of the receiver
@param _weight The weight assigned to the receiver
"""
ownable._check_owner()
assert _receiver != empty(address), "zeroaddr: receiver"
assert _weight > 0, "receivers: invalid weight, use remove_receiver"
old_weight: uint256 = self.receiver_weights[_receiver]
new_total_weight: uint256 = self.total_weight
if old_weight > 0:
new_total_weight = new_total_weight - old_weight + _weight
else:
assert (len(self.receivers) < MAX_RECEIVERS), "receivers: max limit reached"
new_total_weight += _weight
assert (new_total_weight <= MAX_TOTAL_WEIGHT), "receivers: exceeds max total weight"
if old_weight == 0:
self.receiver_indices[_receiver] = (
len(self.receivers) + 1
) # offset by 1, 0 is for deleted receivers
self.receivers.append(_receiver)
self.receiver_weights[_receiver] = _weight
self.total_weight = new_total_weight
log ReceiverSet(receiver=_receiver, old_weight=old_weight, new_weight=_weight)
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> FeeAllocator.set_multiple_receivers([("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", 1000), ("0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B", 2000)])
```
::::
### `remove_receiver`
::::description[`FeeAllocator.remove_receiver(_receiver: address)`]
:::guard[Guarded Method by [Snekmate 🐍](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Function to remove a receiver from the list. The receiver's weight is subtracted from the total weight, effectively increasing the share that flows to the `FeeDistributor`.
| Input | Type | Description |
| ----------- | --------- | --------------------------------- |
| `_receiver` | `address` | Address of the receiver to remove |
Emits: `ReceiverRemoved` event.
```vyper
@external
def remove_receiver(_receiver: address):
"""
@notice Remove a receiver from the list
@param _receiver The address of the receiver to remove
"""
ownable._check_owner()
weight: uint256 = self.receiver_weights[_receiver]
assert weight > 0, "receivers: does not exist"
index_to_remove: uint256 = self.receiver_indices[_receiver] - 1
last_index: uint256 = len(self.receivers) - 1
assert self.receivers[index_to_remove] == _receiver
if index_to_remove < last_index:
last_receiver: address = self.receivers[last_index]
self.receivers[index_to_remove] = last_receiver
self.receiver_indices[last_receiver] = index_to_remove + 1
self.receivers.pop()
self.receiver_weights[_receiver] = 0
self.receiver_indices[_receiver] = 0
self.total_weight -= weight
log ReceiverRemoved(receiver=_receiver)
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> FeeAllocator.remove_receiver("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
```
::::
### `receivers`
::::description[`FeeAllocator.receivers(arg0: uint256) -> address: view`]
Getter for the receiver address at a given index.
| Input | Type | Description |
| ------ | --------- | -------------------- |
| `arg0` | `uint256` | Index of the receiver |
Returns: receiver address (`address`).
```vyper
receivers: public(DynArray[address, MAX_RECEIVERS])
```
::::
### `receiver_weights`
::::description[`FeeAllocator.receiver_weights(arg0: address) -> uint256: view`]
Getter for the weight of a specific receiver in basis points.
| Input | Type | Description |
| ------ | --------- | ---------------------- |
| `arg0` | `address` | Address of the receiver |
Returns: receiver weight in bps (`uint256`).
```vyper
receiver_weights: public(HashMap[address, uint256])
```
::::
### `n_receivers`
::::description[`FeeAllocator.n_receivers() -> uint256: view`]
Getter for the number of receivers currently registered.
Returns: number of receivers (`uint256`).
```vyper
@external
@view
def n_receivers() -> uint256:
"""
@notice Get the number of receivers
@return The number of receivers
"""
return len(self.receivers)
```
::::
### `total_weight`
::::description[`FeeAllocator.total_weight() -> uint256: view`]
Getter for the sum of all receiver weights in basis points.
Returns: total weight in bps (`uint256`).
```vyper
total_weight: public(uint256)
```
::::
### `distributor_weight`
::::description[`FeeAllocator.distributor_weight() -> uint256: view`]
Getter for the portion of fees that flows to the `FeeDistributor` for veCRV holders. This is calculated as `MAX_BPS - total_weight`, meaning it is the complement of all receiver weights combined.
Returns: distributor weight in bps (`uint256`).
```vyper
@external
@view
def distributor_weight() -> uint256:
"""
@notice Get the portion of fees going to the fee distributor for veCRV
@return The distributors' weight
"""
return MAX_BPS - self.total_weight
```
::::
---
## Contract Ownership
Ownership of the contract is managed using the [`ownable.vy`](https://github.com/pcaversaccio/snekmate/blob/main/src/snekmate/auth/ownable.vy) module from [Snekmate](https://github.com/pcaversaccio/snekmate) which implements a basic access control mechanism, where there is an `owner` that can be granted exclusive access to specific functions.
### `owner`
::::description[`FeeAllocator.owner() -> address: view`]
Getter for the owner of the contract. The owner can manage receivers and their weights via `set_receiver`, `set_multiple_receivers`, and `remove_receiver`.
Returns: contract owner (`address`).
```vyper
from snekmate.auth import ownable
initializes: ownable
exports: (ownable.transfer_ownership, ownable.owner)
@deploy
def __init__(
_fee_distributor: FeeDistributor,
_fee_collector: FeeCollector,
_owner: address,
):
assert _owner != empty(address), "zeroaddr: owner"
assert _fee_distributor.address != empty(address), "zeroaddr: fee_distributor"
assert _fee_collector.address != empty(address), "zeroaddr: fee_collector"
ownable.__init__()
ownable._transfer_ownership(_owner)
fee_distributor = _fee_distributor
fee_collector = _fee_collector
fee_token = IERC20(staticcall fee_collector.target())
extcall fee_token.approve(
fee_distributor.address, max_value(uint256), default_return_value=True
)
```
```vyper
owner: public(address)
@deploy
@payable
def __init__():
self._transfer_ownership(msg.sender)
@internal
def _transfer_ownership(new_owner: address):
old_owner: address = self.owner
self.owner = new_owner
log OwnershipTransferred(old_owner, new_owner)
```
::::
### `transfer_ownership`
::::description[`FeeAllocator.transfer_ownership(new_owner: address)`]
:::guard[Guarded Method by [Snekmate 🐍](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Function to transfer the ownership of the contract to a new address.
| Input | Type | Description |
| ----------- | --------- | ---------------------------- |
| `new_owner` | `address` | New owner of the contract |
Emits: `OwnershipTransferred` event.
```vyper
from snekmate.auth import ownable
initializes: ownable
exports: (ownable.transfer_ownership, ownable.owner)
```
```vyper
owner: public(address)
event OwnershipTransferred:
previous_owner: indexed(address)
new_owner: indexed(address)
@external
def transfer_ownership(new_owner: address):
self._check_owner()
assert new_owner != empty(address), "ownable: new owner is the zero address"
self._transfer_ownership(new_owner)
@internal
def _check_owner():
assert msg.sender == self.owner, "ownable: caller is not the owner"
@internal
def _transfer_ownership(new_owner: address):
old_owner: address = self.owner
self.owner = new_owner
log OwnershipTransferred(old_owner, new_owner)
```
```shell
>>> FeeAllocator.transfer_ownership("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
```
::::
---
## Other Methods
### `fee_distributor`
::::description[`FeeAllocator.fee_distributor() -> address: view`]
Getter for the immutable address of the `FeeDistributor` contract that receives the remaining fees after receiver allocations.
Returns: fee distributor address (`address`).
```vyper
fee_distributor: public(immutable(FeeDistributor))
```
::::
### `fee_collector`
::::description[`FeeAllocator.fee_collector() -> address: view`]
Getter for the immutable address of the `FeeCollector` contract. The `hooker` of this contract is authorized to call `distribute_fees`.
Returns: fee collector address (`address`).
```vyper
fee_collector: public(immutable(FeeCollector))
```
::::
### `fee_token`
::::description[`FeeAllocator.fee_token() -> address: view`]
Getter for the immutable address of the fee token (crvUSD). This is set at deployment by reading the `target` from the `FeeCollector`.
Returns: fee token address (`address`).
```vyper
fee_token: public(immutable(IERC20))
```
::::
### `MAX_RECEIVERS`
::::description[`FeeAllocator.MAX_RECEIVERS() -> uint256: view`]
Getter for the maximum number of receivers that can be registered. This is a constant set to 10.
Returns: maximum number of receivers (`uint256`).
```vyper
MAX_RECEIVERS: public(constant(uint256)) = 10
```
::::
### `MAX_TOTAL_WEIGHT`
::::description[`FeeAllocator.MAX_TOTAL_WEIGHT() -> uint256: view`]
Getter for the maximum total weight that can be assigned to all receivers combined. This is a constant set to 5,000 bps (50%), ensuring at least half of all fees always go to the `FeeDistributor`.
Returns: maximum total weight in bps (`uint256`).
```vyper
MAX_TOTAL_WEIGHT: public(constant(uint256)) = 5_000 # in bps
```
::::
### `VERSION`
::::description[`FeeAllocator.VERSION() -> String[8]: view`]
Getter for the version of the contract.
Returns: contract version (`String[8]`).
```vyper
VERSION: public(constant(String[8])) = "0.1.0"
```
::::
---
## FeeCollector
The `FeeCollector` serves as an entry point for the fee burning and distribution mechanism, acting as a universal contract that collects all admin fees from various revenue sources within the Curve ecosystem.
:::vyper[`FeeCollector.vy`]
The source code for the `FeeCollector.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-burners/blob/main/contracts/FeeCollector.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
This version of `FeeCollector` is only deployed on the following chains, as CowSwap is only deployed on these chains:
- :logos-ethereum: Ethereum at [`0xa2Bcd1a4Efbd04B63cd03f5aFf2561106ebCCE00`](https://etherscan.io/address/0xa2Bcd1a4Efbd04B63cd03f5aFf2561106ebCCE00)
- :logos-gnosis: Gnosis at [`0xBb7404F9965487a9DdE721B3A5F0F3CcfA9aa4C5`](https://gnosisscan.io/address/0xBb7404F9965487a9DdE721B3A5F0F3CcfA9aa4C5)
```json
[{"name":"SetMaxFee","inputs":[{"name":"epoch","type":"uint256","indexed":true},{"name":"max_fee","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"SetBurner","inputs":[{"name":"burner","type":"address","indexed":true}],"anonymous":false,"type":"event"},{"name":"SetHooker","inputs":[{"name":"hooker","type":"address","indexed":true}],"anonymous":false,"type":"event"},{"name":"SetTarget","inputs":[{"name":"target","type":"address","indexed":true}],"anonymous":false,"type":"event"},{"name":"SetKilled","inputs":[{"name":"coin","type":"address","indexed":true},{"name":"epoch_mask","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"SetOwner","inputs":[{"name":"owner","type":"address","indexed":true}],"anonymous":false,"type":"event"},{"name":"SetEmergencyOwner","inputs":[{"name":"emergency_owner","type":"address","indexed":true}],"anonymous":false,"type":"event"},{"stateMutability":"nonpayable","type":"constructor","inputs":[{"name":"_target_coin","type":"address"},{"name":"_weth","type":"address"},{"name":"_owner","type":"address"},{"name":"_emergency_owner","type":"address"}],"outputs":[]},{"stateMutability":"payable","type":"fallback"},{"stateMutability":"nonpayable","type":"function","name":"withdraw_many","inputs":[{"name":"_pools","type":"address[]"}],"outputs":[]},{"stateMutability":"payable","type":"function","name":"burn","inputs":[{"name":"_coin","type":"address"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"epoch","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"epoch","inputs":[{"name":"ts","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"epoch_time_frame","inputs":[{"name":"_epoch","type":"uint256"}],"outputs":[{"name":"","type":"uint256"},{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"epoch_time_frame","inputs":[{"name":"_epoch","type":"uint256"},{"name":"_ts","type":"uint256"}],"outputs":[{"name":"","type":"uint256"},{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"fee","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"fee","inputs":[{"name":"_epoch","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"fee","inputs":[{"name":"_epoch","type":"uint256"},{"name":"_ts","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"transfer","inputs":[{"name":"_transfers","type":"tuple[]","components":[{"name":"coin","type":"address"},{"name":"to","type":"address"},{"name":"amount","type":"uint256"}]}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"collect","inputs":[{"name":"_coins","type":"address[]"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"collect","inputs":[{"name":"_coins","type":"address[]"},{"name":"_receiver","type":"address"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"can_exchange","inputs":[{"name":"_coins","type":"address[]"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"payable","type":"function","name":"forward","inputs":[{"name":"_hook_inputs","type":"tuple[]","components":[{"name":"hook_id","type":"uint8"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"}]}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"payable","type":"function","name":"forward","inputs":[{"name":"_hook_inputs","type":"tuple[]","components":[{"name":"hook_id","type":"uint8"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"}]},{"name":"_receiver","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"recover","inputs":[{"name":"_recovers","type":"tuple[]","components":[{"name":"coin","type":"address"},{"name":"amount","type":"uint256"}]},{"name":"_receiver","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_max_fee","inputs":[{"name":"_epoch","type":"uint256"},{"name":"_max_fee","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_burner","inputs":[{"name":"_new_burner","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_hooker","inputs":[{"name":"_new_hooker","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_target","inputs":[{"name":"_new_target","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_killed","inputs":[{"name":"_input","type":"tuple[]","components":[{"name":"coin","type":"address"},{"name":"killed","type":"uint256"}]}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_owner","inputs":[{"name":"_new_owner","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_emergency_owner","inputs":[{"name":"_new_owner","type":"address"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"target","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"max_fee","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"burner","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"hooker","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"is_killed","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"owner","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"emergency_owner","inputs":[],"outputs":[{"name":"","type":"address"}]}]
```
:::
This new architecture simplifies the collection of fees and the burning of these fees into a designated fee token. The `FeeCollector` introduces a [`target`](#target) variable that represents the token into which all collected fees are burned. This variable can be changed to any token, but such a change requires a successfully passed on-chain vote, as the contract is fully controlled by the Curve DAO.
:::telegram[Telegram]
If you are running or planning to run fee collection for Curve DAO, there is a Telegram channel and a group for necessary updates. Also, many hooks for automation are coming in the future which will be written about in the group.
[→ Join the Telegram group](https://t.me/curve_automation)
:::
---
## Epochs
The contract operates in different [epochs](#epochs) (phases) in which certain actions are possible.
The `epoch` function and its related internal functions are used to determine the current operational phase of the contract based on the timestamp. The contract operates in different phases (epochs) that dictate what actions can be performed at any given time. This helps in organizing the contract's workflow and ensuring that certain operations only occur during specific periods.
```vyper
enum Epoch:
SLEEP # 1
COLLECT # 2
EXCHANGE # 4
FORWARD # 8
```
*Each epoch represents a different state of the contract:*
- `SLEEP`: The contract is idle.
- `COLLECT`: The contract is in a state where it collects fees.
- `EXCHANGE`: The contract "burns" (exchanges) collected fees into the target coin.
- `FORWARD`: The contract forwards the accumulated target coin to the `FeeDistributor`.
*The `EPOCH_TIMESTAMPS` constant defines the start times for each epoch within a week:*
```vyper
START_TIME: constant(uint256) = 1600300800 # ts of distribution start
WEEK: constant(uint256) = 7 * 24 * 3600
EPOCH_TIMESTAMPS: constant(uint256[17]) = [
0, 0, # 1
4 * 24 * 3600, # 2
0, 5 * 24 * 3600, # 4
0, 0, 0, 6 * 24 * 3600, # 8
0, 0, 0, 0, 0, 0, 0, WEEK, # 16, next period
]
```
:::info[Start and Duration of Epochs]
The `SLEEP` epoch lasts for a total of four days, followed by one day of `COLLECT`, one day of `EXCHANGE`, and one day of `FORWARD`.
Epoch start is not on Monday. The first fee distribution started on `Thu Sep 17 2020 00:00:00 GMT+0000` (1600300800)`. Therefore, day 0 of each new epoch starts on Thursday at 00:00:00 GMT.
:::
### `epoch`
::::description[`FeeCollector.epoch(ts: uint256=block.timestamp) -> Epoch: view`]
Getter for the current epoch based on a given timestamp.
| Input | Type | Description |
| ----- | --------- | ------------------------------------- |
| `ts` | `uint256` | Timestamp; defaults to `block.timestamp` |
Returns: current epoch value (`Epoch`).
```vyper
enum Epoch:
SLEEP # 1
COLLECT # 2
EXCHANGE # 4
FORWARD # 8
@external
@view
def epoch(ts: uint256=block.timestamp) -> Epoch:
"""
@notice Get epoch at certain timestamp
@param ts Timestamp. Current by default
@return Epoch
"""
return self._epoch_ts(ts)
@internal
@pure
def _epoch_ts(ts: uint256) -> Epoch:
ts = (ts - START_TIME) % WEEK
for epoch in [Epoch.SLEEP, Epoch.COLLECT, Epoch.EXCHANGE, Epoch.FORWARD]:
if ts < EPOCH_TIMESTAMPS[2 * convert(epoch, uint256)]:
return epoch
raise UNREACHABLE
```
::::
### `epoch_time_frame`
::::description[`FeeCollector.epoch_time_frame(_epoch: Epoch, _ts: uint256=block.timestamp) -> (uint256, uint256): view`]
Getter for the time frame for a specific epoch and timestamp.
| Input | Type | Description |
| -------- | --------- | ------------------------------------------------------------------------ |
| `_epoch` | `Epoch` | Epoch enum for which to check start and end for |
| `_ts` | `uint256` | Timestamp to anchor to. Defaults to the current one (`block.timestamp`) |
Returns: start and end of the epoch (`uint256, uint256`).
```vyper
@external
@view
def epoch_time_frame(_epoch: Epoch, _ts: uint256=block.timestamp) -> (uint256, uint256):
"""
@notice Get time frame of certain epoch
@param _epoch Epoch
@param _ts Timestamp to anchor to. Current by default
@return [start, end) time frame boundaries
"""
return self._epoch_time_frame(_epoch, _ts)
@internal
@pure
def _epoch_time_frame(epoch: Epoch, ts: uint256) -> (uint256, uint256):
subset: uint256 = convert(epoch, uint256)
assert subset & (subset - 1) == 0, "Bad Epoch"
ts = ts - (ts - START_TIME) % WEEK
return (ts + EPOCH_TIMESTAMPS[convert(epoch, uint256)], ts + EPOCH_TIMESTAMPS[2 * convert(epoch, uint256)])
```
::::
---
## Keeper's Fee
The `FeeCollector` contract has a keeper's fee, which incentivizes external users or bots to perform specific actions at the appropriate times within the different epochs. The fee mechanism ensures that these operations are carried out reliably and efficiently by rewarding the entities that execute them.
### `fee`
::::description[`FeeCollector.fee(_epoch: Epoch=empty(Epoch), _ts: uint256=block.timestamp) -> uint256: view`]
Getter for the caller fee based on an epoch and timestamp. If no input is given, it returns the caller fee of the current epoch. The fee is dependent on the current epoch. The `fee` (except the one for the `forward` function) is optional and up to the burner implementation. The value starts at `0` and continuously increases to `max_fee` (`1%`), but burner contracts *can* have their own fee values. The reason for this is that it makes sense to pay the fee in the `target` token instead of many different coins, but the current CoWSwap architecture makes this very complicated to do.
| Input | Type | Description |
| -------- | --------- | ------------------------------------------------- |
| `_epoch` | `Epoch` | Index of the epoch; defaults to the current epoch |
| `_ts` | `uint256` | Timestamp; defaults to `block.timestamp` |
Returns: fee of the epoch (`uint256`).
```vyper
@external
@view
def fee(_epoch: Epoch=empty(Epoch), _ts: uint256=block.timestamp) -> uint256:
"""
@notice Calculate keeper's fee
@param _epoch Epoch to count fee for
@param _ts Timestamp of collection
@return Fee with base 10^18
"""
if _epoch == empty(Epoch):
return self._fee(self._epoch_ts(_ts), _ts)
return self._fee(_epoch, _ts)
@internal
@view
def _fee(epoch: Epoch, ts: uint256) -> uint256:
start: uint256 = 0
end: uint256 = 0
start, end = self._epoch_time_frame(epoch, ts)
if ts >= end:
return 0
return self.max_fee[convert(epoch, uint256)] * (ts + 1 - start) / (end - start)
@internal
@pure
def _epoch_ts(ts: uint256) -> Epoch:
ts = (ts - START_TIME) % WEEK
for epoch in [Epoch.SLEEP, Epoch.COLLECT, Epoch.EXCHANGE, Epoch.FORWARD]:
if ts < EPOCH_TIMESTAMPS[2 * convert(epoch, uint256)]:
return epoch
raise UNREACHABLE
```
::::
### `max_fee`
::::description[`FeeCollector.max_fee(arg0: uint256) -> uint256: view`]
Getter for the maximum fee of an epoch. Maximum fee is set to 1% for the `COLLECT` and `FORWARD` epochs. This value can be changed by the `owner` of the contract using the [`set_max_fee`](#set_max_fee) function.
| Input | Type | Description |
| ------ | --------- | --------------------------------------------- |
| `arg0` | `uint256` | Epoch enum for which to check the maximum fee |
Returns: maximum fee (`uint256`).
```vyper
event SetMaxFee:
epoch: indexed(Epoch)
max_fee: uint256
enum Epoch:
SLEEP # 1
COLLECT # 2
EXCHANGE # 4
FORWARD # 8
max_fee: public(uint256[9]) # max_fee[Epoch]
@external
def __init__(_target_coin: ERC20, _weth: wETH, _owner: address, _emergency_owner: address):
"""
@notice Contract constructor
@param _target_coin Coin to swap to
@param _weth Wrapped ETH(native coin) address
@param _owner Owner address
@param _emergency_owner Emergency owner address. Can kill the contract
"""
...
self.max_fee[convert(Epoch.COLLECT, uint256)] = ONE / 100 # 1%
self.max_fee[convert(Epoch.FORWARD, uint256)] = ONE / 100 # 1%
...
log SetMaxFee(Epoch.COLLECT, ONE / 100)
log SetMaxFee(Epoch.FORWARD, ONE / 100)
...
```
::::
### `set_max_fee`
::::description[`FeeCollector.set_max_fee(_epoch: Epoch, _max_fee: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to set `max_fee` for a specific epoch. The maximum fee cannot be greater than 1 (100%).
| Input | Type | Description |
| ---------- | --------- | ------------------------------------------- |
| `_epoch` | `Epoch` | Epoch enum for which to set the maximum fee |
| `_max_fee` | `uint256` | Maximum fee |
Emits: `SetMaxFee` event.
```vyper
event SetMaxFee:
epoch: indexed(Epoch)
max_fee: uint256
@external
def set_max_fee(_epoch: Epoch, _max_fee: uint256):
"""
@notice Set keeper's max fee
@dev Callable only by owner
@param _epoch Epoch to set fee for
@param _max_fee Maximum fee to set
"""
assert msg.sender == self.owner, "Only owner"
subset: uint256 = convert(_epoch, uint256)
assert subset & (subset - 1) == 0, "Bad Epoch"
assert _max_fee <= ONE, "Bad max_fee"
self.max_fee[convert(_epoch, uint256)] = _max_fee
log SetMaxFee(_epoch, _max_fee)
```
This example sets the maximum fee for the `COLLECT` epoch to `0.05`.
```shell
>>> FeeCollector.set_max_fee(Epoch.COLLECT, 0.05)
```
::::
---
## Burn Process
The `FeeCollector` contract has a [`target`](#target) variable, which represents the coin into which all the collected fees are "burned" into. This variable can be changed by the [`owner`](#owner) of the contract using the [`set_target`](#set_target) function. As the owner of the contract is the Curve DAO, a on-chain proposal needs to be successfully passed to make any changes.
*The general flow of the fee burning process is the following:*
1. Admin fees are collected from pools or other revenue sources using the `withdraw_many` function. While fees from older pools need to be claimed manually, the accrued fees from newer pools (mostly NG pools) are periodically claimed when removing liquidity from the pool.
2. The accrued tokens can be burned by calling the `collect` function. This creates, if there isn't already one, a conditional order on CowSwap which automatically exchanges the fee tokens into the `target` coin. Admin fees can only be burned during the `EXCHANGE` epoch. If `collect` is called during the `COLLECT epoch, the coins are transferred to the CowSwapBurner, and a conditional order is created, but the order is not yet valid and is waiting for the WatchTower to place the order with the CowSwap API.
3. After burning the tokens, they can be forwarded to the `FeeDistributor` using the `forward` function.
### `target`
::::description[`FeeCollector.target() -> address: view`]
Getter for the target coin to which the fees are converted to. This is essentially the reward token that is being distributed to veCRV holders.
Returns: target coin (`address`).
```vyper
event SetTarget:
target: indexed(ERC20)
target: public(ERC20) # coin swapped into
@external
def __init__(_target_coin: ERC20, _weth: wETH, _owner: address, _emergency_owner: address):
"""
@notice Contract constructor
@param _target_coin Coin to swap to
@param _weth Wrapped ETH(native coin) address
@param _owner Owner address
@param _emergency_owner Emergency owner address. Can kill the contract
"""
self.target = _target_coin
...
log SetTarget(_target_coin)
...
```
::::
### `set_target`
::::description[`FeeCollector.set_target(_new_target: ERC20)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to change the target coin of the contract.
| Input | Type | Description |
| ------------- | ------- | ------------------------------------ |
| `_new_target` | `ERC20` | Token address of the new target coin |
Emits: `SetTarget` event.
```vyper
event SetTarget:
target: indexed(ERC20)
target: public(ERC20) # coin swapped into
@external
def set_target(_new_target: ERC20):
"""
@notice Set new coin for fees accumulation
@dev Callable only by owner
@param _new_target Address of the new target coin
"""
assert msg.sender == self.owner, "Only owner"
target: ERC20 = self.target
self.is_killed[target] = empty(Epoch) # allow to collect and exchange
log SetKilled(target, empty(Epoch))
self.target = _new_target
self.is_killed[_new_target] = Epoch.COLLECT | Epoch.EXCHANGE # Keep target coin in contract
log SetTarget(_new_target)
log SetKilled(_new_target, Epoch.COLLECT | Epoch.EXCHANGE)
```
This example sets the `target` coin to `0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2`.
```shell
>>> FeeCollector.target()
"0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E"
>>> FeeCollector.set_target("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")
>>> FeeCollector.target()
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
```
::::
### `withdraw_many`
::::description[`FeeCollector.withdraw_many(_pools: DynArray[address, MAX_LEN])`]
:::guard[Guarded Method]
This function does not work with all pools, as some of them have a `msg.sender == owner` guard with a pool proxy as the owner.
:::
Function to withdraw admin fees from multiple Curve pools. Maximum amount of pools to withdraw from within a single function call is `64`. This function can be called by anyone and at any time. While the fee claiming of new-generation (NG) pools is partly automated, the fees of older pools or crvUSD market need to claimed manually. This function only works on contracts with a `withdraw_admin_fees` function. E.g. accrued fees from crvUSD markets are collected via a `collect_fees` function, therefore this function can not be used to claim those fees into this contract.
| Input | Type | Description |
| -------- | ---------------------------- | ------------------------------------------------------------------------ |
| `_pools` | `DynArray[address, MAX_LEN]` | Dynamic array containing the pool addresses to claim the admin fees from |
```vyper
MAX_LEN: constant(uint256) = 64
interface Curve:
def withdraw_admin_fees(): nonpayable
@external
def withdraw_many(_pools: DynArray[address, MAX_LEN]):
"""
@notice Withdraw admin fees from multiple pools
@param _pools List of pool address to withdraw admin fees from
"""
for pool in _pools:
Curve(pool).withdraw_admin_fees()
```
```shell
>>> FeeCollector.withdraw_many(["0x090D543868463090389830384630903846309038", "0x090D543868463090389830384630903846309038"])
```
::::
### `collect`
::::description[`FeeCollector.collect(_coins: DynArray[ERC20, MAX_LEN], _receiver: address=msg.sender)`]
Function that is the primary mechanism for burning coins and can only be called during the `COLLECT` epoch. It calls the `burn` function of the burner contract, which creates a [conditional order](https://github.com/cowprotocol/composable-cow) on CowSwap if one has not already been created. This process effectively "burns" the collected coins by swapping them into the target coin. Additionally, the caller is awarded a [keeper fee](#keepers-fee) for their role in the process.
:::colab[Google Colab Notebook]
Coin addresses to collect are converted into `uint160` and sorted from small to big.
A Google Colab notebook that converts addresses into `uint160` and orders them by ascending order can be found here: [Google Colab Notebook](https://colab.research.google.com/drive/1XBP4YDXEhy2AO3U-qtlbhat4HHhZfUVp?usp=sharing).
:::
| Input | Type | Description |
| ----------- | -------------------------- | ---------------------------------------------------------- |
| `_coins` | `DynArray[ERC20, MAX_LEN]` | Dynamic array of coin addresses sorted in ascending order |
| `_receiver` | `address` | Receiver of keeper fee |
```vyper
MAX_LEN: constant(uint256) = 64
@external
@nonreentrant("collect")
def collect(_coins: DynArray[ERC20, MAX_LEN], _receiver: address=msg.sender):
"""
@notice Collect earned fees. Collection should happen under callback to earn caller fees.
@param _coins Coins to collect sorted in ascending order
@param _receiver Receiver of caller `collect_fee`s
"""
assert self._epoch_ts(block.timestamp) == Epoch.COLLECT, "Wrong epoch"
assert not self.is_killed[ALL_COINS] in Epoch.COLLECT, "Killed epoch"
for i in range(len(_coins), bound=MAX_LEN):
assert not self.is_killed[_coins[i]] in Epoch.COLLECT, "Killed coin"
# Eliminate case of repeated coins
if i > 0:
assert convert(_coins[i].address, uint160) > convert(_coins[i - 1].address, uint160), "Coins not sorted"
self.burner.burn(_coins, _receiver)
```
```vyper
interface FeeCollector:
def fee(_epoch: Epoch=empty(Epoch), _ts: uint256=block.timestamp) -> uint256: view
def target() -> ERC20: view
def owner() -> address: view
def emergency_owner() -> address: view
def epoch_time_frame(epoch: Epoch, ts: uint256=block.timestamp) -> (uint256, uint256): view
def can_exchange(_coins: DynArray[ERC20, MAX_COINS_LEN]) -> bool: view
def transfer(_transfers: DynArray[Transfer, MAX_COINS_LEN]): nonpayable
struct ConditionalOrderParams:
# The contract implementing the conditional order logic
handler: address # self
# Allows for multiple conditional orders of the same type and data
salt: bytes32 # Not used for now
# Data available to ALL discrete orders created by the conditional order
staticData: Bytes[STATIC_DATA_LEN] # Using coin address
interface ComposableCow:
def create(params: ConditionalOrderParams, dispatch: bool): nonpayable
def domainSeparator() -> bytes32: view
def isValidSafeSignature(
safe: address, sender: address, _hash: bytes32, _domainSeparator: bytes32, typeHash: bytes32,
encodeData: Bytes[15 * 32],
payload: Bytes[(32 + 3 + 1 + 8) * 32],
) -> bytes4: view
@external
def burn(_coins: DynArray[ERC20, MAX_COINS_LEN], _receiver: address):
"""
@notice Post hook after collect to register coins for burn
@dev Registers new orders in ComposableCow
@param _coins Which coins to burn
@param _receiver Receiver of profit
"""
assert msg.sender == fee_collector.address, "Only FeeCollector"
fee: uint256 = fee_collector.fee(Epoch.COLLECT)
fee_payouts: DynArray[Transfer, MAX_COINS_LEN] = []
self_transfers: DynArray[Transfer, MAX_COINS_LEN] = []
for coin in _coins:
if not self.created[coin]:
composable_cow.create(ConditionalOrderParams({
handler: self,
salt: empty(bytes32),
staticData: concat(b"", convert(coin.address, bytes20)),
}), True)
coin.approve(vault_relayer, max_value(uint256))
self.created[coin] = True
amount: uint256 = coin.balanceOf(fee_collector.address) * fee / ONE
fee_payouts.append(Transfer({coin: coin, to: _receiver, amount: amount}))
self_transfers.append(Transfer({coin: coin, to: self, amount: max_value(uint256)}))
fee_collector.transfer(fee_payouts)
fee_collector.transfer(self_transfers)
```
```shell
>>> FeeCollector.collect(["0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "0x6b175474e89094c44da98b954eedeac495271d0f"])
```
::::
### `can_exchange`
::::description[`FeeCollector.can_exchange(_coins: DynArray[ERC20, MAX_LEN]) -> bool: view`]
Function to check whether specified coins are allowed to be exchanged at the current timestamp. It verifies that the current epoch is `EXCHANGE` and that the coins to be exchanged are not marked as killed.
| Input | Type | Description |
| -------- | -------------------------- | ------------------------------------------------------------------------ |
| `_coins` | `DynArray[ERC20, MAX_LEN]` | Dynamic array of ERC20 token addresses to check for exchange eligibility |
Returns: true or false (`bool`).
```vyper
MAX_LEN: constant(uint256) = 64
@external
@view
def can_exchange(_coins: DynArray[ERC20, MAX_LEN]) -> bool:
"""
@notice Check whether coins are allowed to be exchanged
@param _coins Coins to exchange
@return Boolean value if coins are allowed to be exchanged
"""
if self._epoch_ts(block.timestamp) != Epoch.EXCHANGE or\
self.is_killed[ALL_COINS] in Epoch.EXCHANGE:
return False
for coin in _coins:
if self.is_killed[coin] in Epoch.EXCHANGE:
return False
return True
```
```shell
>>> FeeCollector.can_exchange(["0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E"])
True
```
::::
### `transfer`
::::description[`FeeCollector.transfer(_transfers: DynArray[Transfer, MAX_LEN])`]
:::guard[Guarded Method]
This function is only callable by the `burner` contract.
:::
Function to transfer coins. This function can only be called during the `COLLECT` or `EXCHANGE` epochs and is used to transfer the different admin fee tokens to the burner contract when calling the `collect` function. This function is effectively needed to remove all approvals from previous burners in case of any malfunctioning or the misuse of any collected coins.
| Input | Type | Description |
| ------------ | ----------------------------- | ----------------------------------- |
| `_transfers` | `DynArray[Transfer, MAX_LEN]` | Dynamic array of `Transfer` structs |
*Each `Transfer` struct contains:*
- `coin:` `address` - The ERC20 token address that is being transferred.
- `to:` `address` - The address to which the tokens will be transferred.
- `amount`: `uint256` - The amount of tokens to transfer. If set to 2^256-1, it transfers the entire balance.
```vyper
struct Transfer:
coin: ERC20
to: address
amount: uint256 # 2^256-1 for the whole balance
MAX_LEN: constant(uint256) = 64
@external
@nonreentrant("transfer")
def transfer(_transfers: DynArray[Transfer, MAX_LEN]):
"""
@dev No approvals so can change burner easily
@param _transfers Transfers to apply
"""
assert msg.sender == self.burner.address, "Only Burner"
epoch: Epoch = self._epoch_ts(block.timestamp)
assert epoch in Epoch.COLLECT | Epoch.EXCHANGE, "Wrong Epoch"
for transfer in _transfers:
assert not self.is_killed[transfer.coin] in epoch, "Killed coin"
amount: uint256 = transfer.amount
if amount == max_value(uint256):
amount = transfer.coin.balanceOf(self)
assert transfer.coin.transfer(transfer.to, amount, default_return_value=True)
```
```vyper
interface FeeCollector:
def fee(_epoch: Epoch=empty(Epoch), _ts: uint256=block.timestamp) -> uint256: view
def target() -> ERC20: view
def owner() -> address: view
def emergency_owner() -> address: view
def epoch_time_frame(epoch: Epoch, ts: uint256=block.timestamp) -> (uint256, uint256): view
def can_exchange(_coins: DynArray[ERC20, MAX_COINS_LEN]) -> bool: view
def transfer(_transfers: DynArray[Transfer, MAX_COINS_LEN]): nonpayable
@external
def burn(_coins: DynArray[ERC20, MAX_COINS_LEN], _receiver: address):
"""
@notice Post hook after collect to register coins for burn
@dev Registers new orders in ComposableCow
@param _coins Which coins to burn
@param _receiver Receiver of profit
"""
assert msg.sender == fee_collector.address, "Only FeeCollector"
fee: uint256 = fee_collector.fee(Epoch.COLLECT)
fee_payouts: DynArray[Transfer, MAX_COINS_LEN] = []
self_transfers: DynArray[Transfer, MAX_COINS_LEN] = []
for coin in _coins:
if not self.created[coin]:
composable_cow.create(ConditionalOrderParams({
handler: self,
salt: empty(bytes32),
staticData: concat(b"", convert(coin.address, bytes20)),
}), True)
coin.approve(vault_relayer, max_value(uint256))
self.created[coin] = True
amount: uint256 = coin.balanceOf(fee_collector.address) * fee / ONE
fee_payouts.append(Transfer({coin: coin, to: _receiver, amount: amount}))
self_transfers.append(Transfer({coin: coin, to: self, amount: max_value(uint256)}))
fee_collector.transfer(fee_payouts)
fee_collector.transfer(self_transfers)
```
```shell
>>> FeeCollector.transfer([Transfer({coin: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", to: "0x090D543868463090389830384630903846309038", amount: 1000000000000000000})])
```
::::
### `forward`
::::description[`FeeCollector.forward(_hook_inputs: DynArray[HookInput, MAX_HOOK_LEN], _receiver: address=msg.sender) -> uint256`]
Function to transfer the target coin to the hooker address. This function can only be called during the `FORWARD` epoch. It charges a keeper fee on the entire balance of the forwarded coins and awards it to the caller. The function also calls the `push_target` function of the burner contract to transfer any remaining target coins back into the `FeeCollector` contract before forwarding the total balance to the hooker. Additionally, the function calls the `duty_act` method of the hooker contract, applying any specified hooks and adjusting the fee accordingly.
| Input | Type | Description |
| -------------- | ----------------------------------- | ------------------------------------- |
| `_hook_inputs` | `DynArray[HookInput, MAX_HOOK_LEN]` | Dynamic array of `HookInput` structs |
| `_receiver` | `address` | Receiver of keeper fee |
*Each `HookInput` struct contains:*
- `hook_id`: `uint8` - ID of the hook to execute. This ID determines which specific hook logic to apply during the `duty_act` call.
- `value`: `uint256` - Value associated with the hook, which can represent the amount or specific parameter needed by the hook logic.
- `data`: `Bytes[8192]` - Additional data required by the hook, encoded as bytes. This can include various parameters or instructions specific to the hook's functionality.
Returns: received keeper fee (`uint256`).
```vyper
struct HookInput:
hook_id: uint8
value: uint256
data: Bytes[8192]
@external
@payable
@nonreentrant("forward")
def forward(_hook_inputs: DynArray[HookInput, MAX_HOOK_LEN], _receiver: address=msg.sender) -> uint256:
"""
@notice Transfer target coin forward
@param _hook_inputs Input parameters for forward hooks
@param _receiver Receiver of caller `forward_fee`
@return Amount of received fee
"""
assert self._epoch_ts(block.timestamp) == Epoch.FORWARD, "Wrong epoch"
target: ERC20 = self.target
assert not (self.is_killed[ALL_COINS] | self.is_killed[target]) in Epoch.FORWARD, "Killed"
self.burner.push_target()
amount: uint256 = target.balanceOf(self)
# Account buffer
hooker: Hooker = self.hooker
hooker_buffer: uint256 = hooker.buffer_amount()
amount -= min(hooker_buffer, amount)
fee: uint256 = self._fee(Epoch.FORWARD, block.timestamp) * amount / ONE
target.transfer(_receiver, fee)
target.transfer(hooker.address, amount - fee)
if self.last_hooker_approve < (block.timestamp - START_TIME) / WEEK: # First time this week
target.approve(hooker.address, hooker_buffer)
self.last_hooker_approve = (block.timestamp - START_TIME) / WEEK
fee += hooker.duty_act(_hook_inputs, _receiver, value=msg.value)
return fee
```
```vyper
@external
def push_target() -> uint256:
"""
@notice In case target coin is left in contract can be pushed to forward
@return Amount of coin pushed further
"""
target: ERC20 = fee_collector.target()
amount: uint256 = target.balanceOf(self)
if amount > 0:
target.transfer(fee_collector.address, amount)
return amount
```
```vyper
event DutyAct:
pass
event Act:
receiver: indexed(address)
compensation: uint256
@external
@payable
def duty_act(_hook_inputs: DynArray[HookInput, MAX_HOOKS_LEN], _receiver: address=msg.sender) -> uint256:
"""
@notice Entry point to run hooks for FeeCollector
@param _hook_inputs Inputs assembled by keepers
@param _receiver Receiver of compensation (sender by default)
@return Compensation received
"""
if msg.sender == fee_collector.address:
self.duty_counter = convert((block.timestamp - START_TIME) / WEEK, uint64) # assuming time frames are divided weekly
hook_mask: uint256 = 0
for solicitation in _hook_inputs:
hook_mask |= 1 << solicitation.hook_id
duties_checklist: uint256 = self.duties_checklist
assert hook_mask & duties_checklist == duties_checklist, "Not all duties"
log DutyAct()
return self._act(_hook_inputs, _receiver)
@internal
def _act(_hook_inputs: DynArray[HookInput, MAX_HOOKS_LEN], _receiver: address) -> uint256:
current_duty_counter: uint64 = self.duty_counter
compensation: uint256 = 0
prev_idx: uint8 = 0
for solicitation in _hook_inputs:
hook: Hook = self.hooks[solicitation.hook_id]
self._shot(hook, solicitation)
if hook.compensation_strategy.cooldown.duty_counter < current_duty_counter:
hook.compensation_strategy.cooldown.used = 0
hook.compensation_strategy.cooldown.duty_counter = current_duty_counter
hook_compensation: uint256 = self._compensate(hook)
if hook_compensation > 0:
compensation += hook_compensation
hook.compensation_strategy.cooldown.used += 1
self.hooks[solicitation.hook_id].compensation_strategy.cooldown = hook.compensation_strategy.cooldown
if prev_idx > solicitation.hook_id:
raise "Hooks not sorted"
prev_idx = solicitation.hook_id
log HookShot(prev_idx, hook_compensation)
log Act(_receiver, compensation)
# happy ending
if compensation > 0:
coin: ERC20 = fee_collector.target()
coin.transferFrom(fee_collector.address, _receiver, compensation)
return compensation
```
```shell
>>> FeeCollector.forward([HookInput({hook_id: 1, value: 0, data: b""})], "0x090D543868463090389830384630903846309038")
```
::::
### `burn`
::::description[`FeeCollector.burn(_coin: address) -> bool`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to transfer coins from the contract with approval. This function is needed for back compatibility along with dealing with raw ETH.
| Input | Type | Description |
| ------- | --------- | ------------------------------------ |
| `_coin` | `address` | Token address of the new target coin |
```vyper
@external
@payable
def burn(_coin: address) -> bool:
"""
@notice Transfer coin from contract with approval
@dev Needed for back compatability along with dealing raw ETH
@param _coin Coin to transfer
@return True if did not fail, back compatability
"""
if _coin == ETH_ADDRESS: # Deposit
WETH.deposit(value=self.balance)
else:
amount: uint256 = ERC20(_coin).balanceOf(msg.sender)
ERC20(_coin).transferFrom(msg.sender, self, amount)
return True
```
```shell
>>> FeeCollector.burn("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")
```
::::
### `recover`
::::description[`FeeCollector.recover(_recovers: DynArray[RecoverInput, MAX_LEN], _receiver: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` or `emergency_owner` of the contract.
:::
Function to recover ERC20 tokens or ETH from the contract by transferring them to `_receiver`.
| Input | Type | Description |
| ----------- | --------------------------------- | --------------------------------------- |
| `_recovers` | `DynArray[RecoverInput, MAX_LEN]` | Dynamic array of `RecoverInput` structs |
| `_receiver` | `address` | Receiver of the recovered coins |
*Each `RecoverInput` struct contains:*
- `coin`: `address` - The address of the ERC20 token to recover.
- `amount`: `uint256` - The amount of the token to recover. Use `2^256-1` to recover the entire balance.
```vyper
struct RecoverInput:
coin: ERC20
amount: uint256
@external
def recover(_recovers: DynArray[RecoverInput, MAX_LEN], _receiver: address):
"""
@notice Recover ERC20 tokens or Ether from this contract
@dev Callable only by owner and emergency owner
@param _recovers (Token, amount) to recover
@param _receiver Receiver of coins
"""
assert msg.sender in [self.owner, self.emergency_owner], "Only owner"
for input in _recovers:
amount: uint256 = input.amount
if input.coin.address == ETH_ADDRESS:
if amount == max_value(uint256):
amount = self.balance
raw_call(_receiver, b"", value=amount)
else:
if amount == max_value(uint256):
amount = input.coin.balanceOf(self)
input.coin.transfer(_receiver, amount) # do not need safe transfer
```
```shell
>>> FeeCollector.recover([RecoverInput({coin: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", amount: 1000000000000000000})], "0x090D543868463090389830384630903846309038")
```
::::
---
## Burner and Hooker Contracts
Burner contracts are used to convert collected coins into the target coins. Hooker contracts facilitate the execution of predefined actions (hooks) through the `Hooker` contract.
When setting up a burner or hooker, they need to support a specific interface structure to comply with the functions used in the FeeCollector contract. Each `burner` and `hooker` contract must implement a `supportsInterface(_interface_id: bytes4)` method, which identifies the interface according to [ERC-165](https://eips.ethereum.org/EIPS/eip-165). This method ensures the contract is compatible with the FeeCollector.
### `burner`
::::description[`FeeCollector.burner() -> address: view`]
Getter for the burner contract. The burner can be set and changed via [`set_burner`](#set_burner).
Returns: burner contract (`address`).
```vyper
interface Burner:
def burn(_coins: DynArray[ERC20, MAX_LEN], _receiver: address): nonpayable
def push_target() -> uint256: nonpayable
def supportsInterface(_interface_id: bytes4) -> bool: view
burner: public(Burner)
@external
def set_burner(_new_burner: Burner):
"""
@notice Set burner for exchanging coins
@dev Callable only by owner
@param _new_burner Address of the new contract
"""
assert msg.sender == self.owner, "Only owner"
assert _new_burner.supportsInterface(BURNER_INTERFACE_ID)
self.burner = _new_burner
```
::::
### `hooker`
::::description[`FeeCollector.hooker() -> address: view`]
Getter for the hooker contract. The hooker can be set and changed via [`set_hooker`](#set_hooker).
Returns: hooker contract (`address`).
```vyper
interface Hooker:
def duty_act(_hook_inputs: DynArray[HookInput, MAX_HOOK_LEN], _receiver: address=msg.sender) -> uint256: payable
def buffer_amount() -> uint256: view
def supportsInterface(_interface_id: bytes4) -> bool: view
HOOKER_INTERFACE_ID: constant(bytes4) = 0xe569b44d
hooker: public(Hooker)
```
::::
### `set_burner`
::::description[`FeeCollector.set_burner(_new_burner: Burner)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to set a new burner contract. When setting, the contract checks if the new burner supports a certain `BURNER_INTERFACE_ID`, if not, the transaction will revert.
| Input | Type | Description |
| ------------- | -------- | ---------------------------------- |
| `_new_burner` | `Burner` | Contract address of the new burner |
Emits: `SetBurner` event.
```vyper
interface Burner:
def burn(_coins: DynArray[ERC20, MAX_LEN], _receiver: address): nonpayable
def push_target() -> uint256: nonpayable
def supportsInterface(_interface_id: bytes4) -> bool: view
event SetBurner:
burner: indexed(Burner)
BURNER_INTERFACE_ID: constant(bytes4) = 0xa3b5e311
burner: public(Burner)
@external
def set_burner(_new_burner: Burner):
"""
@notice Set burner for exchanging coins, must implement BURNER_INTERFACE
@dev Callable only by owner
@param _new_burner Address of the new contract
"""
assert msg.sender == self.owner, "Only owner"
assert _new_burner.supportsInterface(BURNER_INTERFACE_ID)
self.burner = _new_burner
log SetBurner(_new_burner)
```
This example sets the `burner` contract to `0x0000000000000000000000000000000000000000`.
```shell
>>> FeeCollector.burner()
"0xC0fC3dDfec95ca45A0D2393F518D3EA1ccF44f8b"
>>> FeeCollector.set_burner("0x0000000000000000000000000000000000000000")
>>> FeeCollector.burner()
"0x0000000000000000000000000000000000000000"
```
::::
### `set_hooker`
::::description[`FeeCollector.set_hooker(_new_hooker: Hooker)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to set a new hooker contract. When setting, the contract checks if the new hooker supports a certain `HOOKER_INTERFACE_ID: constant(bytes4) = 0xe569b44d`.
| Input | Type | Description |
| ------------- | -------- | --------------------------------- |
| `_new_hooker` | `Hooker` | Address of hooker contract to set |
Emits: `SetHooker` event.
```vyper
interface Hooker:
def duty_act(_hook_inputs: DynArray[HookInput, MAX_HOOK_LEN], _receiver: address=msg.sender) -> uint256: payable
def buffer_amount() -> uint256: view
def supportsInterface(_interface_id: bytes4) -> bool: view
event SetHooker:
hooker: indexed(Hooker)
HOOKER_INTERFACE_ID: constant(bytes4) = 0xe569b44d
hooker: public(Hooker)
@external
def set_hooker(_new_hooker: Hooker):
"""
@notice Set contract for hooks, must implement HOOKER_INTERFACE
@dev Callable only by owner
@param _new_hooker Address of the new contract
"""
assert msg.sender == self.owner, "Only owner"
assert _new_hooker.supportsInterface(HOOKER_INTERFACE_ID)
if self.hooker != empty(Hooker):
self.target.approve(self.hooker.address, 0)
self.hooker = _new_hooker
log SetHooker(_new_hooker)
```
This example sets the `hooker` contract to `0x0000000000000000000000000000000000000000`.
```shell
>>> FeeCollector.hooker()
"0x9A9DF35cd8E88565694CA6AD5093c236C7f6f69D"
>>> FeeCollector.set_hooker("0x0000000000000000000000000000000000000000")
>>> FeeCollector.hooker()
"0x0000000000000000000000000000000000000000"
```
::::
---
## Ownership and Killing Coins
The `FeeCollector` contract features a dual ownership structure, consisting of a regular `owner` and an `emergency_owner`.
The contract includes a mechanism to "kill" certain coins across specific epochs. When a coin is killed, certain functions related to that coin will no longer be callable. This capability is crucial for managing and mitigating risks associated with specific tokens.
*The `owner`[^1] is able to call the following functions:*
[^1]: The owner of the contract is the Curve DAO. To make any changes, a successful on-chain vote needs to pass.
- `recover`: Recover ERC20 tokens or ETH from the contract.
- `set_max_fee`: Set the maximum fee for a specified epoch.
- `set_burner`: Set the burner contract for exchanging coins.
- `set_hooker`: Set the hooker contract.
- `set_target`: Set a new target coin for fee accumulation.
- `set_killed`: Mark certain coins as killed to prevent them from being burned, or mark entire epochs to prevent all coins from being burned in a specific epoch.
- `set_owner`: Assign a new owner to the contract.
- `set_emergency_owner`: Assign a new emergency owner to the contract.
*The `emergency_owner`[^2] has limited power, intended for emergency situations. They can call:*
[^2]: The `emergency_owner` is a [5 of 9 multisig](https://resources.curve.fi/governance/understanding-governance/?h=multis#emergency-dao).
- `recover`: Recover ERC20 tokens or ETH from the contract.
- `set_killed`: Mark certain coins as killed to prevent them from being burnt.
### `is_killed`
::::description[`FeeCollector.is_killed(arg0: ERC20) -> Epoch: view`]
Function to check if a coin is killed for a certain epoch. Depending on the epoch the coin is killed for, the contract restricts function calls. For example, if a coin is killed for the `COLLECT` epoch, the `collect` function cannot be called for that coin.
| Input | Type | Description |
| ------ | ------- | ------------------------------ |
| `arg0` | `ERC20` | Address of the coin to check |
Returns: sum of the epoch indices in the enum (`Epoch`).
```vyper
struct KilledInput:
coin: ERC20
killed: Epoch # True where killed
```
::::
### `set_killed`
::::description[`FeeCollector.set_killed(_input: DynArray[KilledInput, MAX_LEN])`]
:::guard[Guarded Method]
This function is only callable by the `owner` or `emergency_owner` of the contract.
:::
Function to kill a coin for a specific epoch.
| Input | Type | Description |
| -------- | -------------------------------- | ------------------------------ |
| `_input` | `DynArray[KilledInput, MAX_LEN]` | Array of `KilledInput` structs |
Emits: `SetKilled` event.
*Each `KilledInput` struct contains:*
- `coin`: `ERC20` - The address of the ERC20 token to be killed.
- `killed`: `Epoch` - The sum of the epoch indices during which the coin is killed.
```vyper
event SetKilled:
coin: indexed(ERC20)
epoch_mask: Epoch
struct KilledInput:
coin: ERC20
killed: Epoch # True where killed
is_killed: public(HashMap[ERC20, Epoch])
@external
def set_killed(_input: DynArray[KilledInput, MAX_LEN]):
"""
@notice Stop a contract or specific coin to be burnt
@dev Callable only by owner or emergency owner
@param _input Array of (coin address, killed phases enum)
"""
assert msg.sender in [self.owner, self.emergency_owner], "Only owner"
for input in _input:
self.is_killed[input.coin] = input.killed
log SetKilled(input.coin, input.killed)
```
The first example kills wETH for the `SLEEP` epoch. The second example kills wETH for the `COLLECT` and `EXCHANGE` epochs.
```shell
# kills wETH for epoch SLEEP
>>> FeeCollector.set_killed([("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 1)])
# kills wETH for epochs COLLECT and EXCHANGE
>>> FeeCollector.set_killed([("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 2 | 4)])
```
::::
### `owner`
::::description[`FeeCollector.owner() -> address: view`]
Getter for the current owner of the contract.
Returns: owner (`address`).
```vyper
event SetOwner:
owner: indexed(address)
owner: public(address)
@external
def __init__(_target_coin: ERC20, _weth: wETH, _owner: address, _emergency_owner: address):
"""
@notice Contract constructor
@param _target_coin Coin to swap to
@param _weth Wrapped ETH(native coin) address
@param _owner Owner address
@param _emergency_owner Emergency owner address. Can kill the contract
"""
...
self.owner = _owner
...
log SetOwner(_owner)
...
```
::::
### `emergency_owner`
::::description[`FeeCollector.emergency_owner() -> address: view`]
Getter for the current emergency owner of the contract.
Returns: emergency owner (`address`).
```vyper
event SetEmergencyOwner:
emergency_owner: indexed(address)
emergency_owner: public(address)
@external
def __init__(_target_coin: ERC20, _weth: wETH, _owner: address, _emergency_owner: address):
"""
@notice Contract constructor
@param _target_coin Coin to swap to
@param _weth Wrapped ETH(native coin) address
@param _owner Owner address
@param _emergency_owner Emergency owner address. Can kill the contract
"""
...
self.emergency_owner = _emergency_owner
...
log SetEmergencyOwner(_emergency_owner)
...
```
::::
### `set_owner`
::::description[`FeeCollector.set_owner(_new_owner: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to set a new owner.
| Input | Type | Description |
| ------------ | --------- | ------------------------ |
| `_new_owner` | `address` | Address of the new owner |
Emits: `SetOwner` event.
```vyper
event SetOwner:
owner: indexed(address)
owner: public(address)
@external
def set_owner(_new_owner: address):
"""
@notice Set owner of the contract
@dev Callable only by current owner
@param _new_owner Address of the new owner
"""
assert msg.sender == self.owner, "Only owner"
assert _new_owner != empty(address)
self.owner = _new_owner
log SetOwner(_new_owner)
```
This example sets the `owner` of the contract to our overlord Vitalik Buterin (`0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045`).
```shell
>>> FeeCollector.owner()
"0x40907540d8a6C65c637785e8f8B742ae6b0b9968"
>>> FeeCollector.set_owner("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
>>> FeeCollector.owner()
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
```
::::
### `set_emergency_owner`
::::description[`FeeCollector.set_emergency_owner(_new_owner: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to set a new emergency owner.
| Input | Type | Description |
| ------------ | --------- | ---------------------------------- |
| `_new_owner` | `address` | Address of the new emergency owner |
Emits: `SetEmergencyOwner` event.
```vyper
event SetEmergencyOwner:
emergency_owner: indexed(address)
emergency_owner: public(address)
@external
def set_emergency_owner(_new_owner: address):
"""
@notice Set emergency owner of the contract
@dev Callable only by current owner
@param _new_owner Address of the new emergency owner
"""
assert msg.sender == self.owner, "Only owner"
assert _new_owner != empty(address)
self.emergency_owner = _new_owner
log SetEmergencyOwner(_new_owner)
```
This example sets the `emergency_owner` of the contract to `0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045`.
```shell
>>> FeeCollector.emergency_owner()
"0x40907540d8a6C65c637785e8f8B742ae6b0b9968"
>>> FeeCollector.set_emergency_owner("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
>>> FeeCollector.emergency_owner()
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
```
::::
---
## FeeDistributor
Fees used to be distributed to [`veCRV`](https://etherscan.io/address/0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2) in the form of [`3CRV`](https://etherscan.io/address/0x6c3f90f043a72fa612cbac8115ee7e52bde6e490) tokens, the LP token of the [`threepool`](https://etherscan.io/address/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7), which consists of `USDT`, `USDC`, and `DAI`. After the release of Curve's own stablecoin [`crvUSD`](https://etherscan.io/token/0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E) and following a successful DAO vote to change the reward token to it, a new `FeeDistributor` contract was deployed to distribute fees in the form of `crvUSD` tokens. **Fee claiming always takes place on Ethereum**.
:::vyper[`FeeDistributor.vy`]
The source code for the `FeeDistributor.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/FeeDistributor.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.2.7` and `0.3.7`.
There are two different `FeeDistributor` contracts deployed on Ethereum, depending on the reward token:
- :logos-3CRV: `3CRV`: [0xA464e6DCda8AC41e03616F95f4BC98a13b8922Dc](https://etherscan.io/address/0xa464e6dcda8ac41e03616f95f4bc98a13b8922dc)
- :logos-crvusd: `crvUSD`: [0xD16d5eC345Dd86Fb63C6a9C43c517210F1027914](https://etherscan.io/address/0xD16d5eC345Dd86Fb63C6a9C43c517210F1027914)
```json
[{"name":"CommitAdmin","inputs":[{"name":"admin","type":"address","indexed":false}],"anonymous":false,"type":"event"},{"name":"ApplyAdmin","inputs":[{"name":"admin","type":"address","indexed":false}],"anonymous":false,"type":"event"},{"name":"ToggleAllowCheckpointToken","inputs":[{"name":"toggle_flag","type":"bool","indexed":false}],"anonymous":false,"type":"event"},{"name":"CheckpointToken","inputs":[{"name":"time","type":"uint256","indexed":false},{"name":"tokens","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"Claimed","inputs":[{"name":"recipient","type":"address","indexed":true},{"name":"amount","type":"uint256","indexed":false},{"name":"claim_epoch","type":"uint256","indexed":false},{"name":"max_epoch","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"stateMutability":"nonpayable","type":"constructor","inputs":[{"name":"_voting_escrow","type":"address"},{"name":"_start_time","type":"uint256"},{"name":"_token","type":"address"},{"name":"_admin","type":"address"},{"name":"_emergency_return","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"checkpoint_token","inputs":[],"outputs":[]},{"stateMutability":"view","type":"function","name":"ve_for_at","inputs":[{"name":"_user","type":"address"},{"name":"_timestamp","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"checkpoint_total_supply","inputs":[],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"claim","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"claim","inputs":[{"name":"_addr","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"claim_many","inputs":[{"name":"_receivers","type":"address[20]"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"burn","inputs":[{"name":"_coin","type":"address"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"commit_admin","inputs":[{"name":"_addr","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"apply_admin","inputs":[],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"toggle_allow_checkpoint_token","inputs":[],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"kill_me","inputs":[],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"recover_balance","inputs":[{"name":"_coin","type":"address"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"start_time","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"time_cursor","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"time_cursor_of","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"user_epoch_of","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"last_token_time","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"tokens_per_week","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"voting_escrow","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"token","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"total_received","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"token_last_balance","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"ve_supply","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"admin","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"future_admin","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"can_checkpoint_token","inputs":[],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"emergency_return","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"is_killed","inputs":[],"outputs":[{"name":"","type":"bool"}]}]
```
:::
---
### `last_token_time`
::::description[`FeeDistributor.last_token_time() -> uint256: view`]
Getter for the timestamp of the last token checkpoint.
Returns: timestamp (`uint256`).
```vyper
last_token_time: public(uint256)
```
::::
### `can_checkpoint_token`
::::description[`FeeDistributor.can_checkpoint_token() -> bool: view`]
Function to check whether the `checkpoint_token` function can be called by anyone or only by the admin. The state of this variable can be changed using the `toggle_allow_checkpoint_token` function.
Returns: true or false (`bool`).
```vyper
can_checkpoint_token: public(bool)
```
::::
### `toggle_allow_checkpoint_token`
::::description[`FeeDistributor.toggle_allow_checkpoint_token()`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to toggle permission for checkpointing by an account.
Emits: `ToggleAllowCheckpointToken` event.
```vyper
event ToggleAllowCheckpointToken:
toggle_flag: bool
@external
def toggle_allow_checkpoint_token():
"""
@notice Toggle permission for checkpointing by any account
"""
assert msg.sender == self.admin
flag: bool = not self.can_checkpoint_token
self.can_checkpoint_token = flag
log ToggleAllowCheckpointToken(flag)
```
```shell
>>> FeeDistributor.toggle_allow_checkpoint_token()
```
::::
---
## ve-Supply Checkpoint
Checkpointing the ve-Supply is an essential process to ensure fair reward distribution. It involves periodically recording the total supply of veCRV for each epoch. This process is crucial for accurately distributing fees to veCRV holders based on their balances.
### `checkpoint_total_supply`
::::description[`FeeDistributor.checkpoint_total_supply()`]
Function to update the total supply checkpoint of veCRV for each epoch. The checkpoint is also updated by the first claimant of each new epoch week. This function can be called independently of a claim to reduce claiming gas costs. It ensures that the contract maintains an accurate record of the total veCRV supply at the start of each week, which is essential for correctly distributing fees based on veCRV holdings.
```vyper
@external
def checkpoint_total_supply():
"""
@notice Update the veCRV total supply checkpoint
@dev The checkpoint is also updated by the first claimant each
new epoch week. This function may be called independently
of a claim, to reduce claiming gas costs.
"""
self._checkpoint_total_supply()
@internal
def _checkpoint_total_supply():
ve: address = self.voting_escrow
t: uint256 = self.time_cursor
rounded_timestamp: uint256 = block.timestamp / WEEK * WEEK
VotingEscrow(ve).checkpoint()
for i in range(20):
if t > rounded_timestamp:
break
else:
epoch: uint256 = self._find_timestamp_epoch(ve, t)
pt: Point = VotingEscrow(ve).point_history(epoch)
dt: int128 = 0
if t > pt.ts:
# If the point is at 0 epoch, it can actually be earlier than the first deposit
# Then make dt 0
dt = convert(t - pt.ts, int128)
self.ve_supply[t] = convert(max(pt.bias - pt.slope * dt, 0), uint256)
t += WEEK
self.time_cursor = t
```
This example checkpoints the total supply of veCRV.
```shell
>>> FeeDistributor.checkpoint_total_supply()
```
::::
### `time_cursor`
::::description[`FeeDistributor.time_cursor() -> uint256: view`]
Getter for the timestamp of the last `checkpoint_total_supply` of veCRV.
Returns: timestamp (`uint256`).
```vyper
time_cursor: public(uint256)
```
::::
### `time_cursor_of`
::::description[`FeeDistributor.time_cursor_of(arg0: address) -> uint256: view`]
Getter for the timestamp of the last `checkpoint_total_supply` of veCRV.
| Input | Type | Description |
| ------ | --------- | -------------------- |
| `arg0` | `address` | Address to check for |
Returns: timestamp (`uint256`).
```vyper
time_cursor_of: public(HashMap[address, uint256])
```
This example returns the `time_cursor_of` for a given address.
```shell
>>> FeeDistributor.time_cursor_of('0x7a16fF8270133F063aAb6C9977183D9e72835428')
1719446400
```
::::
### `ve_for_at`
::::description[`FeeDistributor.ve_for_at(_user: address, _timestamp: uint256) -> uint256: view`]
Getter for the veCRV balance of a user at a certain timestamp.
| Input | Type | Description |
| ------------ | --------- | -------------------------------------- |
| `_user` | `address` | Address to query the veCRV balance for |
| `_timestamp` | `uint256` | Timestamp |
Returns: veCRV balance (`uint256`).
```vyper
@view
@external
def ve_for_at(_user: address, _timestamp: uint256) -> uint256:
"""
@notice Get the veCRV balance for `_user` at `_timestamp`
@param _user Address to query balance for
@param _timestamp Epoch time
@return uint256 veCRV balance
"""
ve: address = self.voting_escrow
max_user_epoch: uint256 = VotingEscrow(ve).user_point_epoch(_user)
epoch: uint256 = self._find_timestamp_user_epoch(ve, _user, _timestamp, max_user_epoch)
pt: Point = VotingEscrow(ve).user_point_history(_user, epoch)
return convert(max(pt.bias - pt.slope * convert(_timestamp - pt.ts, int128), 0), uint256)
```
```shell
>>> FeeDistributor.ve_for_at("0x989AEb4d175e16225E39E87d0D97A3360524AD80", 1685972555)
290896146145001156884162140
```
::::
### `ve_supply`
::::description[`FeeDistributor.ve_supply(arg0: uint256) -> uint256: view`]
Getter for the total supply of veCRV at the beginning of an epoch.
| Input | Type | Description |
| ------ | --------- | ---------------------------- |
| `arg0` | `uint256` | Timestamp of the epoch start |
Returns: veCRV supply (`uint256`).
```vyper
ve_supply: public(uint256[1000000000000000]) # VE total supply at week bounds
```
```shell
>>> FeeDistributor.ve_supply(1718841600)
667140493408797243694521600
```
::::
---
## Killing The FeeDistributor
The `FeeDistributor` can be killed by the `admin` of the contract, which is the Curve DAO. Doing so, transfers the entire token balance to the `emergency_return` address and blocks the ability to claim or burn. The contract can not be unkilled.
:::colab[Google Colab Notebook]
A Google Colab notebook that simulates killing the `FeeDistributor` and its respective consequences can be found here: [ Google Colab Notebook](https://colab.research.google.com/drive/1YgjNqZ4TdDEVoa-xTbZIDSPdtOwxuiH9?usp=sharing).
:::
### `is_killed`
::::description[`FeeDistributor.is_killed() -> bool: view`]
Getter method to check if the `FeeDistributor` contract is killed. When killed, the contract blocks `claim` and `burn` and the entire token balance is transferred to the `emergency_return` address.
Returns: true or false (`bool`).
```vyper
is_killed: public(bool)
```
::::
### `kill_me`
::::description[`FeeDistributor.kill_me()`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
By killing the `FeeDistributor`, the entire token balance is transferred to the [`emergency_return`](#emergency_return) address, and the ability to further call the `claim`, `claim_many`, or `burn` functions is blocked.
```vyper
is_killed: public(bool)
@external
def kill_me():
"""
@notice Kill the contract
@dev Killing transfers the entire 3CRV balance to the emergency return address
and blocks the ability to claim or burn. The contract cannot be unkilled.
"""
assert msg.sender == self.admin
self.is_killed = True
token: address = self.token
assert ERC20(token).transfer(self.emergency_return, ERC20(token).balanceOf(self))
```
```shell
>>> FeeDistributor.kill_me()
```
::::
### `emergency_return`
::::description[`FeeDistributor.emergency_return() -> address: view`]
Getter for the emergency return address. This address can not be changed.
Returns: emergency return (`address`).
```vyper
emergency_return: public(address)
```
Due to the fact that the emergency return address can not be changed and Curve used a ownership agent back then when the distributor contract for 3CRV was deployed, this one was set as the emergency return address.
The second fee distributor contract (crvUSD) uses a 5 of 9 multisig, which replaced the ownership agent.
```shell
>>> FeeDistributor.emergency_return() # 3CRV distributor
'0x00669DF67E4827FCc0E48A1838a8d5AB79281909'
>>> FeeDistributor.emergency_return() # crvUSD distributor
'0x467947EE34aF926cF1DCac093870f613C96B1E0c'
```
::::
### `recover_balance`
::::description[`FeeDistributor.recover_balance(_coin: address) -> bool`]
Function to recover ERC20 tokens from the contract. Tokens are sent to the emergency return address. This function only works for tokens other than the address set for `token`. E.g. this function on the 3CRV distributor contract can not be called to transfer 3CRV. The same applied to crvUSD distributor.
| Input | Type | Description |
| ------- | --------- | ----------------- |
| `_coin` | `address` | Tokens to recover |
Returns: true (`bool`).
```vyper
@external
def recover_balance(_coin: address) -> bool:
"""
@notice Recover ERC20 tokens from this contract
@dev Tokens are sent to the emergency return address.
@param _coin Token address
@return bool success
"""
assert msg.sender == self.admin
assert _coin != self.token
amount: uint256 = ERC20(_coin).balanceOf(self)
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("transfer(address,uint256)"),
convert(self.emergency_return, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
return True
```
This example recovers the balance of a given token.
```shell
>>> FeeDistributor.recover_balance("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
true
```
::::
---
## Admin Ownership
### `admin`
::::description[`FeeDistributor.admin() -> address: view`]
Getter for the admin of the contract.
Returns: admin (`address`).
```vyper
admin: public(address)
```
::::
### `future_admin`
::::description[`FeeDistributor.future_admin() -> address: view`]
Getter for the future admin of the contract.
Returns: future admin (`address`).
```vyper
future_admin: public(address)
```
::::
### `commit_admin`
::::description[`FeeDistributor.commit_admin(_addr: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to commit transfer of the ownership.
| Input | Type | Description |
| ------- | --------- | --------------------------------------------- |
| `_addr` | `address` | Address to commit the ownership transfer to |
Emits: `CommitAdmin` event.
```vyper
event CommitAdmin:
admin: address
admin: public(address)
future_admin: public(address)
@external
def commit_admin(_addr: address):
"""
@notice Commit transfer of ownership
@param _addr New admin address
"""
assert msg.sender == self.admin # dev: access denied
self.future_admin = _addr
log CommitAdmin(_addr)
```
This example commits the transfer of the ownership.
```shell
>>> FeeDistributor.commit_admin("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
```
::::
### `apply_admin`
::::description[`FeeDistributor.apply_admin()`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to apply the transfer of the ownership.
Emits: `ApplyAdmin` event.
```vyper
event ApplyAdmin:
admin: address
admin: public(address)
future_admin: public(address)
@external
def apply_admin():
"""
@notice Apply transfer of ownership
"""
assert msg.sender == self.admin
assert self.future_admin != ZERO_ADDRESS
future_admin: address = self.future_admin
self.admin = future_admin
log ApplyAdmin(future_admin)
```
This example applies the transfer of the ownership.
```shell
>>> FeeDistributor.apply_admin()
```
::::
---
## Other Methods
### `start_time`
::::description[`FeeDistributor.start_time() -> uint256: view`]
Getter for the epoch time for fee distribution to start.
Returns: epoch time (`uint256`).
```vyper
start_time: public(uint256)
@external
def __init__(
_voting_escrow: address,
_start_time: uint256,
_token: address,
_admin: address,
_emergency_return: address
):
"""
@notice Contract constructor
@param _voting_escrow VotingEscrow contract address
@param _start_time Epoch time for fee distribution to start
@param _token Fee token address (3CRV)
@param _admin Admin address
@param _emergency_return Address to transfer `_token` balance to
if this contract is killed
"""
t: uint256 = _start_time / WEEK * WEEK
self.start_time = t
self.last_token_time = t
self.time_cursor = t
self.token = _token
self.voting_escrow = _voting_escrow
self.admin = _admin
self.emergency_return = _emergency_return
```
This example returns the `start_time` of the first distribution of rewards.
```shell
>>> FeeDistributor.start_time() # 3CRV Distributor
1600300800 # Thu Sep 17 2020 00:00:00 GMT+0000
>>> FeeDistributor.start_time() # crvUSD Distributor
1718841600 # Thu Jun 20 2024 00:00:00 GMT+0000
```
::::
### `voting_escrow`
::::description[`FeeDistributor.voting_escrow() -> address: view`]
Getter for the voting escrow contract.
Returns: voting escrow (`address`).
```vyper
voting_escrow: public(address)
@external
def __init__(
_voting_escrow: address,
_start_time: uint256,
_token: address,
_admin: address,
_emergency_return: address
):
"""
@notice Contract constructor
@param _voting_escrow VotingEscrow contract address
@param _start_time Epoch time for fee distribution to start
@param _token Fee token address (3CRV)
@param _admin Admin address
@param _emergency_return Address to transfer `_token` balance to
if this contract is killed
"""
t: uint256 = _start_time / WEEK * WEEK
self.start_time = t
self.last_token_time = t
self.time_cursor = t
self.token = _token
self.voting_escrow = _voting_escrow
self.admin = _admin
self.emergency_return = _emergency_return
```
::::
### `token`
::::description[`FeeDistributor.token() -> address: view`]
Getter for the token address in which the fees are distributed.
Returns: reward token (`address`).
```vyper
token: public(address)
@external
def __init__(
_voting_escrow: address,
_start_time: uint256,
_token: address,
_admin: address,
_emergency_return: address
):
"""
@notice Contract constructor
@param _voting_escrow VotingEscrow contract address
@param _start_time Epoch time for fee distribution to start
@param _token Fee token address (crvUSD)
@param _admin Admin address
@param _emergency_return Address to transfer `_token` balance to
if this contract is killed
"""
t: uint256 = _start_time / WEEK * WEEK
self.start_time = t
self.last_token_time = t
self.time_cursor = t
self.token = _token
self.voting_escrow = _voting_escrow
self.admin = _admin
self.emergency_return = _emergency_return
```
::::
### `user_epoch_of`
::::description[`FeeDistributor.user_epoch_of(arg0: address) -> uint256: view`]
Getter for the user epoch of an address. This value increments by one each time rewards are claimed.
| Input | Type | Description |
| ------ | --------- | --------------------------------- |
| `arg0` | `address` | Address to get the user epoch for |
Returns: user epoch (`uint256`).
```vyper
user_epoch_of: public(HashMap[address, uint256])
```
This example returns the user epoch of a given address.
```shell
>>> FeeDistributor.user_epoch_of("0x989AEb4d175e16225E39E87d0D97A3360524AD80")
7739
```
::::
---
## FeeSplitter
The `FeeSplitter` is a contract that collects and splits accumulated crvUSD fees from crvUSD Controllers[^1] in a single transaction and distributes them across other contracts according to predetermined weights.
[^1]: These are Controllers from where crvUSD is minted. See here: https://crvusd.curve.fi/
:::vyper[`FeeSplitter.vy`]
The source code for the `FeeSplitter.vy` contract can be found on [GitHub](https://github.com/curvefi/fee-splitter/blob/main/contracts/FeeSplitter.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.4.0` and utilizes a [snekmate module](https://github.com/pcaversaccio/snekmate/blob/main/src/snekmate/auth/ownable.vy) to handle contract ownership.
The contract is deployed on :logos-ethereum: Ethereum at [`0x2dFd89449faff8a532790667baB21cF733C064f2`](https://etherscan.io/address/0x2dfd89449faff8a532790667bab21cf733c064f2).
The source code was audited by [:logos-chainsecurity: ChainSecurity](https://www.chainsecurity.com/). The full audit report can be found [here](https://github.com/curvefi/fee-splitter/blob/main/audits/ChainSecurity.pdf).
```json
[{"anonymous":false,"inputs":[],"name":"SetReceivers","type":"event"},{"anonymous":false,"inputs":[],"name":"LivenessProtectionTriggered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"receiver","type":"address"},{"indexed":false,"name":"weight","type":"uint256"}],"name":"FeeDispatched","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previous_owner","type":"address"},{"indexed":true,"name":"new_owner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"inputs":[{"name":"new_owner","type":"address"}],"name":"transfer_ownership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounce_ownership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"update_controllers","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"n_controllers","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"}],"name":"allowed_controllers","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint256"}],"name":"controllers","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"dispatch_fees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"controllers","type":"address[]"}],"name":"dispatch_fees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"name":"addr","type":"address"},{"name":"weight","type":"uint256"}],"name":"receivers","type":"tuple[]"}],"name":"set_receivers","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"excess_receiver","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"n_receivers","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"version","outputs":[{"name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint256"}],"name":"receivers","outputs":[{"components":[{"name":"addr","type":"address"},{"name":"weight","type":"uint256"}],"name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_crvusd","type":"address"},{"name":"_factory","type":"address"},{"components":[{"name":"addr","type":"address"},{"name":"weight","type":"uint256"}],"name":"receivers","type":"tuple[]"},{"name":"owner","type":"address"}],"outputs":[],"stateMutability":"nonpayable","type":"constructor"}]
```
:::

---
## Dispatching Fees, Receivers and Weights
The contract consolidates the process of claiming and distributing fees into a single external function called `dispatch_fees`. Calling this function is fully permissionless and can be done by anyone.
The function makes use of a helper module called `ControllerMulticlaim.vy`, which aims to track all `crvUSD Controllers` from the `Factory` and provides an interface for claiming fees from them. By default, the `dispatch_fees` function claims fees from all the controllers registered in the [`controllers`](#controllers) array of the `ControllerMulticlaim.vy` module, but the function also allows for only claiming fees from specified controllers.
All receiving addresses are stored in the `receivers` variable and are stored in a `Receiver` struct, which includes the address and its corresponding weight:
```py
struct Receiver:
addr: address
weight: uint256
```
The weight assigned to a receiver is set when a receiver address is added using the `set_receivers` function. In addition to static weights, the contract supports dynamic weights based on various conditions defined in the receiver contract itself. To support dynamic weights, the receiver contract must implement `DYNAMIC_WEIGHT_EIP165_ID: constant(bytes4) = 0xA1AAB33F` a la EIP-165 and a `weight()` function which returns the actual dynamic weight.
If a weight is dynamic, the `weight` value in the struct acts as an upper cap. If the actual dynamic weight returned by the receiving contract is less than the defined weight in the struct, the unused weight is rolled over to the weight of the `excess_receiver`.
The FeeSplitter supports both static and dynamic weights for fee distribution. Dynamic weights allow for more flexible allocation based on changing conditions, while still respecting a maximum cap.
Consider the following receivers and their respective weight caps:
- `receiver1` has a **dynamic** weight with a cap of 10%
- `receiver2` has a **static** weight of 10%
- `receiver3` has a **static** weight of 80%
Due to the dynamic nature of `receiver1`'s weight, the actual weight is determined in the receiver contract based on different conditions (e.g. ratio of staked assets, etc.). If the receiver contract would ask for more than 10% of the total weight, the weight is ultimately capped at 10%. If he asks for less than 10%, the spare weight is then rolled over to the weight of the `excess_receiver` (in this case `receiver3`).
As a result, the final weights are adjusted as follows:
- `receiver1` ends up with a weight of 8%
- `receiver2` remains at 10%
- `receiver3` receives an adjusted weight of 82%, which includes the 2% rolled over from `receiver1`.
---
The general logic of dynamic weights is as follows:
```mermaid
flowchart TD
IsDynamic["Dynamic weight?"]
IsDynamic -->|Yes| DynamicCalc["Weight calculation in receiver contract"]
IsDynamic -->|No| StaticWeight["Use weight from Receiver struct"]
DynamicCalc --> CheckCap["Returned weightexceeds defined weightin Receiver struct?"]
CheckCap -->|Yes| CapWeight["Use defined weightin Receiver struct"]
CheckCap -->|No| UseWeight["Use actual dynamic weight"]
UseWeight --> RollOver["Roll over unused weightto excess_receiver"]
style IsDynamic fill:#e6e6fa,stroke:#483d8b,stroke-width:2px
style CheckCap fill:#e6e6fa,stroke:#483d8b,stroke-width:2px
style DynamicCalc fill:#f5f5f5,stroke:#708090,stroke-width:1px
style StaticWeight fill:#f5f5f5,stroke:#708090,stroke-width:1px
style CapWeight fill:#f5f5f5,stroke:#708090,stroke-width:1px
style UseWeight fill:#f5f5f5,stroke:#708090,stroke-width:1px
style RollOver fill:#f5f5f5,stroke:#708090,stroke-width:1px
```
---
### `dispatch_fees`
::::description[`FeeSplitter.dispatch_fees(controllers: DynArray[multiclaim.Controller, multiclaim.MAX_CONTROLLERS]=[])`]
:::warning[Claiming from Controllers not in `controllers`]
Function reverts when trying to claim from `Controllers` that are not registered in the `controllers` array.
:::
Function to claim crvUSD fees from crvUSD Controllers and distribute them to addresses and weights defined in the `receivers` variable. This function is callable by anyone.
| Input | Type | Description |
| ------------- | ------------------------------------------------------------- | ----------- |
| `controllers` | `DynArray[multiclaim.Controller, multiclaim.MAX_CONTROLLERS]` | Array of `Controllers` to claim from; defaults to claiming fees from all Controllers in [`controllers`](#controllers) |
Emits: `FeeDispatched` event.
```vyper
struct Receiver:
addr: address
weight: uint256
# maximum number of splits
MAX_RECEIVERS: constant(uint256) = 100
# maximum basis points (100%)
MAX_BPS: constant(uint256) = 10_000
DYNAMIC_WEIGHT_EIP165_ID: constant(bytes4) = 0xA1AAB33F
# receiver logic
receivers: public(DynArray[Receiver, MAX_RECEIVERS])
crvusd: immutable(IERC20)
@nonreentrant
@external
def dispatch_fees(
controllers: DynArray[
multiclaim.Controller, multiclaim.MAX_CONTROLLERS
] = []
):
"""
@notice Claim fees from all controllers and distribute them
@param controllers The list of controllers to claim fees from (default: all)
@dev Splits and transfers the balance according to the receivers weights
"""
multiclaim.claim_controller_fees(controllers)
balance: uint256 = staticcall crvusd.balanceOf(self)
excess: uint256 = 0
# by iterating over the receivers, rather than the indices,
# we avoid an oob check at every iteration.
i: uint256 = 0
for r: Receiver in self.receivers:
weight: uint256 = r.weight
if self._is_dynamic(r.addr):
dynamic_weight: uint256 = staticcall DynamicWeight(r.addr).weight()
# `weight` acts as a cap to the dynamic weight, preventing
# receivers to ask for more than what they are allowed to.
if dynamic_weight < weight:
excess += weight - dynamic_weight
weight = dynamic_weight
# if we're at the last iteration, it means `r` is the excess
# receiver, therefore we add the excess to its weight.
if i == len(self.receivers) - 1:
weight += excess
extcall crvusd.transfer(r.addr, balance * weight // MAX_BPS)
log FeeDispatched(r.addr, weight)
i += 1
def _is_dynamic(addr: address) -> bool:
"""
This function covers the following cases without reverting:
1. The address is an EIP-165 compliant contract that supports
the dynamic weight interface (returns True).
2. The address is a contract that does not comply to EIP-165
(returns False).
3. The address is an EIP-165 compliant contract that does not
support the dynamic weight interface (returns False).
4. The address is an EOA (returns False).
"""
success: bool = False
response: Bytes[32] = b""
success, response = raw_call(
addr,
abi_encode(
DYNAMIC_WEIGHT_EIP165_ID,
method_id("supportsInterface(bytes4)"),
),
max_outsize=32,
is_static_call=True,
revert_on_failure=False,
)
return success and convert(response, bool) or len(response) > 32
```
```vyper
factory: immutable(ControllerFactory)
allowed_controllers: public(HashMap[Controller, bool])
controllers: public(DynArray[Controller, MAX_CONTROLLERS])
# maximum number of claims in a single transaction
MAX_CONTROLLERS: constant(uint256) = 50
@deploy
def __init__(_factory: ControllerFactory):
assert _factory.address != empty(address), "zeroaddr: factory"
factory = _factory
def claim_controller_fees(controllers: DynArray[Controller, MAX_CONTROLLERS]):
"""
@notice Claims admin fees from a list of controllers.
@param controllers The list of controllers to claim fees from.
@dev For the claim to succeed, the controller must be in the list of
allowed controllers. If the list of controllers is empty, all
controllers in the factory are claimed from.
"""
if len(controllers) == 0:
for c: Controller in self.controllers:
extcall c.collect_fees()
else:
for c: Controller in controllers:
if not self.allowed_controllers[c]:
raise "controller: not in factory"
extcall c.collect_fees()
@nonreentrant
@external
def update_controllers():
"""
@notice Update the list of controllers so that it corresponds to the
list of controllers in the factory.
@dev The list of controllers can only add new controllers from the
factory when updated.
"""
old_len: uint256 = len(self.controllers)
new_len: uint256 = staticcall factory.n_collaterals()
for i: uint256 in range(new_len - old_len, bound=MAX_CONTROLLERS):
i_shifted: uint256 = i + old_len
c: Controller = Controller(staticcall factory.controllers(i_shifted))
self.allowed_controllers[c] = True
self.controllers.append(c)
```
The following example demonstrates how to dispatch fees from all `Controller` contracts listed in the [`controllers`](#controllers) section.
```shell
>>> FeeSplitter.dispatch_fees()
```
The next example shows how to dispatch fees from specific `Controller` contracts by directly providing their addresses - specifically, the `sfrxETH` and `wstETH` controllers.
```shell
>>> FeeSplitter.dispatch_fees([
"0x8472A9A7632b173c8Cf3a86D3afec50c35548e76", # sfrxETH Controller
"0x100dAa78fC509Db39Ef7D04DE0c1ABD299f4C6CE"]) # wstETH Controller
```
::::
### `receivers`
::::description[`FeeSplitter.receivers(arg0: uint256) -> Receiver: view`]
Getter for the addresses and weights of receivers at index `arg0`. Receivers can be added/removed/modified by the DAO using the `set_receivers` function.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `arg0` | `uint256` | Index of the receiver |
Returns: `Receiver` struct consisting of `address` and `weight` (`Receiver`).
```vyper
struct Receiver:
addr: address
weight: uint256
receivers: public(DynArray[Receiver, MAX_RECEIVERS])
```
::::
### `n_receivers`
::::description[`FeeSplitter.n_receivers() -> uint256: view`]
Getter for the total number of receivers the fees are split to.
Returns: number of receivers (`uint256`).
```vyper
receivers: public(DynArray[Receiver, MAX_RECEIVERS])
@view
@external
def n_receivers() -> uint256:
"""
@notice Get the number of receivers
@return The number of receivers
"""
return len(self.receivers)
```
::::
### `excess_receiver`
::::description[`FeeSplitter.excess_receiver() -> address: view`]
Getter for the excess receiver. That is the last receiver address in [`receivers`](#receivers) and is the one that receives additional weight on top of its own weight, if prior receivers with a dynamic weight allocate less than their cap (see this example at the top).
Returns: excess receiver (`address`).
```vyper
receivers: public(DynArray[Receiver, MAX_RECEIVERS])
@view
@external
def excess_receiver() -> address:
"""
@notice Get the excess receiver, that is the receiver
that, on top of his weight, will receive an additional
weight if other receivers (with a dynamic weight) ask
for less than their cap.
@return The address of the excess receiver.
"""
receivers_length: uint256 = len(self.receivers)
return self.receivers[receivers_length - 1].addr
```
::::
### `set_receivers`
::::description[`FeeSplitter.set_receivers(receivers: DynArray[Receiver, MAX_RECEIVERS])`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Function to set receivers and their respective weights. New receivers can not simply be added or removed from the existing array of receivers. One must include the current receivers in the array of `Receiver` structs. The weight is based on a scale of 1e5, meaning e.g. 100% corresponds to a weight value of 10000, and 50% would be a weight value of 5000.
The function will revert if a receiver address is `ZERO_ADDRESS`, if the weight is `0` or greater than `10000` (`MAX_BPS`), or if the sum of the weights of all receivers does not equal `10000` (100%).
Additionally, when adding receivers with dynamic weights, they must support the `DYNAMIC_WEIGHT_EIP165_ID` as specified in EIP-165 and implement a `weight()` function which returns the weight the receiver asks for.
| Input | Type | Description |
| ----------- | ----------------------------------- | ------------------------------------------------------------ |
| `receivers` | `DynArray[Receiver, MAX_RECEIVERS]` | Array of `Receiver` structs containing of address and weight |
Emits: `SetReceivers` event.
The following source code includes all changes up to commit hash [581b897](https://github.com/curvefi/autobribe/tree/581b8978f91e426c648cf6243420fee5276166b7); any changes made after this commit are not included.
```vyper
from snekmate.auth import ownable
struct Receiver:
addr: address
weight: uint256
# maximum number of splits
MAX_RECEIVERS: constant(uint256) = 100
# maximum basis points (100%)
MAX_BPS: constant(uint256) = 10_000
DYNAMIC_WEIGHT_EIP165_ID: constant(bytes4) = 0xA1AAB33F
receivers: public(DynArray[Receiver, MAX_RECEIVERS])
@external
def set_receivers(receivers: DynArray[Receiver, MAX_RECEIVERS]):
"""
@notice Set the receivers, the last one is the excess receiver.
@param receivers The new receivers's list.
@dev The excess receiver is always the last element in the
`self.receivers` array.
"""
ownable._check_owner()
self._set_receivers(receivers)
def _set_receivers(receivers: DynArray[Receiver, MAX_RECEIVERS]):
assert len(receivers) > 0, "receivers: empty"
total_weight: uint256 = 0
for r: Receiver in receivers:
assert r.addr != empty(address), "zeroaddr: receivers"
assert r.weight > 0 and r.weight <= MAX_BPS, "receivers: invalid weight"
total_weight += r.weight
assert total_weight == MAX_BPS, "receivers: total weight != MAX_BPS"
self.receivers = receivers
log SetReceivers()
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
In this example, two receiver addresses are set with specific weights:
1. The first address is the Vyper Gitcoin address with a weight of 10%.
2. The second address is the `FeeCollector`, assigned the remaining 90%.
```shell
>>> FeeSplitter.set_receivers([
("0x70CCBE10F980d80b7eBaab7D2E3A73e87D67B775", 1000),
("0xa2Bcd1a4Efbd04B63cd03f5aFf2561106ebCCE00", 9000)])
```
If, later on, a third receiver with a weight of 1000 (at the cost of reducing the second receiver's weight) should be added, we would include the first two receivers in the updated list:
```shell
>>> FeeSplitter.set_receivers([
("0x70CCBE10F980d80b7eBaab7D2E3A73e87D67B775", 1000),
("0x1234567890123456789012345678901234567890", 1000),
("0xa2Bcd1a4Efbd04B63cd03f5aFf2561106ebCCE00", 8000)])
```
::::
---
## Controller Management
The contract maintains a list of [`controllers`](#controllers) from which fees can be claimed. This list is updated to match the controllers registered in the crvUSD Factory contract. The [`update_controllers`](#update_controllers) function is used to keep this list current.
### `controllers`
::::description[`FeeSplitter.controllers(arg0: uint256) -> IController: view`]
Getter for the `Controller` at index `arg0`.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `arg0` | `uint256` | Index of the `Controller` |
Returns: controller (`IController`).
```python
initializes: multiclaim
exports: (
multiclaim.update_controllers,
multiclaim.n_controllers,
multiclaim.allowed_controllers,
multiclaim.controllers
)
```
```vyper
from contracts.interfaces import IController
controllers: public(DynArray[IController, MAX_CONTROLLERS])
MAX_CONTROLLERS: constant(uint256) = 50
```
```vyper
@external
def collect_fees() -> uint256:
...
```
::::
### `allowed_controllers`
::::description[`FeeSplitter.allowed_controllers(arg0: address) -> bool: view`]
Getter method to check whether a controller is allowed.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `arg0` | `address` | Address of the `Controller` |
Returns: true or false whether the controller is allowed (`bool`).
The following source code includes all changes up to commit hash [581b897](https://github.com/curvefi/autobribe/tree/581b8978f91e426c648cf6243420fee5276166b7); any changes made after this commit are not included.
```python
initializes: multiclaim
exports: (
multiclaim.update_controllers,
multiclaim.n_controllers,
multiclaim.allowed_controllers,
multiclaim.controllers
)
```
```python
from contracts.interfaces import IController
allowed_controllers: public(HashMap[IController, bool])
```
::::
### `n_controllers`
::::description[`FeeSplitter.n_controllers() -> uint256: view`]
Getter for the number of `Controllers` added to the contract from which potentially (if they are allowed) fees can be claimed from.
Returns: number of controllers (`uint256`).
The following source code includes all changes up to commit hash [581b897](https://github.com/curvefi/autobribe/tree/581b8978f91e426c648cf6243420fee5276166b7); any changes made after this commit are not included.
```python
initializes: multiclaim
exports: (
multiclaim.update_controllers,
multiclaim.n_controllers,
multiclaim.allowed_controllers,
multiclaim.controllers
)
```
```vyper
controllers: public(DynArray[IController, MAX_CONTROLLERS])
MAX_CONTROLLERS: constant(uint256) = 50
@view
@external
def n_controllers() -> uint256:
return len(self.controllers)
```
::::
### `update_controllers`
::::description[`FeeSplitter.update_controllers()`]
Function to update the list of `Controllers` to correspond with the list of `Controllers` in the `Factory`. Calling this function is fully permissionless and can be done by anyone.
This function uses the `n_collaterals` function from the `IControllerFactory` interface to determine the total number of Controllers. If the local list of Controllers is not up to date, the function adds the missing Controllers to the dynamic array in `controllers`. Simultaneously, it updates the `allowed_controllers` mapping to permit claiming from the newly added Controllers.
The following source code includes all changes up to commit hash [581b897](https://github.com/curvefi/autobribe/tree/581b8978f91e426c648cf6243420fee5276166b7); any changes made after this commit are not included.
```python
initializes: multiclaim
exports: (
multiclaim.update_controllers,
multiclaim.n_controllers,
multiclaim.allowed_controllers,
multiclaim.controllers
)
```
```vyper
from contracts.interfaces import IControllerFactory
from contracts.interfaces import IController
factory: immutable(IControllerFactory)
allowed_controllers: public(HashMap[IController, bool])
controllers: public(DynArray[IController, MAX_CONTROLLERS])
# maximum number of claims in a single transaction
MAX_CONTROLLERS: constant(uint256) = 50
@nonreentrant
@external
def update_controllers():
"""
@notice Update the list of controllers so that it corresponds to the
list of controllers in the factory.
@dev The list of controllers can only add new controllers from the
factory when updated.
"""
old_len: uint256 = len(self.controllers)
new_len: uint256 = staticcall factory.n_collaterals()
for i: uint256 in range(old_len, new_len, bound=MAX_CONTROLLERS):
c: IController = IController(staticcall factory.controllers(i))
self.allowed_controllers[c] = True
self.controllers.append(c)
```
```vyper
@external
@view
def controllers(index: uint256) -> address:
...
@external
@view
def n_collaterals() -> uint256:
...
```
In this example, the `update_controllers` function is called to synchronize the list of `Controllers` with the actual list in the `Factory`. To demonstrate the function's functionality, we assume an additional `Controller` has been created since the last update.
```shell
>>> FeeSplitter.n_controllers()
5
>>> FeeSplitter.update_controllers()
>>> FeeSplitter.n_controllers()
6
```
::::
---
## Contract Ownership
Ownership of the contract is managed using the [`ownable.vy`](https://github.com/pcaversaccio/snekmate/blob/main/src/snekmate/auth/ownable.vy) module from [Snekmate](https://github.com/pcaversaccio/snekmate) which implements a basic control access mechanism, where there is an `owner` that can be granted exclusive access to specific functions.
### `owner`
::::description[`FeeSplitter.owner() -> address: view`]
Getter for the owner of the contract. This is the address that can call restricted functions like `transfer_ownership`, `renounce_ownership` or `set_receivers`.
Returns: contract owner (`address`).
The following source code includes all changes up to commit hash [581b897](https://github.com/curvefi/autobribe/tree/581b8978f91e426c648cf6243420fee5276166b7); any changes made after this commit are not included.
```vyper
from snekmate.auth import ownable
initializes: ownable
exports: (
ownable.transfer_ownership,
ownable.renounce_ownership,
ownable.owner
)
@deploy
def __init__(
_crvusd: IERC20,
_factory: multiclaim.IControllerFactory,
receivers: DynArray[Receiver, MAX_RECEIVERS],
owner: address,
):
"""
@notice Contract constructor
@param _crvusd The address of the crvUSD token contract
@param _factory The address of the crvUSD controller factory
@param receivers The list of receivers (address, weight).
Last item in the list is the excess receiver by default.
@param owner The address of the contract owner
"""
assert _crvusd.address != empty(address), "zeroaddr: crvusd"
assert owner != empty(address), "zeroaddr: owner"
ownable.__init__()
ownable._transfer_ownership(owner)
multiclaim.__init__(_factory)
# setting immutables
crvusd = _crvusd
# set the receivers
self._set_receivers(receivers)
```
```vyper
owner: public(address)
@deploy
@payable
def __init__():
"""
@dev To omit the opcodes for checking the `msg.value`
in the creation-time EVM bytecode, the constructor
is declared as `payable`.
@notice The `owner` role will be assigned to
the `msg.sender`.
"""
self._transfer_ownership(msg.sender)
@internal
def _transfer_ownership(new_owner: address):
"""
@dev Transfers the ownership of the contract
to a new account `new_owner`.
@notice This is an `internal` function without
access restriction.
@param new_owner The 20-byte address of the new owner.
"""
old_owner: address = self.owner
self.owner = new_owner
log OwnershipTransferred(old_owner, new_owner)
```
::::
### `transfer_ownership`
::::description[`FeeSplitter.transfer_ownership(new_owner: address)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Function to transfer the ownership of the contract to a new address.
| Input | Type | Description |
| ---------- | --------- | ------------ |
| `new_owner` | `address` | New owner of the contract |
Emits: `OwnershipTransferred` event.
The following source code includes all changes up to commit hash [581b897](https://github.com/curvefi/autobribe/tree/581b8978f91e426c648cf6243420fee5276166b7); any changes made after this commit are not included.
```vyper
from snekmate.auth import ownable
initializes: ownable
exports: (
ownable.transfer_ownership,
ownable.renounce_ownership,
ownable.owner
)
@deploy
def __init__(
_crvusd: IERC20,
_factory: multiclaim.IControllerFactory,
receivers: DynArray[Receiver, MAX_RECEIVERS],
owner: address,
):
"""
@notice Contract constructor
@param _crvusd The address of the crvUSD token contract
@param _factory The address of the crvUSD controller factory
@param receivers The list of receivers (address, weight).
Last item in the list is the excess receiver by default.
@param owner The address of the contract owner
"""
assert _crvusd.address != empty(address), "zeroaddr: crvusd"
assert owner != empty(address), "zeroaddr: owner"
ownable.__init__()
ownable._transfer_ownership(owner)
multiclaim.__init__(_factory)
# setting immutables
crvusd = _crvusd
# set the receivers
self._set_receivers(receivers)
```
```vyper
owner: public(address)
event OwnershipTransferred:
previous_owner: indexed(address)
new_owner: indexed(address)
@external
def transfer_ownership(new_owner: address):
"""
@dev Transfers the ownership of the contract
to a new account `new_owner`.
@notice Note that this function can only be
called by the current `owner`. Also,
the `new_owner` cannot be the zero address.
@param new_owner The 20-byte address of the new owner.
"""
self._check_owner()
assert new_owner != empty(address), "ownable: new owner is the zero address"
self._transfer_ownership(new_owner)
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
@internal
def _transfer_ownership(new_owner: address):
"""
@dev Transfers the ownership of the contract
to a new account `new_owner`.
@notice This is an `internal` function without
access restriction.
@param new_owner The 20-byte address of the new owner.
"""
old_owner: address = self.owner
self.owner = new_owner
log OwnershipTransferred(old_owner, new_owner)
```
In this example, the ownership of the contract is transferred to a new address. The ownership is transferred from the Curve DAO to our overlord Vitalik Buterin.
```shell
>>> FeeSplitter.owner()
"0x40907540d8a6C65c637785e8f8B742ae6b0b9968"
>>> FeeSplitter.transfer_ownership("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
>>> FeeSplitter.owner()
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
```
::::
### `renounce_ownership`
::::description[`FeeSplitter.renounce_ownership()`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current `owner` of the contract.
:::
Function to renounce the ownership of the contract. Calling this method will leave the contract without an owner, thereby removing any functionality that is only available to the owner.
Emits: `OwnershipTransferred` event.
The following source code includes all changes up to commit hash [581b897](https://github.com/curvefi/autobribe/tree/581b8978f91e426c648cf6243420fee5276166b7); any changes made after this commit are not included.
```vyper
from snekmate.auth import ownable
initializes: ownable
exports: (
ownable.transfer_ownership,
ownable.renounce_ownership,
ownable.owner
)
@deploy
def __init__(
_crvusd: IERC20,
_factory: multiclaim.IControllerFactory,
receivers: DynArray[Receiver, MAX_RECEIVERS],
owner: address,
):
"""
@notice Contract constructor
@param _crvusd The address of the crvUSD token contract
@param _factory The address of the crvUSD controller factory
@param receivers The list of receivers (address, weight).
Last item in the list is the excess receiver by default.
@param owner The address of the contract owner
"""
assert _crvusd.address != empty(address), "zeroaddr: crvusd"
assert owner != empty(address), "zeroaddr: owner"
ownable.__init__()
ownable._transfer_ownership(owner)
multiclaim.__init__(_factory)
# setting immutables
crvusd = _crvusd
# set the receivers
self._set_receivers(receivers)
```
```vyper
owner: public(address)
event OwnershipTransferred:
previous_owner: indexed(address)
new_owner: indexed(address)
@external
def renounce_ownership():
"""
@dev Leaves the contract without an owner.
@notice Renouncing ownership will leave the
contract without an owner, thereby
removing any functionality that is
only available to the owner.
"""
self._check_owner()
self._transfer_ownership(empty(address))
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
@internal
def _transfer_ownership(new_owner: address):
"""
@dev Transfers the ownership of the contract
to a new account `new_owner`.
@notice This is an `internal` function without
access restriction.
@param new_owner The 20-byte address of the new owner.
"""
old_owner: address = self.owner
self.owner = new_owner
log OwnershipTransferred(old_owner, new_owner)
```
In this example, the ownership of the contract is renounced.
```shell
>>> FeeSplitter.owner()
"0x40907540d8a6C65c637785e8f8B742ae6b0b9968"
>>> FeeSplitter.renounce_ownership()
>>> FeeSplitter.owner()
"0x0000000000000000000000000000000000000000"
```
::::
---
## Other Methods
### `version`
::::description[`FeeSplitter.version() -> String[8]: view`]
Getter for the version of the contract.
Returns: version (`String[8]`).
```vyper
version: public(constant(String[8])) = "0.1.0" # no guarantees on abi stability
```
This example fetches the version of the `FeeSplitter` contract.
::::
---
## Hooker
The `Hooker` contract is a versatile and essential component within the Curve Finance ecosystem, designed to support and manage hooks that interact with the `FeeCollector` contract. This contract enables the execution of predefined actions (hooks) that can be triggered under specific conditions, such as during the fee collection process. It handles the calculation and distribution of compensations, ensuring that hooks are executed correctly and at the appropriate times.
:::vyper[`Hooker.vy`]
The source code for the `Hooker.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-burners/blob/main/contracts/hooks/Hooker.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
The `Hooker` contract is deployed on the following chains:
- :logos-ethereum: Ethereum at [`0x9A9DF35cd8E88565694CA6AD5093c236C7f6f69D`](https://etherscan.io/address/0x9A9DF35cd8E88565694CA6AD5093c236C7f6f69D)
- :logos-gnosis: Gnosis at [`0xE898893ebAe7b75dc4cAB0fb16e24137309ff178`](https://gnosisscan.io/address/0xE898893ebAe7b75dc4cAB0fb16e24137309ff178)
```json
[{"name":"DutyAct","inputs":[],"anonymous":false,"type":"event"},{"name":"Act","inputs":[{"name":"receiver","type":"address","indexed":true},{"name":"compensation","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"HookShot","inputs":[{"name":"hook_id","type":"uint8","indexed":true},{"name":"compensation","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"stateMutability":"nonpayable","type":"constructor","inputs":[{"name":"_fee_collector","type":"address"},{"name":"_initial_oth","type":"tuple[]","components":[{"name":"to","type":"address"},{"name":"foreplay","type":"bytes"},{"name":"compensation_strategy","type":"tuple","components":[{"name":"amount","type":"uint256"},{"name":"cooldown","type":"tuple","components":[{"name":"duty_counter","type":"uint64"},{"name":"used","type":"uint64"},{"name":"limit","type":"uint64"}]},{"name":"start","type":"uint256"},{"name":"end","type":"uint256"},{"name":"dutch","type":"bool"}]},{"name":"duty","type":"bool"}]},{"name":"_initial_oth_inputs","type":"tuple[]","components":[{"name":"hook_id","type":"uint8"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"}]},{"name":"_initial_hooks","type":"tuple[]","components":[{"name":"to","type":"address"},{"name":"foreplay","type":"bytes"},{"name":"compensation_strategy","type":"tuple","components":[{"name":"amount","type":"uint256"},{"name":"cooldown","type":"tuple","components":[{"name":"duty_counter","type":"uint64"},{"name":"used","type":"uint64"},{"name":"limit","type":"uint64"}]},{"name":"start","type":"uint256"},{"name":"end","type":"uint256"},{"name":"dutch","type":"bool"}]},{"name":"duty","type":"bool"}]}],"outputs":[]},{"stateMutability":"view","type":"function","name":"calc_compensation","inputs":[{"name":"_hook_inputs","type":"tuple[]","components":[{"name":"hook_id","type":"uint8"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"}]}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"calc_compensation","inputs":[{"name":"_hook_inputs","type":"tuple[]","components":[{"name":"hook_id","type":"uint8"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"}]},{"name":"_duty","type":"bool"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"calc_compensation","inputs":[{"name":"_hook_inputs","type":"tuple[]","components":[{"name":"hook_id","type":"uint8"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"}]},{"name":"_duty","type":"bool"},{"name":"_ts","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"payable","type":"function","name":"duty_act","inputs":[{"name":"_hook_inputs","type":"tuple[]","components":[{"name":"hook_id","type":"uint8"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"}]}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"payable","type":"function","name":"duty_act","inputs":[{"name":"_hook_inputs","type":"tuple[]","components":[{"name":"hook_id","type":"uint8"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"}]},{"name":"_receiver","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"payable","type":"function","name":"act","inputs":[{"name":"_hook_inputs","type":"tuple[]","components":[{"name":"hook_id","type":"uint8"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"}]}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"payable","type":"function","name":"act","inputs":[{"name":"_hook_inputs","type":"tuple[]","components":[{"name":"hook_id","type":"uint8"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"}]},{"name":"_receiver","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"payable","type":"function","name":"one_time_hooks","inputs":[{"name":"_hooks","type":"tuple[]","components":[{"name":"to","type":"address"},{"name":"foreplay","type":"bytes"},{"name":"compensation_strategy","type":"tuple","components":[{"name":"amount","type":"uint256"},{"name":"cooldown","type":"tuple","components":[{"name":"duty_counter","type":"uint64"},{"name":"used","type":"uint64"},{"name":"limit","type":"uint64"}]},{"name":"start","type":"uint256"},{"name":"end","type":"uint256"},{"name":"dutch","type":"bool"}]},{"name":"duty","type":"bool"}]},{"name":"_inputs","type":"tuple[]","components":[{"name":"hook_id","type":"uint8"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"}]}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_hooks","inputs":[{"name":"_new_hooks","type":"tuple[]","components":[{"name":"to","type":"address"},{"name":"foreplay","type":"bytes"},{"name":"compensation_strategy","type":"tuple","components":[{"name":"amount","type":"uint256"},{"name":"cooldown","type":"tuple","components":[{"name":"duty_counter","type":"uint64"},{"name":"used","type":"uint64"},{"name":"limit","type":"uint64"}]},{"name":"start","type":"uint256"},{"name":"end","type":"uint256"},{"name":"dutch","type":"bool"}]},{"name":"duty","type":"bool"}]}],"outputs":[]},{"stateMutability":"pure","type":"function","name":"supportsInterface","inputs":[{"name":"_interface_id","type":"bytes4"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"recover","inputs":[{"name":"_coins","type":"address[]"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"fee_collector","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"hooks","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"tuple","components":[{"name":"to","type":"address"},{"name":"foreplay","type":"bytes"},{"name":"compensation_strategy","type":"tuple","components":[{"name":"amount","type":"uint256"},{"name":"cooldown","type":"tuple","components":[{"name":"duty_counter","type":"uint64"},{"name":"used","type":"uint64"},{"name":"limit","type":"uint64"}]},{"name":"start","type":"uint256"},{"name":"end","type":"uint256"},{"name":"dutch","type":"bool"}]},{"name":"duty","type":"bool"}]}]},{"stateMutability":"view","type":"function","name":"buffer_amount","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"duty_counter","inputs":[],"outputs":[{"name":"","type":"uint64"}]}]
```
:::
*The contract has the following key features:*
- **Hook Management**: Hooks can be added to the contract via the `set_hooks` function. These hooks define actions to be executed, including target addresses, method data, and compensation strategies.
- **Compensation Calculation**: The contract includes a comprehensive system for calculating compensation for executing hooks, based on predefined strategies. This ensures that those who execute hooks are fairly rewarded.
- **Execution Control**: The `duty_act` function ensures that mandatory hooks (duty hooks) are executed as part of the fee collection process, while the `act` function allows for general hook execution by anyone.
- **Cooldown Management**: Compensation strategies include cooldown periods to prevent abuse and ensure fair distribution of rewards. The `duty_counter` helps manage these cooldown periods by tracking the epochs in which compensations are made.
- **Security and Access Control**: Certain functions, like `set_hooks` and `one_time_hooks`, are restricted to the contract owner to maintain security and control over the contract's behavior.
Hooks need to be added to the contract via the [`set_hooks`](#set_hooks) function. Once added, these hooks can be executed by anyone using the [`act`](#act) function. Mandatory hooks, marked with the duty flag, are executed during the fee collection process using the [`duty_act`](#duty_act) function.
:::telegram[Telegram]
If you are running or planning to run fee collection for Curve DAO, there is a Telegram channel and a group for necessary updates. Also, many hooks for automation are coming in the future which will be written about in the group.
[→ Join the Telegram group](https://t.me/curve_automation)
:::
---
## Compensation Strategy
Each `hook` includes a `compensation_strategy` that defines how and when the executor of the hook will be compensated. This ensures that there is an incentive to call hooks according to predefined rules.
The `CompensationStrategy` consists of the following values:
```vyper
struct CompensationStrategy:
amount: uint256 # In case of Dutch auction max amount
cooldown: CompensationCooldown
start: uint256
end: uint256
dutch: bool
struct CompensationCooldown:
duty_counter: uint64 # last compensation epoch
used: uint64
limit: uint64 # Maximum number of compensations between duty acts (week)
```
- **`amount`**: The maximum amount of compensation available for executing the hook.
- **`CompensationCooldown`**: Contains details about the cooldown period between compensations, consisting of:
- `duty_counter`: Keeps track of the last epoch in which a compensation was made.
- `used`: Indicates the number of compensations made within the current cooldown period.
- `limit`: Represents the maximum number of compensations allowed within the cooldown period.
- **`start`**: Defines the starting time of the compensation period within a week.
- **`end`**: Defines the ending time of the compensation period within a week.
- **`dutch`**: A boolean indicating if the compensation uses a Dutch auction mechanism. If `true`, the compensation amount decreases linearly over time from the `start` to the `end`. This encourages earlier execution of the hook to receive a higher reward.
---
## Hooks
Before hooks can be executed, they need to be added via `set_hooks`. These hooks can then be externally executed by anyone.
### `hooks`
::::description[`Hooker.hooks(arg0: uint256) -> Hook: view`]
Getter for the hooks recorded in the contract.
| Input | Type | Description |
| ------- | --------- | ----------------- |
| `arg0` | `uint256` | Index of the hook |
Returns: `Hook` struct consisting of the target address (`address`), a byte array containing the method identifier and additional data (`Bytes[1024]`), compensation strategy (`CompensationStrategy`) and if the hook is a duty hook or not (`bool`).
```vyper
struct Hook:
to: address
foreplay: Bytes[1024] # including method_id
compensation_strategy: CompensationStrategy
duty: bool # Hooks mandatory to act after fee_collector transfer
hooks: public(DynArray[Hook, MAX_HOOKS_LEN])
```
::::
### `set_hooks`
::::description[`Hooker.set_hooks(_new_hooks: DynArray[Hook, MAX_HOOKS_LEN])`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set new hooks.
| Input | Type | Description |
| ------------ | ------------------------------- | ----------------------- |
| `_new_hooks` | `DynArray[Hook, MAX_HOOKS_LEN]` | Array of `Hook` structs |
*Each `Hook` struct contains:*
- `to`: The target address for the hook action.
- `foreplay`: A byte array containing the method identifier and additional data.
- `compensation`: The strategy for compensating the hook executor.
- `duty`: A flag bool if the hook is mandatory or not.
```vyper
struct Hook:
to: address
foreplay: Bytes[1024] # including method_id
compensation_strategy: CompensationStrategy
duty: bool # Hooks mandatory to act after fee_collector transfer
@external
def set_hooks(_new_hooks: DynArray[Hook, MAX_HOOKS_LEN]):
"""
@notice Set new hooks
@dev Callable only by owner
@param _new_hooks New list of hooks
"""
assert msg.sender == fee_collector.owner(), "Only owner"
self._set_hooks(_new_hooks)
@internal
def _set_hooks(new_hooks: DynArray[Hook, MAX_HOOKS_LEN]):
self.hooks = new_hooks
buffer_amount: uint256 = 0
mask: uint256 = 0
for i in range(len(new_hooks), bound=MAX_HOOKS_LEN):
assert new_hooks[i].compensation_strategy.start < WEEK
assert new_hooks[i].compensation_strategy.end < WEEK
buffer_amount += new_hooks[i].compensation_strategy.amount *\
convert(new_hooks[i].compensation_strategy.cooldown.limit, uint256)
if new_hooks[i].duty:
mask |= 1 << i
self.buffer_amount = buffer_amount
self.duties_checklist = mask
```
This example sets a new hook with the target address `0xD16d5eC345Dd86Fb63C6a9C43c517210F1027914`.
```shell
>>> Hooker.set_hooks([
... Hooker.Hook(
... to=address("0xD16d5eC345Dd86Fb63C6a9C43c517210F1027914"),
... foreplay=b"",
... compensation_strategy=Hooker.CompensationStrategy(
... amount=1000000000000000000,
... cooldown=Hooker.CompensationCooldown(
... duty_counter=0,
... used=0,
... limit=1000000000000000000
... ),
... start=0,
... end=WEEK,
... dutch=True
... ),
... duty=True
... )
... ])
```
::::
---
## Executing Hooks
There are two functions to execute hooks: `duty_act` and `act`.
The `duty_act` method is designed to be called by the `FeeCollector` contract during the `FORWARD` epoch. This function is called when coins are forwarded from the `FeeCollector` using the `forward` function.
The `act` function is a more general function to execute hooks and compensate the caller, callable by anyone.
**Compensation for executing hooks**The compensation strategy in the Hooker contract determines how and when callers (keepers) are compensated for executing hooks. This strategy includes parameters for managing compensation amounts, cooldowns, and execution limits, ensuring fair and controlled distribution of rewards.
*The compensation strategy is defined within the `CompensationStrategy` struct, which includes several fields:*
```vyper
struct CompensationStrategy:
amount: uint256 # In case of Dutch auction max amount
cooldown: CompensationCooldown
start: uint256
end: uint256
dutch: bool
```
- `amount`: The maximum compensation amount. For Dutch auction strategies, this represents the starting maximum amount.
- `cooldown`: A nested struct (CompensationCooldown) that manages the cooldown period and usage limits for compensations.
- `start`: The start time (within a week) for the compensation period.
- `end`: The end time (within a week) for the compensation period.
- `dutch`: A boolean indicating whether the compensation follows a Dutch auction strategy, where the compensation decreases over time.
*The `CompensationCooldown` struct includes fields to manage the number of compensations within a duty cycle and track the duty counter:*
```vyper
struct CompensationCooldown:
duty_counter: uint64 # last compensation epoch
used: uint64
limit: uint64 # Maximum number of compensations between duty acts (week)
```
- `duty_counter`: Tracks the last duty cycle in which compensation was provided.
- `used`: The number of compensations already provided in the current cycle.
- `limit`: The maximum number of compensations allowed within a single duty cycle
*To see the actual value of compensation, see [`calc_compensation`](#calc_compensation).*
### `duty_act`
::::description[`Hooker.duty_act(_hook_inputs: DynArray[HookInput, MAX_HOOKS_LEN], _receiver: address=msg.sender) -> uint256`]
Function which executes hooks as part of the fee collection process. It ensures all mandatory hooks, which are marked with the `duty` flag, are executed and handles the distribution of any associated compensation. The function checks that all mandatory duty hooks are included in the `_hook_inputs`.
| Input | Type | Description |
| -------------- | ------------------------------------ | ------------------------------------------------------------------ |
| `_hook_inputs` | `DynArray[HookInput, MAX_HOOKS_LEN]` | Array of `HookInput` structs representing the hooks to be executed |
| `_receiver` | `address` | Receiver of the compensation. Defaults to `msg.sender` |
*Each `HookInput` struct contains:*
- `hook_id:` `uint8` - The identifier for the hook to be executed.
- `value:` `uint256` - The amount of raw ETH to be sent with the hook execution.
- `data:` `Bytes[8192]` - The data payload for the hook, including the method identifier and parameters.
Returns: received compensation (`uint256`).
Emits: `DutyAct`, `HookShot` and `Act` events.
```vyper
event DutyAct:
pass
event Act:
receiver: indexed(address)
compensation: uint256
event HookShot:
hook_id: indexed(uint8)
compensation: uint256
# Property: no future changes in FeeCollector
struct HookInput:
hook_id: uint8
value: uint256
data: Bytes[8192]
hooks: public(DynArray[Hook, MAX_HOOKS_LEN])
duties_checklist: uint256 # mask of hooks with `duty` flag
buffer_amount: public(uint256)
duty_counter: public(uint64)
@external
@payable
def duty_act(_hook_inputs: DynArray[HookInput, MAX_HOOKS_LEN], _receiver: address=msg.sender) -> uint256:
"""
@notice Entry point to run hooks for FeeCollector
@param _hook_inputs Inputs assembled by keepers
@param _receiver Receiver of compensation (sender by default)
@return Compensation received
"""
if msg.sender == fee_collector.address:
self.duty_counter = convert((block.timestamp - START_TIME) / WEEK, uint64) # assuming time frames are divided weekly
hook_mask: uint256 = 0
for solicitation in _hook_inputs:
hook_mask |= 1 << solicitation.hook_id
duties_checklist: uint256 = self.duties_checklist
assert hook_mask & duties_checklist == duties_checklist, "Not all duties"
log DutyAct()
return self._act(_hook_inputs, _receiver)
@internal
def _act(_hook_inputs: DynArray[HookInput, MAX_HOOKS_LEN], _receiver: address) -> uint256:
current_duty_counter: uint64 = self.duty_counter
compensation: uint256 = 0
prev_idx: uint8 = 0
for solicitation in _hook_inputs:
hook: Hook = self.hooks[solicitation.hook_id]
self._shot(hook, solicitation)
if hook.compensation_strategy.cooldown.duty_counter < current_duty_counter:
hook.compensation_strategy.cooldown.used = 0
hook.compensation_strategy.cooldown.duty_counter = current_duty_counter
hook_compensation: uint256 = self._compensate(hook)
if hook_compensation > 0:
compensation += hook_compensation
hook.compensation_strategy.cooldown.used += 1
self.hooks[solicitation.hook_id].compensation_strategy.cooldown = hook.compensation_strategy.cooldown
if prev_idx > solicitation.hook_id:
raise "Hooks not sorted"
prev_idx = solicitation.hook_id
log HookShot(prev_idx, hook_compensation)
log Act(_receiver, compensation)
@internal
def _shot(hook: Hook, hook_input: HookInput):
"""
@notice Hook run implementation
"""
raw_call(
hook.to,
concat(hook.foreplay, hook_input.data),
value=hook_input.value,
)
```
```shell
>>> Hooker.duty_act([Hooker.HookInput(hook_id=0, value=1000000000000000000, data=b"")], Hooker.address)
```
::::
### `duty_counter`
::::description[`Hooker.duty_counter() -> uint64: view`]
Getter for the duty counter value. This variable is used to record the current week number and is used to manage and reset the cooldown periods for hook compensations, ensuring that hooks do not exceed their compensation limits within a given week.
Returns: duty counter (`uint64`).
```vyper
duty_counter: public(uint64)
```
::::
### `act`
::::description[`Hooker.act(_hook_inputs: DynArray[HookInput, MAX_HOOKS_LEN], _receiver: address=msg.sender) -> uint256`]
Function to execute hooks. Unlike, `duty_act` (which is specifically for the fee distribution process), this function allows the execution of more general hooks.
| Input | Type | Description |
| -------------- | ------------------------------------ | ------------------------------------------------------------------ |
| `_hook_inputs` | `DynArray[HookInput, MAX_HOOKS_LEN]` | Array of `HookInput` structs representing the hooks to be executed |
| `_receiver` | `address` | Receiver of the compensation. Defaults to `msg.sender` |
*Each `HookInput` struct contains:*
- `hook_id:` `uint8` - The identifier for the hook to be executed.
- `value:` `uint256` - The amount of raw ETH to be sent with the hook execution.
- `data:` `Bytes[8192]` - The data payload for the hook, including the method identifier and parameters.
Returns: received compensation (`uint256`).
Emits: `HookShot` and `Act` events.
```vyper
event Act:
receiver: indexed(address)
compensation: uint256
event HookShot:
hook_id: indexed(uint8)
compensation: uint256
struct CompensationCooldown:
duty_counter: uint64 # last compensation epoch
used: uint64
limit: uint64 # Maximum number of compensations between duty acts (week)
struct CompensationStrategy:
amount: uint256 # In case of Dutch auction max amount
cooldown: CompensationCooldown
start: uint256
end: uint256
dutch: bool
@external
@payable
def act(_hook_inputs: DynArray[HookInput, MAX_HOOKS_LEN], _receiver: address=msg.sender) -> uint256:
"""
@notice Entry point to run hooks and receive compensation
@param _hook_inputs Inputs assembled by keepers
@param _receiver Receiver of compensation (sender by default)
@return Compensation received
"""
return self._act(_hook_inputs, _receiver)
@internal
def _act(_hook_inputs: DynArray[HookInput, MAX_HOOKS_LEN], _receiver: address) -> uint256:
current_duty_counter: uint64 = self.duty_counter
compensation: uint256 = 0
prev_idx: uint8 = 0
for solicitation in _hook_inputs:
hook: Hook = self.hooks[solicitation.hook_id]
self._shot(hook, solicitation)
if hook.compensation_strategy.cooldown.duty_counter < current_duty_counter:
hook.compensation_strategy.cooldown.used = 0
hook.compensation_strategy.cooldown.duty_counter = current_duty_counter
hook_compensation: uint256 = self._compensate(hook)
if hook_compensation > 0:
compensation += hook_compensation
hook.compensation_strategy.cooldown.used += 1
self.hooks[solicitation.hook_id].compensation_strategy.cooldown = hook.compensation_strategy.cooldown
if prev_idx > solicitation.hook_id:
raise "Hooks not sorted"
prev_idx = solicitation.hook_id
log HookShot(prev_idx, hook_compensation)
log Act(_receiver, compensation)
@internal
def _shot(hook: Hook, hook_input: HookInput):
"""
@notice Hook run implementation
"""
raw_call(
hook.to,
concat(hook.foreplay, hook_input.data),
value=hook_input.value,
)
```
```shell
>>> Hooker.act([Hooker.HookInput(hook_id=0, value=1000000000000000000, data=b"")], Hooker.address)
```
::::
### `calc_compensation`
::::description[`Hooker.calc_compensation(_hook_inputs: DynArray[HookInput, MAX_HOOKS_LEN], _duty: bool=False, _ts: uint256=block.timestamp) -> uint256: view`]
Function to calculate the compensation for executing specific hooks.
| Input | Type | Description |
| -------------- | ------------------------------------ | ----------------------------------------------------------------------------------- |
| `_hook_inputs` | `DynArray[HookInput, MAX_HOOKS_LEN]` | Array of `HookInput` structs representing the hooks to be executed |
| `_duty` | `bool` | Whether the act is performed by the FeeCollector; defaults to `False` |
| `_ts` | `uint256` | Timestamp at which to calculate the compensation for; defaults to `block.timestamp` |
*Each `HookInput` struct contains:*
- `hook_id:` `uint8` - The identifier for the hook to be executed.
- `value:` `uint256` - The amount of raw ETH to be sent with the hook execution.
- `data:` `Bytes[8192]` - The data payload for the hook, including the method identifier and parameters.
Returns: amount of target coins to receive as compensation (`uint256`).
```vyper
struct HookInput:
hook_id: uint8
value: uint256
data: Bytes[8192]
@view
@external
def calc_compensation(_hook_inputs: DynArray[HookInput, MAX_HOOKS_LEN],
_duty: bool=False, _ts: uint256=block.timestamp) -> uint256:
"""
@notice Calculate compensation for acting hooks. Checks input according to execution rules.
Older timestamps might work incorrectly.
@param _hook_inputs HookInput of hooks to act, only ids are used
@param _duty Bool whether act is through fee_collector (False by default).
If True, assuming calling from fee_collector if possible
@param _ts Timestamp at which to calculate compensations (current by default)
@return Amount of target coin to receive as compensation
"""
current_duty_counter: uint64 = self.duty_counter
if _duty:
hook_mask: uint256 = 0
for solicitation in _hook_inputs:
hook_mask |= 1 << solicitation.hook_id
duties_checklist: uint256 = self.duties_checklist
assert hook_mask & duties_checklist == duties_checklist, "Not all duties"
time_frame: (uint256, uint256) = fee_collector.epoch_time_frame(Epoch.FORWARD, _ts)
if time_frame[0] <= _ts and _ts < time_frame[1]:
current_duty_counter = convert((_ts - START_TIME) / WEEK, uint64)
compensation: uint256 = 0
prev_idx: uint8 = 0
num: uint64 = 0
for solicitation in _hook_inputs:
if prev_idx > solicitation.hook_id:
raise "Hooks not sorted"
else:
num = num + 1 if prev_idx == solicitation.hook_id else 1
hook: Hook = self.hooks[solicitation.hook_id]
if hook.compensation_strategy.cooldown.duty_counter < current_duty_counter:
hook.compensation_strategy.cooldown.used = 0
compensation += self._compensate(hook, _ts, num)
prev_idx = solicitation.hook_id
return compensation
```
```shell
>>> Hooker.calc_compensation([Hooker.HookInput(hook_id=0, value=1000000000000000000, data=b"")], _duty=True, _ts=1717286400)
1000000000000000000
```
::::
### `one_time_hooks`
::::description[`Hooker.one_time_hooks(_hooks: DynArray[Hook, MAX_HOOKS_LEN], _inputs: DynArray[HookInput, MAX_HOOKS_LEN])`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the `FeeCollector` contract.
:::
Function to execute one-time-hooks. These are hooks that only need to be executed once, like coin approvals.
| Input | Type | Description |
| --------- | ------------------------------------ | ------------------------------------------------------------------ |
| `_hooks` | `DynArray[Hook, MAX_HOOKS_LEN]` | Array of `Hook` structs |
| `_inputs` | `DynArray[HookInput, MAX_HOOKS_LEN]` | Array of `HookInput` structs representing the hooks to be executed |
*Each `Hook` struct contains:*
- `to`: The target address for the hook action.
- `foreplay`: A byte array containing the method identifier and additional data.
- `compensation`: The strategy for compensating the hook executor.
- `duty`: A flag bool if the hook is mandatory or not.
*Each `HookInput` struct contains:*
- `hook_id:` `uint8` - The identifier for the hook to be executed.
- `value:` `uint256` - The amount of raw ETH to be sent with the hook execution.
- `data:` `Bytes[8192]` - The data payload for the hook, including the method identifier and parameters.
```vyper
struct Hook:
to: address
foreplay: Bytes[1024] # including method_id
compensation_strategy: CompensationStrategy
duty: bool # Hooks mandatory to act after fee_collector transfer
# Property: no future changes in FeeCollector
struct HookInput:
hook_id: uint8
value: uint256
data: Bytes[8192]
@external
@payable
def one_time_hooks(_hooks: DynArray[Hook, MAX_HOOKS_LEN], _inputs: DynArray[HookInput, MAX_HOOKS_LEN]):
"""
@notice Coin approvals, any settings that need to be executed once
@dev Callable only by owner
@param _hooks Hook input
@param _inputs May be used to include native coin
"""
assert msg.sender == fee_collector.owner(), "Only owner"
self._one_time_hooks(_hooks, _inputs)
@internal
def _one_time_hooks(hooks: DynArray[Hook, MAX_HOOKS_LEN], inputs: DynArray[HookInput, MAX_HOOKS_LEN]):
for i in range(len(hooks), bound=MAX_HOOKS_LEN):
self._shot(hooks[i], inputs[i])
@internal
def _shot(hook: Hook, hook_input: HookInput):
"""
@notice Hook run implementation
"""
raw_call(
hook.to,
concat(hook.foreplay, hook_input.data),
value=hook_input.value,
)
```
```shell
>>> Hooker.one_time_hooks([Hooker.Hook(to=address("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), foreplay=b"", compensation_strategy=Hooker.CompensationStrategy(amount=1000000000000000000, cooldown=Hooker.CompensationCooldown(duty_counter=0, used=0, limit=1000000000000000000), start=0, end=WEEK, dutch=True), duty=True)], [Hooker.HookInput(hook_id=0, value=1000000000000000000, data=b"")])
```
::::
### `buffer_amount`
::::description[`Hooker.buffer_amount() -> uint256: view`]
Getter for the buffer amount which represents the total potential compensation amount that might be required to execute all the hooks under their respective compensation strategies.
Returns: buffer amount (`uint256`).
```vyper
buffer_amount: public(uint256)
```
::::
---
## Valid Interface a la ERC-165
In order for the Burner contract to be fully compatible with the `FeeCollector`, a specific interface needs to hold up as per [ERC-165](https://eips.ethereum.org/EIPS/eip-165):
```vyper
SUPPORTED_INTERFACES: constant(bytes4[2]) = [
# ERC165: method_id("supportsInterface(bytes4)") == 0x01ffc9a7
0x01ffc9a7,
# Hooker:
# method_id("duty_act((uint8,uint256,bytes)[],address)") == 0x8c88eb86
# method_id("buffer_amount()") == 0x69e15fcb
0xe569b44d,
]
```
### `supportsInterface`
::::description[`Hooker.supportsInterface(_interface_id: bytes4) -> bool: pure`]
Function to check if the burner supports the correct interface, as specified by the [ERC-165](https://eips.ethereum.org/EIPS/eip-165) standard. This method makes sure the contract is compatible with the `FeeCollector` contract.
| Input | Type | Description |
| ------- | --------- | ------------------------------ |
| `_interface_id` | `bytes4` | ID of the interface. |
Returns: true or false (`bool`).
```vyper
SUPPORTED_INTERFACES: constant(bytes4[2]) = [
# ERC165: method_id("supportsInterface(bytes4)") == 0x01ffc9a7
0x01ffc9a7,
# Hooker:
# method_id("duty_act((uint8,uint256,bytes)[],address)") == 0x8c88eb86
# method_id("buffer_amount()") == 0x69e15fcb
0xe569b44d,
]
@pure
@external
def supportsInterface(_interface_id: bytes4) -> bool:
"""
@dev Interface identification is specified in ERC-165.
@param _interface_id Id of the interface
"""
return _interface_id in SUPPORTED_INTERFACES
```
::::
---
## Recovering ERC-20 Tokens and ETH
### `recover`
::::description[`Hooker.recover(_coins: DynArray[ERC20, MAX_LEN])`]
:::guard[Guarded Method]
This function is only callable by the `owner` or `emergency_owner` of the `FeeCollector`.
:::
Function to recover ERC20 tokens or ETH from the contract by transferring them to the `FeeCollector`.
| Input | Type | Description |
| -------- | -------------------------- | ---------------------------------- |
| `_coins` | `DynArray[ERC20, MAX_LEN]` | Array of coin addresses to recover |
```vyper
@external
def recover(_coins: DynArray[ERC20, MAX_LEN]):
"""
@notice Recover ERC20 tokens or Ether from this contract
@dev Callable only by owner and emergency owner
@param _coins Token addresses
"""
assert msg.sender in [fee_collector.owner(), fee_collector.emergency_owner()], "Only owner"
for coin in _coins:
if coin.address == ETH_ADDRESS:
raw_call(fee_collector.address, b"", value=self.balance)
else:
coin.transfer(fee_collector.address, coin.balanceOf(self)) # do not need safe transfer
```
This example recovers ETH from the Hooker contract and transfers it to the address defined in `fee_collector`.
```shell
>>> Hooker.recover(["0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"])
```
::::
### `fee_collector`
::::description[`Hooker.fee_collector() -> address: view`]
Getter for the `FeeCollector` contract.
Returns: fee collector (`address`).
```vyper
fee_collector: public(immutable(FeeCollector))
@external
def __init__(_fee_collector: FeeCollector,
_initial_oth: DynArray[Hook, MAX_HOOKS_LEN], _initial_oth_inputs: DynArray[HookInput, MAX_HOOKS_LEN],
_initial_hooks: DynArray[Hook, MAX_HOOKS_LEN]):
"""
@notice Contract constructor
@param _fee_collector Hooker is _hooked_ to fee_collector contract with no update possibility
@param _initial_oth One time hooks at initialization
@param _initial_oth_inputs One time hooks input at initialization
@param _initial_hooks Hooks to set at initialization
"""
fee_collector = _fee_collector
self._one_time_hooks(_initial_oth, _initial_oth_inputs)
self._set_hooks(_initial_hooks)
```
::::
---
## Burner
## Overview
Burning is handled on a per-coin basis. The process is initiated by calling the `PoolProxy.burn` or `PoolProxy.burn_many` functions. Calling to burn a coin transfers that coin into the burner and then calls the `burn` function on the burner.
Each `burn` action typically performs one conversion into another asset; either 3CRV itself, or something that is a step closer to reaching 3CRV. As an example, here is the sequence of conversions required to burn wstETH:
**`wstETH -> stETH -> ETH -> USDT`**1. `wstETH` to `stETH` via *unwrapping (wstETH Burner)*
2. `stETH` to `ETH` via *swap through stETH/ETH curve pool (SwapStableBurner)*
3. `ETH` to `USDT` via *swap through tricrypto pool (CryptoSwapBurner)*
4. `USDT` to `3CRV` via *depositing into 3pool (StableDepositBurner)*
**Simplified burn pattern:**
```mermaid
flowchart LR
p[(0xECB)]
v1(StableSwap) ..->|withdraw admin fees| p;
v2(CryptoSwap) ..-> |claim admin fees| p;
crvUSD(crvUSD Markets) ..-> |collect fees| p;
p --->|burn| b1([Burner 1]);
p -->|burn| b2([Burner 2]);
p -->|burn| b3([Burner 3]);
b1 --> ub\{\{StableDepositBurner\}\}
b1 --> |if burned directly for 3CRV| fd(((FeeDistributor)))
b2 --> ub
b3 --> ub
ub --> |burn for 3CRV| fd
```
:::warning[Burning Efficiency]
Efficiency within the intermediate conversions is the reason it is important to run the burn process in a specific order. For example, if you burn stETH prior to burning wstETH, you will have to burn stETH a second time!
:::
**There are multiple burner contracts, each of which handles a different category of fee coin.**
## Deployed Burner Contracts
:::deploy[Contract Source & Deployment]
Source code for burners is available on [Github](https://github.com/curvefi/curve-dao-contracts/tree/master/contracts/burners).
:::
| Burner Type | Description | Address |
| -------- | -------|-------|
|`ABurner`|`Aave lending tokens`|[0x12220a63a2013133D54558C9d03c35288eAC9B34](https://etherscan.io/address/0x12220a63a2013133d54558c9d03c35288eac9b34#code)|
|`CBurner`|`Compound lending tokens`|[0xdd0e10857d952c73b2fa39ce86308299df8774b8](https://etherscan.io/address/0xdd0e10857d952c73b2fa39ce86308299df8774b8#code)|
|`YBurner`|`Yearn lending tokens`|[0xd16ea3e5681234da84419512eb597362135cd8c9](https://etherscan.io/address/0xd16ea3e5681234da84419512eb597362135cd8c9#code)|
|`CryptoSwapBurner`|`Swaps crypto LP tokens`|[0xdc237b4B882Fa1d1fd1dD5B59A08F8dB3416DbE3](https://etherscan.io/address/0xdc237b4B882Fa1d1fd1dD5B59A08F8dB3416DbE3#code)|
|`SwapStableBurner`|` Swaps into another asset using a stable pool`|[0x90B4508e8F91523e5c8854eA73AFD8c22d8c27b7](https://etherscan.io/address/0x90B4508e8F91523e5c8854eA73AFD8c22d8c27b7#code)|
|`LPBurner`|`Burner for LP Tokens`|[0xaa42C0CD9645A58dfeB699cCAeFBD30f19B1ff81](https://etherscan.io/address/0xaa42C0CD9645A58dfeB699cCAeFBD30f19B1ff81#code)|
|`MetaBurner`|`USD denominated assets that are directly swappable for 3CRV`|[0xE4b65889469ad896e866331f0AB5652C1EcfB3E6](https://etherscan.io/address/0xE4b65889469ad896e866331f0AB5652C1EcfB3E6#code)|
|`SynthBurner`|`non-USD denominated assets that are synths or can be swapped into synths`|[0x67a0213310202dbc2cbe788f4349b72fba90f9fa](https://etherscan.io/address/0x67a0213310202dbc2cbe788f4349b72fba90f9fa#code)|
|`UniswapBurner`|`Assets that must be swapped on Uniswap/Sushiswap`|[0xf3b64840b39121b40d8685f1576b64c157ce2e24](https://etherscan.io/address/0xf3b64840b39121b40d8685f1576b64c157ce2e24#code)|
|`UnderlyingBurner`|`Assets that can be directly deposited into 3pool, or swapped for an asset that is deposited into 3pool`|[0x786b374b5eef874279f4b7b4de16940e57301a58](https://etherscan.io/address/0x786b374b5eef874279f4b7b4de16940e57301a58#code)|
|`Wrapped stETH Burner`|`Unwraps wstETH into stETH` |[0x072C93f12dC274300c79E92Eb61a0feCFa8E8918](https://etherscan.io/address/0x072C93f12dC274300c79E92Eb61a0feCFa8E8918#code)|
|`Stable Deposit Burner`| `Deposits stables into Threepool`|[0x1D56495c76d99435d10ecd5b0C3bd6a8EE7cC3bb](https://etherscan.io/address/0x1D56495c76d99435d10ecd5b0C3bd6a8EE7cC3bb#code)|
|`Tricrypto Factory LP Burner`| `Withdraws LP Tokens`|[0xA6a0103f8F185786143f3EFe3Ddf268d8E070813](https://etherscan.io/address/0xA6a0103f8F185786143f3EFe3Ddf268d8E070813#code)|
|`crvUSD Burner`| `Withdraws LP tokens from crvUSD pools and exchanges crvUSD`|[0xA6a0103f8F185786143f3EFe3Ddf268d8E070813](https://etherscan.io/address/0xA6a0103f8F185786143f3EFe3Ddf268d8E070813#code)|
## Burners
### ABurner / CBurner / YBurner
`ABurner`, `CBurner` and `YBurner` are collectively known as “lending burners”. They unwrap lending tokens into the underlying asset and transfer those assets onward into the underlying burner.
*There is no configuration required for this burner.*
### CryptoSwap Burner
The CryptoSwapBurner is used to burn fees from Crypto Pools.
### StableSwap Burner
Swaps an asset into another asset using a Stable pool and forwards to another burner.
### LP Burner
The LP Burner handles non-3CRV LP tokens. This burner is primarily used for [FRAXBP LP tokens](https://etherscan.io/address/0x3175df0976dfa876431c2e9ee6bc45b65d3473cc#code) which are converted to USDC and then sent to 0xECB for a further burn process.
LP burner calls to **`StableSwap.remove_liquidity_one_coin`** to unwrap the LP token. The new asset is then transferred on to another burner.
#### `swap_data`
::::description[`LPBurner.swap_data(arg0: address) -> pool: address, coin: address, burner: address, i: int128: view`]
Getter method for information about the LP Token burn process.
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `address` | LP Token Address |
Returns: pool (`address`) of the LP token, coin (`address`) in which the LP token is withdrawn, burner (`address`) where the output token is forwarded to and i (`index`) of `coin` in the pool.
```vyper hl_lines="1"
struct SwapData:
pool: address
coin: address
burner: address
i: int128
```
```shell
>>> LPBurner.swap_data("0x3175df0976dfa876431c2e9ee6bc45b65d3473cc")
pool: '0xDcEF968d416a41Cdac0ED8702fAC8128A64241A2'
coin: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
burner: '0x786B374B5eef874279f4B7b4de16940e57301A58'
i: 1
```
::::
#### `set_swap_data`
::::description[`LPBurner.set_swap_data(_lp_token: address, _coin: address, _burner: address) -> bool`]
:::guard[Guarded Method]
This function is only callable by the `owner` or `emergency_owner` of the contract.
:::
Function to set the swap data of a LP token.
| Input | Type | Description |
| ----------- | -------| ----|
| `_lp_token` | `address` | LP token address |
| `_coin` | `address` | Coin address to swap the LP token to |
| `_burner` | `address` | Burner address to forward to |
Returns: true (`bool`).
```vyper hl_lines="2"
@external
def set_swap_data(_lp_token: address, _coin: address, _burner: address) -> bool:
"""
@notice Set conversion and transfer data for `_lp_token`
@param _lp_token LP token address
@param _coin Underlying coin to remove liquidity in
@param _burner Burner to transfer `_coin` to
@return bool success
"""
assert msg.sender in [self.owner, self.emergency_owner] # dev: only owner
# if another burner was previous set, revoke approvals
pool: address = self.swap_data[_lp_token].pool
if pool != ZERO_ADDRESS:
# we trust that LP tokens always return True, so no need for `raw_call`
ERC20(_lp_token).approve(pool, 0)
coin: address = self.swap_data[_lp_token].coin
if coin != ZERO_ADDRESS:
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("approve(address,uint256)"),
convert(self.swap_data[_lp_token].burner, bytes32),
convert(0, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
# find `i` for `_coin` within the pool, approve transfers and save to storage
registry: address = AddressProvider(ADDRESS_PROVIDER).get_registry()
pool = Registry(registry).get_pool_from_lp_token(_lp_token)
coins: address[8] = Registry(registry).get_coins(pool)
for i in range(8):
if coins[i] == ZERO_ADDRESS:
raise
if coins[i] == _coin:
self.swap_data[_lp_token] = SwapData(\{
pool: pool,
coin: _coin,
burner: _burner,
i: i
\})
ERC20(_lp_token).approve(pool, MAX_UINT256)
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("approve(address,uint256)"),
convert(_burner, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
return True
raise
```
```shell
>>> LPBurner.set_swap_data("0x3175df0976dfa876431c2e9ee6bc45b65d3473cc", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "0x786B374B5eef874279f4B7b4de16940e57301A58")
'true'
```
::::
### MetaBurner
The MetaBurner converts Metapool-paired coins to 3CRV and transfers to the FeeDistributor. It uses the registry’s **`exchange_with_best_rate`** and transfers 3CRV directly to the fee distributor.
*There is no configuration required for this burner.*
### SynthBurner
Swaps non-USD denominated assets for synths, converts synths to sUSD and transfers to `UnderlyingBurner`.
The synth burner is used to convert non-USD denominated assets into sUSD. This is accomplished via synth conversion, the same mechanism used in cross-asset swaps.
When the synth burner is called to burn a non-synthetic asset, it uses `RegistrySwap.exchange_with_best_rate` to swap into a related synth. If no direct path to a synth is available, a swap is made into an intermediate asset.
For synths, the burner first transfers to the [Underlying](#underlyingburner). Then it calls `UnderlyingBurner.convert_synth`, performing the cross-asset swap within the underlying burner. This is done to avoid requiring another transfer call after the settlement period has passed.
The optimal sequence when burning assets using the synth burner is thus:
1. Coins that cannot directly swap to synths
2. Coins that can directly swap to synths
3. Synthetic assets
*The burner is configurable via the following functions:*
#### `set_swap_for`
::::description[`SynthBurner.set_swap_for(_coins: address[10], _targets: address[10]) -> bool`]
Function to set target coins that the burner will swap into.
For assets that can be directly swapped for a synth, the target should be set as that synth. For assets that cannot be directly swapped, the target must be an asset that has already had it’s own target registered (e.g. can be swapped for a synth).
| Input | Type | Description |
| ----------- | -------| ----|
| `_coins` | `address[10]` | List of coins to be burned |
| `_targets` | `address[10]` | List of coins to be swapped for |
Returns: true (`bool`).
:::tip
If you wish to set less than 10 `_coins`, fill the remaining array slots with `ZERO_ADDRESS`.
The address as index `n` within this list corresponds to the address at index `n` within `coins`.
:::
```vyper hl_lines="2"
@external
def set_swap_for(_coins: address[10], _targets: address[10]) -> bool:
"""
@notice Set target coins that will be swapped into
@dev If any target coin is not a synth, it must have already
had it's own target coin registered
@param _coins List of coins to be burned
@param _targets List of coins to be swapped for
@return bool success
"""
registry: address = AddressProvider(ADDRESS_PROVIDER).get_registry()
for i in range(10):
coin: address = _coins[i]
if coin == ZERO_ADDRESS:
break
target: address = _targets[i]
assert Registry(registry).find_pool_for_coins(coin, target) != ZERO_ADDRESS
if self.currency_keys[target] == EMPTY_BYTES32:
# if target is not a synth, ensure target already has a target set
assert self.swap_for[target] != ZERO_ADDRESS
self.swap_for[coin] = target
return True
```
```shell
>>> SynthBurner.set_swap_for(["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"], ["0x5e74C9036fb86BD7eCdcb084a0673EFc32eA31cb"])
```
::::
#### `add_synths`
::::description[`SynthBurner.add_synths(_synths: address[10]) -> bool`]
Register synthetic assets within the burner. This function is unguarded. For each synth to be added, a call is made to `Synth.currencyKey` to validate the address and obtain the synth currency key.
| Input | Type | Description |
| ----------- | -------| ----|
| `_synths` | `address[10]` | List of synth tokens to register |
Returns: true (`bool`).
:::note
If you wish to set less than 10 `_synths`, fill the remaining array slots with `ZERO_ADDRESS`.
The address at index `n` within this list corresponds to the address at index `n` within `_synths`.
:::
```vyper hl_lines="3"
@external
@nonreentrant("lock")
def add_synths(_synths: address[10]) -> bool:
"""
@notice Registry synth token addresses
@param _synths List of synth tokens to register
@return bool success
"""
for synth in _synths:
if synth == ZERO_ADDRESS:
break
# this will revert if `_synth` is not actually a synth
self.currency_keys[synth] = Synth(synth).currencyKey()
return True
```
```shell
>>> SynthBurner.add_synths(["0x57Ab1ec28D129707052df4dF418D58a2D46d5f51"])
```
::::
### Wrapped stETH Burner
This burner unwraps wstETH to stETH and sends it back to 0xECB.
### UnderlyingBurner
The underlying burner handles assets that can be directly swapped to USDC and deposits DAI/USDC/USDT into [3pool](https://curve.fi/#/ethereum/pools/3pool/deposit/) to obtain 3CRV. This is the **final step of the burn process** for many assets that require multiple intermediate swaps.
:::note
Prior to burning any assets with the UnderlyingBurner, you should have completed the entire burn process with `SynthBurner`, `UniswapBurner` and `all of the lending burners`.
:::
The burn process consists of:
- For sUSD: First call settles to complete any pending synth conversions. Then swaps into USDC.
- For all other assets that are not DAI/USDC/USDT: Swap into USDC.
- For DAI/USDC/USDT: Only transfer the assets into the burner.
*Once the entire burn process has been completed you must call **`execute`** as the final action:*
::::description[`UnderlyingBurner.execute() -> bool`]
Function to deposit all the tokens into 3pool and transfer the received 3CRV to the FeeDistributor contract.
Returns: true (`bool`).
:::note
This is the final function to be called in the burn process, after all other steps are completed. Calling this function does not do anything if the burner has a balance of zero for DAI, USDC and USDT.
:::
```vyper hl_lines="1"
def execute() -> bool:
"""
@notice Add liquidity to 3pool and transfer 3CRV to the fee distributor
@return bool success
"""
assert not self.is_killed # dev: is killed
amounts: uint256[3] = [
ERC20(TRIPOOL_COINS[0]).balanceOf(self),
ERC20(TRIPOOL_COINS[1]).balanceOf(self),
ERC20(TRIPOOL_COINS[2]).balanceOf(self),
]
if amounts[0] != 0 and amounts[1] != 0 and amounts[2] != 0:
StableSwap(TRIPOOL).add_liquidity(amounts, 0)
amount: uint256 = ERC20(TRIPOOL_LP).balanceOf(self)
if amount != 0:
ERC20(TRIPOOL_LP).transfer(self.receiver, amount)
return True
```
```shell
>>> UnderlyingBurner.execute()
```
::::
### Stable Deposit Burner
This burner converts DAI, USDC and USDT into 3CRV by adding liquidity to the 3pool and then transfers them to the FeeDistributor.
::::description[`StableDepositBurner.burn(_coin: ERC20) -> bool`]
Function to add the entire burner's balance of `_coin` to the 3pool.
| Input | Type | Description |
| ----------- | -------| ----|
| `_coin` | `ERC20` | Address of the coin being converted |
Returns: true (`bool`).
```vyper
@external
def burn(_coin: ERC20) -> bool:
"""
@notice Convert `_coin` by depositing
@param _coin Address of the coin being converted
@return bool success
"""
assert not self.is_killed # dev: is killed
assert _coin in COINS
amount: uint256 = _coin.balanceOf(msg.sender)
assert _coin.transferFrom(msg.sender, self, amount, default_return_value=True) # safe transfer
if _coin == COINS[N_COINS - 1]: # Do it once
amounts: uint256[N_COINS] = empty(uint256[N_COINS])
for i in range(N_COINS):
amounts[i] = COINS[i].balanceOf(self)
self._burn(amounts)
return True
@internal
def _burn(_amounts: uint256[N_COINS]):
amount: uint256 = 0
for i in range(N_COINS):
amount += _amounts[i] * DEC[i]
min_amount: uint256 = amount * ONE / POOL.get_virtual_price()
min_amount -= min_amount * self.slippage / BPS
POOL.add_liquidity(_amounts, min_amount)
amount = LP.balanceOf(self)
LP.transfer(FEE_DISTRIBUTER, amount)
```
```shell
>>> StableDepositBurner.burn("0xdAC17F958D2ee523a2206206994597C13D831ec7")
```
::::
### Metapool Burner
This is not a burner contract in itself. Some metapools transfer *coin 0* of the admin fees to the Factory, where it is swapped for *coin 1* (e.g., 3CRV), which is then sent directly to the FeeDistributor.
```vyper
@external
def withdraw_admin_fees():
# transfer coin 0 to Factory and call `convert_fees` to swap it for coin 1
factory: address = self.factory
coin: address = self.coins[0]
amount: uint256 = ERC20(coin).balanceOf(self) - self.balances[0]
if amount > 0:
response: Bytes[32] = raw_call(
coin,
concat(
method_id("transfer(address,uint256)"),
convert(factory, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
)
if len(response) > 0:
assert convert(response, bool)
Factory(factory).convert_metapool_fees()
# transfer coin 1 to the receiver
coin = self.coins[1]
amount = ERC20(coin).balanceOf(self) - self.balances[1]
if amount > 0:
receiver: address = Factory(factory).get_fee_receiver(self)
response: Bytes[32] = raw_call(
coin,
concat(
method_id("transfer(address,uint256)"),
convert(receiver, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
)
if len(response) > 0:
assert convert(response, bool)
```
## Configuring Fee Burners
**Burners are configured within the 0xECB contract.**
### `burners`
::::description[`PoolProxy.burners(coin: address) -> address: view`]
Getter for the burner contract address for `coin`.
| Input | Type | Description |
| ----------- | -------| ----|
| `coin` | `address` | Token Address |
Returns: burner of a coin (`address`).
```vyper hl_lines="1"
burners: public(HashMap[address, address])
```
```shell
>>> PoolProxy.burners("0x056fd409e1d7a124bd7017459dfea2f387b6d5cd")
'0xE4b65889469ad896e866331f0AB5652C1EcfB3E6'
```
::::
### `set_burner`
::::description[`PoolProxy.set_burner(_coin: address, _burner: address)`]
:::guard[Guarded Method]
This function is only callable by the `ownership_admin` of the contract.
:::
Function to set burner of `_coin` to `_burner` address.
| Input | Type | Description |
| ----------- | -------| ----|
| `_coin` | `address` | Token Address |
| `_burner` | `address` | Burner Address |
Emits: `AddBurner` event.
```vyper hl_lines="1 6 12 14 17"
event AddBurner:
burner: address
@external
@nonreentrant('lock')
def set_burner(_coin: address, _burner: address):
"""
@notice Set burner of `_coin` to `_burner` address
@param _coin Token address
@param _burner Burner contract address
"""
assert msg.sender == self.ownership_admin, "Access denied"
self._set_burner(_coin, _burner)
@internal
def _set_burner(_coin: address, _burner: address):
old_burner: address = self.burners[_coin]
if _coin != 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE:
if old_burner != ZERO_ADDRESS:
# revoke approval on previous burner
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("approve(address,uint256)"),
convert(old_burner, bytes32),
convert(0, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
if _burner != ZERO_ADDRESS:
# infinite approval for current burner
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("approve(address,uint256)"),
convert(_burner, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
self.burners[_coin] = _burner
log AddBurner(_burner)
```
```shell
>>> PoolProxy.set_burner("0xdAC17F958D2ee523a2206206994597C13D831ec7", "0x786B374B5eef874279f4B7b4de16940e57301A58")
```
::::
### `set_many_burners`
::::description[`PoolProxy.set_many_burners(_coins: address[20], _burners: address[20])`]
:::guard[Guarded Method]
This function is only callable by the `ownership_admin` of the contract.
:::
Function to set many burners for multiple coins at once.
| Input | Type | Description |
| ----------- | -------| ----|
| `_coins` | `address[20]` | Token Addresses. The address at index `n` within this list corresponds to the address at index `n` within `coins` |
| `_burners` | `address[20]` | Burner Addresses. If less than 20 burners are set, the remaining array slots need to be filled with `ZERO_ADDRESS`. |
Emits: `AddBurner` event.
```vyper hl_lines="1 5 38 42"
event AddBurner:
burner: address
@internal
def _set_burner(_coin: address, _burner: address):
old_burner: address = self.burners[_coin]
if _coin != 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE:
if old_burner != ZERO_ADDRESS:
# revoke approval on previous burner
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("approve(address,uint256)"),
convert(old_burner, bytes32),
convert(0, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
if _burner != ZERO_ADDRESS:
# infinite approval for current burner
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("approve(address,uint256)"),
convert(_burner, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
self.burners[_coin] = _burner
log AddBurner(_burner)
@external
@nonreentrant('lock')
def set_many_burners(_coins: address[20], _burners: address[20]):
"""
@notice Set burner of `_coin` to `_burner` address
@param _coins Token address
@param _burners Burner contract address
"""
assert msg.sender == self.ownership_admin, "Access denied"
for i in range(20):
coin: address = _coins[i]
if coin == ZERO_ADDRESS:
break
self._set_burner(coin, _burners[i])
```
```shell
>>> PoolProxy.set_many_burners(["0xdAC17F958D2ee523a2206206994597C13D831ec7"], ["0x786B374B5eef874279f4B7b4de16940e57301A58"])
```
::::
### `set_burner_kill`
::::description[`PoolProxy.set_burner_kill(_is_killed: bool)`]
:::guard[Guarded Method]
This function is only callable by the `ownership_admin` or `emergency_admin` of the contract.
:::
Disable or enable the process of fee burning.
| Input | Type | Description |
| ----------- | -------| ----|
| `_is_killed` | `bool` | Burner kill status |
```vyper hl_lines="1 2 5 10 11"
ownership_admin: public(address)
emergency_admin: public(address)
@external
def set_burner_kill(_is_killed: bool):
"""
@notice Kill or unkill `burn` functionality
@param _is_killed Burner kill status
"""
assert msg.sender == self.emergency_admin or msg.sender == self.ownership_admin, "Access denied"
self.burner_kill = _is_killed
```
```shell
>>> PoolProxy.set_burner_kill("False")
```
::::
---
## Distributor
Fees are distributed to veCRV holders through the FeeDistributor contract in the form of 3CRV tokens.
:::deploy[Contract Source & Deployment]
**FeeDistributor** contract is deployed to the Ethereum mainnet at: [0xA464e6DCda8AC41e03616F95f4BC98a13b8922Dc](https://etherscan.io/address/0xa464e6dcda8ac41e03616f95f4bc98a13b8922dc).
Source code available on [GitHub](https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/FeeDistributor.vy).
:::
**Fees are distributed on a weekly basis.** The proportional amount of fees that each user is to receive is calculated based on their veCRV balance relative to the total veCRV supply. This amount is calculated at the start of the week.
The actual distribution occurs at the end of the week based on the fees that were collected. As such, a user that creates a new vote-lock should expect to receive their first fee payout at the end of the following epoch week.
:::info[Changing the Reward Token]
Changing the reward token from 3CRV to, for example, crvUSD, would require the creation of a new FeeDistributor, as the reward token cannot be configured within the existing contract.
:::
The available 3CRV balance to distribute is tracked via the “**token checkpoint**”. This is updated at minimum every 24 hours. Fees that are received between the last checkpoint of the previous week and first checkpoint of the new week will be split evenly between the weeks.
---
## Claiming Fees
### `token`
::::description[`FeeDistributor.token() -> address: view`]
Getter for the token address in which the fees are distributed.
Returns: reward token (`address`).
```vyper
token: public(address)
@external
def __init__(
_voting_escrow: address,
_start_time: uint256,
_token: address,
_admin: address,
_emergency_return: address
):
"""
@notice Contract constructor
@param _voting_escrow VotingEscrow contract address
@param _start_time Epoch time for fee distribution to start
@param _token Fee token address (crvUSD)
@param _admin Admin address
@param _emergency_return Address to transfer `_token` balance to
if this contract is killed
"""
t: uint256 = _start_time / WEEK * WEEK
self.start_time = t
self.last_token_time = t
self.time_cursor = t
self.token = _token
self.voting_escrow = _voting_escrow
self.admin = _admin
self.emergency_return = _emergency_return
```
::::
### `claim`
::::description[`FeeDistributor.claim(_addr: address = msg.sender) -> uint256`]
Function to claim fees for an account.
:::note
For off-chain integrators, this function can be called as though it were a view method in order to check the claimable amount.
Every veCRV related action (locking, extending a lock, increasing the locktime) increments a user’s veCRV epoch. A call to claim will consider at most 50 user epochs. For accounts that performed many veCRV actions, it may be required to call claim more than once to receive the fees. In such cases it can be more efficient to use `claim_many`.
:::
| Input | Type | Description |
| ------- | ------- | ----|
| `_addr` | `address` | Addresses to claim for. Defaults to `msg.sender`. |
Returns: amount of rewards claimed (`uint256`).
Emits: `Claimed` event.
```vyper
event Claimed:
recipient: indexed(address)
amount: uint256
claim_epoch: uint256
max_epoch: uint256
@external
@nonreentrant('lock')
def claim(_addr: address = msg.sender) -> uint256:
"""
@notice Claim fees for `_addr`
@dev Each call to claim look at a maximum of 50 user veCRV points.
For accounts with many veCRV related actions, this function
may need to be called more than once to claim all available
fees. In the `Claimed` event that fires, if `claim_epoch` is
less than `max_epoch`, the account may claim again.
@param _addr Address to claim fees for
@return uint256 Amount of fees claimed in the call
"""
assert not self.is_killed
if block.timestamp >= self.time_cursor:
self._checkpoint_total_supply()
last_token_time: uint256 = self.last_token_time
if self.can_checkpoint_token and (block.timestamp > last_token_time + TOKEN_CHECKPOINT_DEADLINE):
self._checkpoint_token()
last_token_time = block.timestamp
last_token_time = last_token_time / WEEK * WEEK
amount: uint256 = self._claim(_addr, self.voting_escrow, last_token_time)
if amount != 0:
token: address = self.token
assert ERC20(token).transfer(_addr, amount)
self.token_last_balance -= amount
return amount
@internal
def _claim(addr: address, ve: address, _last_token_time: uint256) -> uint256:
# Minimal user_epoch is 0 (if user had no point)
user_epoch: uint256 = 0
to_distribute: uint256 = 0
max_user_epoch: uint256 = VotingEscrow(ve).user_point_epoch(addr)
_start_time: uint256 = self.start_time
if max_user_epoch == 0:
# No lock = no fees
return 0
week_cursor: uint256 = self.time_cursor_of[addr]
if week_cursor == 0:
# Need to do the initial binary search
user_epoch = self._find_timestamp_user_epoch(ve, addr, _start_time, max_user_epoch)
else:
user_epoch = self.user_epoch_of[addr]
if user_epoch == 0:
user_epoch = 1
user_point: Point = VotingEscrow(ve).user_point_history(addr, user_epoch)
if week_cursor == 0:
week_cursor = (user_point.ts + WEEK - 1) / WEEK * WEEK
if week_cursor >= _last_token_time:
return 0
if week_cursor < _start_time:
week_cursor = _start_time
old_user_point: Point = empty(Point)
# Iterate over weeks
for i in range(50):
if week_cursor >= _last_token_time:
break
if week_cursor >= user_point.ts and user_epoch <= max_user_epoch:
user_epoch += 1
old_user_point = user_point
if user_epoch > max_user_epoch:
user_point = empty(Point)
else:
user_point = VotingEscrow(ve).user_point_history(addr, user_epoch)
else:
# Calc
# + i * 2 is for rounding errors
dt: int128 = convert(week_cursor - old_user_point.ts, int128)
balance_of: uint256 = convert(max(old_user_point.bias - dt * old_user_point.slope, 0), uint256)
if balance_of == 0 and user_epoch > max_user_epoch:
break
if balance_of > 0:
to_distribute += balance_of * self.tokens_per_week[week_cursor] / self.ve_supply[week_cursor]
week_cursor += WEEK
user_epoch = min(max_user_epoch, user_epoch - 1)
self.user_epoch_of[addr] = user_epoch
self.time_cursor_of[addr] = week_cursor
log Claimed(addr, to_distribute, user_epoch, max_user_epoch)
return to_distribute
```
::::
### `claim_many`
::::description[`FeeDistributor.claim_many(_receivers: address[20]) -> bool`]
Function to perform multiple claims in a single call. This is useful to claim for multiple accounts at once, or for making many claims against the same account if that account has performed more than 50 veCRV related actions.
| Input | Type | Description |
| ------- | ------- | ----|
| `_receivers` | `address[20]` | List of addresses to claim for. |
Returns: true (`bool`).
Emits: `Claimed` event.
```vyper
event Claimed:
recipient: indexed(address)
amount: uint256
claim_epoch: uint256
max_epoch: uint256
@external
@nonreentrant('lock')
def claim_many(_receivers: address[20]) -> bool:
"""
@notice Make multiple fee claims in a single call
@dev Used to claim for many accounts at once, or to make
multiple claims for the same address when that address
has significant veCRV history
@param _receivers List of addresses to claim for. Claiming
terminates at the first `ZERO_ADDRESS`.
@return bool success
"""
assert not self.is_killed
if block.timestamp >= self.time_cursor:
self._checkpoint_total_supply()
last_token_time: uint256 = self.last_token_time
if self.can_checkpoint_token and (block.timestamp > last_token_time + TOKEN_CHECKPOINT_DEADLINE):
self._checkpoint_token()
last_token_time = block.timestamp
last_token_time = last_token_time / WEEK * WEEK
voting_escrow: address = self.voting_escrow
token: address = self.token
total: uint256 = 0
for addr in _receivers:
if addr == ZERO_ADDRESS:
break
amount: uint256 = self._claim(addr, voting_escrow, last_token_time)
if amount != 0:
assert ERC20(token).transfer(addr, amount)
total += amount
if total != 0:
self.token_last_balance -= total
return True
@internal
def _claim(addr: address, ve: address, _last_token_time: uint256) -> uint256:
# Minimal user_epoch is 0 (if user had no point)
user_epoch: uint256 = 0
to_distribute: uint256 = 0
max_user_epoch: uint256 = VotingEscrow(ve).user_point_epoch(addr)
_start_time: uint256 = self.start_time
if max_user_epoch == 0:
# No lock = no fees
return 0
week_cursor: uint256 = self.time_cursor_of[addr]
if week_cursor == 0:
# Need to do the initial binary search
user_epoch = self._find_timestamp_user_epoch(ve, addr, _start_time, max_user_epoch)
else:
user_epoch = self.user_epoch_of[addr]
if user_epoch == 0:
user_epoch = 1
user_point: Point = VotingEscrow(ve).user_point_history(addr, user_epoch)
if week_cursor == 0:
week_cursor = (user_point.ts + WEEK - 1) / WEEK * WEEK
if week_cursor >= _last_token_time:
return 0
if week_cursor < _start_time:
week_cursor = _start_time
old_user_point: Point = empty(Point)
# Iterate over weeks
for i in range(50):
if week_cursor >= _last_token_time:
break
if week_cursor >= user_point.ts and user_epoch <= max_user_epoch:
user_epoch += 1
old_user_point = user_point
if user_epoch > max_user_epoch:
user_point = empty(Point)
else:
user_point = VotingEscrow(ve).user_point_history(addr, user_epoch)
else:
# Calc
# + i * 2 is for rounding errors
dt: int128 = convert(week_cursor - old_user_point.ts, int128)
balance_of: uint256 = convert(max(old_user_point.bias - dt * old_user_point.slope, 0), uint256)
if balance_of == 0 and user_epoch > max_user_epoch:
break
if balance_of > 0:
to_distribute += balance_of * self.tokens_per_week[week_cursor] / self.ve_supply[week_cursor]
week_cursor += WEEK
user_epoch = min(max_user_epoch, user_epoch - 1)
self.user_epoch_of[addr] = user_epoch
self.time_cursor_of[addr] = week_cursor
log Claimed(addr, to_distribute, user_epoch, max_user_epoch)
return to_distribute
```
```shell
>>> FeeDistributor.claim_many()
'True'
```
::::
### `can_checkpoint_token`
::::description[`FeeDistributor.can_checkpoint_token() -> bool: view`]
Getter to check if tokens can be checkpointed.
Returns: true or false (`bool`).
```vyper
can_checkpoint_token: public(bool)
```
::::
### `checkpoint_token`
::::description[`FeeDistributor.checkpoint_token()`]
Function to update the token checkpoint. The token checkpoint tracks the balance of 3CRV within the distributor to determine the amount of fees to distribute in the given week. The checkpoint can be updated at most once every 24 hours. Fees that are received between the last checkpoint of the previous week and first checkpoint of the new week will be split evenly between the weeks. To ensure full distribution of fees in the current week, the burn process must be completed prior to the last checkpoint within the week. A token checkpoint is automatically taken during any `claim` action, if the last checkpoint is more than 24 hours old.
Emits: `CheckpointToken` event.
```vyper
event CheckpointToken:
time: uint256
tokens: uint256
can_checkpoint_token: public(bool)
@external
def checkpoint_token():
"""
@notice Update the token checkpoint
@dev Calculates the total number of tokens to be distributed in a given week.
During setup for the initial distribution this function is only callable
by the contract owner. Beyond initial distro, it can be enabled for anyone
to call.
"""
assert (msg.sender == self.admin) or\
(self.can_checkpoint_token and (block.timestamp > self.last_token_time + TOKEN_CHECKPOINT_DEADLINE))
self._checkpoint_token()
@internal
def _checkpoint_token():
token_balance: uint256 = ERC20(self.token).balanceOf(self)
to_distribute: uint256 = token_balance - self.token_last_balance
self.token_last_balance = token_balance
t: uint256 = self.last_token_time
since_last: uint256 = block.timestamp - t
self.last_token_time = block.timestamp
this_week: uint256 = t / WEEK * WEEK
next_week: uint256 = 0
for i in range(20):
next_week = this_week + WEEK
if block.timestamp < next_week:
if since_last == 0 and block.timestamp == t:
self.tokens_per_week[this_week] += to_distribute
else:
self.tokens_per_week[this_week] += to_distribute * (block.timestamp - t) / since_last
break
else:
if since_last == 0 and next_week == t:
self.tokens_per_week[this_week] += to_distribute
else:
self.tokens_per_week[this_week] += to_distribute * (next_week - t) / since_last
t = next_week
this_week = next_week
log CheckpointToken(block.timestamp, to_distribute)
```
```shell
>>> FeeDistributor.checkpoint_token()
```
::::
### `toggle_allow_checkpoint_token`
::::description[`FeeDistributor.toggle_allow_checkpoint_token()`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to toggle permission for checkpointing by an account.
Emits: `ToggleAllowCheckpointToken` event.
```vyper
event ToggleAllowCheckpointToken:
toggle_flag: bool
@external
def toggle_allow_checkpoint_token():
"""
@notice Toggle permission for checkpointing by any account
"""
assert msg.sender == self.admin
flag: bool = not self.can_checkpoint_token
self.can_checkpoint_token = flag
log ToggleAllowCheckpointToken(flag)
```
```shell
>>> FeeDistributor.toggle_allow_checkpoint_token()
```
::::
### `checkpoint_total_supply`
::::description[`FeeDistributor.checkpoint_total_supply()`]
Function to update the veCRV total supply checkpoint. The checkpoint is also updated by the first claimant each new epoch week. This function may be called independently of a claim, to reduce claiming gas costs.
```vyper
@external
def checkpoint_total_supply():
"""
@notice Update the veCRV total supply checkpoint
@dev The checkpoint is also updated by the first claimant each
new epoch week. This function may be called independently
of a claim, to reduce claiming gas costs.
"""
self._checkpoint_total_supply()
@internal
def _checkpoint_total_supply():
ve: address = self.voting_escrow
t: uint256 = self.time_cursor
rounded_timestamp: uint256 = block.timestamp / WEEK * WEEK
VotingEscrow(ve).checkpoint()
for i in range(20):
if t > rounded_timestamp:
break
else:
epoch: uint256 = self._find_timestamp_epoch(ve, t)
pt: Point = VotingEscrow(ve).point_history(epoch)
dt: int128 = 0
if t > pt.ts:
# If the point is at 0 epoch, it can actually be earlier than the first deposit
# Then make dt 0
dt = convert(t - pt.ts, int128)
self.ve_supply[t] = convert(max(pt.bias - pt.slope * dt, 0), uint256)
t += WEEK
self.time_cursor = t
```
```shell
>>> FeeDistributor.checkpoint_total_supply()
```
::::
### `burn`
::::description[`FeeDistributor.burn(_coin: address) -> bool`]
Function to receive 3CRV or crvUSD into the contract and trigger a token checkpoint.
| Input | Type | Description |
| ------- | -------| ----|
| `_coin` | `address` | Address of the coin being received. |
Returns: true (`bool`).
```vyper
@external
def burn(_coin: address) -> bool:
"""
@notice Receive 3CRV into the contract and trigger a token checkpoint
@param _coin Address of the coin being received (must be 3CRV)
@return bool success
"""
assert _coin == self.token
assert not self.is_killed
amount: uint256 = ERC20(_coin).balanceOf(msg.sender)
if amount != 0:
ERC20(_coin).transferFrom(msg.sender, self, amount)
if self.can_checkpoint_token and (block.timestamp > self.last_token_time + TOKEN_CHECKPOINT_DEADLINE):
self._checkpoint_token()
return True
```
```shell
>>> FeeDistributor.burn("0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490")
```
::::
---
## Killing The FeeDistributor
The `FeeDistributor` can be killed by the `admin` of the contract, which is the Curve DAO. Doing so, transfers the entire token balance to the `emergency_return` address and blocks the ability to claim or burn. The contract cannot be unkilled.
### `is_killed`
::::description[`FeeDistributor.is_killed() -> bool: view`]
Getter method to check if the `FeeDistributor` contract is killed. When killed, the contract blocks `claim` and `burn` and the entire token balance is transferred to the `emergency_return` address.
Returns: true or false (`bool`).
```vyper
is_killed: public(bool)
```
::::
### `kill_me`
::::description[`FeeDistributor.kill_me()`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to kill the `FeeDistributor` contract.
:::danger
Killing transfers the entire token balance to the [`emergency_return`](#emergency_return) address and blocks the ability to `claim` or `burn`.
:::
```vyper
is_killed: public(bool)
@external
def kill_me():
"""
@notice Kill the contract
@dev Killing transfers the entire 3CRV balance to the emergency return address
and blocks the ability to claim or burn. The contract cannot be unkilled.
"""
assert msg.sender == self.admin
self.is_killed = True
token: address = self.token
assert ERC20(token).transfer(self.emergency_return, ERC20(token).balanceOf(self))
```
```shell
>>> FeeDistributor.kill_me()
```
::::
### `emergency_return`
::::description[`FeeDistributor.emergency_return() -> address: view`]
Getter for the emergency return address. This address cannot be changed.
Returns: emergency return (`address`).
```vyper
emergency_return: public(address)
```
::::
### `recover_balance`
::::description[`FeeDistributor.recover_balance(_coin: address) -> bool`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to recover ERC20 tokens from the contract. Tokens are sent to the emergency return address. This function only works for tokens other than the address set for `token`. E.g. this function on the 3CRV distributor contract cannot be called to transfer 3CRV. The same applies to the crvUSD distributor.
| Input | Type | Description |
| ----------- | -------| ----|
| `_coin` | `address` | Coin address to recover |
Returns: true (`bool`).
```vyper
@external
def recover_balance(_coin: address) -> bool:
"""
@notice Recover ERC20 tokens from this contract
@dev Tokens are sent to the emergency return address.
@param _coin Token address
@return bool success
"""
assert msg.sender == self.admin
assert _coin != self.token
amount: uint256 = ERC20(_coin).balanceOf(self)
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("transfer(address,uint256)"),
convert(self.emergency_return, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
return True
```
```shell
>>> FeeDistributor.recover_balance("0xdAC17F958D2ee523a2206206994597C13D831ec7")
```
::::
---
## Admin Ownership
### `admin`
::::description[`FeeDistributor.admin() -> address: view`]
Getter for the admin of the contract.
Returns: admin (`address`).
```vyper
admin: public(address)
```
::::
### `future_admin`
::::description[`FeeDistributor.future_admin() -> address: view`]
Getter for the future admin of the contract.
Returns: future admin (`address`).
```vyper
future_admin: public(address)
```
::::
### `commit_admin`
::::description[`FeeDistributor.commit_admin(_addr: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to commit transfer of the ownership.
| Input | Type | Description |
| ----------- | -------| ---- |
| `_addr` | `address` | Address to commit the ownership transfer to. |
Emits: `CommitAdmin` event.
```vyper
event CommitAdmin:
admin: address
admin: public(address)
future_admin: public(address)
@external
def commit_admin(_addr: address):
"""
@notice Commit transfer of ownership
@param _addr New admin address
"""
assert msg.sender == self.admin # dev: access denied
self.future_admin = _addr
log CommitAdmin(_addr)
```
```shell
>>> FeeDistributor.commit_admin("0x0000000000000000000000000000000000000000")
```
::::
### `apply_admin`
::::description[`FeeDistributor.apply_admin()`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to apply the transfer of the ownership.
Emits: `ApplyAdmin` event.
```vyper
event ApplyAdmin:
admin: address
admin: public(address)
future_admin: public(address)
@external
def apply_admin():
"""
@notice Apply transfer of ownership
"""
assert msg.sender == self.admin
assert self.future_admin != ZERO_ADDRESS
future_admin: address = self.future_admin
self.admin = future_admin
log ApplyAdmin(future_admin)
```
```shell
>>> FeeDistributor.apply_admin()
```
::::
---
## Query Contract Information
### `ve_for_at`
::::description[`FeeDistributor.ve_for_at(_user: address, _timestamp: uint256) -> uint256: view`]
Getter for the veCRV balance for `_user` at `_timestamp`.
Returns: veCRV balance (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `_user` | `address` | Address to query the veCRV balance for. |
| `_timestamp` | `uint256` | Timestamp. |
```vyper
@view
@external
def ve_for_at(_user: address, _timestamp: uint256) -> uint256:
"""
@notice Get the veCRV balance for `_user` at `_timestamp`
@param _user Address to query balance for
@param _timestamp Epoch time
@return uint256 veCRV balance
"""
ve: address = self.voting_escrow
max_user_epoch: uint256 = VotingEscrow(ve).user_point_epoch(_user)
epoch: uint256 = self._find_timestamp_user_epoch(ve, _user, _timestamp, max_user_epoch)
pt: Point = VotingEscrow(ve).user_point_history(_user, epoch)
return convert(max(pt.bias - pt.slope * convert(_timestamp - pt.ts, int128), 0), uint256)
```
::::
### `start_time`
::::description[`FeeDistributor.start_time() -> uint256: view`]
Getter for the epoch time for fee distribution to start.
Returns: epoch time (`uint256`).
```vyper
start_time: public(uint256)
@external
def __init__(
_voting_escrow: address,
_start_time: uint256,
_token: address,
_admin: address,
_emergency_return: address
):
"""
@notice Contract constructor
@param _voting_escrow VotingEscrow contract address
@param _start_time Epoch time for fee distribution to start
@param _token Fee token address (3CRV)
@param _admin Admin address
@param _emergency_return Address to transfer `_token` balance to
if this contract is killed
"""
t: uint256 = _start_time / WEEK * WEEK
self.start_time = t
self.last_token_time = t
self.time_cursor = t
self.token = _token
self.voting_escrow = _voting_escrow
self.admin = _admin
self.emergency_return = _emergency_return
```
::::
### `user_epoch_of`
::::description[`FeeDistributor.user_epoch_of(arg0: address) -> uint256: view`]
Getter for the user epoch of `arg0`.
| Input | Type | Description |
| ----------- | -------| ----|
| `arg0` | `address` | Address to get the user epoch for. |
Returns: user epoch (`uint256`).
```vyper
user_epoch_of: public(HashMap[address, uint256])
```
::::
### `voting_escrow`
::::description[`FeeDistributor.voting_escrow() -> address: view`]
Getter for the voting escrow contract.
Returns: voting-escrow (`address`).
```vyper
voting_escrow: public(address)
@external
def __init__(
_voting_escrow: address,
_start_time: uint256,
_token: address,
_admin: address,
_emergency_return: address
):
"""
@notice Contract constructor
@param _voting_escrow VotingEscrow contract address
@param _start_time Epoch time for fee distribution to start
@param _token Fee token address (3CRV)
@param _admin Admin address
@param _emergency_return Address to transfer `_token` balance to
if this contract is killed
"""
t: uint256 = _start_time / WEEK * WEEK
self.start_time = t
self.last_token_time = t
self.time_cursor = t
self.token = _token
self.voting_escrow = _voting_escrow
self.admin = _admin
self.emergency_return = _emergency_return
```
::::
### `token_last_balance`
::::description[`FeeDistributor.token_last_balance() -> uint256: view`]
Getter for the token balance of `token`.
Returns: last token balance (`uint256`).
```vyper
token_last_balance: public(uint256)
```
::::
---
## Curve DAO: Fee Collection and Distribution
:::danger[PARTLY OUTDATED: New Fee Collection, Burn, and Distribution System]
In June 2024, Curve deployed a new system to collect, burn, and distribute fees in a much more efficient manner. For full documentation of this system, please read here: [Fee Collection & Distribution](../overview.md).
:::
Curve exchange contracts have the capability to charge an **admin fee**, claimable by the contract owner. The admin fee is represented as a percentage of the total fee collected on a swap.
**There are multiple ways on how fees are obtained:**
- For **stableswap exchanges** the **fee is taken in the output token** of the swap and calculated against the final amount received. For example, if swapping from USDT to USDC, the fee is taken in USDC.
- For **cryptoswap exchanges** the **fee is taken in the LP token of the pool**. For these kind of pools additional mechanisms like auto-rebalancing parameters need to be taken into consideration.
- **Curve Stablecoin** borrow **fee is taken in crvUSD**. 100% of crvUSD borrow rate fees are "admin fees".
Liquidity providers also incur fees when adding or removing liquidity. The fee is applied such that, for example, a swap between USDC and USDT would pay roughly the same amount of fees as depositing USDC into the pool and then withdrawing USDT. The only case where a fee is not applied on withdrawal is when removing liquidity, as this does not change the imbalance of the pool in any way.
Exchange contracts are indirectly owned by the Curve DAO via a proxy ownership contract. This contract includes functionality to withdraw the fees, convert them to 3CRV, and forward them into the fee distributor contract. Collectively, this process is referred to as “burning”.
:::info
The burn process involves multiple transactions and is very gas intensive. Anyone can execute any step of the burn process at any time and there is no hard requirement that it happens in the correct order. However, running the steps out of order can be highly inefficient. If you wish to burn, it is recommended that you review all of the following information so you understand exactly what is happening.
:::
---
## Sidechains
Fee collection on sidechains works similarly to that on the Ethereum mainnet. Collected fees are sent to a fee receiver contract and then burned. On most sidechains, tokens are burnt for [MIM](https://etherscan.io/address/0x99d8a9c45b2eca8864373a26d1459e3dff1e17f3), as it's an easy asset to bridge back to the mainnet. These proxy contracts have a `bridge()` function to bridge the tokens to the Ethereum mainnet.
MIM is then burnt for 3CRV on Ethereum and sent to the FeeDistributor.
:::note
The contract owner can bridge any token in any quantity, other accounts can only bridge approved tokens, where the balance exceeds a minimum amount defined by the owner. This prevents bridging tokens when the amount is so small that claiming on the root chain becomes economically unfeasible.
:::
```vyper
@external
def bridge(_coin: address):
"""
@notice Transfer a coin to the root chain via the bridging contract.
@dev The contract owner can bridge any token in any quantity,
other accounts can only bridge approved tokens, where
the balance exceeds a minimum amount defined by the owner.
This prevents bridging tokens when the amount is so small
that claiming on the root chain becomes economically unfeasible.
@param _coin Address of the coin to be bridged.
"""
bridging_contract: address = self.bridging_contract
amount: uint256 = ERC20(_coin).balanceOf(self)
if amount > 0:
response: Bytes[32] = raw_call(
_coin,
_abi_encode(bridging_contract, amount, method_id=method_id("transfer(address,uint256)")),
max_outsize=32,
)
if msg.sender != self.admin:
minimum: uint256 = self.bridge_minimums[_coin]
assert minimum != 0, "Coin not approved for bridging"
assert minimum <= ERC20(_coin).balanceOf(bridging_contract), "Balance below minimum bridge amount"
Bridger(bridging_contract).bridge(_coin)
```
## Bridging
:::warning
The methods to burn and bridge assets *might slightly vary based on the chain*. The examples down below are taken from [Optimism](https://www.optimism.io/).
:::
### `bridge`
::::description[`Bridge.bridge(coin: address) -> bool`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract. Additionally, only `TOKEN` can be bridged.
:::
Function to bridge the entire balance of `_coin` to the root chain. This function must be called from its proxy contract, as its the owner.
| Input | Type | Description |
| ----------- | -------| ----|
| `_coin` | `address` | Coin to bridge |
Returns: true (`bool`).
Emits: `AssetBridged` event.
```vyper
event AssetBridged:
token: indexed(address)
amount: uint256
@external
def bridge(coin: address) -> bool:
assert msg.sender == self.owner and coin == TOKEN
start: uint256 = self.start
assert block.timestamp > start + 1800
amount: uint256 = ERC20(coin).balanceOf(self)
if amount == 0:
amount = ERC20(coin).balanceOf(msg.sender)
assert ERC20(coin).transferFrom(msg.sender, self, amount)
if start == 0:
ERC20(coin).transfer(msg.sender, ERC20(coin).balanceOf(self) - amount / 100)
amount = amount / 100
self.start = block.timestamp
receiver: address = self.receiver
adapter_params: Bytes[128] = concat(
b"\x00\x02",
convert(100_000, bytes32),
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
convert(receiver, bytes32)
)
fee: uint256 = ProxyOFT(PROXY_OFT).estimateSendFee(
ETH_CHAIN_ID, convert(receiver, bytes32), amount, False, adapter_params,
)
assert ERC20(coin).approve(PROXY_OFT, amount)
ProxyOFT(PROXY_OFT).sendFrom(
self,
ETH_CHAIN_ID,
convert(receiver, bytes32),
amount,
CallParams({
refund: self,
zero_payer: empty(address),
adapter_params: adapter_params,
}),
value=fee
)
log AssetBridged(coin, amount)
return True
```
```shell
>>> Bridge.bridge('0xB153FB3d196A8eB25522705560ac152eeEc57901')
```
::::
## Contract Info Methods
### `PROXY_OFT`
::::description[`Bridge.PROXY_OFT() -> address: view`]
Getter for the OFT bridger.
Returns: proxy OFT (`address`).
```vyper
PROXY_OFT: public(immutable(address))
@external
def __init__(proxy_oft: address, receiver: address, token: address):
self.owner = msg.sender
self.receiver = receiver
PROXY_OFT = proxy_oft
TOKEN = token
log AcceptOwnership(msg.sender)
```
```shell
>>> Bridge.PROXY_OFT()
'0x48686c24697fe9042531B64D792304e514E74339'
```
::::
### `TOKEN`
::::description[`Bridge.TOKEN() -> address: view`]
Getter for the bridge token.
Returns: token (`address`).
```vyper
TOKEN: public(immutable(address))
@external
def __init__(proxy_oft: address, receiver: address, token: address):
self.owner = msg.sender
self.receiver = receiver
PROXY_OFT = proxy_oft
TOKEN = token
log AcceptOwnership(msg.sender)
```
```shell
>>> Bridge.TOKEN()
'0xB153FB3d196A8eB25522705560ac152eeEc57901'
```
::::
## Receiver
Receiver of the bridged funds is the 0xECB contract on Ethereum Mainnet.
### `receiver`
::::description[`Bridge.receiver() -> address: view`]
Getter for the receiver address of the bridged funds.
Returns: receiver (`address`).
:::note
Receiver is the 0xECB contract (FeeCollector/Proxy on Ethereum Mainnet).
:::
```vyper
PROXY_OFT: public(immutable(address))
@external
def __init__(proxy_oft: address, receiver: address, token: address):
self.owner = msg.sender
self.receiver = receiver
PROXY_OFT = proxy_oft
TOKEN = token
log AcceptOwnership(msg.sender)
```
```shell
>>> Bridge.receiver()
'0xeCb456EA5365865EbAb8a2661B0c503410e9B347'
```
::::
### `set_root_receiver`
::::description[`Bridge.set_root_receiver(receiver: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to set a new receiver address for the bridged funds.
| Input | Type | Description |
| ----------- | -------| ----|
| `receiver` | `address` | New receiver address |
```vyper
@external
def set_root_receiver(receiver: address):
assert msg.sender == self.owner
assert receiver != empty(address)
self.receiver = receiver
```
```shell
>>> Bridge.set_root_receiver('0x0000000000000000000000000000000000000000')
```
::::
---
## Withdraw & Burn
In order to be able to burn admin fees into the fee token, those fees have to be claimed prior. **Admin fees can be claimed by anyone.** Sometimes, the function to claim the fees is guarded and therefore only called by the proxy contract (admin or owner of the pool). If that's the case, users can just call the claim function via the proxy contract (as the function is not guarded there).
Claiming fees can differ based on which source they are claimed from:
## StableSwap Pools
Admin fees are stored within each exchange contract and viewable via the public getter method **`admin_balances`**. Users may call **`withdraw_admin_fees`**to claim the fees at any time.
Fees are usually claimed via the **`withdraw_many`**function of the PoolProxy. This withdraws fees from multiple pools at once, pulling them into the [PoolProxy](https://etherscan.io/address/0xeCb456EA5365865EbAb8a2661B0c503410e9B347#writeContract) contract.
:::tip
Admin fees can either be claimed through the corresponding PoolProxy or directly by calling the **`withdraw_admin_fees`**function on the pool contract itself (if the function is not guarded).
:::
### `admin_balances`
::::description[`Pool.admin_balances(i: uint256) -> uint256: view`]
Getter for the admin fees of coin `i` in a specific pool.
Returns: admin balances (`uint256`).
| Input | Type | Description |
| ----------- | -------| ----|
| `i` | `uint256` | Coin index |
```vyper
@view
@external
def admin_balances(i: uint256) -> uint256:
return ERC20(self.coins[i]).balanceOf(self) - self.balances[i]
```
```shell
>>> Pool.admin_balances(0)
466943482298782278664
```
::::
### `withdraw_admin_fees`
::::description[`PoolProxy.withdraw_admin_fees(_pool: address)`]
Function to claim admin fees from `_pool` into this contract. This is the first step in the fee burning process.
:::info
This function is called from the PoolProxy.
:::
| Input | Type | Description |
| ----------- | -------| ----|
| `_pool` | `address` | Pool address |
```vyper
interface Curve:
def withdraw_admin_fees(): nonpayable
@external
@nonreentrant('lock')
def withdraw_admin_fees(_pool: address):
"""
@notice Withdraw admin fees from `_pool`
@param _pool Pool address to withdraw admin fees from
"""
Curve(_pool).withdraw_admin_fees()
```
```shell
>>> PoolProxy.withdraw_admin_fees("0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7")
```
::::
### `withdraw_many`
::::description[`PoolProxy.withdraw_many(_pools: address[20])`]
Function to withdraw fees from multiple pools in a single call.
:::info
This function is called from the PoolProxy.
:::
| Input | Type | Description |
| ----------- | -------| ----|
| `_pools` | `address[20]` | Pool Addresses |
```vyper
interface Curve:
def withdraw_admin_fees(): nonpayable
@external
@nonreentrant('lock')
def withdraw_many(_pools: address[20]):
"""
@notice Withdraw admin fees from multiple pools
@param _pools List of pool address to withdraw admin fees from
"""
for pool in _pools:
if pool == ZERO_ADDRESS:
break
Curve(pool).withdraw_admin_fees()
```
```shell
>>> PoolProxy.withdraw_many(["0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7", "0xA5407eAE9Ba41422680e2e00537571bcC53efBfD"])
```
::::
## CryptoSwap Pools
Fees of crypto pools are a bit different from stableswap pools. These pools have an auto-rebalancing mechanism which uses parts of the admin fees for rebalancing purposes. After taking this into consideration, fees are claimed by minting the admin's share (which essentially is the admin fee) of the pool as LP tokens.
Fees are mostly claimed directly from the pool.
### `claim_admin_fees`
::::description[`Pool.claim_admin_fees()`]
Function to claim admin fees from a crypto pool.
Emits: `ClaimAdminFee` event.
```vyper
event ClaimAdminFee:
admin: indexed(address)
tokens: uint256
@external
@nonreentrant("lock")
def claim_admin_fees():
"""
@notice Claim admin fees. Callable by anyone.
"""
self._claim_admin_fees()
@internal
def _claim_admin_fees():
"""
@notice Claims admin fees and sends it to fee_receiver set in the factory.
"""
A_gamma: uint256[2] = self._A_gamma()
xcp_profit: uint256 = self.xcp_profit # <---------- Current pool profits.
xcp_profit_a: uint256 = self.xcp_profit_a # <- Profits at previous claim.
total_supply: uint256 = self.totalSupply
# Do not claim admin fees if:
# 1. insufficient profits accrued since last claim, and
# 2. there are less than 10**18 (or 1 unit of) lp tokens, else it can lead
# to manipulated virtual prices.
if xcp_profit <= xcp_profit_a or total_supply < 10**18:
return
# Claim tokens belonging to the admin here. This is done by 'gulping'
# pool tokens that have accrued as fees, but not accounted in pool's
# `self.balances` yet: pool balances only account for incoming and
# outgoing tokens excluding fees. Following 'gulps' fees:
for i in range(N_COINS):
if coins[i] == WETH20:
self.balances[i] = self.balance
else:
self.balances[i] = ERC20(coins[i]).balanceOf(self)
# If the pool has made no profits, `xcp_profit == xcp_profit_a`
# and the pool gulps nothing in the previous step.
vprice: uint256 = self.virtual_price
# Admin fees are calculated as follows.
# 1. Calculate accrued profit since last claim. `xcp_profit`
# is the current profits. `xcp_profit_a` is the profits
# at the previous claim.
# 2. Take out admin's share, which is hardcoded at 5 * 10**9.
# (50% => half of 100% => 10**10 / 2 => 5 * 10**9).
# 3. Since half of the profits go to rebalancing the pool, we
# are left with half; so divide by 2.
fees: uint256 = unsafe_div(
unsafe_sub(xcp_profit, xcp_profit_a) * ADMIN_FEE, 2 * 10**10
)
# ------------------------------ Claim admin fees by minting admin's share
# of the pool in LP tokens.
receiver: address = Factory(self.factory).fee_receiver()
if receiver != empty(address) and fees > 0:
frac: uint256 = vprice * 10**18 / (vprice - fees) - 10**18
claimed: uint256 = self.mint_relative(receiver, frac)
xcp_profit -= fees * 2
self.xcp_profit = xcp_profit
log ClaimAdminFee(receiver, claimed)
# ------------------------------------------- Recalculate D b/c we gulped.
D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], self.xp(), 0)
self.D = D
# ------------------- Recalculate virtual_price following admin fee claim.
# In this instance we do not check if current virtual price is greater
# than old virtual price, since the claim process can result
# in a small decrease in pool's value.
self.virtual_price = 10**18 * self.get_xcp(D) / self.totalSupply
self.xcp_profit_a = xcp_profit # <------------ Cache last claimed profit.
```
```shell
>>> Pool.claim_admin_fees()
```
::::
## Curve Stablecoin
crvUSD fees are based on the borrow rate of the corresponding markets. Fees are accrued in crvUSD token. They can be claimed from the according Controller.
### `admin_fees`
::::description[`Controller.admin_fees() -> uint256: view`]
Getter for the currently claimable admin fees from a Controller. These fees can be collected via the `collect_fees()` function (see below).
Returns: claimable admin fees (`uint256`).
```vyper
@external
@view
def admin_fees() -> uint256:
"""
@notice Calculate the amount of fees obtained from the interest
"""
rate_mul: uint256 = AMM.get_rate_mul()
loan: Loan = self._total_debt
loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul
loan.initial_debt += self.redeemed
minted: uint256 = self.minted
return unsafe_sub(max(loan.initial_debt, minted), minted)
```
```shell
>>> Controller.admin_fees()
14630333074120584376402
```
::::
### `collect_fees`
::::description[`Controller.collect_fees()`]
Function to collect all fees, including borrowing-based fees (interest rate) and AMM-based fees (swap fee, if applicable).
Returns: amount of fees collected (`uint256`).
Emits: `CollectFees` event.
```vyper
@external
@nonreentrant('lock')
def collect_fees() -> uint256:
"""
@notice Collect the fees charged as interest
"""
_to: address = FACTORY.fee_receiver()
# AMM-based fees
borrowed_fees: uint256 = AMM.admin_fees_x()
collateral_fees: uint256 = AMM.admin_fees_y()
if borrowed_fees > 0:
STABLECOIN.transferFrom(AMM.address, _to, borrowed_fees)
if collateral_fees > 0:
assert COLLATERAL_TOKEN.transferFrom(AMM.address, _to, collateral_fees, default_return_value=True)
AMM.reset_admin_fees()
# Borrowing-based fees
rate_mul: uint256 = self._rate_mul_w()
loan: Loan = self._total_debt
loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul
loan.rate_mul = rate_mul
self._total_debt = loan
# Amount which would have been redeemed if all the debt was repaid now
to_be_redeemed: uint256 = loan.initial_debt + self.redeemed
# Amount which was minted when borrowing + all previously claimed admin fees
minted: uint256 = self.minted
# Difference between to_be_redeemed and minted amount is exactly due to interest charged
if to_be_redeemed > minted:
self.minted = to_be_redeemed
to_be_redeemed = unsafe_sub(to_be_redeemed, minted) # Now this is the fees to charge
STABLECOIN.transfer(_to, to_be_redeemed)
log CollectFees(to_be_redeemed, loan.initial_debt)
return to_be_redeemed
else:
log CollectFees(0, loan.initial_debt)
return 0
```
```shell
>>> Controller.collect_fees()
```
::::
## Burning Admin Fees
All admin fees are accumulated in the [0xECB](https://etherscan.io/address/0xeCb456EA5365865EbAb8a2661B0c503410e9B347) \{ title="shhhh!! don't tell Christine Lagarde!" \} contract and are burned according to the fee-burner settings designated for each specific coin.
*These functions need to be called from the 0xECB contract.*
### `burn`
::::description[`0xECB.burn(_coin: address)`]
:::guard[Guarded Method]
This function is only callable by EOA to prevent flashloan exploits.
:::
Transfer the contract’s balance of `coin` into the according burner and execute the burn process.
| Input | Type | Description |
| ----------- | -------| ----|
| `_coin` | `address` | Token Address |
```vyper
interface Burner:
def burn(_coin: address) -> bool: payable
@external
@nonreentrant('burn')
def burn(_coin: address):
"""
@notice Burn accrued `_coin` via a preset burner
@dev Only callable by an EOA to prevent flashloan exploits
@param _coin Coin address
"""
assert tx.origin == msg.sender
assert not self.burner_kill
_value: uint256 = 0
if _coin == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE:
_value = self.balance
Burner(self.burners[_coin]).burn(_coin, value=_value) # dev: should implement burn()
```
```shell
>>> PoolProxy.burn("0xdAC17F958D2ee523a2206206994597C13D831ec7")
```
::::
### `burn_many`
::::description[`0xECB.burn_many(_coins: address[20])`]
:::guard[Guarded Method]
This function is only callable by EOA to prevent flashloan exploits.
:::
Executes the burn process on many coins at once.
Burning can be very gas intensive. In some cases burning 20 coins at once is not possible due to the block gas limit.
| Input | Type | Description |
| ----------- | -------| ----|
| `_coins` | `address[20]` | Token Addresses |
```vyper
@external
@nonreentrant('burn')
def burn_many(_coins: address[20]):
"""
@notice Burn accrued admin fees from multiple coins
@dev Only callable by an EOA to prevent flashloan exploits
@param _coins List of coin addresses
"""
assert tx.origin == msg.sender
assert not self.burner_kill
for coin in _coins:
if coin == ZERO_ADDRESS:
break
_value: uint256 = 0
if coin == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE:
_value = self.balance
Burner(self.burners[coin]).burn(coin, value=_value) # dev: should implement burn()
```
```shell
>>> PoolProxy.burn_many(["0xdAC17F958D2ee523a2206206994597C13D831ec7", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"])
```
::::
### `donate_admin_fees`
::::description[`0xECB.donate_admin_fees(_pool: address)`]
:::guard[Guarded Method]
This function is only callable by the `ownership_admin` or its prior approved wallets.
:::
Function to donate a pool’s current admin fees to the pool LPs.
:::warning
**Most pools do not have this donation function implemented!**
:::
| Input | Type | Description |
| ----------- | -------| ----|
| `_pool` | `address` | Pool address |
```vyper
interface Curve:
def donate_admin_fees(): nonpayable
@external
@nonreentrant(‘lock’)
def donate_admin_fees(_pool: address):
"""
@notice Donate admin fees of `_pool` pool
@param _pool Pool address
"""
if msg.sender != self.ownership_admin:
assert self.donate_approval[_pool][msg.sender], "Access denied"
Curve(_pool).donate_admin_fees() # dev: if implemented by the pool
```
```shell
>>> PoolProxy.donate_admin_fees("0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7")
```
::::
### `set_donate_approval`
::::description[`0xECB.set_donate_approval(_pool: address, _caller: address, _is_approved: bool)`]
:::guard[Guarded Method]
This function is only callable by the `ownership_admin` of the contract.
:::
Function to set donation approval for `_pool` to `_caller`.
:::warning
**Most pools do not have this donation function implemented!**
:::
| Input | Type | Description |
| ----------- | -------| ----|
| `_pool` | `address` | Pool address |
| `_caller` | `address` | Address to set approval for |
| `_is_approved` | `bool` | Approval status |
```vyper
# pool -> caller -> can call `donate_admin_fees`
donate_approval: public(HashMap[address, HashMap[address, bool]])
@external
def set_donate_approval(_pool: address, _caller: address, _is_approved: bool):
"""
@notice Set approval of `_caller` to donate admin fees for `_pool`
@param _pool Pool address
@param _caller Adddress to set approval for
@param _is_approved Approval status
"""
assert msg.sender == self.ownership_admin, "Access denied"
self.donate_approval[_pool][_caller] = _is_approved
```
```shell
>>> PoolProxy.set_donate_approval("0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7", "0x989AEb4d175e16225E39E87d0D97A3360524AD80", True)
```
::::
---
## Fee Collection, Burning, and Distribution
The Curve ecosystem generates revenue from various sources, primarily through trading fees from liquidity pools and interest from crvUSD markets. This page explains how these fees are collected, converted, and distributed to veCRV holders, detailing the contracts and processes involved.
The two primary revenue sources of the DAO are **fees collected from liquidity pools** and **interest rate fees from crvUSD markets**.
Usually, admin fees of liquidity pools are collected just as the tokens in the pool, though sometimes fees are collected in LP tokens. For the system, this distinction does not make any significant difference.
:::telegram[Telegram]
If you are running or planning to run fee collection for Curve DAO, there is a Telegram channel and a group for necessary updates. Also, many hooks for automation are coming in the future which will be written about in the group.
[→ Join the Telegram group](https://t.me/curve_automation)
:::
---
## System Overview
The state of the system can be roughly summarized as follows:
Overview of Curve's fee collection, burning, and distribution system
*The collected admin fees can accrue in any type of tokens, whether they are LP or "regular" tokens. For simplicity, the `Hooker.vy` contract, which is essentially responsible for transferring the reward tokens from the `FeeCollector` to the `FeeDistributor`, was omitted from the graph. This infrastructure functions exactly the same on chains where the CowSwap Protocol is deployed (Gnosis and soon Arbitrum). The only difference is that instead of transferring the reward tokens to a `FeeDistributor` contract, they are bridged to the `FeeCollector` on the Ethereum mainnet.*
---
## Key Contracts
The fee collection, burning, and distribution system of Curve involves the following main contracts:
Contract which acts as the entry point for the fee burning system. All admin fees in various kinds of tokens are collected here.
Contract that collects accumulated crvUSD fees from crvUSD Controllers and distributes them to other contracts according to predetermined weights in a single transaction.
Contract which burns the collected admin fees into a unified token. The current system utilizes CowSwap's conditional orders to burn the accumulated fees into a specific target token.
Contract that allows users to execute certain hooks like forwarding crvUSD from the `FeeCollector` to the `FeeDistributor`.
The `FeeDistributor` is the contract which distributes the fee token to veCRV holders. This contract is only deployed on Ethereum mainnet. There are actually two `FeeDistributors` deployed, as rewards were distributed in `3CRV` tokens, before a DAO vote changed the reward token to `crvUSD`.
---
## Fee Burning
The process of burning coins into the target coin involves the following flow:
1. **Collecting Fees:** Admin fees are collected in various token types in the `FeeCollector`.
2. **Burning Admin Fees:** The burn process is initiated via the `collect` function, creating conditional orders for tokens to be burned.
3. **Forwarding Fees:** Collected target coins are forwarded to the `FeeDistributor` using the `forward` function.
4. **Claiming Fees:** Accrued fees can be claimed from the `FeeDistributor` using the `claim` function.
5. **Splitting crvUSD Fees:** The `FeeSplitter` handles the collection and distribution of crvUSD fees from crvUSD markets.
This system ensures efficient fee collection, conversion, and distribution across the Curve ecosystem, rewarding veCRV holders and supporting the ongoing development and maintenance of the protocol.
*Curve has implemented different fee burning architectures over time to optimize the process:*
A more efficient system using contracts like FeeCollector, CowSwapBurner, and FeeSplitter. This architecture is currently available on Ethereum and Gnosis Chain, with plans to deploy on Arbitrum.
An older system using multiple burner contracts with manually added and hardcoded exchange routes. This architecture is still in use on some sidechains where the CowSwap system hasn't been implemented yet.
The choice of architecture depends on the blockchain and available infrastructure. For chains where the CowSwap system isn't deployed, admin fees are burned using the original architecture and then transferred to Ethereum via bridging contracts.
This page will primarily focus on the CowSwap fee system while also providing information on the original architecture for context and comparison.
---
## CowSwap Fee System
The current fee system utilizes a set of contracts to efficiently collect, convert, and distribute fees:
1. **FeeCollector**: Acts as the entry point for fee collection from various sources.
2. **FeeSplitter**: Handles the distribution of crvUSD fees from crvUSD markets.
3. **CowSwapBurner**: Converts collected fees into a unified token using CowSwap's conditional orders.
4. **Hooker**: Facilitates the execution of specific actions, such as forwarding fees.
5. **FeeDistributor**: Distributes the converted fees to veCRV holders.
This system ensures that all types of fees can be efficiently processed **without the need to manually add coins to burners or hardcode exchange routes.**
:::warning[Current Limitation]
It's important to note that this new fee system is currently only available on Ethereum and Gnosis Chain, with plans to deploy on Arbitrum soon. Other chains where Curve is deployed still use the previous fee burning architecture.
For chains not yet using this new system, admin fees are burned using the [original architecture](./original-architecture/sidechains.md) and then transferred via a bridging contract to Ethereum.
:::
## Previous Architecture
Prior to this system, Curve used multiple different kinds of burners where the **exchange routes for the to-be-burned coins had to be manually added**. Additionally, exchange routes were hardcoded, which often led to semi-efficient fee burning. If coins were not manually added to the burners, they could not be burned, which resulted in unburned (but obviously not lost) fees. The old burner contracts required lots of maintenance and dev resources.
The new system can and is deployed on other chains besides Ethereum but is **partly dependent on, e.g., CoWSwap deployments** if the `CowSwapBurner` is used. **If the CowSwap protocol is deployed on a sidechain, fees can be burned there. For chains where this is not the case, the admin fees are still being burned using the [original architecture](./original-architecture/sidechains.md) and then transferred via a bridging contract to Ethereum.**
---
## Further Reading
- [Old Fee Burning Architecture](./original-architecture/overview.md)
- [CowSwap Protocol Documentation](https://docs.cow.fi/)
- [veCRV Documentation](../curve-dao/voting-escrow/voting-escrow.md)
---
## Updater
The `Updater` contract is deployed on Ethereum mainnet and is used to transmit veCRV information across chains to a `L2 VotingEscrow Oracle`.
:::vyper[`Updater.vy`]
The source code for the `Updater.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-xchain-factory/tree/master/contracts/updaters). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
:::
---
### `VOTING_ESCROW`
::::description[`Updater.VOTING_ESCROW() -> address: view`]
Getter for the address of the voting escrow contract. This variable is constant and points to the voting escrow contract on Ethereum mainnet and cannot be changed.
Returns: `VotingEscrow` contract (`address`).
```vyper
interface VotingEscrow:
def epoch() -> uint256: view
def point_history(_idx: uint256) -> Point: view
def user_point_epoch(_user: address) -> uint256: view
def user_point_history(_user: address, _idx: uint256) -> Point: view
def locked(_user: address) -> LockedBalance: view
def slope_changes(_ts: uint256) -> int128: view
VOTING_ESCROW: public(constant(address)) = 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2
```
```shell
>>> Updater.VOTING_ESCROW()
'0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2'
```
::::
### `ovm_chain`
::::description[`Updater.ovm_chain() -> address: view`]
Getter for the `CanonicalTransactionChain` address on the alternate chain.
Returns: `CanonicalTransactionChain` contract (`address`).
```vyper
ovm_chain: public(address) # CanonicalTransactionChain
ovm_messenger: public(address) # CrossDomainMessenger
@external
def __init__(_ovm_chain: address, _ovm_messenger: address):
self.ovm_chain = _ovm_chain
self.ovm_messenger = _ovm_messenger
```
```shell
>>> Updater.ovm_chain()
'0x0000000000000000000000000000000000000000'
```
::::
### `ovm_messenger`
::::description[`Updater.ovm_messenger() -> address: view`]
Getter for the address of the `CrossDomainMessenger` contract on the alternate chain.
Returns: `CrossDomainMessenger` contract (`address`).
```vyper
ovm_chain: public(address) # CanonicalTransactionChain
ovm_messenger: public(address) # CrossDomainMessenger
@external
def __init__(_ovm_chain: address, _ovm_messenger: address):
self.ovm_chain = _ovm_chain
self.ovm_messenger = _ovm_messenger
```
```shell
>>> Updater.ovm_messenger()
'0x126bcc31Bc076B3d515f60FBC81FddE0B0d542Ed' # Fraxtal L1 Cross Domain Messenger Proxy
```
::::
### `update`
::::description[`Updater.update(_user: address = msg.sender, _gas_limit: uint32 = 0)`]
Function to update the voting escrow information on the alternate chain. This call transmits the following information: current `epoch`, `point_history` of the current epoch, `user_point_epoch` of the user, `user_point_history` of the user at their `user_point_epoch`, `locked` balance, and `slope_changes` for the past 12 weeks.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_user` | `address` | The user to update the voting escrow information for. Defaults to the caller of the function. |
| `_gas_limit` | `uint32` | The gas limit for the transaction. If 0, the function will attempt to retrieve the gas limit from the alternate chain. |
```vyper
interface OVMMessenger:
def sendMessage(_target: address, _data: Bytes[1024], _gas_limit: uint32): nonpayable
interface OVMChain:
def enqueueL2GasPrepaid() -> uint32: view
interface VotingEscrow:
def epoch() -> uint256: view
def point_history(_idx: uint256) -> Point: view
def user_point_epoch(_user: address) -> uint256: view
def user_point_history(_user: address, _idx: uint256) -> Point: view
def locked(_user: address) -> LockedBalance: view
def slope_changes(_ts: uint256) -> int128: view
struct LockedBalance:
amount: int128
end: uint256
struct Point:
bias: int128
slope: int128
ts: uint256
blk: uint256
WEEK: constant(uint256) = 86400 * 7
VOTING_ESCROW: public(constant(address)) = 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2
ovm_chain: public(address) # CanonicalTransactionChain
ovm_messenger: public(address) # CrossDomainMessenger
@external
def __init__(_ovm_chain: address, _ovm_messenger: address):
self.ovm_chain = _ovm_chain
self.ovm_messenger = _ovm_messenger
@external
def update(_user: address = msg.sender, _gas_limit: uint32 = 0):
# https://community.optimism.io/docs/developers/bridge/messaging/#for-l1-%E2%87%92-l2-transactions
gas_limit: uint32 = _gas_limit
if gas_limit == 0:
gas_limit = OVMChain(self.ovm_chain).enqueueL2GasPrepaid()
epoch: uint256 = VotingEscrow(VOTING_ESCROW).epoch()
point_history: Point = VotingEscrow(VOTING_ESCROW).point_history(epoch)
user_point_epoch: uint256 = VotingEscrow(VOTING_ESCROW).user_point_epoch(_user)
user_point_history: Point = VotingEscrow(VOTING_ESCROW).user_point_history(_user, user_point_epoch)
locked: LockedBalance = VotingEscrow(VOTING_ESCROW).locked(_user)
start_time: uint256 = WEEK + (point_history.ts / WEEK) * WEEK
slope_changes: int128[12] = empty(int128[12])
for i in range(12):
slope_changes[i] = VotingEscrow(VOTING_ESCROW).slope_changes(start_time + WEEK * i)
OVMMessenger(self.ovm_messenger).sendMessage(
self,
_abi_encode(
_user,
epoch,
point_history,
user_point_epoch,
user_point_history,
locked,
slope_changes,
method_id=method_id("update(address,uint256,(int128,int128,uint256,uint256),uint256,(int128,int128,uint256,uint256),(int128,uint256),int128[12])")
),
gas_limit
)
```
```shell
>>> Updater.update()
```
::::
---
## GaugeController
The `GaugeController` contract is responsible for managing and coordinating the distribution of rewards to liquidity providers in various liquidity pools. It **determines the allocation of CRV emissions based on the liquidity provided** by users. By analyzing the gauges, which are parameters that define how rewards are distributed across different pools, the GaugeController ensures a fair and balanced distribution of incentives, encouraging liquidity provision and participation in Curve's ecosystem.
:::vyper[`GaugeController.vy`]
The source code for the `GaugeController.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/GaugeController.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.2.4`.
The contract is deployed on :logos-ethereum: Ethereum at [`0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB`](https://etherscan.io/address/0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB).
```json
[{"name":"CommitOwnership","inputs":[{"type":"address","name":"admin","indexed":false}],"anonymous":false,"type":"event"},{"name":"ApplyOwnership","inputs":[{"type":"address","name":"admin","indexed":false}],"anonymous":false,"type":"event"},{"name":"AddType","inputs":[{"type":"string","name":"name","indexed":false},{"type":"int128","name":"type_id","indexed":false}],"anonymous":false,"type":"event"},{"name":"NewTypeWeight","inputs":[{"type":"int128","name":"type_id","indexed":false},{"type":"uint256","name":"time","indexed":false},{"type":"uint256","name":"weight","indexed":false},{"type":"uint256","name":"total_weight","indexed":false}],"anonymous":false,"type":"event"},{"name":"NewGaugeWeight","inputs":[{"type":"address","name":"gauge_address","indexed":false},{"type":"uint256","name":"time","indexed":false},{"type":"uint256","name":"weight","indexed":false},{"type":"uint256","name":"total_weight","indexed":false}],"anonymous":false,"type":"event"},{"name":"VoteForGauge","inputs":[{"type":"uint256","name":"time","indexed":false},{"type":"address","name":"user","indexed":false},{"type":"address","name":"gauge_addr","indexed":false},{"type":"uint256","name":"weight","indexed":false}],"anonymous":false,"type":"event"},{"name":"NewGauge","inputs":[{"type":"address","name":"addr","indexed":false},{"type":"int128","name":"gauge_type","indexed":false},{"type":"uint256","name":"weight","indexed":false}],"anonymous":false,"type":"event"},{"outputs":[],"inputs":[{"type":"address","name":"_token"},{"type":"address","name":"_voting_escrow"}],"stateMutability":"nonpayable","type":"constructor"},{"name":"commit_transfer_ownership","outputs":[],"inputs":[{"type":"address","name":"addr"}],"stateMutability":"nonpayable","type":"function","gas":37597},{"name":"apply_transfer_ownership","outputs":[],"inputs":[],"stateMutability":"nonpayable","type":"function","gas":38497},{"name":"gauge_types","outputs":[{"type":"int128","name":""}],"inputs":[{"type":"address","name":"_addr"}],"stateMutability":"view","type":"function","gas":1625},{"name":"add_gauge","outputs":[],"inputs":[{"type":"address","name":"addr"},{"type":"int128","name":"gauge_type"}],"stateMutability":"nonpayable","type":"function"},{"name":"add_gauge","outputs":[],"inputs":[{"type":"address","name":"addr"},{"type":"int128","name":"gauge_type"},{"type":"uint256","name":"weight"}],"stateMutability":"nonpayable","type":"function"},{"name":"checkpoint","outputs":[],"inputs":[],"stateMutability":"nonpayable","type":"function","gas":18033784416},{"name":"checkpoint_gauge","outputs":[],"inputs":[{"type":"address","name":"addr"}],"stateMutability":"nonpayable","type":"function","gas":18087678795},{"name":"gauge_relative_weight","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"addr"}],"stateMutability":"view","type":"function"},{"name":"gauge_relative_weight","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"addr"},{"type":"uint256","name":"time"}],"stateMutability":"view","type":"function"},{"name":"gauge_relative_weight_write","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"addr"}],"stateMutability":"nonpayable","type":"function"},{"name":"gauge_relative_weight_write","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"addr"},{"type":"uint256","name":"time"}],"stateMutability":"nonpayable","type":"function"},{"name":"add_type","outputs":[],"inputs":[{"type":"string","name":"_name"}],"stateMutability":"nonpayable","type":"function"},{"name":"add_type","outputs":[],"inputs":[{"type":"string","name":"_name"},{"type":"uint256","name":"weight"}],"stateMutability":"nonpayable","type":"function"},{"name":"change_type_weight","outputs":[],"inputs":[{"type":"int128","name":"type_id"},{"type":"uint256","name":"weight"}],"stateMutability":"nonpayable","type":"function","gas":36246310050},{"name":"change_gauge_weight","outputs":[],"inputs":[{"type":"address","name":"addr"},{"type":"uint256","name":"weight"}],"stateMutability":"nonpayable","type":"function","gas":36354170809},{"name":"vote_for_gauge_weights","outputs":[],"inputs":[{"type":"address","name":"_gauge_addr"},{"type":"uint256","name":"_user_weight"}],"stateMutability":"nonpayable","type":"function","gas":18142052127},{"name":"get_gauge_weight","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"addr"}],"stateMutability":"view","type":"function","gas":2974},{"name":"get_type_weight","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"int128","name":"type_id"}],"stateMutability":"view","type":"function","gas":2977},{"name":"get_total_weight","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":2693},{"name":"get_weights_sum_per_type","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"int128","name":"type_id"}],"stateMutability":"view","type":"function","gas":3109},{"name":"admin","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":1841},{"name":"future_admin","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":1871},{"name":"token","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":1901},{"name":"voting_escrow","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":1931},{"name":"n_gauge_types","outputs":[{"type":"int128","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":1961},{"name":"n_gauges","outputs":[{"type":"int128","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":1991},{"name":"gauge_type_names","outputs":[{"type":"string","name":""}],"inputs":[{"type":"int128","name":"arg0"}],"stateMutability":"view","type":"function","gas":8628},{"name":"gauges","outputs":[{"type":"address","name":""}],"inputs":[{"type":"uint256","name":"arg0"}],"stateMutability":"view","type":"function","gas":2160},{"name":"vote_user_slopes","outputs":[{"type":"uint256","name":"slope"},{"type":"uint256","name":"power"},{"type":"uint256","name":"end"}],"inputs":[{"type":"address","name":"arg0"},{"type":"address","name":"arg1"}],"stateMutability":"view","type":"function","gas":5020},{"name":"vote_user_power","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"arg0"}],"stateMutability":"view","type":"function","gas":2265},{"name":"last_user_vote","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"arg0"},{"type":"address","name":"arg1"}],"stateMutability":"view","type":"function","gas":2449},{"name":"points_weight","outputs":[{"type":"uint256","name":"bias"},{"type":"uint256","name":"slope"}],"inputs":[{"type":"address","name":"arg0"},{"type":"uint256","name":"arg1"}],"stateMutability":"view","type":"function","gas":3859},{"name":"time_weight","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"arg0"}],"stateMutability":"view","type":"function","gas":2355},{"name":"points_sum","outputs":[{"type":"uint256","name":"bias"},{"type":"uint256","name":"slope"}],"inputs":[{"type":"int128","name":"arg0"},{"type":"uint256","name":"arg1"}],"stateMutability":"view","type":"function","gas":3970},{"name":"time_sum","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"uint256","name":"arg0"}],"stateMutability":"view","type":"function","gas":2370},{"name":"points_total","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"uint256","name":"arg0"}],"stateMutability":"view","type":"function","gas":2406},{"name":"time_total","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":2321},{"name":"points_type_weight","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"int128","name":"arg0"},{"type":"uint256","name":"arg1"}],"stateMutability":"view","type":"function","gas":2671},{"name":"time_type_weight","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"uint256","name":"arg0"}],"stateMutability":"view","type":"function","gas":2490}]
```
:::
The contract also **acts as a registry for the gauges**, storing information such as the gauge data, minted amounts, and more.
---
## Adding Gauges and Gauge Data
After a liquidity gauge was deployed, it can be added to the `GaugeController` for it to be eligible to receive CRV emissions. Adding a gauge requires a successfully passed DAO vote.
:::info[Check if a Gauge has been added to the GaugeController]
The contract does not have a public getter to check whether a gauge has been added. Alternatively, one can try to query the `gauge_types` of the gauge.
```shell
>>> GaugeController.gauge_types('0xbfcf63294ad7105dea65aa58f8ae5be2d9d0952a')
0
>>> GaugeController.gauge_types('0xc840e5ed7a1b6a9c1a6bf1ecaca6ddb151b2fd6e')
Error: Returned error: execution reverted
```
If the gauge returns an `int128`, this means the gauge has been added. The returned value represents the [gauge type](#gauge_types). If the query call reverts, this means the gauge has not been added.
:::
### `add_gauge`
::::description[`GaugeController.add_gauge(addr: address, gauge_type: int128, weight: uint256 = 0)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract. The `admin` in this case is the Curve DAO. So, adding a gauge to the `GaugeController` is in the hands of the DAO.
:::
:::warning
Once a gauge has been added, it cannot be removed. Therefore, new gauges should undergo thorough verification by the community before being added to the `GaugeController`. However, it is possible to 'kill' a gauge, which sets its emission rate to zero. As a result, a 'killed' gauge becomes ineligible for any CRV emissions.
:::
Function to add a new gauge to the `GaugeController`. Doing this makes the gauge eligible to receive CRV emissions.
| Input | Type | Description |
| ----------- | --------- | ----------- |
| `addr` | `address` | Gauge address |
| `gauge_type`| `int128` | Gauge type |
| `weight` | `uint256` | Gauge weight; defaults to 0 |
Emits: `NewGauge` event.
```vyper
event NewGauge:
addr: address
gauge_type: int128
weight: uint256
@external
def add_gauge(addr: address, gauge_type: int128, weight: uint256 = 0):
"""
@notice Add gauge `addr` of type `gauge_type` with weight `weight`
@param addr Gauge address
@param gauge_type Gauge type
@param weight Gauge weight
"""
assert msg.sender == self.admin
assert (gauge_type >= 0) and (gauge_type < self.n_gauge_types)
assert self.gauge_types_[addr] == 0 # dev: cannot add the same gauge twice
n: int128 = self.n_gauges
self.n_gauges = n + 1
self.gauges[n] = addr
self.gauge_types_[addr] = gauge_type + 1
next_time: uint256 = (block.timestamp + WEEK) / WEEK * WEEK
if weight > 0:
_type_weight: uint256 = self._get_type_weight(gauge_type)
_old_sum: uint256 = self._get_sum(gauge_type)
_old_total: uint256 = self._get_total()
self.points_sum[gauge_type][next_time].bias = weight + _old_sum
self.time_sum[gauge_type] = next_time
self.points_total[next_time] = _old_total + _type_weight * weight
self.time_total = next_time
self.points_weight[addr][next_time].bias = weight
if self.time_sum[gauge_type] == 0:
self.time_sum[gauge_type] = next_time
self.time_weight[addr] = next_time
log NewGauge(addr, gauge_type, weight)
```
This example adds the gauge at address `0x41af8cC0811DD07F167752B821CF5B11DBa7Ca85` to the `GaugeController`.
```shell
>>> GaugeController.add_gauge('0x41af8cC0811DD07F167752B821CF5B11DBa7Ca85')
```
::::
### `gauges`
::::description[`GaugeController.gauges(arg0: uint256) -> address: view`]
Getter for the gauge address at a specific index. Every time a new gauge is added, the variable is populated with the new gauge address. Index 0 equals to the first gauge added.
| Input | Type | Description |
| ------ | --------- | ----------- |
| `arg0` | `uint256` | Gauge index |
Returns: gauge (`address`).
```vyper
# Needed for enumeration
gauges: public(address[1000000000])
```
This example returns the gauge address at index 0. The value is fetched live from the blockchain.
::::
### `n_gauges`
::::description[`GaugeController.n_gauges() -> int128: view`]
Getter for the total number of gauges added to the `GaugeController`. This variable is incremented by one each time a new gauge is added via the `add_gauge` function.
Returns: total number of gauges (`int128`).
```vyper
n_gauges: public(int128)
```
This example returns the total number of gauges. The value is fetched live from the blockchain.
::::
---
## Vote-Weighting and Gauge Weights
Users who have a positive veCRV balance can use their voting power to vote for specific gauges. Only gauges who have been added to the `GaugeController` by the DAO can be voted for. These gauge weights define how much CRV emissions a gauge receives.
Users do not need to allocate 100% of their voting power to a single gauge. They can distribute their voting power across multiple gauges.
Gauge weights are updated every Thursday at 00:00 UTC. At this timestamp, the CRV emissions for one week are based on the gauge weights. The current weights remain the same until someone votes. If there are no votes for several weeks in a row, the gauge weights and CRV emissions will stay the same for all subsequent weeks.
:::example[Example: CRV emissions and Gauge Weights]
If a gauge receives 10% of the total weight, it will receive 10% of the emissions for the current week.
At the time of writing, the inflation rate per second of CRV is `5181574864521283150 (CRV.rate())`, which equals 5.18157486452128315 CRV per second.
The gauge will, therefore, receive approximately 313,381.65 CRV tokens as emissions for the current week, calculated as 5.18157486452128315 CRV per second * 10% * (7 * 86400 seconds).
:::
### `vote_for_gauge_weights`
::::description[`GaugeController.vote_for_gauge_weights(_gauge_addr: address, _user_weight: uint256)`]
:::warning[`WEIGHT_VOTE_DELAY`]
Gauge weight votes may only be modified once every 10 days.
:::
Function to allocate a specific amount of voting power to a gauge. The voting power is expressed and measured in bps (units of 0.01%). Minimal weight is 0.01%.
| Input | Type | Description |
| -------------- | ---------- | ------------- |
| `_gauge_addr` | `address` | Gauge address to allocate weight to |
| `_user_weight` | `uint256` | Weight to allocate |
Emits: `VoteForGauge` event.
```vyper
event VoteForGauge:
time: uint256
user: address
gauge_addr: address
weight: uint256
# Cannot change weight votes more often than once in 10 days
WEIGHT_VOTE_DELAY: constant(uint256) = 10 * 86400
vote_user_slopes: public(HashMap[address, HashMap[address, VotedSlope]]) # user -> gauge_addr -> VotedSlope
vote_user_power: public(HashMap[address, uint256]) # Total vote power used by user
last_user_vote: public(HashMap[address, HashMap[address, uint256]]) # Last user vote's timestamp for each gauge address
# Past and scheduled points for gauge weight, sum of weights per type, total weight
# Point is for bias+slope
# changes_* are for changes in slope
# time_* are for the last change timestamp
# timestamps are rounded to whole weeks
points_weight: public(HashMap[address, HashMap[uint256, Point]]) # gauge_addr -> time -> Point
changes_weight: HashMap[address, HashMap[uint256, uint256]] # gauge_addr -> time -> slope
time_weight: public(HashMap[address, uint256]) # gauge_addr -> last scheduled time (next week)
points_sum: public(HashMap[int128, HashMap[uint256, Point]]) # type_id -> time -> Point
changes_sum: HashMap[int128, HashMap[uint256, uint256]] # type_id -> time -> slope
time_sum: public(uint256[1000000000]) # type_id -> last scheduled time (next week)
points_total: public(HashMap[uint256, uint256]) # time -> total weight
time_total: public(uint256) # last scheduled time
points_type_weight: public(HashMap[int128, HashMap[uint256, uint256]]) # type_id -> time -> type weight
time_type_weight: public(uint256[1000000000]) # type_id -> last scheduled time (next week)
@external
def vote_for_gauge_weights(_gauge_addr: address, _user_weight: uint256):
"""
@notice Allocate voting power for changing pool weights
@param _gauge_addr Gauge which `msg.sender` votes for
@param _user_weight Weight for a gauge in bps (units of 0.01%). Minimal is 0.01%. Ignored if 0
"""
escrow: address = self.voting_escrow
slope: uint256 = convert(VotingEscrow(escrow).get_last_user_slope(msg.sender), uint256)
lock_end: uint256 = VotingEscrow(escrow).locked__end(msg.sender)
_n_gauges: int128 = self.n_gauges
next_time: uint256 = (block.timestamp + WEEK) / WEEK * WEEK
assert lock_end > next_time, "Your token lock expires too soon"
assert (_user_weight >= 0) and (_user_weight <= 10000), "You used all your voting power"
assert block.timestamp >= self.last_user_vote[msg.sender][_gauge_addr] + WEIGHT_VOTE_DELAY, "Cannot vote so often"
gauge_type: int128 = self.gauge_types_[_gauge_addr] - 1
assert gauge_type >= 0, "Gauge not added"
# Prepare slopes and biases in memory
old_slope: VotedSlope = self.vote_user_slopes[msg.sender][_gauge_addr]
old_dt: uint256 = 0
if old_slope.end > next_time:
old_dt = old_slope.end - next_time
old_bias: uint256 = old_slope.slope * old_dt
new_slope: VotedSlope = VotedSlope({
slope: slope * _user_weight / 10000,
end: lock_end,
power: _user_weight
})
new_dt: uint256 = lock_end - next_time # dev: raises when expired
new_bias: uint256 = new_slope.slope * new_dt
# Check and update powers (weights) used
power_used: uint256 = self.vote_user_power[msg.sender]
power_used = power_used + new_slope.power - old_slope.power
self.vote_user_power[msg.sender] = power_used
assert (power_used >= 0) and (power_used <= 10000), 'Used too much power'
## Remove old and schedule new slope changes
# Remove slope changes for old slopes
# Schedule recording of initial slope for next_time
old_weight_bias: uint256 = self._get_weight(_gauge_addr)
old_weight_slope: uint256 = self.points_weight[_gauge_addr][next_time].slope
old_sum_bias: uint256 = self._get_sum(gauge_type)
old_sum_slope: uint256 = self.points_sum[gauge_type][next_time].slope
self.points_weight[_gauge_addr][next_time].bias = max(old_weight_bias + new_bias, old_bias) - old_bias
self.points_sum[gauge_type][next_time].bias = max(old_sum_bias + new_bias, old_bias) - old_bias
if old_slope.end > next_time:
self.points_weight[_gauge_addr][next_time].slope = max(old_weight_slope + new_slope.slope, old_slope.slope) - old_slope.slope
self.points_sum[gauge_type][next_time].slope = max(old_sum_slope + new_slope.slope, old_slope.slope) - old_slope.slope
else:
self.points_weight[_gauge_addr][next_time].slope += new_slope.slope
self.points_sum[gauge_type][next_time].slope += new_slope.slope
if old_slope.end > block.timestamp:
# Cancel old slope changes if they still didn't happen
self.changes_weight[_gauge_addr][old_slope.end] -= old_slope.slope
self.changes_sum[gauge_type][old_slope.end] -= old_slope.slope
# Add slope changes for new slopes
self.changes_weight[_gauge_addr][new_slope.end] += new_slope.slope
self.changes_sum[gauge_type][new_slope.end] += new_slope.slope
self._get_total()
self.vote_user_slopes[msg.sender][_gauge_addr] = new_slope
# Record last action time
self.last_user_vote[msg.sender][_gauge_addr] = block.timestamp
log VoteForGauge(block.timestamp, msg.sender, _gauge_addr, _user_weight)
@internal
def _get_total() -> uint256:
"""
@notice Fill historic total weights week-over-week for missed checkins
and return the total for the future week
@return Total weight
"""
t: uint256 = self.time_total
_n_gauge_types: int128 = self.n_gauge_types
if t > block.timestamp:
# If we have already checkpointed - still need to change the value
t -= WEEK
pt: uint256 = self.points_total[t]
for gauge_type in range(100):
if gauge_type == _n_gauge_types:
break
self._get_sum(gauge_type)
self._get_type_weight(gauge_type)
for i in range(500):
if t > block.timestamp:
break
t += WEEK
pt = 0
# Scales as n_types * n_unchecked_weeks (hopefully 1 at most)
for gauge_type in range(100):
if gauge_type == _n_gauge_types:
break
type_sum: uint256 = self.points_sum[gauge_type][t].bias
type_weight: uint256 = self.points_type_weight[gauge_type][t]
pt += type_sum * type_weight
self.points_total[t] = pt
if t > block.timestamp:
self.time_total = t
return pt
```
This example allocates 100% of the voting power to `0x4e6bb6b7447b7b2aa268c16ab87f4bb48bf57939`.
```shell
>>> GaugeController.vote_for_gauge_weights('0x4e6bb6b7447b7b2aa268c16ab87f4bb48bf57939', 10000)
```
::::
### `vote_user_power`
::::description[`GaugeController.vote_user_power(arg0: address) -> uint256: view`]
Getter method for the total allocated voting power by a specific user. If a user has a veCRV balance but has not yet voted, this function will return 0.
| Input | Type | Description |
| ------ | --------- | ------------ |
| `arg0` | `address` | User address |
Returns: used voting power (`uint256`).
```vyper
vote_user_power: public(HashMap[address, uint256]) # Total vote power used by user
```
This example returns the total allocated voting power for a specific user. The value is fetched live from the blockchain.
::::
### `last_user_vote`
::::description[`GaugeController.last_user_vote(arg0: address, arg1: address) -> uint256: view`]
Getter for the last timestamp a specific user voted for a specific gauge.
| Input | Type | Description |
|--------|-----------|---------------|
| `arg0` | `address` | User address |
| `arg1` | `address` | Gauge address |
Returns: timestamp (`uint256`).
```vyper
last_user_vote: public(HashMap[address, HashMap[address, uint256]]) # Last user vote's timestamp for each gauge address
```
This example returns the last vote timestamp for a specific user and gauge. The value is fetched live from the blockchain.
::::
### `vote_user_slopes`
::::description[`GaugeController.vote_user_slopes(arg0: address, arg1: address) -> slope: uint256, power: uint256, end: uint256`]
Getter method for information about the current vote weight of a specific user for a specific gauge. In this variable, information is stored at the time of voting.
| Input | Type | Description |
|-------|----------|---------------|
| `arg0`| `address`| User address |
| `arg1`| `address`| Gauge address |
Returns: slope (`uint256`), allocated voting-power (`uint256`) and veCRV lock end (`uint256`).
```vyper
vote_user_slopes: public(HashMap[address, HashMap[address, VotedSlope]]) # user -> gauge_addr -> VotedSlope
```
This example returns the vote user slope data for a specific user and gauge. The value is fetched live from the blockchain.
::::
### `gauge_relative_weight`
::::description[`GaugeController.gauge_relative_weight(addr: address, time: uint256 = block.timestamp) -> uint256: view`]
Getter for the relative weight of specific gauge at a specific time.
| Input | Type | Description |
| ------ | ---------- | ----------- |
| `addr` | `address` | Gauge address |
| `time` | `uint256` | Timestamp to check the weight at; Defaults to `block.timestamp` |
Returns: relative gauge weight normalized to 1e18 (`uint256`).
```vyper
@external
@view
def gauge_relative_weight(addr: address, time: uint256 = block.timestamp) -> uint256:
"""
@notice Get Gauge relative weight (not more than 1.0) normalized to 1e18
(e.g. 1.0 == 1e18). Inflation which will be received by it is
inflation_rate * relative_weight / 1e18
@param addr Gauge address
@param time Relative weight at the specified timestamp in the past or present
@return Value of relative weight normalized to 1e18
"""
return self._gauge_relative_weight(addr, time)
@internal
@view
def _gauge_relative_weight(addr: address, time: uint256) -> uint256:
"""
@notice Get Gauge relative weight (not more than 1.0) normalized to 1e18
(e.g. 1.0 == 1e18). Inflation which will be received by it is
inflation_rate * relative_weight / 1e18
@param addr Gauge address
@param time Relative weight at the specified timestamp in the past or present
@return Value of relative weight normalized to 1e18
"""
t: uint256 = time / WEEK * WEEK
_total_weight: uint256 = self.points_total[t]
if _total_weight > 0:
gauge_type: int128 = self.gauge_types_[addr] - 1
_type_weight: uint256 = self.points_type_weight[gauge_type][t]
_gauge_weight: uint256 = self.points_weight[addr][t].bias
return MULTIPLIER * _type_weight * _gauge_weight / _total_weight
else:
return 0
```
This example returns the relative gauge weight for a specific gauge. The value is fetched live from the blockchain.
::::
### `gauge_relative_weight_write`
::::description[`GaugeController.gauge_relative_weight_write(addr: address, time: uint256 = block.timestamp) -> uint256`]
Function to get the gauge relative weight and also checkpoint the gauge, filling in any missing gauge data for the past weeks. This is a state-changing version of [`gauge_relative_weight`](#gauge_relative_weight).
| Input | Type | Description |
| ------ | --------- | ----------- |
| `addr` | `address` | Gauge address |
| `time` | `uint256` | Timestamp to check the weight at; defaults to `block.timestamp` |
Returns: relative gauge weight normalized to 1e18 (`uint256`).
```vyper
@external
def gauge_relative_weight_write(addr: address, time: uint256 = block.timestamp) -> uint256:
"""
@notice Get gauge weight normalized to 1e18 and also fill all the unfilled
values for type and gauge records
@dev Any address can call, however nothing is recorded if the values are filled already
@param addr Gauge address
@param time Relative weight at the specified timestamp in the past or present
@return Value of relative weight normalized to 1e18
"""
self._get_weight(addr)
self._get_total() # Also calculates get_sum
return self._gauge_relative_weight(addr, time)
```
```shell
>>> GaugeController.gauge_relative_weight_write('0x4e6bb6b7447b7b2aa268c16ab87f4bb48bf57939')
50000000000000000
```
::::
### `checkpoint`
::::description[`GaugeController.checkpoint()`]
Function to checkpoint to fill data common for all gauges.
```vyper
@external
def checkpoint():
"""
@notice Checkpoint to fill data common for all gauges
"""
self._get_total()
```
```shell
>>> GaugeController.checkpoint()
```
::::
### `checkpoint_gauge`
::::description[`GaugeController.checkpoint_gauge(addr: address)`]
Function to checkpoint a specific gauge, filling in any missing weight data.
| Input | Type | Description |
| ------ | --------- | ------------- |
| `addr` | `address` | Gauge address |
```vyper
@external
def checkpoint_gauge(addr: address):
"""
@notice Checkpoint to fill data for both a specific gauge and common for all gauges
@param addr Gauge address
"""
self._get_weight(addr)
self._get_total()
```
```shell
>>> GaugeController.checkpoint_gauge('0x4e6bb6b7447b7b2aa268c16ab87f4bb48bf57939')
```
::::
### `change_gauge_weight`
::::description[`GaugeController.change_gauge_weight(addr: address, weight: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to change the gauge weight for a specific gauge.
| Input | Type | Description |
| -------- | --------- | ---------------- |
| `addr` | `address` | Gauge address |
| `weight` | `uint256` | New gauge weight |
Emits: `NewGaugeWeight` event.
```vyper
event NewGaugeWeight:
gauge_address: address
time: uint256
weight: uint256
total_weight: uint256
@external
def change_gauge_weight(addr: address, weight: uint256):
"""
@notice Change weight of gauge `addr` to `weight`
@param addr `GaugeController` contract address
@param weight New Gauge weight
"""
assert msg.sender == self.admin
self._change_gauge_weight(addr, weight)
```
```shell
>>> GaugeController.change_gauge_weight('0x4e6bb6b7447b7b2aa268c16ab87f4bb48bf57939', 1000000000000000000)
```
::::
### `get_gauge_weight`
::::description[`GaugeController.get_gauge_weight(addr: address) -> uint256: view`]
Getter for the current gauge weight of gauge `addr`.
| Input | Type | Description |
| ------ | --------- | ----------- |
| `addr` | `address` | Gauge address |
Returns: gauge weight normalized to 1e18 (`uint256`).
```vyper
points_weight: public(HashMap[address, HashMap[uint256, Point]]) # gauge_addr -> time -> Point
@external
@view
def get_gauge_weight(addr: address) -> uint256:
"""
@notice Get current gauge weight
@param addr Gauge address
@return Gauge weight
"""
return self.points_weight[addr][self.time_weight[addr]].bias
```
This example returns the current gauge weight for a specific gauge. The value is fetched live from the blockchain.
::::
### `get_total_weight`
::::description[`GaugeController.get_total_weight() -> uint256: view`]
Getter for the current total weight.
Returns: total weight (`uint256`).
```vyper
points_total: public(HashMap[uint256, uint256]) # time -> total weight
@external
@view
def get_total_weight() -> uint256:
"""
@notice Get current total (type-weighted) weight
@return Total weight
"""
return self.points_total[self.time_total]
```
This example returns the current total weight. The value is fetched live from the blockchain.
::::
### `get_weights_sum_per_type`
::::description[`GaugeController.get_weights_sum_per_type(type_id: int128) -> uint256: view`]
Getter for the summed weight of gauge type `type_id`.
| Input | Type | Description |
| --------- | -------- | ------------- |
| `type_id` | `int128` | Gauge type ID |
Returns: summed weight (`uint256`).
```vyper
points_sum: public(HashMap[int128, HashMap[uint256, Point]]) # type_id -> time -> Point
@external
@view
def get_weights_sum_per_type(type_id: int128) -> uint256:
"""
@notice Get sum of gauge weights per type
@param type_id Type id
@return Sum of gauge weights
"""
return self.points_sum[type_id][self.time_sum[type_id]].bias
```
This example returns the summed weight for gauge type 0. The value is fetched live from the blockchain.
::::
---
## Points
GaugeController records points (bias + slope) per gauge in `vote_points`, and scheduled changes in biases and slopes for those points in `vote_bias_changes` and `vote_slope_changes`. New changes are applied at the start of each epoch week.
*A `Point` is composed of a `bias` and a `slope`:*
```vyper
struct Point:
bias: uint256
slope: uint256
```
### `points_weight`
::::description[`GaugeController.points_weight(arg0: address, arg1: uint256) -> bias: uint256, slope: uint256: view`]
Getter for the `Point` information of a gauge `arg0`.
| Input | Type | Description |
| ------ | --------- | --------------- |
| `arg0` | `address` | Gauge address |
| `arg1` | `uint256` | Timestamp |
Returns: bias (`uint256`) and slope (`uint256`).
```vyper
points_weight: public(HashMap[address, HashMap[uint256, Point]]) # gauge_addr -> time -> Point
```
This example returns the point weight data for a specific gauge at a given timestamp. The value is fetched live from the blockchain.
::::
### `time_weight`
::::description[`GaugeController.time_weight(arg0: address) -> uint256: view`]
Getter for the last scheduled time the gauge weight of gauge `arg0` updates. This should always be the coming Thursday at 00:00 UTC and is updated when a gauge weight is updated.
| Input | Type | Description |
| ------ | --------- | ------------- |
| `arg0` | `address` | Gauge address |
Returns: timestamp (`uint256`).
```vyper
time_weight: public(HashMap[address, uint256]) # gauge_addr -> last scheduled time (next week)
```
This example returns the last scheduled time for a specific gauge. The value is fetched live from the blockchain.
::::
### `points_sum`
::::description[`GaugeController.points_sum(arg0: int128, arg1: uint256) -> bias: uint256, slope: uint256: view`]
Getter for information from `Point` struct.
| Input | Type | Description |
| ------ | --------- | ------------- |
| `arg0` | `int128` | Gauge type ID |
| `arg1` | `uint256` | Timestamp |
Returns: bias (`uint256`) and slope (`uint256`).
```vyper
points_sum: public(HashMap[int128, HashMap[uint256, Point]]) # type_id -> time -> Point
```
This example returns the point sum data for gauge type 0 at a given timestamp. The value is fetched live from the blockchain.
::::
### `time_sum`
::::description[`GaugeController.time_sum(arg0: uint256) -> uint256: view`]
Getter for the last scheduled time (next week).
| Input | Type | Description |
| ------ | --------- | ------------- |
| `arg0` | `uint256` | Gauge type ID |
Returns: timestamp (`uint256`).
```vyper
time_sum: public(uint256[1000000000]) # type_id -> last scheduled time (next week)
```
This example returns the last scheduled time for gauge type 0. The value is fetched live from the blockchain.
::::
### `points_total`
::::description[`GaugeController.points_total(arg0: uint256) -> uint256: view`]
Getter for the current future total weight at timestamp `arg0`.
| Input | Type | Description |
| ------ | --------- | ------------- |
| `arg0` | `uint256` | Timestamp of the next gauge weight update |
Returns: total points (`uint256`).
```vyper
points_total: public(HashMap[uint256, uint256]) # time -> total weight
```
This example returns the total weight at a specific timestamp. The value is fetched live from the blockchain.
::::
### `time_total`
::::description[`GaugeController.time_total() -> uint256: view`]
Getter for the last scheduled time when the gauge weights will update.
Returns: timestamp (`uint256`).
```vyper
time_total: public(uint256) # last scheduled time
```
This example returns the last scheduled time for gauge weight updates. The value is fetched live from the blockchain.
::::
### `points_type_weight`
::::description[`GaugeController.points_type_weight(arg0: int128, arg1: uint256) -> uint256: view`]
Getter for the weight for gauge type `arg0` at the next update, which is at timestamp `arg1`.
| Input | Type | Description |
| ------ | --------- | ------------- |
| `arg0` | `int128` | Gauge type ID |
| `arg1` | `uint256` | Timestamp |
Returns: type weight (`uint256`).
```vyper
points_type_weight: public(HashMap[int128, HashMap[uint256, uint256]]) # type_id -> time -> type weight
```
This example returns the type weight for gauge type 0 at a given timestamp. The value is fetched live from the blockchain.
::::
### `time_type_weight`
::::description[`GaugeController.time_type_weight(arg0: uint256) -> uint256: view`]
Getter for the last scheduled time, when the type weights update.
| Input | Type | Description |
| ------ | --------- | ------------- |
| `arg0` | `uint256` | Type ID |
Returns: timestamp (`uint256`).
```vyper
time_type_weight: public(uint256[1000000000]) # type_id -> last scheduled time (next week)
```
This example returns the last scheduled time for type weight updates. The value is fetched live from the blockchain.
::::
---
## Gauge Types
Each liquidity gauge is assigned a type within the `GaugeController`. Grouping gauges by type allows the DAO to adjust the emissions according to type, making it possible to e.g. end all emissions for a single type.
| Description | Gauge Type |
| ------------------------------------------ | :--------: |
| :logos-ethereum: `Ethereum (stable pools)` | `0` |
| :logos-fantom: `Fantom` | `1` |
| :logos-polygon: `Polygon` | `2` |
| :no_entry_sign: `deprecated` | `3` |
| :logos-gnosis: `Gnosis` | `4` |
| :logos-ethereum: `Ethereum (crypto pools)` | `5` |
| :no_entry_sign: `deprecated` | `6` |
| :logos-arbitrum: `Arbitrum` | `7` |
| :logos-avalanche: `Avalance` | `8` |
| :logos-harmony: `Harmony` | `9` |
| :moneybag: `Fundraising` | `10` |
| :logos-optimism: `Optimism` | `11` |
| :logos-bsc: `BinanceSmartChain` | `12` |
### `gauge_types`
::::description[`GaugeController.gauge_types(_addr: address) -> int128: view`]
Getter for the gauge type of a specific gauge.
| Input | Type | Description |
| ------- | --------- | ------------- |
| `_addr` | `address` | Gauge address |
Returns: gauge type (`int128`).
```vyper
gauge_types_: HashMap[address, int128]
@external
@view
def gauge_types(_addr: address) -> int128:
"""
@notice Get gauge type for address
@param _addr Gauge address
@return Gauge type id
"""
gauge_type: int128 = self.gauge_types_[_addr]
assert gauge_type != 0
return gauge_type - 1
```
This example returns the gauge type for a specific gauge. The value is fetched live from the blockchain.
::::
### `n_gauge_types`
::::description[`GaugeController.n_gauge_types() -> int128: view`]
Getter for the total number of gauge types. New gauge types can be added via the [`add_type`](#add_type) function.
Returns: total number of types (`int128`).
```vyper
n_gauge_types: public(int128)
```
This example returns the total number of gauge types. The value is fetched live from the blockchain.
::::
### `gauge_type_names`
::::description[`GaugeController.gauge_type_names(arg0: int128) -> String[64]: view`]
Getter for the name of a specific gauge type.
| Input | Type | Description |
| ------- | --------- | ------------- |
| `arg0` | `int128` | Gauge type index |
Returns: type name (`string`).
```vyper
gauge_type_names: public(HashMap[int128, String[64]])
```
This example returns the name of gauge type 0. The value is fetched live from the blockchain.
::::
### `get_type_weight`
::::description[`GaugeController.get_type_weight(type_id: int128) -> uint256: view`]
Getter for the type weight of a specific gauge type.
| Input | Type | Description |
| ----------- | -------| ----|
| `type_id` | `int128` | Gauge type ID |
Returns: type weight (`uint256`).
```vyper
@external
@view
def get_type_weight(type_id: int128) -> uint256:
"""
@notice Get current type weight
@param type_id Type id
@return Type weight
"""
return self.points_type_weight[type_id][self.time_type_weight[type_id]]
@internal
def _get_type_weight(gauge_type: int128) -> uint256:
"""
@notice Fill historic type weights week-over-week for missed checkins
and return the type weight for the future week
@param gauge_type Gauge type id
@return Type weight
"""
t: uint256 = self.time_type_weight[gauge_type]
if t > 0:
w: uint256 = self.points_type_weight[gauge_type][t]
for i in range(500):
if t > block.timestamp:
break
t += WEEK
self.points_type_weight[gauge_type][t] = w
if t > block.timestamp:
self.time_type_weight[gauge_type] = t
return w
else:
return 0
```
This example returns the type weight for gauge type 0. The value is fetched live from the blockchain.
::::
### `add_type`
::::description[`GaugeController.add_type(_name: String[64], weight: uint256 = 0)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to add a new gauge type.
| Input | Type | Description |
| ----------- | -------| ----|
| `_name` | `String[64]` | Gauge type name |
| `weight` | `uint256` | Gauge weight. Defaults to 0 |
Emits: `AddType` event.
```vyper
event AddType:
name: String[64]
type_id: int128
# Gauge parameters
# All numbers are "fixed point" on the basis of 1e18
n_gauge_types: public(int128)
n_gauges: public(int128)
gauge_type_names: public(HashMap[int128, String[64]])
points_weight: public(HashMap[address, HashMap[uint256, Point]]) # gauge_addr -> time -> Point
changes_weight: HashMap[address, HashMap[uint256, uint256]] # gauge_addr -> time -> slope
time_weight: public(HashMap[address, uint256]) # gauge_addr -> last scheduled time (next week)
points_sum: public(HashMap[int128, HashMap[uint256, Point]]) # type_id -> time -> Point
changes_sum: HashMap[int128, HashMap[uint256, uint256]] # type_id -> time -> slope
time_sum: public(uint256[1000000000]) # type_id -> last scheduled time (next week)
points_total: public(HashMap[uint256, uint256]) # time -> total weight
time_total: public(uint256) # last scheduled time
points_type_weight: public(HashMap[int128, HashMap[uint256, uint256]]) # type_id -> time -> type weight
time_type_weight: public(uint256[1000000000]) # type_id -> last scheduled time (next week)
@external
def add_type(_name: String[64], weight: uint256 = 0):
"""
@notice Add gauge type with name `_name` and weight `weight`
@param _name Name of gauge type
@param weight Weight of gauge type
"""
assert msg.sender == self.admin
type_id: int128 = self.n_gauge_types
self.gauge_type_names[type_id] = _name
self.n_gauge_types = type_id + 1
if weight != 0:
self._change_type_weight(type_id, weight)
log AddType(_name, type_id)
@internal
def _change_type_weight(type_id: int128, weight: uint256):
"""
@notice Change type weight
@param type_id Type id
@param weight New type weight
"""
old_weight: uint256 = self._get_type_weight(type_id)
old_sum: uint256 = self._get_sum(type_id)
_total_weight: uint256 = self._get_total()
next_time: uint256 = (block.timestamp + WEEK) / WEEK * WEEK
_total_weight = _total_weight + old_sum * weight - old_sum * old_weight
self.points_total[next_time] = _total_weight
self.points_type_weight[type_id][next_time] = weight
self.time_total = next_time
self.time_type_weight[type_id] = next_time
log NewTypeWeight(type_id, next_time, weight, _total_weight)
```
This example adds a new gauge type with the name `New Test GaugeType` and a weight of `0`. Adding a new gauge type increases the `n_gauge_types` by `1`. Consequently, the new gauge type will have an ID of `14` (as there are already `13` gauge types before this new addition).
```shell
>>> GaugeController.n_gauge_types()
13
>>> GaugeController.add_type('New Test GaugeType', 0)
>>> GaugeController.n_gauge_types()
14
```
::::
### `change_type_weight`
::::description[`GaugeController.change_type_weight(type_id: int128, weight: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to change the weight for a specific gauge type.
| Input | Type | Description |
| --------- | --------- | ------------- |
| `type_id` | `int128` | Gauge type ID |
| `weight` | `uint256` | New gauge type weight |
Emits: `NewTypeWeight` event.
```vyper
event NewTypeWeight:
type_id: int128
time: uint256
weight: uint256
total_weight: uint256
points_weight: public(HashMap[address, HashMap[uint256, Point]]) # gauge_addr -> time -> Point
changes_weight: HashMap[address, HashMap[uint256, uint256]] # gauge_addr -> time -> slope
time_weight: public(HashMap[address, uint256]) # gauge_addr -> last scheduled time (next week)
points_sum: public(HashMap[int128, HashMap[uint256, Point]]) # type_id -> time -> Point
changes_sum: HashMap[int128, HashMap[uint256, uint256]] # type_id -> time -> slope
time_sum: public(uint256[1000000000]) # type_id -> last scheduled time (next week)
points_total: public(HashMap[uint256, uint256]) # time -> total weight
time_total: public(uint256) # last scheduled time
points_type_weight: public(HashMap[int128, HashMap[uint256, uint256]]) # type_id -> time -> type weight
time_type_weight: public(uint256[1000000000]) # type_id -> last scheduled time (next week)
@external
def change_type_weight(type_id: int128, weight: uint256):
"""
@notice Change gauge type `type_id` weight to `weight`
@param type_id Gauge type id
@param weight New Gauge weight
"""
assert msg.sender == self.admin
self._change_type_weight(type_id, weight)
@internal
def _change_type_weight(type_id: int128, weight: uint256):
"""
@notice Change type weight
@param type_id Type id
@param weight New type weight
"""
old_weight: uint256 = self._get_type_weight(type_id)
old_sum: uint256 = self._get_sum(type_id)
_total_weight: uint256 = self._get_total()
next_time: uint256 = (block.timestamp + WEEK) / WEEK * WEEK
_total_weight = _total_weight + old_sum * weight - old_sum * old_weight
self.points_total[next_time] = _total_weight
self.points_type_weight[type_id][next_time] = weight
self.time_total = next_time
self.time_type_weight[type_id] = next_time
log NewTypeWeight(type_id, next_time, weight, _total_weight)
```
This example changes the weight of a gauge type with ID `14` to `1000000000000000000`.
```shell
>>> GaugeController.get_type_weight(14)
0
>>> GaugeController.change_type_weight(14, 1000000000000000000)
>>> GaugeController.get_type_weight(14)
1000000000000000000
```
::::
---
## Contract Info Methods
### `token`
::::description[`GaugeController.token() -> address: view`]
Getter for the Curve DAO Token. This variable can not be changed.
Returns: crv token (`address`).
```vyper
token: public(address) # CRV token
@external
def __init__(_token: address, _voting_escrow: address):
"""
@notice Contract constructor
@param _token `ERC20CRV` contract address
@param _voting_escrow `VotingEscrow` contract address
"""
assert _token != ZERO_ADDRESS
...
self.token = _token
...
```
This example returns the CRV token address. The value is fetched live from the blockchain.
::::
### `voting_escrow`
::::description[`GaugeController.voting_escrow() -> address: view`]
Getter for the VotingEscrow contract.
Returns: voting escrow (`address`).
```vyper
voting_escrow: public(address) # Voting escrow
@external
def __init__(_token: address, _voting_escrow: address):
"""
@notice Contract constructor
@param _token `ERC20CRV` contract address
@param _voting_escrow `VotingEscrow` contract address
"""
...
assert _voting_escrow != ZERO_ADDRESS
...
self.voting_escrow = _voting_escrow
```
This example returns the VotingEscrow contract address. The value is fetched live from the blockchain.
::::
---
## Contract Ownership
Admin ownership can be commited by calling `commit_transfer_ownership`. Changes then need to be applied. The current `admin` is the OwnershipAgent, which would require a DAO vote to change it.
### `admin`
::::description[`GaugeController.admin() -> address: view`]
Getter for the admin of the contract.
Returns: admin (`address`).
```vyper
admin: public(address) # Can and will be a smart contract
@external
def __init__(_token: address, _voting_escrow: address):
"""
@notice Contract constructor
@param _token `ERC20CRV` contract address
@param _voting_escrow `VotingEscrow` contract address
"""
...
self.admin = msg.sender
...
```
This example returns the admin address of the contract. The value is fetched live from the blockchain.
::::
### `future_admin`
::::description[`GaugeController.future_admin() -> address: view`]
Getter for the future admin of the contract.
Returns: future admin (`address`).
```vyper
future_admin: public(address) # Can and will be a smart contract
```
This example returns the future admin address of the contract. The value is fetched live from the blockchain.
::::
### `commit_transfer_ownership`
::::description[`GaugeController.commit_transfer_ownership(addr: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to commit the ownership of the contract to `addr`.
| Input | Type | Description |
| ----------- | -------| ----|
| `addr` | `address` | new admin address |
Emits: `CommitOwnership` event.
```vyper
event CommitOwnership:
admin: address
future_admin: public(address) # Can and will be a smart contract
@external
def commit_transfer_ownership(addr: address):
"""
@notice Transfer ownership of GaugeController to `addr`
@param addr Address to have ownership transferred to
"""
assert msg.sender == self.admin # dev: admin only
self.future_admin = addr
log CommitOwnership(addr)
```
This example commits the ownership of the contract to `0xd8da6bf26964af9d7eed9e03e53415d37aa96045`.
```shell
>>> GaugeController.admin()
'0x40907540d8a6C65c637785e8f8B742ae6b0b9968'
>>> GaugeController.commit_transfer_ownership("0xd8da6bf26964af9d7eed9e03e53415d37aa96045")
>>> GaugeController.future_admin()
'0xd8da6bf26964af9d7eed9e03e53415d37aa96045'
```
::::
### `apply_transfer_ownership`
::::description[`GaugeController.apply_transfer_ownership()`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to apply the new ownership.
Emits: `ApplyOwnership` event.
```vyper
event ApplyOwnership:
admin: address
@external
def apply_transfer_ownership():
"""
@notice Apply pending ownership transfer
"""
assert msg.sender == self.admin # dev: admin only
_admin: address = self.future_admin
assert _admin != ZERO_ADDRESS # dev: admin not set
self.admin = _admin
log ApplyOwnership(_admin)
```
This example applies the new ownership of the contract to `0xd8da6bf26964af9d7eed9e03e53415d37aa96045` (see the example above).
```shell
>>> GaugeController.admin()
'0x40907540d8a6C65c637785e8f8B742ae6b0b9968'
>>> GaugeController.apply_transfer_ownership()
>>> GaugeController.admin()
'0xd8da6bf26964af9d7eed9e03e53415d37aa96045'
```
::::
---
## Liquidity Gauge V6
:::vyper[`LiquidityGaugeV6.vy`]
The source code for the `LiquidityGaugeV6.vy` contract can be found on [GitHub](https://github.com/curvefi/stableswap-ng/blob/main/contracts/main/LiquidityGauge.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.3.10`.
The contract is deployed on :logos-ethereum: Ethereum at [`0x38D9BdA812da2C68dFC6aDE85A7F7a54E77F8325`](https://etherscan.io/address/0x38D9BdA812da2C68dFC6aDE85A7F7a54E77F8325).
```json
[{"name":"Deposit","inputs":[{"name":"provider","type":"address","indexed":true},{"name":"value","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"Withdraw","inputs":[{"name":"provider","type":"address","indexed":true},{"name":"value","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"UpdateLiquidityLimit","inputs":[{"name":"user","type":"address","indexed":true},{"name":"original_balance","type":"uint256","indexed":false},{"name":"original_supply","type":"uint256","indexed":false},{"name":"working_balance","type":"uint256","indexed":false},{"name":"working_supply","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"CommitOwnership","inputs":[{"name":"admin","type":"address","indexed":false}],"anonymous":false,"type":"event"},{"name":"ApplyOwnership","inputs":[{"name":"admin","type":"address","indexed":false}],"anonymous":false,"type":"event"},{"name":"SetGaugeManager","inputs":[{"name":"_gauge_manager","type":"address","indexed":false}],"anonymous":false,"type":"event"},{"name":"Transfer","inputs":[{"name":"_from","type":"address","indexed":true},{"name":"_to","type":"address","indexed":true},{"name":"_value","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"Approval","inputs":[{"name":"_owner","type":"address","indexed":true},{"name":"_spender","type":"address","indexed":true},{"name":"_value","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"stateMutability":"nonpayable","type":"constructor","inputs":[{"name":"_lp_token","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"deposit","inputs":[{"name":"_value","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"deposit","inputs":[{"name":"_value","type":"uint256"},{"name":"_addr","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"deposit","inputs":[{"name":"_value","type":"uint256"},{"name":"_addr","type":"address"},{"name":"_claim_rewards","type":"bool"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"withdraw","inputs":[{"name":"_value","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"withdraw","inputs":[{"name":"_value","type":"uint256"},{"name":"_claim_rewards","type":"bool"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"claim_rewards","inputs":[],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"claim_rewards","inputs":[{"name":"_addr","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"claim_rewards","inputs":[{"name":"_addr","type":"address"},{"name":"_receiver","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"transferFrom","inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"transfer","inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"approve","inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"permit","inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"},{"name":"_deadline","type":"uint256"},{"name":"_v","type":"uint8"},{"name":"_r","type":"bytes32"},{"name":"_s","type":"bytes32"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"increaseAllowance","inputs":[{"name":"_spender","type":"address"},{"name":"_added_value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"decreaseAllowance","inputs":[{"name":"_spender","type":"address"},{"name":"_subtracted_value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"user_checkpoint","inputs":[{"name":"addr","type":"address"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"set_rewards_receiver","inputs":[{"name":"_receiver","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"kick","inputs":[{"name":"addr","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_gauge_manager","inputs":[{"name":"_gauge_manager","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"deposit_reward_token","inputs":[{"name":"_reward_token","type":"address"},{"name":"_amount","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"deposit_reward_token","inputs":[{"name":"_reward_token","type":"address"},{"name":"_amount","type":"uint256"},{"name":"_epoch","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"add_reward","inputs":[{"name":"_reward_token","type":"address"},{"name":"_distributor","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_reward_distributor","inputs":[{"name":"_reward_token","type":"address"},{"name":"_distributor","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_killed","inputs":[{"name":"_is_killed","type":"bool"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"claimed_reward","inputs":[{"name":"_addr","type":"address"},{"name":"_token","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"claimable_reward","inputs":[{"name":"_user","type":"address"},{"name":"_reward_token","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"claimable_tokens","inputs":[{"name":"addr","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"integrate_checkpoint","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"future_epoch_time","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"inflation_rate","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"decimals","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"version","inputs":[],"outputs":[{"name":"","type":"string"}]},{"stateMutability":"view","type":"function","name":"DOMAIN_SEPARATOR","inputs":[],"outputs":[{"name":"","type":"bytes32"}]},{"stateMutability":"view","type":"function","name":"salt","inputs":[],"outputs":[{"name":"","type":"bytes32"}]},{"stateMutability":"view","type":"function","name":"balanceOf","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"totalSupply","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"allowance","inputs":[{"name":"arg0","type":"address"},{"name":"arg1","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"name","inputs":[],"outputs":[{"name":"","type":"string"}]},{"stateMutability":"view","type":"function","name":"symbol","inputs":[],"outputs":[{"name":"","type":"string"}]},{"stateMutability":"view","type":"function","name":"nonces","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"factory","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"manager","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"lp_token","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"is_killed","inputs":[],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"reward_count","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"reward_data","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"tuple","components":[{"name":"token","type":"address"},{"name":"distributor","type":"address"},{"name":"period_finish","type":"uint256"},{"name":"rate","type":"uint256"},{"name":"last_update","type":"uint256"},{"name":"integral","type":"uint256"}]}]},{"stateMutability":"view","type":"function","name":"rewards_receiver","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"reward_integral_for","inputs":[{"name":"arg0","type":"address"},{"name":"arg1","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"working_balances","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"working_supply","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"integrate_inv_supply_of","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"integrate_checkpoint_of","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"integrate_fraction","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"period","inputs":[],"outputs":[{"name":"","type":"int128"}]},{"stateMutability":"view","type":"function","name":"reward_tokens","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"period_timestamp","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"integrate_inv_supply","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]}]
```
:::
## Depositing and Withdrawing
Liquidity pool (LP) tokens can be deposited into or withdrawn from a gauge at any time.
In user interfaces and documentation, the terms "staking" and "unstaking" are often used when referring to gauges. However, the terminology used in the actual source code is `deposit` and `withdraw`.
When LP tokens are deposited into a gauge, the smart contract mints an equivalent amount of "gauge tokens" to the depositor. This mechanism ensures that when tokens are withdrawn, the depositor receives the same amount of LP tokens originally deposited. LP tokens are ERC20 tokens and transferable.
:::example[Example of Depositing and Earning Rewards]
Alice deposits 100 crvUSD into the crvusd/USDC liquidity pool and receives 99 LP tokens in return. Observing significant gauge weight and subsequent CRV emissions to this pool, she decides to deposit (stake) her LP tokens into the gauge. Consequently, she begins to earn CRV rewards based on her liquidity share and her boost factor within the pool. Alice can claim rewards or withdraw her LP tokens at any point in time.
:::
### `deposit`
::::description[`LiquidityGaugeV6.deposit(_value: uint256, _addr: address = msg.sender, _claim_rewards: bool = False)`]
Function to deposit `_value` of LP tokens into the gauge. When depositing LP tokens into the gauge, the contract mints the equivalent amount of "gauge token" to the user.
Emits: `Deposit`, `Transfer`, `UpdateLiquidityLimit` events.
| Input | Type | Description |
| ---------------- | --------- | ---------------------------------- |
| `_value` | `uint256` | Number of LP tokens to deposit. |
| `_addr` | `address` | Address to deposit the LP tokens for. Defaults to `msg.sender`. |
| `_claim_rewards` | `bool` | Whether to additionally claim rewards or not. |
```vyper
event Deposit:
provider: indexed(address)
value: uint256
event Transfer:
_from: indexed(address)
_to: indexed(address)
_value: uint256
@external
@nonreentrant('lock')
def deposit(_value: uint256, _addr: address = msg.sender, _claim_rewards: bool = False):
"""
@notice Deposit `_value` LP tokens
@dev Depositting also claims pending reward tokens
@param _value Number of tokens to deposit
@param _addr Address to deposit for
"""
assert _addr != empty(address) # dev: cannot deposit for zero address
self._checkpoint(_addr)
if _value != 0:
is_rewards: bool = self.reward_count != 0
total_supply: uint256 = self.totalSupply
if is_rewards:
self._checkpoint_rewards(_addr, total_supply, _claim_rewards, empty(address))
total_supply += _value
new_balance: uint256 = self.balanceOf[_addr] + _value
self.balanceOf[_addr] = new_balance
self.totalSupply = total_supply
self._update_liquidity_limit(_addr, new_balance, total_supply)
ERC20(self.lp_token).transferFrom(msg.sender, self, _value)
log Deposit(_addr, _value)
log Transfer(empty(address), _addr, _value)
```
```shell
>>> LiquidityGaugeV6.deposit(1000000000000000000, '0x989AEb4d175e16225E39E87d0D97A3360524AD80', False)
```
::::
### `withdraw`
::::description[`LiquidityGaugeV6.withdraw(_value: uint256, _claim_rewards: bool = False)`]
Function to withdraw `_value` of LP tokens from the gauge.
Emits: `Withdraw`, `Transfer`, `UpdateLiquidityLimit` events.
| Input | Type | Description |
| ---------------- | --------- | ---------------------------------- |
| `_value` | `uint256` | Number of LP tokens to withdraw. |
| `_claim_rewards` | `bool` | Whether to additionally claim rewards or not. |
```vyper
event Withdraw:
provider: indexed(address)
value: uint256
event Transfer:
_from: indexed(address)
_to: indexed(address)
_value: uint256
@external
@nonreentrant('lock')
def withdraw(_value: uint256, _claim_rewards: bool = False):
"""
@notice Withdraw `_value` LP tokens
@dev Withdrawing also claims pending reward tokens
@param _value Number of tokens to withdraw
"""
self._checkpoint(msg.sender)
if _value != 0:
is_rewards: bool = self.reward_count != 0
total_supply: uint256 = self.totalSupply
if is_rewards:
self._checkpoint_rewards(msg.sender, total_supply, _claim_rewards, empty(address))
total_supply -= _value
new_balance: uint256 = self.balanceOf[msg.sender] - _value
self.balanceOf[msg.sender] = new_balance
self.totalSupply = total_supply
self._update_liquidity_limit(msg.sender, new_balance, total_supply)
ERC20(self.lp_token).transfer(msg.sender, _value)
log Withdraw(msg.sender, _value)
log Transfer(msg.sender, empty(address), _value)
```
```shell
>>> LiquidityGaugeV6.withdraw(1000000000000000000, False)
```
::::
---
## Claiming Rewards
Reward tokens can be claimed using the `claim_rewards` function. This function claims all externally added rewards from the gauge in a single transaction.
:::warning[Which rewards does `claim_rewards` claim?]
The `claim_rewards` function only claims ["permissionless rewards"](#permissionless-rewards), not CRV emissions directed to the gauge. If there are multiple reward tokens, calling the function will result in a claim of all reward tokens at once.
CRV emissions directed to the gauge are claimable from the [`Minter.vy`](../minter.md) contract using the [`mint`](../minter.md#mint) function.
:::
The liquidity gauge records checkpoints to determine how much external rewards each user is entitled to claim.
```py
@internal
def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _receiver: address):
"""
@notice Claim pending rewards and checkpoint rewards for a user
"""
user_balance: uint256 = 0
receiver: address = _receiver
if _user != empty(address):
user_balance = self.balanceOf[_user]
if _claim and _receiver == empty(address):
# if receiver is not explicitly declared, check if a default receiver is set
receiver = self.rewards_receiver[_user]
if receiver == empty(address):
# if no default receiver is set, direct claims to the user
receiver = _user
reward_count: uint256 = self.reward_count
for i in range(MAX_REWARDS):
if i == reward_count:
break
token: address = self.reward_tokens[i]
integral: uint256 = self.reward_data[token].integral
last_update: uint256 = min(block.timestamp, self.reward_data[token].period_finish)
duration: uint256 = last_update - self.reward_data[token].last_update
if duration != 0 and _total_supply != 0:
self.reward_data[token].last_update = last_update
integral += duration * self.reward_data[token].rate * 10**18 / _total_supply
self.reward_data[token].integral = integral
if _user != empty(address):
integral_for: uint256 = self.reward_integral_for[token][_user]
new_claimable: uint256 = 0
if integral_for < integral:
self.reward_integral_for[token][_user] = integral
new_claimable = user_balance * (integral - integral_for) / 10**18
claim_data: uint256 = self.claim_data[_user][token]
total_claimable: uint256 = (claim_data >> 128) + new_claimable
if total_claimable > 0:
total_claimed: uint256 = claim_data % 2**128
if _claim:
assert ERC20(token).transfer(receiver, total_claimable, default_return_value=True)
self.claim_data[_user][token] = total_claimed + total_claimable
elif new_claimable > 0:
self.claim_data[_user][token] = total_claimed + (total_claimable << 128)
```
*These checkpoints occur:*
- When a reward token is deposited (this does not record a checkpoint for an individual user but creates a general checkpoint).
- When transferring LP tokens (records a checkpoint for both the sender and the receiver).
- When depositing (staking) LP tokens into the gauge.
- When withdrawing (unstaking) LP tokens from the gauge.
- When rewards (excluding CRV emission rewards, which are claimed via the `Minter.vy` contract) are claimed.
### `claim_rewards`
::::description[`LiquidityGaugeV6.claim_rewards(_addr: address = msg.sender, _receiver: address = empty(address))`]
:::warning[Claiming for another user]
When claiming for another user, the rewards cannot be redirected to another wallet.
:::
Function to claim rewards from the gauge.
| Input | Type | Description |
| ----------- | --------- | ---------------------------------- |
| `_addr` | `address` | Address to claim the rewards for. Defaults to `msg.sender`. |
| `_receiver` | `address` | Receiver of the rewards. |
```vyper
@external
@nonreentrant('lock')
def claim_rewards(_addr: address = msg.sender, _receiver: address = empty(address)):
"""
@notice Claim available reward tokens for `_addr`
@param _addr Address to claim for
@param _receiver Address to transfer rewards to - if set to
empty(address), uses the default reward receiver
for the caller
"""
if _receiver != empty(address):
assert _addr == msg.sender # dev: cannot redirect when claiming for another user
self._checkpoint_rewards(_addr, self.totalSupply, True, _receiver)
@internal
def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _receiver: address):
"""
@notice Claim pending rewards and checkpoint rewards for a user
"""
user_balance: uint256 = 0
receiver: address = _receiver
if _user != empty(address):
user_balance = self.balanceOf[_user]
if _claim and _receiver == empty(address):
# if receiver is not explicitly declared, check if a default receiver is set
receiver = self.rewards_receiver[_user]
if receiver == empty(address):
# if no default receiver is set, direct claims to the user
receiver = _user
reward_count: uint256 = self.reward_count
for i in range(MAX_REWARDS):
if i == reward_count:
break
token: address = self.reward_tokens[i]
integral: uint256 = self.reward_data[token].integral
last_update: uint256 = min(block.timestamp, self.reward_data[token].period_finish)
duration: uint256 = last_update - self.reward_data[token].last_update
if duration != 0 and _total_supply != 0:
self.reward_data[token].last_update = last_update
integral += duration * self.reward_data[token].rate * 10**18 / _total_supply
self.reward_data[token].integral = integral
if _user != empty(address):
integral_for: uint256 = self.reward_integral_for[token][_user]
new_claimable: uint256 = 0
if integral_for < integral:
self.reward_integral_for[token][_user] = integral
new_claimable = user_balance * (integral - integral_for) / 10**18
claim_data: uint256 = self.claim_data[_user][token]
total_claimable: uint256 = (claim_data >> 128) + new_claimable
if total_claimable > 0:
total_claimed: uint256 = claim_data % 2**128
if _claim:
assert ERC20(token).transfer(receiver, total_claimable, default_return_value=True)
self.claim_data[_user][token] = total_claimed + total_claimable
elif new_claimable > 0:
self.claim_data[_user][token] = total_claimed + (total_claimable << 128)
```
```shell
>>> LiquidityGaugeV6.claim_rewards('0x989AEb4d175e16225E39E87d0D97A3360524AD80')
```
::::
### `claimed_reward`
::::description[`LiquidityGaugeV6.claimed_reward(_addr: address, _token: address) -> uint256: view`]
Getter for the total amount of `_token` claimed by `_addr`.
Returns: claimed tokens (`uint256`).
| Input | Type | Description |
| -------- | --------- | -------------------------- |
| `_addr` | `address` | User address to check for. |
| `_token` | `address` | Reward token to check for. |
```vyper
# user -> [uint128 claimable amount][uint128 claimed amount]
claim_data: HashMap[address, HashMap[address, uint256]]
@view
@external
def claimed_reward(_addr: address, _token: address) -> uint256:
"""
@notice Get the number of already-claimed reward tokens for a user
@param _addr Account to get reward amount for
@param _token Token to get reward amount for
@return uint256 Total amount of `_token` already claimed by `_addr`
"""
return self.claim_data[_addr][_token] % 2**128
```
```shell
>>> LiquidityGaugeV6.claimed_reward('0x989AEb4d175e16225E39E87d0D97A3360524AD80', '0xfe18aE03741a5b84e39C295Ac9C856eD7991C38e')
30563368675260319
```
::::
### `claimable_reward`
::::description[`LiquidityGaugeV6.claimable_reward(_user: address, _reward_token: address) -> uint256: view`]
Function to check the claimable amount of `_reward_token` for `_user`.
Returns: claimable tokens (`uint256`).
| Input | Type | Description |
| --------------- | --------- | -------------------------- |
| `_user` | `address` | User address to check for. |
| `_reward_token` | `address` | Reward token to check for. |
```vyper
reward_integral_for: public(HashMap[address, HashMap[address, uint256]])
@view
@external
def claimable_reward(_user: address, _reward_token: address) -> uint256:
"""
@notice Get the number of claimable reward tokens for a user
@param _user Account to get reward amount for
@param _reward_token Token to get reward amount for
@return uint256 Claimable reward token amount
"""
integral: uint256 = self.reward_data[_reward_token].integral
total_supply: uint256 = self.totalSupply
if total_supply != 0:
last_update: uint256 = min(block.timestamp, self.reward_data[_reward_token].period_finish)
duration: uint256 = last_update - self.reward_data[_reward_token].last_update
integral += (duration * self.reward_data[_reward_token].rate * 10**18 / total_supply)
integral_for: uint256 = self.reward_integral_for[_reward_token][_user]
new_claimable: uint256 = self.balanceOf[_user] * (integral - integral_for) / 10**18
return (self.claim_data[_user][_reward_token] >> 128) + new_claimable
```
```shell
>>> LiquidityGaugeV6.claimable_reward('0x989AEb4d175e16225E39E87d0D97A3360524AD80', '0xfe18aE03741a5b84e39C295Ac9C856eD7991C38e')
121423107585280954
```
::::
### `rewards_receiver`
::::description[`LiquidityGaugeV6.rewards_receiver(arg0: address) -> address: view`]
Getter for the reward receiver of the caller. By default, this value is set to `empty(address)`, which means the rewards will be claimed to the user. But e.g. for integrations like Convex, the `rewards_receiver` is set to another contract address, from which the rewards are further distributed.
Returns: reward receiver (`address`).
| Input | Type | Description |
| ------ | --------- | ---------------------------------- |
| `arg0` | `address` | Receiver of the rewards. |
```vyper
rewards_receiver: public(HashMap[address, address])
```
```shell
>>> LiquidityGaugeV6.rewards_receiver('0x2618F4c64805526a3092d41f25597CcfE4Dd8216') # random user
'0x0000000000000000000000000000000000000000'
>>> LiquidityGaugeV6.rewards_receiver('0x989AEb4d175e16225E39E87d0D97A3360524AD80') # convex
'0xF681fd1C9118085c3aCB0Eec9d57e25A6e99208f'
```
::::
### `set_rewards_receiver`
::::description[`LiquidityGaugeV6.set_rewards_receiver(_receiver: address)`]
Function to set the default reward receiver for the caller. When set to `empty(address)`, rewards are sent to the caller.
| Input | Type | Description |
| ----------- | --------- | ---------------------------------- |
| `_receiver` | `address` | Receiver address for any rewards claimed. |
```vyper
rewards_receiver: public(HashMap[address, address])
@external
def set_rewards_receiver(_receiver: address):
"""
@notice Set the default reward receiver for the caller.
@dev When set to empty(address), rewards are sent to the caller
@param _receiver Receiver address for any rewards claimed via `claim_rewards`
"""
self.rewards_receiver[msg.sender] = _receiver
```
```shell
>>> LiquidityGaugeV6.set_rewards_receiver('0x0000000000000000000000000000000000000000')
```
::::
---
## Permissionless Rewards
Newer liquidity gauges (from `LiquidityGaugeV3.vy` and upwards) introduce the possibility to add what are termed "permissionless rewards." However, the term "permissionless" might be misleading as only a `distributor` address, set by the gauge's `manager`, can add these rewards. The `manager` address is set to [`tx.origin`](https://docs.vyperlang.org/en/stable/constants-and-vars.html?highlight=tx.origin#block-and-transaction-properties) at the time of contract deployment.
To add rewards to a gauge, a reward token and a distributor must be set by calling the `set_reward_distributor` function. This action can only be performed by the `manager` or the `admin` of the Factory contract, wich deployed the pool. Each reward token can have only one distributor. The "right to add a reward token" can be transfered. Tokens are added as rewards to the gauge via the `add_reward` method.
:::warning[NOT BOOSTABLE: Distribution of Externally Added Rewards]
Externally added rewards are not boostable and are distributed purely based on the user's unboosted share of liquidity in the gauge.
For example, if Alice holds 10% of the LP tokens staked in the gauge, she will receive 10% of the externally added rewards, assuming there are no changes in her liquidity share or the amount of rewards.
:::
### `reward_data`
::::description[`LiquidityGaugeV6.reward_data(arg0: address) -> tuple: view`]
Getter for the data of a specific reward token.
Returns: token (`address`), distributor (`address`), finish_period (`uint256`), rate (`uint256`), last_update (`uint256`) and integral (`uint256`).
| Input | Type | Description |
| ------ | --------- | ---------------------------------- |
| `arg0` | `address` | Address of the reward token. |
```vyper
struct Reward:
token: address
distributor: address
period_finish: uint256
rate: uint256
last_update: uint256
integral: uint256
reward_data: public(HashMap[address, Reward])
```
```shell
>>> LiquidityGaugeV6.reward_data('0xfe18aE03741a5b84e39C295Ac9C856eD7991C38e')
'0x0000000000000000000000000000000000000000', '0xC56706334afE5a1638845ED9168E2ca3b3dbCCe7', 1715673839, 1186851500823, 1713351359, 16346318221475032
```
::::
### `reward_tokens`
::::description[`LiquidityGaugeV6.reward_tokens(arg0: uint256) -> address: view`]
Getter for the added reward token at index `arg0`. New tokens are populated to this variable when calling the `add_reward` function.
Returns: reward token (`address`).
| Input | Type | Description |
| ------ | --------- | ---------------------------------- |
| `arg0` | `uint256` | Index. |
```vyper
MAX_REWARDS: constant(uint256) = 8
# array of reward tokens
reward_tokens: public(address[MAX_REWARDS])
```
```shell
>>> LiquidityGaugeV6.reward_tokens(0)
'0xfe18aE03741a5b84e39C295Ac9C856eD7991C38e'
>>> LiquidityGaugeV6.reward_tokens(1)
'0x0000000000000000000000000000000000000000'
```
::::
### `reward_count`
::::description[`LiquidityGaugeV6.reward_count() -> uint256: view`]
Getter for the count of added reward tokens. This variable is incremented by one each time `add_reward` is called.
Returns: number of reward tokens added (`uint256`).
```vyper
reward_count: public(uint256)
```
```shell
>>> LiquidityGaugeV6.reward_count()
1
```
::::
### `manager`
::::description[`LiquidityGaugeV6.manager() -> address: view`]
Getter for the gauge manager. This address can add new reward tokens or set distributors for those tokens. The variable is populated when initializing the contract and is set to `tx.origin`, meaning the signer of the transaction which deploys the gauge is assigned as the gauge manager. The gauge manager is upgradable. It can be changed via the `set_gauge_manager` function.
Returns: gauge manager (`address`).
```vyper
manager: public(address)
@external
def __init__(_lp_token: address):
"""
@notice Contract constructor
@param _lp_token Liquidity Pool contract address
"""
self.lp_token = _lp_token
self.factory = msg.sender
self.manager = tx.origin
...
```
```shell
>>> LiquidityGaugeV6.manager()
'0xC56706334afE5a1638845ED9168E2ca3b3dbCCe7'
```
::::
### `add_reward`
::::description[`LiquidityGaugeV6.add_reward(_reward_token: address, _distributor: address)`]
:::guard[Guarded Methods]
This function can only be called by the `manager` of the gauge or the `owner` of the Factory.
:::
Function to add specify a reward token and distributor for the gauge. Once a reward token is added, it cannot be removed anymore.
| Input | Type | Description |
| --------------- | --------- | ---------------------------------- |
| `_reward_token` | `address` | Reward token address to add. |
| `_distributor` | `address` | Address which can deposit the reward token. |
```vyper
@external
def add_reward(_reward_token: address, _distributor: address):
"""
@notice Add additional rewards to be distributed to stakers
@param _reward_token The token to add as an additional reward
@param _distributor Address permitted to fund this contract with the reward token
"""
assert msg.sender in [self.manager, Factory(self.factory).admin()] # dev: only manager or factory admin
assert _distributor != empty(address) # dev: distributor cannot be zero address
reward_count: uint256 = self.reward_count
assert reward_count < MAX_REWARDS
assert self.reward_data[_reward_token].distributor == empty(address)
self.reward_data[_reward_token].distributor = _distributor
self.reward_tokens[reward_count] = _reward_token
self.reward_count = reward_count + 1
```
```shell
>>> LiquidityGaugeV6.add_reward('0xfe18aE03741a5b84e39C295Ac9C856eD7991C38e', '0xC56706334afE5a1638845ED9168E2ca3b3dbCCe7')
```
::::
### `set_gauge_manager`
::::description[`LiquidityGaugeV6.set_gauge_manager(_gauge_manager: address)`]
:::guard[Guarded Methods]
This function can only be called by the `manager` of the gauge or the `admin` of the Factory.
:::
Function to set a new gauge manager.
Emits: `SetGaugeManager`
| Input | Type | Description |
| ------------------- | --------- | ---------------------------------- |
| `set_gauge_manager` | `address` | New gauge manager address. |
```vyper
event SetGaugeManager:
_gauge_manager: address
manager: public(address)
@external
def set_gauge_manager(_gauge_manager: address):
"""
@notice Change the gauge manager for a gauge
@dev The manager of this contract, or the ownership admin can outright modify gauge
managership. A gauge manager can also transfer managership to a new manager via this
method, but only for the gauge which they are the manager of.
@param _gauge_manager The account to set as the new manager of the gauge.
"""
assert msg.sender in [self.manager, Factory(self.factory).admin()] # dev: only manager or factory admin
self.manager = _gauge_manager
log SetGaugeManager(_gauge_manager)
```
```shell
>>> LiquidityGaugeV6.set_gauge_manager('0xC56706334afE5a1638845ED9168E2ca3b3dbCCe7')
```
::::
### `set_reward_distributor`
::::description[`LiquidityGaugeV6.set_reward_distributor(_reward_token: address, _distributor: address)`]
:::guard[Guarded Methods]
This function can only be called by the `manager` of the gauge or the `admin` of the Factory.
:::
Function to reassign the reward distributor for a reward token.
| Input | Type | Description |
| --------------- | --------- | ---------------------------------- |
| `_reward_token` | `address` | Reward token to reassign the distribution rights for. |
| `_distributor` | `address` | New reward distributor. |
```vyper
reward_data: public(HashMap[address, Reward])
@external
def set_reward_distributor(_reward_token: address, _distributor: address):
"""
@notice Reassign the reward distributor for a reward token
@param _reward_token The reward token to reassign distribution rights to
@param _distributor The address of the new distributor
"""
current_distributor: address = self.reward_data[_reward_token].distributor
assert msg.sender in [current_distributor, Factory(self.factory).admin(), self.manager]
assert current_distributor != empty(address)
assert _distributor != empty(address)
self.reward_data[_reward_token].distributor = _distributor
```
```shell
>>> LiquidityGaugeV6.set_reward_distributor('0xfe18aE03741a5b84e39C295Ac9C856eD7991C38e', '0xC56706334afE5a1638845ED9168E2ca3b3dbCCe7')
```
::::
### `deposit_reward_token`
::::description[`LiquidityGaugeV6.deposit_reward_token(_reward_token: address, _amount: uint256, _epoch: uint256 = WEEK)`]
:::guard[Guarded Methods]
This function can only be called by the `manager` of the gauge or the `admin` of the Factory.
:::
Function to deposit a specific amount of reward tokens for a specified duration. If additional amounts of the same reward tokens are added, the leftover from the current distribution will be rolled over into the next distribution.
:::example[Example]
The gauge manager deposits 70 tokens right at the beginning of the week. Distribution payoff is the following:
$\frac{70}{604800} = 0.00011574074$ tokens per second, which equals to 10 tokens per day for the next 7 days.
After six days, the gauge manager decides to add 70 additional tokens, again for a duration of 7 days. The leftover 10 tokens which have not yet been distributed are rolled into the next "distribution phase":
$\frac{10 + 70}{604800} = 0.00013227513$ tokens per second, which equals to around 11.43 tokens per day for the next 7 days.
:::
| Input | Type | Description |
| --------------- | --------- | ----------------------------------------------------- |
| `_reward_token` | `address` | Reward token to deposit. |
| `_amount` | `uint256` | Amount of reward tokens to deposit. |
| `_epoch` | `uint256` | Duration the rewards are distributed across, denominated in seconds. Defaults to a week (604800s). |
```vyper
WEEK: constant(uint256) = 604800
@external
@nonreentrant("lock")
def deposit_reward_token(_reward_token: address, _amount: uint256, _epoch: uint256 = WEEK):
"""
@notice Deposit a reward token for distribution
@param _reward_token The reward token being deposited
@param _amount The amount of `_reward_token` being deposited
@param _epoch The duration the rewards are distributed across.
"""
assert msg.sender == self.reward_data[_reward_token].distributor
self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address))
# transferFrom reward token and use transferred amount henceforth:
amount_received: uint256 = ERC20(_reward_token).balanceOf(self)
assert ERC20(_reward_token).transferFrom(
msg.sender,
self,
_amount,
default_return_value=True
)
amount_received = ERC20(_reward_token).balanceOf(self) - amount_received
period_finish: uint256 = self.reward_data[_reward_token].period_finish
assert amount_received > _epoch # dev: rate will tend to zero!
if block.timestamp >= period_finish:
self.reward_data[_reward_token].rate = amount_received / _epoch
else:
remaining: uint256 = period_finish - block.timestamp
leftover: uint256 = remaining * self.reward_data[_reward_token].rate
self.reward_data[_reward_token].rate = (amount_received + leftover) / _epoch
self.reward_data[_reward_token].last_update = block.timestamp
self.reward_data[_reward_token].period_finish = block.timestamp + _epoch
```
```shell
>>> LiquidityGaugeV6.deposit_reward_token('0xfe18aE03741a5b84e39C295Ac9C856eD7991C38e', 1000000000000000000, 604800)
```
::::
---
## Boosting Your LP Tokens
Provided liquidity is boosted by the veCRV balance of the user, allowing for boosts up to 2.5 times. Gauges measure liquidity with respect to the user's boost in the `working_balances` variable. The total liquidity deposited in the gauge is represented by the `working_supply` method.
The [`working_balances`](#working_balances) of a user and the total [`working_supply`](#working_supply) are adjusted via the internal `_update_liquidity_limit` function when the following actions occur:
- **Transferring Tokens**: `working_balances` are adjusted for both the sender and the receiver of the LP tokens.
- **Depositing LP Tokens into the Gauge**: Adjusts the balance to reflect the new total.
- **Withdrawing LP Tokens from the Gauge**: Reduces the balance according to the amount withdrawn.
- **Performing a Manual Checkpoint**: Using the `user_checkpoint` function.
- **When a User is 'Kicked' for Abusing Their Boost**: For more information on what constitutes abuse and the repercussions, see [here](#kick).
```py
TOKENLESS_PRODUCTION: constant(uint256) = 40
@internal
def _update_liquidity_limit(addr: address, l: uint256, L: uint256):
"""
@notice Calculate limits which depend on the amount of CRV token per-user.
Effectively it calculates working balances to apply amplification
of CRV production by CRV
@param addr User address
@param l User's amount of liquidity (LP tokens)
@param L Total amount of liquidity (LP tokens)
"""
# To be called after totalSupply is updated
voting_balance: uint256 = VotingEscrowBoost(VEBOOST_PROXY).adjusted_balance_of(addr)
voting_total: uint256 = ERC20(VOTING_ESCROW).totalSupply()
lim: uint256 = l * TOKENLESS_PRODUCTION / 100
if voting_total > 0:
lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100
lim = min(l, lim)
old_bal: uint256 = self.working_balances[addr]
self.working_balances[addr] = lim
_working_supply: uint256 = self.working_supply + lim - old_bal
self.working_supply = _working_supply
log UpdateLiquidityLimit(addr, l, L, lim, _working_supply)
```
*General formula for calculating the boost:*
$$\text{lim} = l \times 0.4$$
$$\text{lim} = \text{lim} + L \times \frac{\text{voting\_balance}}{\text{voting\_total}} \times 0.6$$
$$\text{lim} = \min(l, \text{lim})$$
$$\text{boost factor} = \frac{\text{lim}}{l \times 0.4}$$
*with:*
| Variable | Description |
| :---------------------: | -------------------------------------------- |
| $l$ | User LP tokens deposited into the gauge.[^1] |
| $L$ | Total LP tokens deposited into the gauge. |
| $\text{voting\_balance}$ | Users veCRV balance. |
| $\text{voting\_total}$ | Total veCRV balance. |
[^1]: A user does not neccessarily need to deposit the LP into the gauge himself. Someone else can deposit for him or the "staked LP token" can be transfered to him.
---
Let's examine two different users. Both are providing the same amount of LP tokens (same liquidity), but the first user does not receive a boost because he does not have any veCRV. The second user has a veCRV balance of 500. The total veCRV balance is assumed to be 10,000.
```shell
l = 1000 # users LP tokens in gauge
L = 50000 # total LP tokens in gauge
voting_balance_user1 = 0 # veCRV balance user1
voting_balance_user2 = 500 # veCRV balance user2
voting_total = 10000 # total veCRV balance
```
---
**NO BOOST**
Lets calculate the LP position of a user that has a vecrv balance of 0:
$\text{lim} = 1000 \cdot 0.4 = 400$
$\text{lim} = 400 + 50000 \cdot \frac{0}{10000} \cdot 0.6 = 400$
$\text{lim} = \min(1000, 400)$
*The working supply of this user is 400 LP tokens. The boost is calculated by:*
$\text{boost factor} = \frac{400}{400} = 1$
---
**BOOST**
Lets calculate the LP position of a user that has a vecrv balance of 500 and therefore receives a boost on his provided liquidity:
$\text{lim} = 1000 \cdot 0.4 = 400$
$\text{lim} = 400 + 50000 \cdot \frac{500}{10000} \cdot 0.6 = 1900$
$\text{lim} = \min(1000, 1900)$
The working supply of this user is 1000 LP tokens. The boost is calculated by:
$\text{boost factor} = \frac{1000}{400} = 2.5$
---
### `working_balances`
::::description[`LiquidityGaugeV6.working_balances(arg0: address) -> uint256: view`]
Getter for the working balances of a user. This represents the effective liquidity of a user, which is used to calculate the CRV rewards they are entitled to. Essentially, it's the boosted balance of a user if they have some veCRV. If a user has no boost at all, their `working_balance` will be 40% of their LP tokens. If the position is fully boosted (2.5x), their `working_balance` will be equal to their LP tokens.
*For example:*
- 1 LP token with no boost = `working_balances(user) = 0.4`
- 1 LP token with 1.5 boost = `working_balances(user) = 1.5`
- 1 LP token with 2.5 boost = `working_balances(user) = 2.5`
Returns: working balance (`uint256`).
| Input | Type | Description |
| ------ | --------- | ----------------------------------------- |
| `arg0` | `address` | Address to check the working balance for. |
```vyper
TOKENLESS_PRODUCTION: constant(uint256) = 40
working_balances: public(HashMap[address, uint256])
@internal
def _update_liquidity_limit(addr: address, l: uint256, L: uint256):
"""
@notice Calculate limits which depend on the amount of CRV token per-user.
Effectively it calculates working balances to apply amplification
of CRV production by CRV
@param addr User address
@param l User's amount of liquidity (LP tokens)
@param L Total amount of liquidity (LP tokens)
"""
# To be called after totalSupply is updated
voting_balance: uint256 = VotingEscrowBoost(VEBOOST_PROXY).adjusted_balance_of(addr)
voting_total: uint256 = ERC20(VOTING_ESCROW).totalSupply()
lim: uint256 = l * TOKENLESS_PRODUCTION / 100
if voting_total > 0:
lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100
lim = min(l, lim)
old_bal: uint256 = self.working_balances[addr]
self.working_balances[addr] = lim
_working_supply: uint256 = self.working_supply + lim - old_bal
self.working_supply = _working_supply
log UpdateLiquidityLimit(addr, l, L, lim, _working_supply)
```
```shell
>>> LiquidityGaugeV6.working_balances('0x989AEb4d175e16225E39E87d0D97A3360524AD80')
11470659994458155726
```
::::
### `working_supply`
::::description[`LiquidityGaugeV6.working_supply() -> uint256: view`]
Getter for the working supply. This variale represents the sum of all `working_balances` of users who provided liquidity in the gauge.
Returns: working supply (`uint256`).
```vyper
working_supply: public(uint256)
@internal
def _update_liquidity_limit(addr: address, l: uint256, L: uint256):
"""
@notice Calculate limits which depend on the amount of CRV token per-user.
Effectively it calculates working balances to apply amplification
of CRV production by CRV
@param addr User address
@param l User's amount of liquidity (LP tokens)
@param L Total amount of liquidity (LP tokens)
"""
# To be called after totalSupply is updated
voting_balance: uint256 = VotingEscrowBoost(VEBOOST_PROXY).adjusted_balance_of(addr)
voting_total: uint256 = ERC20(VOTING_ESCROW).totalSupply()
lim: uint256 = l * TOKENLESS_PRODUCTION / 100
if voting_total > 0:
lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100
lim = min(l, lim)
old_bal: uint256 = self.working_balances[addr]
self.working_balances[addr] = lim
_working_supply: uint256 = self.working_supply + lim - old_bal
self.working_supply = _working_supply
log UpdateLiquidityLimit(addr, l, L, lim, _working_supply)
```
```shell
>>> LiquidityGaugeV6.working_supply()
12665099687428791483
```
::::
---
## Checkpoints
### `user_checkpoint`
::::description[`LiquidityGaugeV6.user_checkpoint(addr: address) -> bool`]
:::guard[Guarded Methods]
This function can only be called by the `addr` himself or the `Minter.vy` contract.
:::
Function to record a checkpoint for `addr`.
Returns: True (`bool`).
| Input | Type | Description |
| ------ | --------- | -------------------------------------- |
| `addr` | `address` | Address who's checkpoint is recoreded. |
```vyper
@external
def user_checkpoint(addr: address) -> bool:
"""
@notice Record a checkpoint for `addr`
@param addr User address
@return bool success
"""
assert msg.sender in [addr, MINTER] # dev: unauthorized
self._checkpoint(addr)
self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply)
return True
@internal
def _checkpoint(addr: address):
"""
@notice Checkpoint for a user
@dev Updates the CRV emissions a user is entitled to receive
@param addr User address
"""
_period: int128 = self.period
_period_time: uint256 = self.period_timestamp[_period]
_integrate_inv_supply: uint256 = self.integrate_inv_supply[_period]
inflation_params: uint256 = self.inflation_params
prev_future_epoch: uint256 = inflation_params >> 216
gauge_is_killed: bool = self.is_killed
rate: uint256 = inflation_params % 2 **216
new_rate: uint256 = rate
if gauge_is_killed:
rate = 0
new_rate = 0
if prev_future_epoch >= _period_time:
future_epoch_time_write: uint256 = CRV20(CRV).future_epoch_time_write()
if not gauge_is_killed:
new_rate = CRV20(CRV).rate()
self.inflation_params = (future_epoch_time_write << 216) + new_rate
# Update integral of 1/supply
if block.timestamp > _period_time:
_working_supply: uint256 = self.working_supply
Controller(GAUGE_CONTROLLER).checkpoint_gauge(self)
prev_week_time: uint256 = _period_time
week_time: uint256 = min((_period_time + WEEK) / WEEK * WEEK, block.timestamp)
for i in range(500):
dt: uint256 = week_time - prev_week_time
w: uint256 = Controller(GAUGE_CONTROLLER).gauge_relative_weight(self, prev_week_time)
if _working_supply > 0:
if prev_future_epoch >= prev_week_time and prev_future_epoch < week_time:
# If we went across one or multiple epochs, apply the rate
# of the first epoch until it ends, and then the rate of
# the last epoch.
# If more than one epoch is crossed - the gauge gets less,
# but that'd meen it wasn't called for more than 1 year
_integrate_inv_supply += rate * w * (prev_future_epoch - prev_week_time) / _working_supply
rate = new_rate
_integrate_inv_supply += rate * w * (week_time - prev_future_epoch) / _working_supply
else:
_integrate_inv_supply += rate * w * dt / _working_supply
# On precisions of the calculation
# rate ~= 10e18
# last_weight > 0.01 * 1e18 = 1e16 (if pool weight is 1%)
# _working_supply ~= TVL * 1e18 ~= 1e26 ($100M for example)
# The largest loss is at dt = 1
# Loss is 1e-9 - acceptable
if week_time == block.timestamp:
break
prev_week_time = week_time
week_time = min(week_time + WEEK, block.timestamp)
_period += 1
self.period = _period
self.period_timestamp[_period] = block.timestamp
self.integrate_inv_supply[_period] = _integrate_inv_supply
# Update user-specific integrals
_working_balance: uint256 = self.working_balances[addr]
self.integrate_fraction[addr] += _working_balance * (_integrate_inv_supply - self.integrate_inv_supply_of[addr]) / 10 ** 18
self.integrate_inv_supply_of[addr] = _integrate_inv_supply
self.integrate_checkpoint_of[addr] = block.timestamp
@internal
def _update_liquidity_limit(addr: address, l: uint256, L: uint256):
"""
@notice Calculate limits which depend on the amount of CRV token per-user.
Effectively it calculates working balances to apply amplification
of CRV production by CRV
@param addr User address
@param l User's amount of liquidity (LP tokens)
@param L Total amount of liquidity (LP tokens)
"""
# To be called after totalSupply is updated
voting_balance: uint256 = VotingEscrowBoost(VEBOOST_PROXY).adjusted_balance_of(addr)
voting_total: uint256 = ERC20(VOTING_ESCROW).totalSupply()
lim: uint256 = l * TOKENLESS_PRODUCTION / 100
if voting_total > 0:
lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100
lim = min(l, lim)
old_bal: uint256 = self.working_balances[addr]
self.working_balances[addr] = lim
_working_supply: uint256 = self.working_supply + lim - old_bal
self.working_supply = _working_supply
log UpdateLiquidityLimit(addr, l, L, lim, _working_supply)
```
```shell
>>> LiquidityGaugeV6.user_checkpoint('0x989AEb4d175e16225E39E87d0D97A3360524AD80')
True
```
::::
### `kick`
::::description[`LiquidityGaugeV6.kick(addr: address)`]
Function to trigger a checkpoint for `addr` and therefore updating their boost. A user can only be kicked if they either had another voting event or their voting escrow lock expired. This function ensures no abusive usage of a boost.
Emits: `UpdateLiquidityLimit`
| Input | Type | Description |
| ------ | --------- | ---------------------------------- |
| `addr` | `address` | Address to kick. |
```vyper
event UpdateLiquidityLimit:
user: indexed(address)
original_balance: uint256
original_supply: uint256
working_balance: uint256
working_supply: uint256
@external
def kick(addr: address):
"""
@notice Kick `addr` for abusing their boost
@dev Only if either they had another voting event, or their voting escrow lock expired
@param addr Address to kick
"""
t_last: uint256 = self.integrate_checkpoint_of[addr]
t_ve: uint256 = VotingEscrow(VOTING_ESCROW).user_point_history__ts(
addr, VotingEscrow(VOTING_ESCROW).user_point_epoch(addr)
)
_balance: uint256 = self.balanceOf[addr]
assert ERC20(VOTING_ESCROW).balanceOf(addr) == 0 or t_ve > t_last # dev: kick not allowed
assert self.working_balances[addr] > _balance * TOKENLESS_PRODUCTION / 100 # dev: kick not needed
self._checkpoint(addr)
self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply)
@internal
def _checkpoint(addr: address):
"""
@notice Checkpoint for a user
@dev Updates the CRV emissions a user is entitled to receive
@param addr User address
"""
_period: int128 = self.period
_period_time: uint256 = self.period_timestamp[_period]
_integrate_inv_supply: uint256 = self.integrate_inv_supply[_period]
inflation_params: uint256 = self.inflation_params
prev_future_epoch: uint256 = inflation_params >> 216
gauge_is_killed: bool = self.is_killed
rate: uint256 = inflation_params % 2 **216
new_rate: uint256 = rate
if gauge_is_killed:
rate = 0
new_rate = 0
if prev_future_epoch >= _period_time:
future_epoch_time_write: uint256 = CRV20(CRV).future_epoch_time_write()
if not gauge_is_killed:
new_rate = CRV20(CRV).rate()
self.inflation_params = (future_epoch_time_write << 216) + new_rate
# Update integral of 1/supply
if block.timestamp > _period_time:
_working_supply: uint256 = self.working_supply
Controller(GAUGE_CONTROLLER).checkpoint_gauge(self)
prev_week_time: uint256 = _period_time
week_time: uint256 = min((_period_time + WEEK) / WEEK * WEEK, block.timestamp)
for i in range(500):
dt: uint256 = week_time - prev_week_time
w: uint256 = Controller(GAUGE_CONTROLLER).gauge_relative_weight(self, prev_week_time)
if _working_supply > 0:
if prev_future_epoch >= prev_week_time and prev_future_epoch < week_time:
# If we went across one or multiple epochs, apply the rate
# of the first epoch until it ends, and then the rate of
# the last epoch.
# If more than one epoch is crossed - the gauge gets less,
# but that'd meen it wasn't called for more than 1 year
_integrate_inv_supply += rate * w * (prev_future_epoch - prev_week_time) / _working_supply
rate = new_rate
_integrate_inv_supply += rate * w * (week_time - prev_future_epoch) / _working_supply
else:
_integrate_inv_supply += rate * w * dt / _working_supply
# On precisions of the calculation
# rate ~= 10e18
# last_weight > 0.01 * 1e18 = 1e16 (if pool weight is 1%)
# _working_supply ~= TVL * 1e18 ~= 1e26 ($100M for example)
# The largest loss is at dt = 1
# Loss is 1e-9 - acceptable
if week_time == block.timestamp:
break
prev_week_time = week_time
week_time = min(week_time + WEEK, block.timestamp)
_period += 1
self.period = _period
self.period_timestamp[_period] = block.timestamp
self.integrate_inv_supply[_period] = _integrate_inv_supply
# Update user-specific integrals
_working_balance: uint256 = self.working_balances[addr]
self.integrate_fraction[addr] += _working_balance * (_integrate_inv_supply - self.integrate_inv_supply_of[addr]) / 10 ** 18
self.integrate_inv_supply_of[addr] = _integrate_inv_supply
self.integrate_checkpoint_of[addr] = block.timestamp
@internal
def _update_liquidity_limit(addr: address, l: uint256, L: uint256):
"""
@notice Calculate limits which depend on the amount of CRV token per-user.
Effectively it calculates working balances to apply amplification
of CRV production by CRV
@param addr User address
@param l User's amount of liquidity (LP tokens)
@param L Total amount of liquidity (LP tokens)
"""
# To be called after totalSupply is updated
voting_balance: uint256 = VotingEscrowBoost(VEBOOST_PROXY).adjusted_balance_of(addr)
voting_total: uint256 = ERC20(VOTING_ESCROW).totalSupply()
lim: uint256 = l * TOKENLESS_PRODUCTION / 100
if voting_total > 0:
lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100
lim = min(l, lim)
old_bal: uint256 = self.working_balances[addr]
self.working_balances[addr] = lim
_working_supply: uint256 = self.working_supply + lim - old_bal
self.working_supply = _working_supply
log UpdateLiquidityLimit(addr, l, L, lim, _working_supply)
```
```shell
>>> LiquidityGaugeV6.kick('0x989AEb4d175e16225E39E87d0D97A3360524AD80')
```
::::
---
## Killing Gauges
Liquidity gauges have a "killed status" stored in the `is_killed` variable. This status can be set by the `admin` of the Factory, which was used to initially deploy the gauge, using the `set_killed` function. If the status is set to `True`, the gauges' `rate` and `future_rate` will be set to zero, and it will not be eligible to receive any more CRV emissions.
"Killing a gauge" can be undone by simply setting the `is_killed` status back to `false` using the `set_killed` function again.
:::warning[Effect of Killing Gauges on Rewards]
"Killing a gauge" affects only CRV emissions; externally added rewards will still be distributed.
:::
### `is_killed`
::::description[`LiquidityGaugeV6.is_killed() -> bool: view`]
Getter function to check if the gauge is killed. If `true`, the inflation rate for the gauge will be set to zero.
Returns: killed status (`bool`).
```vyper
is_killed: public(bool)
@internal
def _checkpoint(addr: address):
"""
@notice Checkpoint for a user
@dev Updates the CRV emissions a user is entitled to receive
@param addr User address
"""
_period: int128 = self.period
_period_time: uint256 = self.period_timestamp[_period]
_integrate_inv_supply: uint256 = self.integrate_inv_supply[_period]
inflation_params: uint256 = self.inflation_params
prev_future_epoch: uint256 = inflation_params >> 216
gauge_is_killed: bool = self.is_killed
rate: uint256 = inflation_params % 2 **216
new_rate: uint256 = rate
if gauge_is_killed:
rate = 0
new_rate = 0
...
```
```shell
>>> LiquidityGaugeV6.is_killed()
'False'
```
::::
### `set_killed`
::::description[`LiquidityGaugeV6.set_killed(_is_killed: bool)`]
:::guard[Guarded Methods]
This function can only be called by the `owner` of the Factory.
:::
Function to kill a gauge.
Emits: `SetKilled` event.
| Input | Type | Description |
| ------------ | ------ | ---------------------------------- |
| `_is_killed` | `bool` | Status to set the killed status to. |
```vyper
is_killed: public(bool)
@external
def set_killed(_is_killed: bool):
"""
@notice Set the killed status for this contract
@dev When killed, the gauge always yields a rate of 0 and so cannot mint CRV
@param _is_killed Killed status to set
"""
assert msg.sender == Factory(self.factory).admin() # dev: only owner
self.is_killed = _is_killed
```
```shell
>>> LiquidityGaugeV6.set_killed(True)
```
::::
---
## Contract Info Methods
*Basic contract informations:*
### `integrate_fraction`
::::description[`LiquidityGaugeV6.integrate_fraction(arg0: address) -> uint256: view`]
Getter for the total amount of CRV, both mintable and already minted, that has been allocated to `arg0` from this gauge.
Returns: integral of accrued rewards (`uint256`).
| Input | Type | Description |
| ------ | --------- | ---------------------------------- |
| `arg0` | `address` | Address to check for. |
```vyper
integrate_fraction: public(HashMap[address, uint256])
```
```shell
>>> LiquidityGaugeV6.integrate_fraction('0x989AEb4d175e16225E39E87d0D97A3360524AD80')
1662908936954145
```
::::
### `period`
::::description[`LiquidityGaugeV6.period() -> int128: view`]
Getter for the period of the gauge. This variable is incremented by one each time a checkpoint was made.
Returns: current period (`int128`).
```vyper
period: public(int128)
```
```shell
>>> LiquidityGaugeV6.period()
6
```
::::
### `period_timestamp`
::::description[`LiquidityGaugeV6.period_timestamp(arg0: uint256) -> uint256: view`]
Getter for the timestamp of a period.
Returns: timestamp
| Input | Type | Description |
| ------ | --------- | ---------------------------------- |
| `arg0` | `uint256` | Period to get the timestamp for. |
```vyper
period_timestamp: public(uint256[100000000000000000000000000000])
```
```shell
>>> LiquidityGaugeV6.period_timestamp(7)
1713351359
```
::::
### `inflation_rate`
::::description[`LiquidityGaugeV6.inflation_rate() -> uint256: view`]
Getter for the current inflation rate per second of CRV. This getter retrieves the lower 216 bits of `inflation_params`, which stores the inflation rate and the future epoch time.
Returns: CRV inflation rate (`uint256`).
```vyper
inflation_params: uint256
@view
@external
def inflation_rate() -> uint256:
"""
@notice Get the locally stored CRV inflation rate
"""
return self.inflation_params % 2 **216
```
```shell
>>> LiquidityGaugeV6.inflation_rate()
5181574864521283150 # 5.18157486452 CRV per second
```
::::
### `future_epoch_time`
::::description[`LiquidityGaugeV6.future_epoch_time() -> uint256: view`]
Getter for the future epoch time. This getter retrieves the upper 216 bits of `inflation_params`, which stores the inflation rate and the future epoch time.
Returns: future epoch time (`uint256`).
```vyper
inflation_params: uint256
@view
@external
def future_epoch_time() -> uint256:
"""
@notice Get the locally stored CRV future epoch start time
"""
return self.inflation_params >> 216
```
```shell
>>> LiquidityGaugeV6.future_epoch_time()
1723501048
```
::::
### `factory`
::::description[`LiquidityGaugeV6.factory() -> address: view`]
Getter for the factory which deployed the gauge.
Returns: factory (`address`).
```vyper
factory: public(address)
@external
def __init__(_lp_token: address):
"""
@notice Contract constructor
@param _lp_token Liquidity Pool contract address
"""
self.lp_token = _lp_token
self.factory = msg.sender
self.manager = tx.origin
...
```
```shell
>>> LiquidityGaugeV6.factory()
'0x98EE851a00abeE0d95D08cF4CA2BdCE32aeaAF7F'
```
::::
### `lp_token`
::::description[`LiquidityGaugeV6.lp_token() -> address: view`]
Getter for the LP token which is deposited into of withdrawn from the gauge.
Returns: LP token (`address`).
```vyper
lp_token: public(address)
@external
def __init__(_lp_token: address):
"""
@notice Contract constructor
@param _lp_token Liquidity Pool contract address
"""
self.lp_token = _lp_token
self.factory = msg.sender
self.manager = tx.origin
...
```
```shell
>>> LiquidityGaugeV6.lp_token()
'0x86EA1191a219989d2dA3a85c949a12A92f8ED3Db'
```
::::
---
## Liquidity Gauges
CRV inflation is directed to users who provide liquidity within the protocol, measured by "Liquidity Gauge" contracts. Each pool has its own liquidity gauge, maintained by the Gauge Controller, which lists gauges and their types along with the weights of each. These gauges not only measure the liquidity provided by users, distributing rewards based on each user's share of liquidity and boost, but can also be implemented for a variety of use cases including liquidity pools, lending vaults, and even [fundraising gauges](https://github.com/vefunder/crvfunder). For more details on implementation, see [here](../overview.md#liquidity-gauges).
:::github[GitHub]
There are several versions of liquidity gauge contracts in use. Source code for all the liquidity gauges can be found on [ GitHub](https://github.com/curvefi/curve-dao-contracts/tree/master/contracts/gauges).
Easiest way to obtain the gauge address of a liquidity pool is by querying [`get_gauge`](../../integration/meta-registry.md#get_gauge) on the [MetaRegistry](../../integration/meta-registry.md).
:::
---
## Rewards
Liquidity gauges can have two types of rewards:
### CRV Emissions
Curve operates such that veCRV holders can decide where future CRV emissions are directed to. Typically, these emissions are allocated to a liquidity gauge. However, before gauges are eligible to receive CRV emissions, they must be added to the `GaugeController.vy` contract. This addition requires a successfully passed DAO vote. Once added, the gauge becomes eligible for gauge weight voting. When a gauge receives gauge weight through user votes, it starts to receive CRV emissions. Changes in weight take effect every Thursday at 00:00 UTC.
Gauges contain logic that enables users to boost their provided liquidity up to 2.5x by locking CRV for veCRV.
### Permissionless Rewards
Besides CRV emissions, there is also the possibility to add "external (also called permissionless) rewards" to the gauge. More on this [here](../gauges/liquidity-gauge-v6.md#permissionless-rewards).
Unlike native CRV rewards, these kinds of rewards cannot be boosted.
---
## Versions
Over time, several improvements and enhancements were made to the liquidity gauges. This documentation will mainly cover the most recent one, `LiquidityGaugeV6`. Source code for all other gauges can be found on [ GitHub](https://github.com/curvefi/curve-dao-contracts/tree/master/contracts/gauges).
---
## How to Deploy a Gauge
Liquidity gauges for liquidity pools can be deployed using the Factory contract. The Factory contract, which initially deployed the liquidity pool, utilizes the `deploy_gauge` function for this purpose.
### `deploy_gauge`
::::description[`Factory.deploy_gauge(_pool: address) -> address`]
Function to deploy a liquidity gauge for `_pool`.
Returns: deployed gauge (`address`).
```vyper
event LiquidityGaugeDeployed:
pool: address
gauge: address
@external
def deploy_gauge(_pool: address) -> address:
"""
@notice Deploy a liquidity gauge for a factory pool
@param _pool Factory pool address to deploy a gauge for
@return Address of the deployed gauge
"""
assert self.pool_data[_pool].coins[0] != empty(address), "Unknown pool"
assert self.pool_data[_pool].liquidity_gauge == empty(address), "Gauge already deployed"
assert self.gauge_implementation != empty(address), "Gauge implementation not set"
gauge: address = create_from_blueprint(self.gauge_implementation, _pool, code_offset=3)
self.pool_data[_pool].liquidity_gauge = gauge
log LiquidityGaugeDeployed(_pool, gauge)
return gauge
```
```shell
>>> Factory.deploy_gauge("0x5f0985a8aad85e82fd592a23cc0501e4345fb18c")
'0x2a1a064348b1ad9ca8b983016606ea84eca8c620'
```
::::
---
## Adding a Gauge to the `GaugeController`
To make a liquidity gauge eligible to receive CRV emissions, it needs to be added to the `GaugeController.vy` contract. This is accomplished through an on-chain vote in which veCRV holders vote on.
The on-chain vote can be created using the classic Curve UI: https://classic.curve.fi/factory/create_gauge.
---
## Minter
The `Minter` is responsible for the **issuance and distribution of CRV tokens** to liquidity providers. It acts as a mechanism to reward users who provide liquidity to Curve's pools. The contract essentially calculates the amount of CRV tokens to be allocated based on various factors such as the duration and amount of liquidity provided.
:::vyper[`Minter.vy`]
The source code for the `Minter.vy` contract is available on [GitHub](https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/Minter.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.2.4`.
The contract is deployed on :logos-ethereum: Ethereum at [`0xd061D61a4d941c39E5453435B6345Dc261C2fcE0`](https://etherscan.io/address/0xd061D61a4d941c39E5453435B6345Dc261C2fcE0#code).
```json
[{"name":"Minted","inputs":[{"type":"address","name":"recipient","indexed":true},{"type":"address","name":"gauge","indexed":false},{"type":"uint256","name":"minted","indexed":false}],"anonymous":false,"type":"event"},{"outputs":[],"inputs":[{"type":"address","name":"_token"},{"type":"address","name":"_controller"}],"stateMutability":"nonpayable","type":"constructor"},{"name":"mint","outputs":[],"inputs":[{"type":"address","name":"gauge_addr"}],"stateMutability":"nonpayable","type":"function","gas":100038},{"name":"mint_many","outputs":[],"inputs":[{"type":"address[8]","name":"gauge_addrs"}],"stateMutability":"nonpayable","type":"function","gas":408502},{"name":"mint_for","outputs":[],"inputs":[{"type":"address","name":"gauge_addr"},{"type":"address","name":"_for"}],"stateMutability":"nonpayable","type":"function","gas":101219},{"name":"toggle_approve_mint","outputs":[],"inputs":[{"type":"address","name":"minting_user"}],"stateMutability":"nonpayable","type":"function","gas":36726},{"name":"token","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":1301},{"name":"controller","outputs":[{"type":"address","name":""}],"inputs":[],"stateMutability":"view","type":"function","gas":1331},{"name":"minted","outputs":[{"type":"uint256","name":""}],"inputs":[{"type":"address","name":"arg0"},{"type":"address","name":"arg1"}],"stateMutability":"view","type":"function","gas":1669},{"name":"allowed_to_mint_for","outputs":[{"type":"bool","name":""}],"inputs":[{"type":"address","name":"arg0"},{"type":"address","name":"arg1"}],"stateMutability":"view","type":"function","gas":1699}]
```
:::
---
## Minting CRV
CRV tokens can be minted in several ways:
- [`mint`](#mint): simple function which mints the eligible CRV tokens to `msg.sender` from a single gauge
- [`mint_many`](#mint_many): function to mint the eligible CRV for `msg.sender` for multiple gauges at once
- [`mint_for`](#mint_for): function to mint CRV for someone else and send it to them. Approval needs to be granted via [`toggle_approve_mint`](#toggle_approve_mint)
### `mint`
::::description[`Minter.mint(gauge_addr: address)`]
Function to mint CRV for the caller from a single gauge.
| Input | Type | Description |
| ------------ | --------- | --------------- |
| `gauge_addr` | `address` | Gauge address to get mintable CRV amount from |
Emits: `Minted` event.
```vyper
interface LiquidityGauge:
# Presumably, other gauges will provide the same interfaces
def integrate_fraction(addr: address) -> uint256: view
def user_checkpoint(addr: address) -> bool: nonpayable
interface GaugeController:
def gauge_types(addr: address) -> int128: view
interface MERC20:
def mint(_to: address, _value: uint256) -> bool: nonpayable
event Minted:
recipient: indexed(address)
gauge: address
minted: uint256
# user -> gauge -> value
minted: public(HashMap[address, HashMap[address, uint256]])
@external
@nonreentrant('lock')
def mint(gauge_addr: address):
"""
@notice Mint everything which belongs to `msg.sender` and send to them
@param gauge_addr `LiquidityGauge` address to get mintable amount from
"""
self._mint_for(gauge_addr, msg.sender)
@internal
def _mint_for(gauge_addr: address, _for: address):
assert GaugeController(self.controller).gauge_types(gauge_addr) >= 0 # dev: gauge is not added
LiquidityGauge(gauge_addr).user_checkpoint(_for)
total_mint: uint256 = LiquidityGauge(gauge_addr).integrate_fraction(_for)
to_mint: uint256 = total_mint - self.minted[_for][gauge_addr]
if to_mint != 0:
MERC20(self.token).mint(_for, to_mint)
self.minted[_for][gauge_addr] = total_mint
log Minted(_for, gauge_addr, total_mint)
```
This example mints all CRV for the caller from `0xe5d5aa1bbe72f68df42432813485ca1fc998de32` (LDO/ETH gauge).
```shell
>>> Minter.mint('0xe5d5aa1bbe72f68df42432813485ca1fc998de32')
```
::::
### `mint_for`
::::description[`Minter.mint_for(gauge_addr: address, _for: address)`]
Function to mint CRV for a different address and transfer it to them. In order to do this, the caller must have been previously approved by `for` using [`toggle_approve_mint`](#toggle_approve_mint).
| Input | Type | Description |
| ------------ | --------- | --------------- |
| `gauge_addr` | `address` | Gauge address to get mintable CRV amount from |
| `_for` | `address` | Address to mint to |
Emits: `Minted` event.
```vyper
interface LiquidityGauge:
# Presumably, other gauges will provide the same interfaces
def integrate_fraction(addr: address) -> uint256: view
def user_checkpoint(addr: address) -> bool: nonpayable
interface GaugeController:
def gauge_types(addr: address) -> int128: view
interface MERC20:
def mint(_to: address, _value: uint256) -> bool: nonpayable
event Minted:
recipient: indexed(address)
gauge: address
minted: uint256
# user -> gauge -> value
minted: public(HashMap[address, HashMap[address, uint256]])
# minter -> user -> can mint?
allowed_to_mint_for: public(HashMap[address, HashMap[address, bool]])
@external
@nonreentrant('lock')
def mint_for(gauge_addr: address, _for: address):
"""
@notice Mint tokens for `_for`
@dev Only possible when `msg.sender` has been approved via `toggle_approve_mint`
@param gauge_addr `LiquidityGauge` address to get mintable amount from
@param _for Address to mint to
"""
if self.allowed_to_mint_for[msg.sender][_for]:
self._mint_for(gauge_addr, _for)
@internal
def _mint_for(gauge_addr: address, _for: address):
assert GaugeController(self.controller).gauge_types(gauge_addr) >= 0 # dev: gauge is not added
LiquidityGauge(gauge_addr).user_checkpoint(_for)
total_mint: uint256 = LiquidityGauge(gauge_addr).integrate_fraction(_for)
to_mint: uint256 = total_mint - self.minted[_for][gauge_addr]
if to_mint != 0:
MERC20(self.token).mint(_for, to_mint)
self.minted[_for][gauge_addr] = total_mint
log Minted(_for, gauge_addr, total_mint)
```
This example mints all CRV for `0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045` from the gauge with the address `0xe5d5aa1bbe72f68df42432813485ca1fc998de32`.
```shell
>>> Minter.mint_for('0xe5d5aa1bbe72f68df42432813485ca1fc998de32', '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')
```
::::
### `mint_many`
::::description[`Minter.mint_many(gauge_addrs: address[8])`]
Function to mint CRV for the caller from multiple gauges. This function does not allow for minting for or to different addresses. It claims for `msg.sender` and transfers the minted tokens to them. The maximum number of gauges that can be specified is eight. For example, if only minting from one gauge, leave the remaining array entries as `ZERO_ADDRESS`.
| Input | Type | Description |
| ------------- | ------------ | ----------- |
| `gauge_addrs` | `address[8]` | List of gauge addresses to mint from |
Emits: `Minted` event.
```vyper
interface LiquidityGauge:
# Presumably, other gauges will provide the same interfaces
def integrate_fraction(addr: address) -> uint256: view
def user_checkpoint(addr: address) -> bool: nonpayable
interface GaugeController:
def gauge_types(addr: address) -> int128: view
interface MERC20:
def mint(_to: address, _value: uint256) -> bool: nonpayable
event Minted:
recipient: indexed(address)
gauge: address
minted: uint256
# user -> gauge -> value
minted: public(HashMap[address, HashMap[address, uint256]])
# minter -> user -> can mint?
allowed_to_mint_for: public(HashMap[address, HashMap[address, bool]])
@external
@nonreentrant('lock')
def mint_many(gauge_addrs: address[8]):
"""
@notice Mint everything which belongs to `msg.sender` across multiple gauges
@param gauge_addrs List of `LiquidityGauge` addresses
"""
for i in range(8):
if gauge_addrs[i] == ZERO_ADDRESS:
break
self._mint_for(gauge_addrs[i], msg.sender)
@internal
def _mint_for(gauge_addr: address, _for: address):
assert GaugeController(self.controller).gauge_types(gauge_addr) >= 0 # dev: gauge is not added
LiquidityGauge(gauge_addr).user_checkpoint(_for)
total_mint: uint256 = LiquidityGauge(gauge_addr).integrate_fraction(_for)
to_mint: uint256 = total_mint - self.minted[_for][gauge_addr]
if to_mint != 0:
MERC20(self.token).mint(_for, to_mint)
self.minted[_for][gauge_addr] = total_mint
log Minted(_for, gauge_addr, total_mint)
```
This example mints all CRV for the caller from three gauges at once.
```shell
>>> Minter.mint_many(['0xe5d5aa1bbe72f68df42432813485ca1fc998de32', '0xbfcf63294ad7105dea65aa58f8ae5be2d9d0952a', '0xb9bdcdcd7c3c1a3255402d44639cb6c7281833cf', '0x0000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000'])
```
::::
### `minted`
::::description[`Minter.minted(arg0: address, arg1: address) -> uint256: view`]
Getter for the total amount of CRV minted from a specific gauge to a specific user.
| Input | Type | Description |
| ------ | --------- | ------------- |
| `arg0` | `address` | User address |
| `arg1` | `address` | Gauge address |
Returns: amount of CRV minted (`uint256`).
```vyper
# user -> gauge -> value
minted: public(HashMap[address, HashMap[address, uint256]])
```
::::
### `allowed_to_mint_for`
::::description[`Minter.allowed_to_mint_for(arg0: address, arg1: address) -> bool: view`]
Function to check if a specific user can mint for another user. Allowance is toggled using the [`toggle_approve_mint`](#toggle_approve_mint) function.
| Input | Type | Description |
| ------ | --------- | ----------------- |
| `arg0` | `address` | Address of minter |
| `arg1` | `address` | Address of user |
Returns: true or false (`bool`).
```vyper
# minter -> user -> can mint?
allowed_to_mint_for: public(HashMap[address, HashMap[address, bool]])
```
::::
### `toggle_approve_mint`
::::description[`Minter.toggle_approve_mint(minting_user: address)`]
Function to toggle approval for a user to mint CRV on behalf of the caller.
| Input | Type | Description |
| -------------- | --------- | --------------- |
| `minting_user` | `address` | Address to toggle permission for |
```vyper
# minter -> user -> can mint?
allowed_to_mint_for: public(HashMap[address, HashMap[address, bool]])
@external
def toggle_approve_mint(minting_user: address):
"""
@notice allow `minting_user` to mint for `msg.sender`
@param minting_user Address to toggle permission for
"""
self.allowed_to_mint_for[minting_user][msg.sender] = not self.allowed_to_mint_for[minting_user][msg.sender]
```
This example toggles approval for `0x989AEb4d175e16225E39E87d0D97A3360524AD80` to mint for `0xF147b8125d2ef93FB6965Db97D6746952a133934`.
```shell
>>> Minter.allowed_to_mint_for('0x989AEb4d175e16225E39E87d0D97A3360524AD80', '0xF147b8125d2ef93FB6965Db97D6746952a133934')
False
>>> Minter.toggle_approve_mint('0x989AEb4d175e16225E39E87d0D97A3360524AD80')
>>> Minter.allowed_to_mint_for('0x989AEb4d175e16225E39E87d0D97A3360524AD80', '0xF147b8125d2ef93FB6965Db97D6746952a133934')
True
```
::::
---
## Other Methods
### `token`
::::description[`Minter.token() -> address: view`]
Getter for the token address of the Curve DAO Token (CRV). This variable is set at initialization and cannot be changed after.
Returns: CRV token contract (`address`).
```vyper
token: public(address)
@external
def __init__(_token: address, _controller: address):
self.token = _token
self.controller = _controller
```
::::
### `controller`
::::description[`Minter.controller() -> address: view`]
Getter for the `GaugeController`.
Returns: `GaugeController` contract (`address`).
```vyper
controller: public(address)
@external
def __init__(_token: address, _controller: address):
self.token = _token
self.controller = _controller
```
::::
---
## Curve DAO: Liquidity Gauges and Minting CRV
Curve is built in a way to incentivise liquidity providers with CRV, the protocols governance token. The protocol works in a way that it directs the inflation of the CRV token to the liquidity providers based on the votes of the veCRV holders. This is done through a system of gauges, the `GaugeController` contract, and the `Minter` contract.
Users who have veCRV, Curve's voting-escrowed token, can vote on DAO-approved gauges to receive CRV emissions.
---
## Smart Contracts
Allocation, distribution and minting of CRV are managed via several related DAO contracts:
Central controller that maintains a list of gauges, weights and type weights, and coordinates the rate of CRV production for each liquidity gauge.
CRV minting contract, generates new CRV according to liquidity gauges.
Measures liquidity provided by users over time, in order to distribute CRV and other rewards.
Liquidity gauges on EVM sidechains use a system of Root and Child Liquidity Gauges which allows gauges on sidechains to receive CRV emissions.
---
## Implementation Details
## CRV Inflation
CRV follows a piecewise linear inflation schedule. The inflation is reduced by around 15.9% each year. Each time the inflation reduces, a new mining epoch starts.
The initial supply of CRV is 1.273 billion tokens, which is 42% of the eventual t -> $\infty$ supply of $\approx$ 3.03 billion tokens. All of these initial tokens are gradually vested (with every block). The initial inflation rate which supports the above inflation schedule is $r = 22.0$% (279.6 millions per year).
All of the inflation is distributed to Curve liquidity providers, according to measurements taken by the gauges. During the first year, the approximate inflow into circulating supply is 2 million CRV per day. The initial circulating supply is 0.
---
## Liquidity Gauges
Inflation is directed to users who provide liquidity within the protocol. This usage is measured via “Liquidity Gauge” contracts. Each pool has an individual liquidity gauge. The Gauge Controller maintains a list of gauges and their types, with the weights of each gauge and type.
To measure liquidity over time, the user deposits their LP tokens into the liquidity gauge. Coin rates which the gauge is getting depends on current inflation rate, gauge weight, and gauge type weights. Each user receives a share of newly minted CRV proportional to the amount of LP tokens locked. Additionally, rewards may be boosted by up to factor of 2.5 if the user vote-locks tokens for Curve governance in the Voting Escrow contract.
Suppose we have the inflation rate $r$ changing with every epoch (1 year), gauge weight $w_{g}$ and gauge type weight $w_{t}$. Then, all the gauge handles the stream of inflation with the rate $r' = w_{g}w_{t}r$ which it can update every time $w_{g}$, $w_{t}$ or mining epoch changes.
To calculate a user’s share of $r'$, we must calculate the integral:
$I_{u} = \int \frac{r'(t)b_{u}(t)}{S(t)}dt,$ where $b_{u}(t)$ is the balance supplied by the user (measured in LP tokens) and $S(t)$ is total liquidity supplied by users, depending on the time $t$; the value $I_{u}$ gives the amount of tokens which the user has to have minted to them. The user’s balance $b_{u}$ changes every time the user $u$ makes a deposit or withdrawal, and $S$ changes every time _any_ user makes a deposit or withdrawal so $S$ can change many times in between two events for the user $u''$. In the liquidity gauge contract, the vaule of $I_{u}$ is recorded per-user in the public `integrate_fraction` mapping.
To avoid requiring that all users to checkpoint periodically, we keep recording values of the following integral (named `integrate_inv_supply` in the contract):
$$I_{is}(t) = \int_0^t \frac{r'(t)}{S(t)}dt$$
The value of $I_{is}$ is recorded at any point any user deposits or withdraws, as well as every time the rate $r$ changes (either due to weight change or change of mining epoch).
When a user deposits or withdraws, the change in $I_{u}$ can be calculated as the current (before user’s action) value of $I_{is}$ multiplied by the pre-action user’s balance, and sumed up across the user’s balances: $I_{u}(t_{k}) = \sum_{k} b_{u}(t_{k})[I_{is}(t_{k})-I_{is}(t_{k-1})]$. The per-user integral is possible to repalce with this sum because $b_{u}(t)$ changed for all times between $t_{k-1}$ and $t_{k}$.
---
## Boosting
In order to incentivize users to participate in governance, and additionally create stickiness for liquidity, we implement the following mechanism. A user’s balance, counted in the liquidity gauge, gets boosted by users locking CRV tokens in Voting Escrow contract, depending on their vote weight $w_{i}:b_{u}^* = \min(0.4b_{u}+0.6S\frac{w_{i}}{W}, b_{u})$.
The value of $w_{i}$ is taken at the time the user performs any action (deposit, withdrawal, withdrawal of minted CRV tokens) and is applied until the next action this user performs.
If no users vote-lock any CRV (or simply don’t have any), the inflation will simply be distributed proportionally to the liquidity $b_{u}$ each one of them provided. However, if a user stakes enough CRV, they are able to boost their stream of CRV by up to factor of 2.5 (reducing it slightly for all users who are not doing that).
Implementation details are such that a user gets the boost at the time of the last action or checkpoint. Since the voting power decreases with time, it is favorable for users to apply a boost and do no further actions until they vote-lock more tokens. However, once the vote-lock expires, everyone can “kick” the user by creating a checkpoint for that user and, essentially, resetting the user to no boost if they have no voting power at that point already.
Finally, the gauge is supposed to not miss a full year of inflation (e.g. if there were no interactions with the guage for the full year). If that ever happens, the abandoned gauge gets less CRV.
---
## Gauge Weight Voting
Users can allocate their veCRV towards one or more liquidity gauges. Gauges receive a fraction of newly minted CRV tokens proportional to how much veCRV the gauge is allocated. Each user with a veCRV balance can change their preference at any time.
When a user applies a new weight vote, it gets applied at the start of the next epoch week. The weight vote for any one gauge cannot be changed more often than once in 10 days. Adding more CRV to your lock or extending the locktime increases your veCRV balance. This increase is not automatically accounted for in your current gauge weight votes. If you want to allocate all of your newly acquired voting power, make sure to re-vote.
:::warning
Resetting your gauge weight before re-voting means you'll need to wait 10 days to vote for the gauges whose weight you've reset. So, please ensure you simply re-vote; there is no need to reset your gauge weight votes before voting again.
:::
---
## GaugeController
The Gauge Controller maintains a list of gauges and their types, with the weights of each gauge and type. In order to implement weight voting, `GaugeController` has to include parameters handling linear character of voting power each user has.
`GaugeController` records points (bias + slope) per gauge in `vote_points`, and *scheduled* changes in biases and slopes for those points in `vote_bias_changes` and `vote_slope_changes`. New changes are applied at the start of each epoch week.
Per-user, per-gauge slopes are stored in `vote_user_slopes`, along with the power the user has used and the time their vote-lock ends.
The totals for slopes and biases for vote weight per gauge, and sums of those per type, are scheduled / recorded for the next week, as well as the points when voting power gets to 0 at lock expiration for some of users.
When a user changes their gauge weight vote, the change is scheduled for the next epoch week, not immediately. This reduces the number of reads from storage which must to be performed by each user: it is proportional to the number of weeks since the last change rather than the number of interactions from other users.
---
## Bridger Wrappers
Bridger wrappers are contracts used to transmit `ERC-20` tokens and especially `CRV` emissions across chains. Due to the increasing number of networks to which Curve deploys, bridge wrappers adhere to a specific interface documented below and allow for a modular bridging system.
:::vyper[`Bridgers.vy`]
The source code for the various `Bridger Wrappers` contracts can be found on [ GitHub](https://github.com/curvefi/curve-xchain-factory/tree/master/contracts/bridgers). The code varies slightly to adapt to different chain-specific implementations.
:::
---
:::warning[Chain-Specific Implementations]
The following function examples are for the :logos-arbitrum: Arbitrum bridger. Due to the varying implementations across different chains, the source code might vary slightly between different bridger implementations.
:::
The following three functions are required for bridge wrappers contracts to be implemented to ensure compatibility with the `RootGaugeFactory` and `RootGauge` contracts.
- [`cost()`](#cost) estimates the cost of bridging.
- [`bridge()`](#bridge) bridges CRV to the child chain.
- [`check()`](#check) verifies if the bridger has been approved by the `RootGauge`.
---
## Must-Implement Methods
The following three functions are required to be implemented to ensure compatibility with the `RootGaugeFactory` and `RootGauge` contracts:
### `cost`
::::description[`Bridger.cost() -> uint256: view`]
Function to estimate the cost of bridging.
Returns: the cost of bridging in ETH (`uint256`).
This source code might vary slightly between different bridger implementations. This example is specific to the `Bridger` contract for Arbitrum.
```vyper
# [gas_limit uint64][gas_price uint64][max_submission_cost uint64]
submission_data: uint256
is_approved: public(HashMap[address, bool])
@view
@external
def cost() -> uint256:
"""
@notice Cost in ETH to bridge
"""
data: uint256 = self.submission_data
# gas_limit * gas_price + max_submission_cost
return shift(data, -128) * (shift(data, -64) % 2 **64) + data % 2 **64
```
This example returns the cost of bridging denominated in `ETH` with a precision of 18 decimals.
```shell
>>> Bridger.cost()
2000000000000000 # 0.002 ETH
```
::::
### `bridge`
::::description[`Bridger.bridge(_token: address, _to: address, _amount: uint256)`]
Function to bridge any ERC20 token to the child chain.
| Input | Type | Description |
| --------- | ---- | ----------- |
| `_token` | `address` | The address of the token to bridge |
| `_to` | `address` | The address to bridge the token to |
| `_amount` | `uint256` | The amount of `_token` to deposit |
This source code might vary slightly between different bridger implementations. This example is specific to the `Bridger` contract for Arbitrum.
```vyper
interface GatewayRouter:
def getGateway(_token: address) -> address: view
def outboundTransfer( # emits DepositInitiated event with Inbox sequence #
_token: address,
_to: address,
_amount: uint256,
_max_gas: uint256,
_gas_price_bid: uint256,
_data: Bytes[128], # _max_submission_cost, _extra_data
): payable
CRV20: constant(address) = 0xD533a949740bb3306d119CC777fa900bA034cd52
GATEWAY: constant(address) = 0xa3A7B6F88361F48403514059F1F16C8E78d60EeC
GATEWAY_ROUTER: constant(address) = 0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef
INBOX: constant(address) = 0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f
# [gas_limit uint64][gas_price uint64][max_submission_cost uint64]
submission_data: uint256
is_approved: public(HashMap[address, bool])
@payable
@external
def bridge(_token: address, _to: address, _amount: uint256):
"""
@notice Bridge an ERC20 token using the Arbitrum standard bridge
@param _token The address of the token to bridge
@param _to The address to deposit token to on L2
@param _amount The amount of `_token` to deposit
"""
assert ERC20(_token).transferFrom(msg.sender, self, _amount)
if _token != CRV20 and not self.is_approved[_token]:
assert ERC20(_token).approve(GatewayRouter(GATEWAY_ROUTER).getGateway(_token), MAX_UINT256)
self.is_approved[_token] = True
data: uint256 = self.submission_data
gas_limit: uint256 = shift(data, -128)
gas_price: uint256 = shift(data, -64) % 2 **64
max_submission_cost: uint256 = data % 2 **64
# NOTE: Excess ETH fee is refunded to this bridger's address on L2.
# After bridging, the token should arrive on Arbitrum within 10 minutes. If it
# does not, the L2 transaction may have failed due to an insufficient amount
# within `max_submission_cost + (gas_limit * gas_price)`
# In this case, the transaction can be manually broadcasted on Arbitrum by calling
# `ArbRetryableTicket(0x000000000000000000000000000000000000006e).redeem(redemption-TxID)`
# The calldata for this manual transaction is easily obtained by finding the reverted
# transaction in the tx history for 0x000000000000000000000000000000000000006e on Arbiscan.
# https://developer.offchainlabs.com/docs/l1_l2_messages#retryable-transaction-lifecycle
GatewayRouter(GATEWAY_ROUTER).outboundTransfer(
_token,
_to,
_amount,
gas_limit,
gas_price,
_abi_encode(max_submission_cost, b""),
value=gas_limit * gas_price + max_submission_cost
)
```
This example bridges 10,000 `CRV` to the address `0x1234567890123456789012345678901234567890` on Arbitrum.
```shell
>>> Bridger.bridge('0xD533a949740bb3306d119CC777fa900bA034cd52', '0x1234567890123456789012345678901234567890', 10000000000000000000000)
```
::::
### `check`
::::description[`Bridger.check(_account: address) -> bool: view`]
Function to check if the bridger contract has been approved by the `RootGauge`.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_account` | `address` | The address of the bridger contract to check |
Returns: `True` if the bridger has been approved by the `RootGauge`, `False` otherwise (`bool`).
This source code might vary slightly between different bridger implementations. This example is specific to the `Bridger` contract for Arbitrum.
```vyper
@pure
@external
def check(_account: address) -> bool:
"""
@notice Verify if `_account` is allowed to bridge using `transmit_emissions`
@param _account The account calling `transmit_emissions`
"""
return True
```
```shell
>>> Bridger.check('0x1234567890123456789012345678901234567890')
True
```
::::
---
## ChildGaugeFactory
The `ChildGaugeFactory` contract is used to deploy liquidity gauges on the child chains. It serves as some sort of registry for the child gauges by storing information such as the gauge data, minted amounts, and more. It is also the contract where CRV emissions are claimed from.
:::vyper[`ChildGaugeFactory.vy`]
The source code for the `ChildGaugeFactory.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-xchain-factory/blob/master/contracts/ChildGaugeFactory.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`
A full list of all deployed `ChildGaugeFactory` contracts can be found [here](../../deployments.md).
:::
---
## Deploy Child Gauge
Child gauges can either be deployed from the `RootChainFactory` or directly from the according `ChildGaugeFactory`.
### `deploy_gauge`
::::description[`ChildGaugeFactory.deploy_gauge(_lp_token: address, _salt: bytes32, _manager: address = msg.sender) -> address`]
Function to deploy a new gauge.
| Input | Type | Description |
| -------- | --------- | ----------- |
| `_lp_token` | `address` | LP token to deploy gauge for |
| `_salt` | `bytes32` | Salt to deterministically deploy gauge |
| `_manager` | `address` | Address to set as manager of the gauge; defaults to `msg.sender` |
Returns: newly deployed gauge (`address`).
Emits: `DeployedGauge` event.
```vyper
interface ChildGauge:
def initialize(_lp_token: address, _root: address, _manager: address): nonpayable
event DeployedGauge:
_implementation: indexed(address)
_lp_token: indexed(address)
_deployer: indexed(address)
_salt: bytes32
_gauge: address
owner: public(address)
future_owner: public(address)
manager: public(address)
root_factory: public(address)
root_implementation: public(address)
call_proxy: public(address)
# [last_request][has_counterpart][is_valid_gauge]
gauge_data: public(HashMap[address, uint256])
# user -> gauge -> value
minted: public(HashMap[address, HashMap[address, uint256]])
get_gauge_from_lp_token: public(HashMap[address, address])
get_gauge_count: public(uint256)
get_gauge: public(address[max_value(int128)])
@external
def deploy_gauge(_lp_token: address, _salt: bytes32, _manager: address = msg.sender) -> address:
"""
@notice Deploy a liquidity gauge
@param _lp_token The token to deposit in the gauge
@param _salt A value to deterministically deploy a gauge
@param _manager The address to set as manager of the gauge
"""
if self.get_gauge_from_lp_token[_lp_token] != empty(address):
# overwriting lp_token -> gauge mapping requires
assert msg.sender == self.owner # dev: only owner
gauge_data: uint256 = 1 # set is_valid_gauge = True
implementation: address = self.get_implementation
salt: bytes32 = keccak256(_abi_encode(chain.id, _salt))
gauge: address = create_minimal_proxy_to(
implementation, salt=salt
)
if msg.sender == self.call_proxy:
gauge_data += 2 # set mirrored = True
log UpdateMirrored(gauge, True)
# issue a call to the root chain to deploy a root gauge
CallProxy(self.call_proxy).anyCall(
self,
_abi_encode(chain.id, _salt, method_id=method_id("deploy_gauge(uint256,bytes32)")),
empty(address),
1
)
self.gauge_data[gauge] = gauge_data
idx: uint256 = self.get_gauge_count
self.get_gauge[idx] = gauge
self.get_gauge_count = idx + 1
self.get_gauge_from_lp_token[_lp_token] = gauge
# derive root gauge address
gauge_codehash: bytes32 = keccak256(
concat(
0x602d3d8160093d39f3363d3d373d3d3d363d73,
convert(self.root_implementation, bytes20),
0x5af43d82803e903d91602b57fd5bf3,
)
)
digest: bytes32 = keccak256(concat(0xFF, convert(self.root_factory, bytes20), salt, gauge_codehash))
root: address = convert(convert(digest, uint256) & convert(max_value(uint160), uint256), address)
# If root is uninitialized, self.owner can always set the root gauge manually
# on the gauge contract itself via set_root_gauge method
ChildGauge(gauge).initialize(_lp_token, root, _manager)
log DeployedGauge(implementation, _lp_token, msg.sender, _salt, gauge)
return gauge
```
```shell
>>> ChildGaugeFactory.deploy_gauge('0x...')
'0x...'
```
::::
---
## Minting Emissions
CRV emissions are minted directly from the child gauge and can be claimed by the user. They cannot be claimed from the `ChildGauge` contract itself.
When claiming emissions via `claim` or `claim_many`, and `is_mirrored` is set to `True` and `last_request` is not the current week, a call to the root chain is made to transmit the emissions to the child gauge.
### `mint`
::::description[`ChildGaugeFactory.mint(_gauge: address)`]
Function to mint all CRV emissions belonging to `msg.sender` from a given gauge.
| Input | Type | Description |
| -------- | --------- | ----------- |
| `_gauge` | `address` | Gauge to mint CRV emissions from |
Emits: `Minted` event.
```vyper
event Minted:
_user: indexed(address)
_gauge: indexed(address)
_new_total: uint256
WEEK: constant(uint256) = 86400 * 7
crv: public(ERC20)
root_factory: public(address)
root_implementation: public(address)
call_proxy: public(address)
# [last_request][has_counterpart][is_valid_gauge]
gauge_data: public(HashMap[address, uint256])
# user -> gauge -> value
minted: public(HashMap[address, HashMap[address, uint256]])
get_gauge_from_lp_token: public(HashMap[address, address])
get_gauge_count: public(uint256)
get_gauge: public(address[max_value(int128)])
@external
@nonreentrant("lock")
def mint(_gauge: address):
"""
@notice Mint everything which belongs to `msg.sender` and send to them
@param _gauge `LiquidityGauge` address to get mintable amount from
"""
self._psuedo_mint(_gauge, msg.sender)
@internal
def _psuedo_mint(_gauge: address, _user: address):
gauge_data: uint256 = self.gauge_data[_gauge]
assert gauge_data != 0 # dev: invalid gauge
# if is_mirrored and last_request != this week
if gauge_data & 2 != 0 and (gauge_data >> 2) / WEEK != block.timestamp / WEEK:
CallProxy(self.call_proxy).anyCall(
self,
_abi_encode(_gauge, method_id=method_id("transmit_emissions(address)")),
empty(address),
1,
)
# update last request time
self.gauge_data[_gauge] = block.timestamp << 2 + 3
assert ChildGauge(_gauge).user_checkpoint(_user)
total_mint: uint256 = ChildGauge(_gauge).integrate_fraction(_user)
to_mint: uint256 = total_mint - self.minted[_user][_gauge]
if to_mint != 0 and self.crv != empty(ERC20):
assert self.crv.transfer(_user, to_mint, default_return_value=True)
self.minted[_user][_gauge] = total_mint
log Minted(_user, _gauge, total_mint)
```
```shell
>>> ChildGaugeFactory.mint('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')
```
::::
### `mint_many`
::::description[`ChildGaugeFactory.mint_many(_gauges: address[32])`]
Function to mint all CRV emissions belonging to `msg.sender` from multiple gauges.
| Input | Type | Description |
| -------- | --------- | ----------- |
| `_gauges` | `address[32]` | Array of gauges to mint CRV emissions from |
Emits: `Minted` event.
```vyper
event Minted:
_user: indexed(address)
_gauge: indexed(address)
_new_total: uint256
WEEK: constant(uint256) = 86400 * 7
crv: public(ERC20)
root_factory: public(address)
root_implementation: public(address)
call_proxy: public(address)
# [last_request][has_counterpart][is_valid_gauge]
gauge_data: public(HashMap[address, uint256])
# user -> gauge -> value
minted: public(HashMap[address, HashMap[address, uint256]])
get_gauge_from_lp_token: public(HashMap[address, address])
get_gauge_count: public(uint256)
get_gauge: public(address[max_value(int128)])
@external
@nonreentrant("lock")
def mint_many(_gauges: address[32]):
"""
@notice Mint everything which belongs to `msg.sender` across multiple gauges
@param _gauges List of `LiquidityGauge` addresses
"""
for i in range(32):
if _gauges[i] == empty(address):
pass
self._psuedo_mint(_gauges[i], msg.sender)
@internal
def _psuedo_mint(_gauge: address, _user: address):
gauge_data: uint256 = self.gauge_data[_gauge]
assert gauge_data != 0 # dev: invalid gauge
# if is_mirrored and last_request != this week
if gauge_data & 2 != 0 and (gauge_data >> 2) / WEEK != block.timestamp / WEEK:
CallProxy(self.call_proxy).anyCall(
self,
_abi_encode(_gauge, method_id=method_id("transmit_emissions(address)")),
empty(address),
1,
)
# update last request time
self.gauge_data[_gauge] = block.timestamp << 2 + 3
assert ChildGauge(_gauge).user_checkpoint(_user)
total_mint: uint256 = ChildGauge(_gauge).integrate_fraction(_user)
to_mint: uint256 = total_mint - self.minted[_user][_gauge]
if to_mint != 0 and self.crv != empty(ERC20):
assert self.crv.transfer(_user, to_mint, default_return_value=True)
self.minted[_user][_gauge] = total_mint
log Minted(_user, _gauge, total_mint)
```
```shell
>>> ChildGaugeFactory.mint_many(['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', '0x...'])
```
::::
### `minted`
::::description[`ChildGaugeFactory.minted(_user: address, _gauge: address) -> uint256: view`]
Getter to check the amount of CRV emissions minted for a user from a given gauge.
| Input | Type | Description |
| -------- | --------- | ----------- |
| `_user` | `address` | User to check minted amount for |
| `_gauge` | `address` | Gauge to check minted amount for |
Returns: Amount of CRV emissions minted (`uint256`).
```vyper
minted: public(HashMap[address, HashMap[address, uint256]])
```
```shell
>>> ChildGaugeFactory.minted('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', '0x...')
0
```
::::
---
## Gauge Data
The `ChildGaugeFactory` contract stores different gauge data for all the child gauges deployed via the factory.
### `gauge_data`
::::description[`ChildGaugeFactory.gauge_data(_gauge: address) -> uint256: view`]
Getter to check gauge data. The variable stores a `uint256` value where the bits are stored as follows:
- `[0:2]`: `is_valid_gauge`
- `[2:3]`: `has_counterpart`
- `[3:256]`: `last_request`
| Input | Type | Description |
| -------- | --------- | ----------- |
| `_gauge` | `address` | Gauge to check data for |
Returns: gauge data (`uint256`).
```vyper
# [last_request][has_counterpart][is_valid_gauge]
gauge_data: public(HashMap[address, uint256])
```
```shell
>>> ChildGaugeFactory.gauge_data('0x...')
0
```
::::
### `is_valid_gauge`
::::description[`ChildGaugeFactory.is_valid_gauge(_gauge: address) -> bool: view`]
Getter to check if a gauge is valid.
| Input | Type | Description |
| -------- | --------- | ----------- |
| `_gauge` | `address` | Gauge to check validity for |
Returns: `True` if the gauge is valid, `False` otherwise (`bool`).
```vyper
gauge_data: public(HashMap[address, uint256])
@view
@external
def is_valid_gauge(_gauge: address) -> bool:
"""
@notice Query whether the gauge is a valid one deployed via the factory
@param _gauge The address of the gauge of interest
"""
return self.gauge_data[_gauge] != 0
```
```shell
>>> ChildGaugeFactory.is_valid_gauge('0x...')
True
```
::::
### `get_gauge_from_lp_token`
::::description[`ChildGaugeFactory.get_gauge_from_lp_token(_lp_token: address) -> address: view`]
Getter for gauge associated with a given LP token.
| Input | Type | Description |
| -------- | --------- | ----------- |
| `_lp_token` | `address` | LP token to check gauge for |
Returns: gauge (`address`).
```vyper
get_gauge_from_lp_token: public(HashMap[address, address])
```
```shell
>>> ChildGaugeFactory.get_gauge_from_lp_token('0x...')
'0x...'
```
::::
### `get_gauge_count`
::::description[`ChildGaugeFactory.get_gauge_count() -> uint256: view`]
Getter for the number of gauges deployed.
Returns: number of gauges deployed (`uint256`).
```vyper
get_gauge_count: public(uint256)
```
```shell
>>> ChildGaugeFactory.get_gauge_count()
3
```
::::
### `get_gauge`
::::description[`ChildGaugeFactory.get_gauge(_idx: uint256) -> address: view`]
Getter for the gauge address at a given index. First gauge has index `0`, second has index `1`, etc.
| Input | Type | Description |
| -------- | --------- | ----------- |
| `_idx` | `uint256` | Index to check gauge for |
Returns: gauge (`address`).
```vyper
get_gauge: public(address[max_value(int128)])
```
This example returns the first two child gauges deployed via the `ChildGaugeFactory` on Fraxtal.
```shell
>>> ChildGaugeFactory.get_gauge(0)
'0x0092782EF5d4dFBB2949c2C147020E7aC644D870'
>>> ChildGaugeFactory.get_gauge(1)
'0xcde3Cdf332E35653A7595bA555c9fDBA3c78Ec04'
```
::::
### `last_request`
::::description[`ChildGaugeFactory.last_request(_gauge: address) -> uint256: view`]
Getter for the last request timestamp for a gauge. This variable updates whenever CRV emissions were minted from the according gauge.
| Input | Type | Description |
| -------- | --------- | ----------- |
| `_gauge` | `address` | Gauge to check last request timestamp for |
Returns: last request timestamp (`uint256`).
```vyper
# [last_request][has_counterpart][is_valid_gauge]
gauge_data: public(HashMap[address, uint256])
@view
@external
def last_request(_gauge: address) -> uint256:
"""
@notice Query the timestamp of the last cross chain request for emissions
@param _gauge The address of the gauge of interest
"""
return self.gauge_data[_gauge] >> 2
```
```shell
>>> ChildGaugeFactory.last_request('0x...')
0
```
::::
### `is_mirrored`
::::description[`ChildGaugeFactory.is_mirrored(_gauge: address) -> bool: view`]
Getter to check if a gauge is mirrored.
| Input | Type | Description |
| -------- | --------- | ----------- |
| `_gauge` | `address` | Gauge to check mirrored status for |
Returns: `True` if the gauge is mirrored, `False` otherwise (`bool`).
```vyper
# [last_request][has_counterpart][is_valid_gauge]
gauge_data: public(HashMap[address, uint256])
@view
@external
def is_mirrored(_gauge: address) -> bool:
"""
@notice Query whether the gauge is mirrored on Ethereum mainnet
@param _gauge The address of the gauge of interest
"""
return (self.gauge_data[_gauge] & 2) != 0
```
```shell
>>> ChildGaugeFactory.is_mirrored('0xcde3Cdf332E35653A7595bA555c9fDBA3c78Ec04')
False
```
::::
### `set_mirrored`
::::description[`ChildGaugeFactory.set_mirrored(_gauge: address, _mirrored: bool)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to set the mirrored status of a gauge.
| Input | Type | Description |
| ---------- | --------- | ----------- |
| `_gauge` | `address` | Gauge to set mirrored status for |
| `_mirrored` | `bool` | New mirrored status |
Emits: `UpdateMirrored` event.
```vyper
event UpdateMirrored:
_gauge: indexed(address)
_mirrored: bool
# [last_request][has_counterpart][is_valid_gauge]
gauge_data: public(HashMap[address, uint256])
@external
def set_mirrored(_gauge: address, _mirrored: bool):
"""
@notice Set the mirrored bit of the gauge data for `_gauge`
@param _gauge The gauge of interest
@param _mirrored Boolean deteremining whether to set the mirrored bit to True/False
"""
gauge_data: uint256 = self.gauge_data[_gauge]
assert gauge_data != 0 # dev: invalid gauge
assert msg.sender == self.owner # dev: only owner
gauge_data = gauge_data | 1 # set is_valid_gauge = True
if _mirrored:
gauge_data += 2 # set is_mirrored = True
self.gauge_data[_gauge] = gauge_data
log UpdateMirrored(_gauge, _mirrored)
```
```shell
>>> ChildGaugeFactory.set_mirrored('0x...', True)
```
::::
---
## Child Gauge Implementation
### `get_implementation`
::::description[`ChildGaugeFactory.get_implementation() -> address: view`]
Getter for the child gauge implementation address.
Returns: `ChildGauge` implementation contract (`address`).
```vyper
get_implementation: public(address)
```
This example returns the `ChildGauge` implementation contract for the `ChildGaugeFactory` on Fraxtal.
```shell
>>> ChildGaugeFactory.get_implementation()
'0x6A611215540555A7feBCB64CB0Ed11Ac90F165Af'
```
::::
### `set_implementation`
::::description[`ChildGaugeFactory.set_implementation(_implementation: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to set the implementation address.
| Input | Type | Description |
| ------------- | ----------- | ----------- |
| `_implementation` | `address` | New implementation address |
Emits: `UpdateImplementation` event.
```vyper
event UpdateImplementation:
_old_implementation: address
_new_implementation: address
get_implementation: public(address)
@external
def set_implementation(_implementation: address):
"""
@notice Set the implementation
@param _implementation The address of the implementation to use
"""
assert msg.sender == self.owner # dev: only owner
log UpdateImplementation(self.get_implementation, _implementation)
self.get_implementation = _implementation
```
```shell
>>> ChildGaugeFactory.get_implementation()
'0x6A611215540555A7feBCB64CB0Ed11Ac90F165Af'
>>> ChildGaugeFactory.set_implementation('0x1234567890123456789012345678901234567892')
>>> ChildGaugeFactory.get_implementation()
'0x1234567890123456789012345678901234567892'
```
::::
---
## Root Factory and Implementation
The `root_factory` and `root_implementation` variables store the addresses of the root factory and implementation, respectively. They are only used as helper variables within this contract. Both variables can be updated by the `owner` or `manager` of the contract via the `set_root` function.
### `root_factory`
::::description[`ChildGaugeFactory.root_factory() -> address: view`]
Getter for the root factory address.
Returns: `RootGaugeFactory` contract on Ethereum (`address`).
```vyper
event UpdateRoot:
_factory: address
_implementation: address
root_factory: public(address)
@external
def __init__(_call_proxy: address, _root_factory: address, _root_impl: address, _crv: address, _owner: address):
"""
@param _call_proxy Contract for
@param _root_factory Root factory to anchor to
@param _root_impl Address of root gauge implementation to calculate mirror (can be updated)
@param _crv Bridged CRV token address (might be zero if not known yet)
@param _owner Owner of factory (xgov)
"""
...
assert _root_factory != empty(address)
assert _root_impl != empty(address)
self.root_factory = _root_factory
self.root_implementation = _root_impl
log UpdateRoot(_root_factory, _root_impl)
...
```
This example returns the `RootGaugeFactory` contract on Ethereum.
```shell
>>> ChildGaugeFactory.root_factory()
'0x306A45a1478A000dC701A6e1f7a569afb8D9DCD6'
```
::::
### `root_implementation`
::::description[`ChildGaugeFactory.root_implementation() -> address: view`]
Getter for the root implementation address.
Returns: `RootGauge` implementation contract on Ethereum (`address`).
```vyper
root_implementation: public(address)
@external
def __init__(_call_proxy: address, _root_factory: address, _root_impl: address, _crv: address, _owner: address):
"""
@param _call_proxy Contract for
@param _root_factory Root factory to anchor to
@param _root_impl Address of root gauge implementation to calculate mirror (can be updated)
@param _crv Bridged CRV token address (might be zero if not known yet)
@param _owner Owner of factory (xgov)
"""
...
assert _root_factory != empty(address)
assert _root_impl != empty(address)
self.root_factory = _root_factory
self.root_implementation = _root_impl
log UpdateRoot(_root_factory, _root_impl)
...
```
This example returns the `RootGauge` implementation contract on Ethereum.
```shell
>>> ChildGaugeFactory.root_implementation()
'0x96720942F9fF22eFd8611F696E5333Fe3671717a'
```
::::
### `set_root`
::::description[`ChildGaugeFactory.set_root(_factory: address, _implementation: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` or `manager` of the contract.
:::
Function to set the `root_factory` and `root_implementation` addresses.
| Input | Type | Description |
| ---------- | --------- | ----------- |
| `_factory` | `address` | New `RootGaugeFactory` address |
| `_implementation` | `address` | New `RootGauge` implementation address |
Emits: `UpdateRoot` event.
```vyper
root_factory: public(address)
root_implementation: public(address)
@external
def set_root(_factory: address, _implementation: address):
"""
@notice Update root addresses
@dev Addresses are used only as helper methods
@param _factory Root gauge factory
@param _implementation Root gauge
"""
assert msg.sender in [self.owner, self.manager] # dev: access denied
self.root_factory = _factory
self.root_implementation = _implementation
log UpdateRoot(_factory, _implementation)
```
```shell
>>> ChildGaugeFactory.set_root('0x1234567890123456789012345678901234567890', '0x1234567890123456789012345678901234567891')
```
::::
---
## CRV Token and Voting Escrow
The `crv` and `voting_escrow` variables store the addresses of the CRV token and `VotingEscrow` contract, respectively. `crv` represents a bridged version of the CRV token, whereas `voting_escrow` represents a `L2 VotingEscrow Oracle` contract. This oracle is responsible for providing data from the `VotingEscrow` contract on Ethereum to the child chain in order to make boosts on sidechains work. If there is no `L2 VotingEscrow Oracle` set, the boosts on the child chain will not work.
### `crv`
::::description[`ChildGaugeFactory.crv() -> address: view`]
Getter for the CRV token address of the child chain.
Returns: CRV token on the child chain (`address`).
```vyper
crv: public(ERC20)
@external
def __init__(_call_proxy: address, _root_factory: address, _root_impl: address, _crv: address, _owner: address):
"""
@param _call_proxy Contract for
@param _root_factory Root factory to anchor to
@param _root_impl Address of root gauge implementation to calculate mirror (can be updated)
@param _crv Bridged CRV token address (might be zero if not known yet)
@param _owner Owner of factory (xgov)
"""
self.crv = ERC20(_crv)
...
```
This example returns the token address of bridged CRV on Fraxtal.
```shell
>>> ChildGaugeFactory.crv()
'0x331B9182088e2A7d6D3Fe4742AbA1fB231aEcc56'
```
::::
### `set_crv`
::::description[`ChildGaugeFactory.set_crv(_crv: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to set the CRV token address.
| Input | Type | Description |
| ------ | --------- | ----------- |
| `_crv` | `address` | New CRV token address |
```vyper
crv: public(ERC20)
@external
def set_crv(_crv: ERC20):
"""
@notice Sets CRV token address
@dev Child gauges reference the factory to fetch CRV address
If empty, the gauges do not mint any CRV tokens.
@param _crv address of CRV token on child chain
"""
assert msg.sender == self.owner
assert _crv != empty(ERC20)
self.crv = _crv
```
```shell
>>> ChildGaugeFactory.crv()
'0x331B9182088e2A7d6D3Fe4742AbA1fB231aEcc56'
>>> ChildGaugeFactory.set_crv('0x1234567890123456789012345678901234567892')
>>> ChildGaugeFactory.crv()
'0x1234567890123456789012345678901234567892'
```
::::
### `voting_escrow`
::::description[`ChildGaugeFactory.voting_escrow() -> address: view`]
Getter for the `VotingEscrow` contract.
Returns: `VotingEscrow` contract (`address`).
```vyper
voting_escrow: public(address)
```
```shell
>>> ChildGaugeFactory.voting_escrow()
'0xc73e8d8f7A68Fc9d67e989250484E57Ae03a5Da3'
```
::::
### `set_voting_escrow`
::::description[`ChildGaugeFactory.set_voting_escrow(_voting_escrow: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to set the `VotingEscrow` contract.
| Input | Type | Description |
| ---------- | --------- | ----------- |
| `_voting_escrow` | `address` | New voting escrow address |
Emits: `UpdateVotingEscrow` event.
```vyper
event UpdateVotingEscrow:
_old_voting_escrow: address
_new_voting_escrow: address
voting_escrow: public(address)
@external
def set_voting_escrow(_voting_escrow: address):
"""
@notice Update the voting escrow contract
@param _voting_escrow Contract to use as the voting escrow oracle
"""
assert msg.sender == self.owner # dev: only owner
log UpdateVotingEscrow(self.voting_escrow, _voting_escrow)
self.voting_escrow = _voting_escrow
```
```shell
>>> ChildGaugeFactory.voting_escrow()
'0xc73e8d8f7A68Fc9d67e989250484E57Ae03a5Da3'
>>> ChildGaugeFactory.set_voting_escrow('0x1234567890123456789012345678901234567893')
>>> ChildGaugeFactory.voting_escrow()
'0x1234567890123456789012345678901234567893'
```
::::
---
## Manager
### `manager`
::::description[`ChildGaugeFactory.manager() -> address: view`]
Getter for the manager address. This variable is set at initialization and can be changed via the `set_manager` function.
Returns: manager (`address`).
```vyper
event UpdateManager:
_manager: address
manager: public(address)
@external
def __init__(_call_proxy: address, _root_factory: address, _root_impl: address, _crv: address, _owner: address):
"""
@param _call_proxy Contract for
@param _root_factory Root factory to anchor to
@param _root_impl Address of root gauge implementation to calculate mirror (can be updated)
@param _crv Bridged CRV token address (might be zero if not known yet)
@param _owner Owner of factory (xgov)
"""
...
self.manager = msg.sender
log UpdateManager(msg.sender)
```
```shell
>>> ChildGaugeFactory.manager()
'0xaE50429025B59C9D62Ae9c3A52a657BC7AB64036'
```
::::
### `set_manager`
::::description[`ChildGaugeFactory.set_manager(_new_manager: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` or `manager` of the contract.
:::
Function to change the manager address.
| Input | Type | Description |
| ---------- | --------- | ----------- |
| `_new_manager` | `address` | New manager address |
Emits: `UpdateManager` event.
```vyper
event UpdateManager:
_manager: address
manager: public(address)
@external
def set_manager(_new_manager: address):
assert msg.sender in [self.owner, self.manager] # dev: access denied
self.manager = _new_manager
log UpdateManager(_new_manager)
```
```shell
>>> ChildGaugeFactory.manager()
'0x71F718D3e4d1449D1502A6A7595eb84eBcCB1683'
>>> ChildGaugeFactory.set_manager('0x1234567890123456789012345678901234567895')
>>> ChildGaugeFactory.manager()
'0x1234567890123456789012345678901234567895'
```
::::
---
## Call Proxy
### `call_proxy`
::::description[`ChildGaugeFactory.call_proxy() -> address: view`]
Getter for the call proxy contract. This contract acts as an intermediary to facilitate cross-chain calls.
Returns: call proxy address (`address`).
```vyper
event UpdateCallProxy:
_old_call_proxy: address
_new_call_proxy: address
call_proxy: public(address)
@external
def __init__(_call_proxy: address, _root_factory: address, _root_impl: address, _crv: address, _owner: address):
"""
@param _call_proxy Contract for
@param _root_factory Root factory to anchor to
@param _root_impl Address of root gauge implementation to calculate mirror (can be updated)
@param _crv Bridged CRV token address (might be zero if not known yet)
@param _owner Owner of factory (xgov)
"""
...
self.call_proxy = _call_proxy
log UpdateCallProxy(empty(address), _call_proxy)
...
```
```shell
>>> ChildGaugeFactory.call_proxy()
'0x0000000000000000000000000000000000000000'
```
::::
### `set_call_proxy`
::::description[`ChildGaugeFactory.set_call_proxy(_new_call_proxy: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to set or update the call proxy address.
| Input | Type | Description |
| ---------- | --------- | ----------- |
| `_new_call_proxy` | `address` | New call proxy address |
Emits: `UpdateCallProxy` event.
```vyper
event UpdateCallProxy:
_old_call_proxy: address
_new_call_proxy: address
call_proxy: public(address)
@external
def set_call_proxy(_new_call_proxy: address):
"""
@notice Set the address of the call proxy used
@dev _new_call_proxy should adhere to the same interface as defined
@param _new_call_proxy Address of the cross chain call proxy
"""
assert msg.sender == self.owner
log UpdateCallProxy(self.call_proxy, _new_call_proxy)
self.call_proxy = _new_call_proxy
```
```shell
>>> ChildGaugeFactory.call_proxy()
'0x0000000000000000000000000000000000000000'
>>> ChildGaugeFactory.set_call_proxy('0x1234567890123456789012345678901234567894')
>>> ChildGaugeFactory.call_proxy()
'0x1234567890123456789012345678901234567894'
```
::::
---
## Ownership
For contract ownership details, see [here](../../resources/curve-practices.md#commit--accept).
---
## Contract Version
### `version`
::::description[`ChildGaugeFactory.version() -> String[8]: view`]
Getter for the contract version.
Returns: contract version (`String[8]`).
```vyper
VERSION: constant(String[8]) = "1.0.0"
@view
@external
def version() -> String[8]:
"""
@notice Get the version of this contract
"""
return VERSION
```
```shell
>>> ChildGaugeFactory.version()
'1.0.0'
```
::::
---
## Child Gauge Implementation
The `ChildGauge` is the liquidity gauge contract on the sidechain. It is used to track the balance of liquidity providers and distribute CRV emissions to them. It is pretty much the same as the `Gauge` contract on Ethereum mainnet.
:::vyper[`ChildGauge.vy`]
The source code for the `ChildGauge.vy` contract can be found on [ GitHub](https://github.com/curvefi/curve-xchain-factory/blob/master/contracts/implementations/ChildGauge.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`
:::
---
## Initialization
### `initialize`
::::description[`ChildGauge.initialize(_lp_token: address, _root: address, _manager: address)`]
Function to initialize the gauge. A child gauge is initialized directly when deploying it from the `ChildGaugeFactory` via the [`deploy_gauge`](./child-gauge-factory.md#deploy_gauge) function.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_lp_token` | `address` | The LP token address |
| `_root` | `address` | The root gauge address |
| `_manager` | `address` | The manager address |
Emits: `SetGaugeManager` event.
```vyper
@external
def initialize(_lp_token: address, _root: address, _manager: address):
assert self.lp_token == empty(address) # dev: already initialized
self.lp_token = _lp_token
self.root_gauge = _root
self.manager = _manager
self.voting_escrow = Factory(msg.sender).voting_escrow()
symbol: String[32] = ERC20Extended(_lp_token).symbol()
name: String[64] = concat("Curve.fi ", symbol, " Gauge Deposit")
self.name = name
self.symbol = concat(symbol, "-gauge")
self.period_timestamp[0] = block.timestamp
self.DOMAIN_SEPARATOR = keccak256(
_abi_encode(
EIP712_TYPEHASH,
keccak256(name),
keccak256(VERSION),
chain.id,
self
)
)
```
```shell
>>> ChildGauge.initialize(lp_token, root, manager)
```
::::
---
## Depositing & Withdrawing
### `deposit`
::::description[`ChildGauge.deposit(_value: uint256, _addr: address = msg.sender, _claim_rewards: bool = False)`]
Function to deposit `_value` of LP tokens into the gauge. When depositing LP tokens into the gauge, the contract mints the equivalent amount of "gauge tokens" to the user which represent the user's share of liquidity in the gauge. Additionally, the function also allows for claiming any pending external rewards (not CRV emissions).
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_value` | `uint256` | The amount of liquidity to deposit |
| `_addr` | `address` | The address to deposit for |
| `_claim_rewards` | `bool` | Whether to claim rewards. Defaults to `False` |
Emits: `Deposit`, `Transfer`, `UpdateLiquidityLimit` events.
```vyper
event Deposit:
provider: indexed(address)
value: uint256
event UpdateLiquidityLimit:
user: indexed(address)
original_balance: uint256
original_supply: uint256
working_balance: uint256
working_supply: uint256
event Transfer:
_from: indexed(address)
_to: indexed(address)
_value: uint256
@external
@nonreentrant('lock')
def deposit(_value: uint256, _addr: address = msg.sender, _claim_rewards: bool = False):
"""
@notice Deposit `_value` LP tokens
@dev Depositting also claims pending reward tokens
@param _value Number of tokens to deposit
@param _addr Address to deposit for
"""
assert _addr != empty(address) # dev: cannot deposit for zero address
self._checkpoint(_addr)
if _value != 0:
is_rewards: bool = self.reward_count != 0
total_supply: uint256 = self.totalSupply
if is_rewards:
self._checkpoint_rewards(_addr, total_supply, _claim_rewards, empty(address))
total_supply += _value
new_balance: uint256 = self.balanceOf[_addr] + _value
self.balanceOf[_addr] = new_balance
self.totalSupply = total_supply
self._update_liquidity_limit(_addr, new_balance, total_supply)
ERC20(self.lp_token).transferFrom(msg.sender, self, _value)
log Deposit(_addr, _value)
log Transfer(empty(address), _addr, _value)
@internal
def _checkpoint(_user: address):
"""
@notice Checkpoint a user calculating their CRV entitlement
@param _user User address
"""
period: int128 = self.period
period_time: uint256 = self.period_timestamp[period]
integrate_inv_supply: uint256 = self.integrate_inv_supply[period]
if block.timestamp > period_time:
working_supply: uint256 = self.working_supply
prev_week_time: uint256 = period_time
week_time: uint256 = min((period_time + WEEK) / WEEK * WEEK, block.timestamp)
for i in range(256):
dt: uint256 = week_time - prev_week_time
if working_supply != 0:
# we don't have to worry about crossing inflation epochs
# and if we miss any weeks, those weeks inflation rates will be 0 for sure
# but that means no one interacted with the gauge for that long
integrate_inv_supply += self.inflation_rate[prev_week_time / WEEK] * 10 ** 18 * dt / working_supply
if week_time == block.timestamp:
break
prev_week_time = week_time
week_time = min(week_time + WEEK, block.timestamp)
# check CRV balance and increase weekly inflation rate by delta for the rest of the week
crv: ERC20 = FACTORY.crv()
if crv != empty(ERC20):
crv_balance: uint256 = crv.balanceOf(self)
if crv_balance != 0:
current_week: uint256 = block.timestamp / WEEK
self.inflation_rate[current_week] += crv_balance / ((current_week + 1) * WEEK - block.timestamp)
crv.transfer(FACTORY.address, crv_balance)
period += 1
self.period = period
self.period_timestamp[period] = block.timestamp
self.integrate_inv_supply[period] = integrate_inv_supply
working_balance: uint256 = self.working_balances[_user]
self.integrate_fraction[_user] += working_balance * (integrate_inv_supply - self.integrate_inv_supply_of[_user]) / 10 ** 18
self.integrate_inv_supply_of[_user] = integrate_inv_supply
self.integrate_checkpoint_of[_user] = block.timestamp
@internal
def _update_liquidity_limit(_user: address, _user_balance: uint256, _total_supply: uint256):
"""
@notice Calculate working balances to apply amplification of CRV production.
@dev https://resources.curve.fi/guides/boosting-your-crv-rewards#formula
@param _user The user address
@param _user_balance User's amount of liquidity (LP tokens)
@param _total_supply Total amount of liquidity (LP tokens)
"""
working_balance: uint256 = _user_balance * TOKENLESS_PRODUCTION / 100
ve: address = self.voting_escrow
if ve != empty(address):
ve_ts: uint256 = ERC20(ve).totalSupply()
if ve_ts != 0:
working_balance += _total_supply * ERC20(ve).balanceOf(_user) / ve_ts * (100 - TOKENLESS_PRODUCTION) / 100
working_balance = min(_user_balance, working_balance)
old_working_balance: uint256 = self.working_balances[_user]
self.working_balances[_user] = working_balance
working_supply: uint256 = self.working_supply + working_balance - old_working_balance
self.working_supply = working_supply
log UpdateLiquidityLimit(_user, _user_balance, _total_supply, working_balance, working_supply)
```
```shell
>>> ChildGauge.deposit(1000000000000000000, '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', False)
```
::::
### `withdraw`
::::description[`ChildGauge.withdraw(_value: uint256, _claim_rewards: bool = False, _receiver: address = msg.sender)`]
Function to withdraw `_value` of LP tokens from the gauge. When withdrawing LP tokens from the gauge, the contract burns the equivalent amount of "gauge tokens" from the user. Additionally, the function also allows for claiming any pending external rewards (not CRV emissions).
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_value` | `uint256` | The amount of liquidity to withdraw |
| `_claim_rewards` | `bool` | Whether to claim rewards. Defaults to `False` |
| `_receiver` | `address` | The address to transfer the withdrawn LP tokens to. Defaults to `msg.sender` |
Emits: `Withdraw`, `Transfer`, `UpdateLiquidityLimit` events.
```vyper
event Withdraw:
provider: indexed(address)
value: uint256
event UpdateLiquidityLimit:
user: indexed(address)
original_balance: uint256
original_supply: uint256
working_balance: uint256
working_supply: uint256
event Transfer:
_from: indexed(address)
_to: indexed(address)
_value: uint256
@external
@nonreentrant('lock')
def withdraw(_value: uint256, _claim_rewards: bool = False, _receiver: address = msg.sender):
"""
@notice Withdraw `_value` LP tokens
@dev Withdrawing also claims pending reward tokens
@param _value Number of tokens to withdraw
@param _claim_rewards Whether to claim rewards
@param _receiver Receiver of withdrawn LP tokens
"""
self._checkpoint(msg.sender)
if _value != 0:
is_rewards: bool = self.reward_count != 0
total_supply: uint256 = self.totalSupply
if is_rewards:
self._checkpoint_rewards(msg.sender, total_supply, _claim_rewards, empty(address))
total_supply -= _value
new_balance: uint256 = self.balanceOf[msg.sender] - _value
self.balanceOf[msg.sender] = new_balance
self.totalSupply = total_supply
self._update_liquidity_limit(msg.sender, new_balance, total_supply)
ERC20(self.lp_token).transfer(_receiver, _value)
log Withdraw(msg.sender, _value)
log Transfer(msg.sender, empty(address), _value)
@internal
def _checkpoint(_user: address):
"""
@notice Checkpoint a user calculating their CRV entitlement
@param _user User address
"""
period: int128 = self.period
period_time: uint256 = self.period_timestamp[period]
integrate_inv_supply: uint256 = self.integrate_inv_supply[period]
if block.timestamp > period_time:
working_supply: uint256 = self.working_supply
prev_week_time: uint256 = period_time
week_time: uint256 = min((period_time + WEEK) / WEEK * WEEK, block.timestamp)
for i in range(256):
dt: uint256 = week_time - prev_week_time
if working_supply != 0:
# we don't have to worry about crossing inflation epochs
# and if we miss any weeks, those weeks inflation rates will be 0 for sure
# but that means no one interacted with the gauge for that long
integrate_inv_supply += self.inflation_rate[prev_week_time / WEEK] * 10 ** 18 * dt / working_supply
if week_time == block.timestamp:
break
prev_week_time = week_time
week_time = min(week_time + WEEK, block.timestamp)
# check CRV balance and increase weekly inflation rate by delta for the rest of the week
crv: ERC20 = FACTORY.crv()
if crv != empty(ERC20):
crv_balance: uint256 = crv.balanceOf(self)
if crv_balance != 0:
current_week: uint256 = block.timestamp / WEEK
self.inflation_rate[current_week] += crv_balance / ((current_week + 1) * WEEK - block.timestamp)
crv.transfer(FACTORY.address, crv_balance)
period += 1
self.period = period
self.period_timestamp[period] = block.timestamp
self.integrate_inv_supply[period] = integrate_inv_supply
working_balance: uint256 = self.working_balances[_user]
self.integrate_fraction[_user] += working_balance * (integrate_inv_supply - self.integrate_inv_supply_of[_user]) / 10 ** 18
self.integrate_inv_supply_of[_user] = integrate_inv_supply
self.integrate_checkpoint_of[_user] = block.timestamp
```
```shell
>>> ChildGauge.withdraw(1000000000000000000, False)
```
::::
---
## External Rewards
External rewards are externally added rewards (not coming from the CRV emissions) and are not boostable. They are distributed linearly over the chosen period to users based on their liquidity share of the gauge. Between 3 days and a year, week by default.
The following functions allow for claiming external rewards (not CRV emissions). CRV emissions can only be claimed directly from the `ChildGaugeFactory`.
## Claiming Rewards
### `claim_rewards`
::::description[`ChildGauge.claim_rewards(_addr: address = msg.sender, _receiver: address = empty(address))`]
Function to claim available reward tokens for a given address. Claimed rewards cannot be redirected to a different address when claiming for another user.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_addr` | `address` | The address to claim rewards for. Defaults to `msg.sender` |
| `_receiver` | `address` | The address to transfer rewards to. Defaults to `empty(address)` |
```vyper
@external
@nonreentrant('lock')
def claim_rewards(_addr: address = msg.sender, _receiver: address = empty(address)):
"""
@notice Claim available reward tokens for `_addr`
@param _addr Address to claim for
@param _receiver Address to transfer rewards to - if set to
empty(address), uses the default reward receiver
for the caller
"""
if _receiver != empty(address):
assert _addr == msg.sender # dev: cannot redirect when claiming for another user
self._checkpoint_rewards(_addr, self.totalSupply, True, _receiver)
@internal
def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _receiver: address):
"""
@notice Claim pending rewards and checkpoint rewards for a user
"""
user_balance: uint256 = 0
receiver: address = _receiver
if _user != empty(address):
user_balance = self.balanceOf[_user]
if _claim and _receiver == empty(address):
# if receiver is not explicitly declared, check if a default receiver is set
receiver = self.rewards_receiver[_user]
if receiver == empty(address):
# if no default receiver is set, direct claims to the user
receiver = _user
reward_count: uint256 = self.reward_count
for i in range(MAX_REWARDS):
if i == reward_count:
break
token: address = self.reward_tokens[i]
integral: uint256 = self.reward_data[token].integral
period_finish: uint256 = self.reward_data[token].period_finish
last_update: uint256 = min(block.timestamp, period_finish)
duration: uint256 = last_update - self.reward_data[token].last_update
if duration != 0 and _total_supply != 0:
self.reward_data[token].last_update = last_update
rate: uint256 = self.reward_data[token].rate
excess: uint256 = self.reward_remaining[token] - (period_finish - last_update + duration) * rate
integral_change: uint256 = (duration * rate + excess) * 10**18 / _total_supply
integral += integral_change
self.reward_data[token].integral = integral
# There is still calculation error in user's claimable amount,
# but it has 18-decimal precision through LP(_total_supply) – safe
self.reward_remaining[token] -= integral_change * _total_supply / 10**18
if _user != empty(address):
integral_for: uint256 = self.reward_integral_for[token][_user]
new_claimable: uint256 = 0
if integral_for < integral:
self.reward_integral_for[token][_user] = integral
new_claimable = user_balance * (integral - integral_for) / 10**18
claim_data: uint256 = self.claim_data[_user][token]
total_claimable: uint256 = (claim_data >> 128) + new_claimable
if total_claimable > 0:
total_claimed: uint256 = claim_data % 2**128
if _claim:
assert ERC20(token).transfer(receiver, total_claimable, default_return_value=True)
self.claim_data[_user][token] = total_claimed + total_claimable
elif new_claimable > 0:
self.claim_data[_user][token] = total_claimed + (total_claimable << 128)
```
```shell
>>> ChildGauge.claim_rewards()
```
::::
### `claimed_reward`
::::description[`ChildGauge.claimed_reward(_addr: address, _token: address) -> uint256: view`]
Function to get the number of claimed reward tokens for a user.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_addr` | `address` | The address to get the number of claimed rewards for |
| `_token` | `address` | The token to get the number of claimed rewards for |
Returns: number of claimed reward tokens for a user (`uint256`).
```vyper
reward_data: public(HashMap[address, Reward])
@view
@external
def claimed_reward(_addr: address, _token: address) -> uint256:
"""
@notice Get the number of already-claimed reward tokens for a user
@param _addr Account to get reward amount for
@param _token Token to get reward amount for
@return uint256 Total amount of `_token` already claimed by `_addr`
"""
return self.claim_data[_addr][_token] % 2**128
```
```shell
>>> ChildGauge.claimed_reward('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', '0x6B175474E89094C44Da98b954EedeAC495271d0F')
0
```
::::
### `claimable_reward`
::::description[`ChildGauge.claimable_reward(_user: address, _reward_token: address) -> uint256: view`]
Function to get the number of claimable reward tokens for a user.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_user` | `address` | The address to get the number of claimable rewards for |
| `_reward_token` | `address` | The token to get the number of claimable rewards for |
Returns: number of claimable reward tokens for a user (`uint256`).
```vyper
totalSupply: public(uint256)
# For tracking external rewards
reward_count: public(uint256)
reward_data: public(HashMap[address, Reward])
reward_remaining: public(HashMap[address, uint256]) # fixes bad precision
@view
@external
def claimable_reward(_user: address, _reward_token: address) -> uint256:
"""
@notice Get the number of claimable reward tokens for a user
@param _user Account to get reward amount for
@param _reward_token Token to get reward amount for
@return uint256 Claimable reward token amount
"""
integral: uint256 = self.reward_data[_reward_token].integral
total_supply: uint256 = self.totalSupply
if total_supply != 0:
last_update: uint256 = min(block.timestamp, self.reward_data[_reward_token].period_finish)
duration: uint256 = last_update - self.reward_data[_reward_token].last_update
integral += (duration * self.reward_data[_reward_token].rate * 10**18 / total_supply)
integral_for: uint256 = self.reward_integral_for[_reward_token][_user]
new_claimable: uint256 = self.balanceOf[_user] * (integral - integral_for) / 10**18
return (self.claim_data[_user][_reward_token] >> 128) + new_claimable
```
```shell
>>> ChildGauge.claimable_reward('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', '0x6B175474E89094C44Da98b954EedeAC495271d0F')
0
```
::::
### `rewards_receiver`
::::description[`ChildGauge.rewards_receiver(_user: address) -> address: view`]
Getter for the reward receiver of the caller. By default, this value is set to `empty(address)`, which means the rewards will be claimed to the user. But e.g. for integrations like Convex, the `rewards_receiver` is set to another contract address, from which the rewards are further distributed.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_user` | `address` | The user to get the reward receiver for. |
Returns: reward receiver for a user (`address`).
```vyper
# claimant -> default reward receiver
rewards_receiver: public(HashMap[address, address])
```
```shell
>>> ChildGauge.rewards_receiver('0x1234567890123456789012345678901234567890')
'0x0000000000000000000000000000000000000000'
```
::::
### `set_rewards_receiver`
::::description[`ChildGauge.set_rewards_receiver(_receiver: address)`]
Function to set the default reward receiver for the caller. When set to empty(address), rewards are sent to the caller.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_receiver` | `address` | The address to set as the default reward receiver for |
```vyper
# claimant -> default reward receiver
rewards_receiver: public(HashMap[address, address])
@external
def set_rewards_receiver(_receiver: address):
"""
@notice Set the default reward receiver for the caller.
@dev When set to empty(address), rewards are sent to the caller
@param _receiver Receiver address for any rewards claimed via `claim_rewards`
"""
self.rewards_receiver[msg.sender] = _receiver
```
```shell
>>> ChildGauge.rewards_receiver('0x1234567890123456789012345678901234567890')
'0x0000000000000000000000000000000000000000'
>>> ChildGauge.set_rewards_receiver('0x1234567890123456789012345678901234567890')
>>> ChildGauge.rewards_receiver('0x1234567890123456789012345678901234567890')
'0x1234567890123456789012345678901234567890'
```
::::
---
## Reward Data
The following functions allow for retrieving reward data for a specific reward token.
### `reward_data`
::::description[`ChildGauge.reward_data(_reward_token: address) -> Reward: view`]
Getter for the reward data for a specific reward token.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_reward_token` | `address` | The reward token to get the reward data for |
Returns: Reward struct containing the distributor (`address`), period finish (`uint256`), rate (`uint256`), last update (`uint256`), and integral (`uint256`).
```vyper
struct Reward:
distributor: address
period_finish: uint256
rate: uint256
last_update: uint256
integral: uint256
reward_data: public(HashMap[address, Reward])
```
```shell
>>> ChildGauge.reward_data('0x6B175474E89094C44Da98b954EedeAC495271d0F')
{'distributor': '0x...', 'period_finish': 0, 'rate': 0, 'last_update': 0, 'integral': 0}
```
::::
### `reward_tokens`
::::description[`ChildGauge.reward_tokens(arg0: uint256) -> address: view`]
Getter for the added reward token at index `arg0`. New tokens are populated to this variable when calling the `add_reward` function.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `arg0` | `uint256` | The index of the reward token to get |
Returns: reward token at index `arg0` (`address`).
```vyper
# array of reward tokens
reward_tokens: public(address[MAX_REWARDS])
```
```shell
>>> ChildGauge.reward_tokens(0)
'0x0000000000000000000000000000000000000000'
```
::::
### `reward_count`
::::description[`ChildGauge.reward_count() -> uint256: view`]
Getter for the number of reward tokens. This value is incremented by one for each new reward token added via `add_reward`.
Returns: number of reward tokens (`uint256`).
```vyper
reward_count: public(uint256)
```
```shell
>>> ChildGauge.reward_count()
0
```
::::
### `reward_integral_for`
::::description[`ChildGauge.reward_integral_for(_reward_token: address, _claiming_address: address) -> uint256: view`]
Getter for the reward integral for a specific reward token and claiming address.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_reward_token` | `address` | The reward token to get the reward integral for |
| `_claiming_address` | `address` | The address to get the reward integral for |
Returns: reward integral (`uint256`).
```vyper
# reward token -> claiming address -> integral
reward_integral_for: public(HashMap[address, HashMap[address, uint256]])
```
```shell
>>> ChildGauge.reward_integral_for('0x6B175474E89094C44Da98b954EedeAC495271d0F', '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')
0
```
::::
### `reward_remaining`
::::description[`ChildGauge.reward_remaining(_reward_token: address) -> uint256: view`]
Getter for the remaining reward for a specific reward token.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_reward_token` | `address` | The reward token to get the remaining reward for |
Returns: the remaining reward for a specific reward token (`uint256`).
```vyper
reward_remaining: public(HashMap[address, uint256]) # fixes bad precision
```
```shell
>>> ChildGauge.reward_remaining('0x6B175474E89094C44Da98b954EedeAC495271d0F')
0
```
::::
### `recover_remaining`
::::description[`ChildGauge.recover_remaining(_reward_token: address)`]
Function to recover remaining reward tokens that have not been distributed. Can only be called by the reward distributor.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_reward_token` | `address` | Reward token to recover |
```vyper
# For tracking external rewards
reward_count: public(uint256)
reward_data: public(HashMap[address, Reward])
reward_remaining: public(HashMap[address, uint256]) # fixes bad precision
@external
def recover_remaining(_reward_token: address):
"""
@notice Recover reward token remaining after calculation errors. Helpful for small decimal tokens.
Remaining tokens will be claimable in favor of distributor. Callable by anyone after reward distribution finished.
@param _reward_token The reward token being recovered
"""
self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address))
period_finish: uint256 = self.reward_data[_reward_token].period_finish
assert period_finish < block.timestamp
assert self.reward_data[_reward_token].last_update >= period_finish
assert ERC20(_reward_token).transfer(self.reward_data[_reward_token].distributor,
self.reward_remaining[_reward_token], default_return_value=True)
self.reward_remaining[_reward_token] = 0
@internal
def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _receiver: address):
"""
@notice Claim pending rewards and checkpoint rewards for a user
"""
user_balance: uint256 = 0
receiver: address = _receiver
if _user != empty(address):
user_balance = self.balanceOf[_user]
if _claim and _receiver == empty(address):
# if receiver is not explicitly declared, check if a default receiver is set
receiver = self.rewards_receiver[_user]
if receiver == empty(address):
# if no default receiver is set, direct claims to the user
receiver = _user
reward_count: uint256 = self.reward_count
for i in range(MAX_REWARDS):
if i == reward_count:
break
token: address = self.reward_tokens[i]
integral: uint256 = self.reward_data[token].integral
period_finish: uint256 = self.reward_data[token].period_finish
last_update: uint256 = min(block.timestamp, period_finish)
duration: uint256 = last_update - self.reward_data[token].last_update
if duration != 0 and _total_supply != 0:
self.reward_data[token].last_update = last_update
rate: uint256 = self.reward_data[token].rate
excess: uint256 = self.reward_remaining[token] - (period_finish - last_update + duration) * rate
integral_change: uint256 = (duration * rate + excess) * 10**18 / _total_supply
integral += integral_change
self.reward_data[token].integral = integral
# There is still calculation error in user's claimable amount,
# but it has 18-decimal precision through LP(_total_supply) – safe
self.reward_remaining[token] -= integral_change * _total_supply / 10**18
if _user != empty(address):
integral_for: uint256 = self.reward_integral_for[token][_user]
new_claimable: uint256 = 0
if integral_for < integral:
self.reward_integral_for[token][_user] = integral
new_claimable = user_balance * (integral - integral_for) / 10**18
claim_data: uint256 = self.claim_data[_user][token]
total_claimable: uint256 = (claim_data >> 128) + new_claimable
if total_claimable > 0:
total_claimed: uint256 = claim_data % 2**128
if _claim:
assert ERC20(token).transfer(receiver, total_claimable, default_return_value=True)
self.claim_data[_user][token] = total_claimed + total_claimable
elif new_claimable > 0:
self.claim_data[_user][token] = total_claimed + (total_claimable << 128)
```
```shell
>>> ChildGauge.recover_remaining('0x6B175474E89094C44Da98b954EedeAC495271d0F')
```
::::
---
## Depositing Rewards
The process for adding external reward tokens follows two steps:
1. **Add Reward Token**(`add_reward`)
- Registers a new reward token in the gauge
- Sets an authorized distributor address
- Only callable by gauge manager or factory admin
- Stores token data in `reward_data` mapping
2. **Deposit Rewards**(`deposit_reward_token`)
- Deposits reward tokens for distribution
- Only callable by the authorized distributor
- Distributes rewards linearly over specified period
- Updates reward data in storage
:::warning[External Rewards are not boostable!]
External rewards are separate from CRV emissions and are not subject to boost multipliers.
:::
### `add_reward`
::::description[`ChildGauge.add_reward(_reward_token: address, _distributor: address)`]
:::guard[Guarded Method]
This function is only callable by the `manager` of the gauge or the `owner` of the `ChildGaugeFactory`.
:::
Function to add a reward token for distribution. When calling this function, a distributor address must be set for the reward token. Only this distributor can deposit the reward token via the `deposit_reward_token` function.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_reward_token` | `address` | The reward token to add |
| `_distributor` | `address` | The distributor of the reward token |
Emits: `AddReward`, `SetDistributor` events.
```vyper
interface Factory:
def owner() -> address: view
def crv() -> ERC20: view
manager: public(address)
# For tracking external rewards
reward_count: public(uint256)
reward_data: public(HashMap[address, Reward])
reward_remaining: public(HashMap[address, uint256]) # fixes bad precision
@external
def add_reward(_reward_token: address, _distributor: address):
"""
@notice Add additional rewards to be distributed to stakers
@param _reward_token The token to add as an additional reward
@param _distributor Address permitted to fund this contract with the reward token
"""
assert msg.sender in [self.manager, FACTORY.owner()] # dev: only manager or factory admin
assert _reward_token != FACTORY.crv().address # dev: can not distinguish CRV reward from CRV emission
assert _distributor != empty(address) # dev: distributor cannot be zero address
reward_count: uint256 = self.reward_count
assert reward_count < MAX_REWARDS
assert self.reward_data[_reward_token].distributor == empty(address)
self.reward_data[_reward_token].distributor = _distributor
self.reward_tokens[reward_count] = _reward_token
self.reward_count = reward_count + 1
```
This example sets the distributor for the `crvUSD` reward token to `0x1234567890123456789012345678901234567890`. Only this address can deposit `crvUSD` to the gauge using the `deposit_reward_token` function.
```shell
>>> ChildGauge.add_reward('0x498Bf2B1e120FeD3ad3D42EA2165E9b73f99C1e5', '0x1234567890123456789012345678901234567890')
```
::::
### `set_reward_distributor`
::::description[`ChildGauge.set_reward_distributor(_reward_token: address, _distributor: address)`]
:::guard[Guarded Method]
This function is only callable by the current distributor of the reward token, the `owner` of the `ChildGaugeFactory`, or the `manager`.
:::
Function to reassign the reward distributor for a reward token.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_reward_token` | `address` | The reward token to reassign the distributor for |
| `_distributor` | `address` | The address of the new distributor |
Emits: `SetDistributor` event.
```vyper
# For tracking external rewards
reward_count: public(uint256)
reward_data: public(HashMap[address, Reward])
reward_remaining: public(HashMap[address, uint256]) # fixes bad precision
@external
def set_reward_distributor(_reward_token: address, _distributor: address):
"""
@notice Reassign the reward distributor for a reward token
@param _reward_token The reward token to reassign distribution rights to
@param _distributor The address of the new distributor
"""
current_distributor: address = self.reward_data[_reward_token].distributor
assert msg.sender in [current_distributor, FACTORY.owner(), self.manager]
assert current_distributor != empty(address)
assert _distributor != empty(address)
self.reward_data[_reward_token].distributor = _distributor
```
This example changes the distributor for the `crvUSD` reward token from `0x1234567890123456789012345678901234567890` to `0x9876543210987654321098765432109876543210`.
```shell
>>> ChildGauge.reward_data('0x498Bf2B1e120FeD3ad3D42EA2165E9b73f99C1e5')
{
'distributor': '0x1234567890123456789012345678901234567890',
'rate': 0,
'last_update': 0,
'period_finish': 0,
'integral': 0
}
>>> ChildGauge.set_reward_distributor('0x498Bf2B1e120FeD3ad3D42EA2165E9b73f99C1e5', '0x9876543210987654321098765432109876543210')
>>> ChildGauge.reward_data('0x498Bf2B1e120FeD3ad3D42EA2165E9b73f99C1e5')
{
'distributor': '0x9876543210987654321098765432109876543210',
'rate': 0,
'last_update': 0,
'period_finish': 0,
'integral': 0
}
```
::::
### `deposit_reward_token`
::::description[`ChildGauge.deposit_reward_token(_reward_token: address, _amount: uint256, _epoch: uint256 = WEEK)`]
:::guard[Guarded Method]
This function is only callable by the authorized `distributor` of the reward token.
:::
Function to deposit a reward token for distribution.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_reward_token` | `address` | The reward token to deposit |
| `_amount` | `uint256` | The amount of the reward token to deposit |
| `_epoch` | `uint256` | The duration the rewards are distributed across. Between 3 days and a year, week by default |
```vyper
# For tracking external rewards
reward_count: public(uint256)
reward_data: public(HashMap[address, Reward])
reward_remaining: public(HashMap[address, uint256]) # fixes bad precision
@external
@nonreentrant("lock")
def deposit_reward_token(_reward_token: address, _amount: uint256, _epoch: uint256 = WEEK):
"""
@notice Deposit a reward token for distribution
@param _reward_token The reward token being deposited
@param _amount The amount of `_reward_token` being deposited
@param _epoch The duration the rewards are distributed across. Between 3 days and a year, week by default
"""
assert msg.sender == self.reward_data[_reward_token].distributor
assert 3 * WEEK / 7 <= _epoch and _epoch <= WEEK * 4 * 12, "Epoch duration"
self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address))
# transferFrom reward token and use transferred amount henceforth:
amount_received: uint256 = ERC20(_reward_token).balanceOf(self)
assert ERC20(_reward_token).transferFrom(
msg.sender,
self,
_amount,
default_return_value=True
)
amount_received = ERC20(_reward_token).balanceOf(self) - amount_received
total_amount: uint256 = amount_received + self.reward_remaining[_reward_token]
self.reward_data[_reward_token].rate = total_amount / _epoch
self.reward_remaining[_reward_token] = total_amount
self.reward_data[_reward_token].last_update = block.timestamp
self.reward_data[_reward_token].period_finish = block.timestamp + _epoch
```
This example deposits `10,000` `crvUSD` tokens as rewards over `7` days.
```shell
>>> ChildGauge.deposit_reward_token('0x498Bf2B1e120FeD3ad3D42EA2165E9b73f99C1e5', 10000000000000000000000, 604800)
```
::::
### `manager`
::::description[`ChildGauge.manager() -> address: view`]
Getter for the gauge manager address. The manager address is set during initialization and can be changed by the `owner` of the factory.
Returns: the gauge manager address (`address`).
```vyper
manager: public(address)
```
This example returns the manager of the gauge, which is `0x1234567890123456789012345678901234567890`.
```shell
>>> ChildGauge.manager()
'0x1234567890123456789012345678901234567890'
```
::::
### `set_gauge_manager`
::::description[`ChildGauge.set_gauge_manager(_gauge_manager: address)`]
:::guard[Guarded Method]
This function is only callable by the `manager` of the gauge or the `owner` of the `ChildGaugeFactory`.
:::
Function to set the gauge manager.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_gauge_manager` | `address` | The address to set as the new manager of the gauge |
Emits: `SetGaugeManager` event.
```vyper
interface Factory:
def owner() -> address: view
event SetGaugeManager:
_gauge_manager: address
manager: public(address)
@external
def set_gauge_manager(_gauge_manager: address):
"""
@notice Change the gauge manager for a gauge
@dev The manager of this contract, or the ownership admin can outright modify gauge
managership. A gauge manager can also transfer managership to a new manager via this
method, but only for the gauge which they are the manager of.
@param _gauge_manager The account to set as the new manager of the gauge.
"""
assert msg.sender in [self.manager, FACTORY.owner()] # dev: only manager or factory admin
self.manager = _gauge_manager
log SetGaugeManager(_gauge_manager)
```
This example changes the manager of the gauge from `0x1234567890123456789012345678901234567890` to `0x9876543210987654321098765432109876543210`.
```shell
>>> ChildGauge.manager()
'0x1234567890123456789012345678901234567890'
>>> ChildGauge.set_gauge_manager('0x9876543210987654321098765432109876543210')
>>> ChildGauge.manager()
'0x9876543210987654321098765432109876543210'
```
::::
### `set_manager`
::::description[`ChildGauge.set_manager(_gauge_manager: address)`]
:::guard[Guarded Method]
This function is only callable by the `manager` of the gauge or the `owner` of the Factory.
:::
Function to set the manager for the gauge. This function is a copy of the `set_gauge_manager` function for back-compatability.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_gauge_manager` | `address` | The address to set as the new manager of the gauge |
Emits: `SetGaugeManager` event.
```vyper
event SetGaugeManager:
_gauge_manager: address
manager: public(address)
@external
def set_manager(_gauge_manager: address):
"""
@notice Change the gauge manager for a gauge
@dev Copy of `set_gauge_manager` for back-compatability
@dev The manager of this contract, or the ownership admin can outright modify gauge
managership. A gauge manager can also transfer managership to a new manager via this
method, but only for the gauge which they are the manager of.
@param _gauge_manager The account to set as the new manager of the gauge.
"""
assert msg.sender in [self.manager, FACTORY.owner()] # dev: only manager or factory admin
self.manager = _gauge_manager
log SetGaugeManager(_gauge_manager)
```
This example changes the manager of the gauge from `0x1234567890123456789012345678901234567890` to `0x9876543210987654321098765432109876543210`. It has the same effect as the `set_gauge_manager` function.
```shell
>>> ChildGauge.manager()
'0x1234567890123456789012345678901234567890'
>>> ChildGauge.set_manager('0x9876543210987654321098765432109876543210')
>>> ChildGauge.manager()
'0x9876543210987654321098765432109876543210'
```
::::
---
## Checkpoints and Boosting
For more information on how boosting works, please refer to the [Boosting Explainer](./overview.md#boosting-on-sidechains) page.
### `user_checkpoint`
::::description[`ChildGauge.user_checkpoint(addr: address) -> bool`]
Function to record a checkpoint for a user.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `addr` | `address` | The user address to record a checkpoint for |
Returns: `True` if the checkpoint was recorded successfully (`bool`).
```vyper
@external
def user_checkpoint(addr: address) -> bool:
"""
@notice Record a checkpoint for `addr`
@param addr User address
@return bool success
"""
assert msg.sender in [addr, FACTORY.address] # dev: unauthorized
self._checkpoint(addr)
self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply)
return True
@internal
def _checkpoint(_user: address):
"""
@notice Checkpoint a user calculating their CRV entitlement
@param _user User address
"""
period: int128 = self.period
period_time: uint256 = self.period_timestamp[period]
integrate_inv_supply: uint256 = self.integrate_inv_supply[period]
if block.timestamp > period_time:
working_supply: uint256 = self.working_supply
prev_week_time: uint256 = period_time
week_time: uint256 = min((period_time + WEEK) / WEEK * WEEK, block.timestamp)
for i in range(256):
dt: uint256 = week_time - prev_week_time
if working_supply != 0:
# we don't have to worry about crossing inflation epochs
# and if we miss any weeks, those weeks inflation rates will be 0 for sure
# but that means no one interacted with the gauge for that long
integrate_inv_supply += self.inflation_rate[prev_week_time / WEEK] * 10 ** 18 * dt / working_supply
if week_time == block.timestamp:
break
prev_week_time = week_time
week_time = min(week_time + WEEK, block.timestamp)
# check CRV balance and increase weekly inflation rate by delta for the rest of the week
crv: ERC20 = FACTORY.crv()
if crv != empty(ERC20):
crv_balance: uint256 = crv.balanceOf(self)
if crv_balance != 0:
current_week: uint256 = block.timestamp / WEEK
self.inflation_rate[current_week] += crv_balance / ((current_week + 1) * WEEK - block.timestamp)
crv.transfer(FACTORY.address, crv_balance)
period += 1
self.period = period
self.period_timestamp[period] = block.timestamp
self.integrate_inv_supply[period] = integrate_inv_supply
working_balance: uint256 = self.working_balances[_user]
self.integrate_fraction[_user] += working_balance * (integrate_inv_supply - self.integrate_inv_supply_of[_user]) / 10 ** 18
self.integrate_inv_supply_of[_user] = integrate_inv_supply
self.integrate_checkpoint_of[_user] = block.timestamp
@internal
def _update_liquidity_limit(_user: address, _user_balance: uint256, _total_supply: uint256):
"""
@notice Calculate working balances to apply amplification of CRV production.
@dev https://resources.curve.fi/guides/boosting-your-crv-rewards#formula
@param _user The user address
@param _user_balance User's amount of liquidity (LP tokens)
@param _total_supply Total amount of liquidity (LP tokens)
"""
working_balance: uint256 = _user_balance * TOKENLESS_PRODUCTION / 100
ve: address = self.voting_escrow
if ve != empty(address):
ve_ts: uint256 = ERC20(ve).totalSupply()
if ve_ts != 0:
working_balance += _total_supply * ERC20(ve).balanceOf(_user) / ve_ts * (100 - TOKENLESS_PRODUCTION) / 100
working_balance = min(_user_balance, working_balance)
old_working_balance: uint256 = self.working_balances[_user]
self.working_balances[_user] = working_balance
working_supply: uint256 = self.working_supply + working_balance - old_working_balance
self.working_supply = working_supply
log UpdateLiquidityLimit(_user, _user_balance, _total_supply, working_balance, working_supply)
```
```shell
>>> ChildGauge.user_checkpoint('0x20a440aECf78c73d484B652C46d582B4D70906A8')
True
```
::::
### `integrate_checkpoint`
::::description[`ChildGauge.integrate_checkpoint() -> uint256: view`]
Getter for the timestamp of the last checkpoint.
Returns: timestamp of the last checkpoint (`uint256`).
```vyper
# The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint
# All values are kept in units of being multiplied by 1e18
period: public(int128)
period_timestamp: public(HashMap[int128, uint256])
@view
@external
def integrate_checkpoint() -> uint256:
"""
@notice Get the timestamp of the last checkpoint
"""
return self.period_timestamp[self.period]
```
```shell
>>> ChildGauge.integrate_checkpoint()
1729778435
```
::::
### `integrate_checkpoint_of`
::::description[`ChildGauge.integrate_checkpoint_of(_user: address) -> uint256: view`]
Getter for the timestamp of the last checkpoint for a user.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_user` | `address` | The user address to get the integrate checkpoint for |
Returns: timestamp of the last checkpoint (`uint256`).
```vyper
# 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint
integrate_inv_supply_of: public(HashMap[address, uint256])
integrate_checkpoint_of: public(HashMap[address, uint256])
```
```shell
>>> ChildGauge.integrate_checkpoint_of('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')
1700000000
```
::::
### `working_balances`
::::description[`ChildGauge.working_balances(_user: address) -> uint256: view`]
Getter for the working balances of a user. This represents the effective liquidity of a user, which is used to calculate the CRV rewards they are entitled to. Essentially, it's the boosted balance of a user if they have some veCRV. If a user has no boost at all, their working_balance will be 40% of their LP tokens. If the position is fully boosted (2.5x), their working_balance will be equal to their LP tokens.
*For example:*
- 1 LP token with no boost = working_balances(user) = 0.4
- 1 LP token with 1.5 boost = working_balances(user) = 1.5
- 1 LP token with 2.5 boost = working_balances(user) = 2.5
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_user` | `address` | The user address to get the working balance for |
Returns: working balance of a user (`uint256`).
```vyper
working_balances: public(HashMap[address, uint256])
```
```shell
>>> ChildGauge.working_balances('0x20a440aECf78c73d484B652C46d582B4D70906A8')
106163327646490
```
::::
### `working_supply`
::::description[`ChildGauge.working_supply() -> uint256: view`]
Getter for the working supply. This variable represents the sum of all `working_balances` of users who provided liquidity in the gauge.
Returns: working supply (`uint256`).
```vyper
working_supply: public(uint256)
```
The working supply in our example is equal to the working_balance of `0x20a440aECf78c73d484B652C46d582B4D70906A8` because its the only user that has provided liquidity so far.
```shell
>>> ChildGauge.working_supply()
106163327646490
```
::::
### `period`
::::description[`ChildGauge.period() -> int128: view`]
:::info
The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint. All values are kept in units of being multiplied by 1e18.
:::
Getter for the current period.
Returns: the current period (`int128`).
```vyper
# The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint
# All values are kept in units of being multiplied by 1e18
period: public(int128)
```
Period is one, because only one checkpoint has been recorded so far (when depositing liquidity).
```shell
>>> ChildGauge.period()
1
```
::::
### `period_timestamp`
::::description[`ChildGauge.period_timestamp(_period: int128) -> uint256: view`]
Getter for the period timestamp for a specific period.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_period` | `int128` | The period to get the timestamp for |
Returns: the period timestamp for a specific period (`uint256`).
```vyper
period_timestamp: public(HashMap[int128, uint256])
```
This example returns the timestamp of the first period, which is the timestamp of the first checkpoint (when depositing liquidity).
```shell
>>> ChildGauge.period_timestamp(1)
1729778435 # exactly the timestamp of the first checkpoint which was the deposit of liquidity
```
::::
### `integrate_fraction`
::::description[`ChildGauge.integrate_fraction(_user: address) -> uint256: view`]
Getter for the total amount of CRV, both mintable and already minted, that has been allocated to `_user` from this gauge.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_user` | `address` | The user address to get the integrate fraction for |
Returns: integral of accrued rewards (`uint256`).
```vyper
# ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint
# Units: rate * t = already number of coins per address to issue
integrate_fraction: public(HashMap[address, uint256])
```
```shell
>>> ChildGauge.integrate_fraction('0x20a440aECf78c73d484B652C46d582B4D70906A8')
0
```
::::
### `claimable_tokens`
::::description[`ChildGauge.claimable_tokens(addr: address) -> uint256`]
Function to get the number of claimable CRV emissions for a user.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `addr` | `address` | The address to get the number of claimable CRV emissions for |
Returns: the number of claimable CRV emissions for a user (`uint256`).
```vyper
# ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint
# Units: rate * t = already number of coins per address to issue
integrate_fraction: public(HashMap[address, uint256])
@external
def claimable_tokens(addr: address) -> uint256:
"""
@notice Get the number of claimable tokens per user
@dev This function should be manually changed to "view" in the ABI
@return uint256 number of claimable tokens per user
"""
self._checkpoint(addr)
return self.integrate_fraction[addr] - FACTORY.minted(addr, self)
@internal
def _checkpoint(_user: address):
"""
@notice Checkpoint a user calculating their CRV entitlement
@param _user User address
"""
period: int128 = self.period
period_time: uint256 = self.period_timestamp[period]
integrate_inv_supply: uint256 = self.integrate_inv_supply[period]
if block.timestamp > period_time:
working_supply: uint256 = self.working_supply
prev_week_time: uint256 = period_time
week_time: uint256 = min((period_time + WEEK) / WEEK * WEEK, block.timestamp)
for i in range(256):
dt: uint256 = week_time - prev_week_time
if working_supply != 0:
# we don't have to worry about crossing inflation epochs
# and if we miss any weeks, those weeks inflation rates will be 0 for sure
# but that means no one interacted with the gauge for that long
integrate_inv_supply += self.inflation_rate[prev_week_time / WEEK] * 10 ** 18 * dt / working_supply
if week_time == block.timestamp:
break
prev_week_time = week_time
week_time = min(week_time + WEEK, block.timestamp)
# check CRV balance and increase weekly inflation rate by delta for the rest of the week
crv: ERC20 = FACTORY.crv()
if crv != empty(ERC20):
crv_balance: uint256 = crv.balanceOf(self)
if crv_balance != 0:
current_week: uint256 = block.timestamp / WEEK
self.inflation_rate[current_week] += crv_balance / ((current_week + 1) * WEEK - block.timestamp)
crv.transfer(FACTORY.address, crv_balance)
period += 1
self.period = period
self.period_timestamp[period] = block.timestamp
self.integrate_inv_supply[period] = integrate_inv_supply
working_balance: uint256 = self.working_balances[_user]
self.integrate_fraction[_user] += working_balance * (integrate_inv_supply - self.integrate_inv_supply_of[_user]) / 10 ** 18
self.integrate_inv_supply_of[_user] = integrate_inv_supply
self.integrate_checkpoint_of[_user] = block.timestamp
```
This example returns the number of claimable CRV emissions for `0x20a440aECf78c73d484B652C46d582B4D70906A8`, which currently is `0`.
```shell
>>> ChildGauge.claimable_tokens('0x20a440aECf78c73d484B652C46d582B4D70906A8')
0
```
::::
### `inflation_rate`
::::description[`ChildGauge.inflation_rate(_period: uint256) -> uint256: view`]
Getter for the CRV emission inflation rate for a specific week.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_period` | `uint256` | The week to get the inflation rate for |
Returns: CRV emission inflation rate (`uint256`).
```vyper
inflation_rate: public(HashMap[uint256, uint256])
```
This example returns the CRV emission inflation rate for the first week, which is `0`.
```shell
>>> ChildGauge.inflation_rate(1)
0
```
::::
### `integrate_inv_supply`
::::description[`ChildGauge.integrate_inv_supply(_period: int128) -> uint256: view`]
Getter for the inverse supply of CRV at a given period that tracks a cumulative measure of inverse supply over time in relation to the CRV emissions.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_period` | `int128` | The period to get the inverse supply for |
Returns: inverse supply of CRV at a given period (`uint256`).
```vyper
# 1e18 * ∫(rate(t) / totalSupply(t) dt) from 0 till checkpoint
integrate_inv_supply: public(HashMap[int128, uint256])
```
This example returns the inverse supply of CRV at the first period, which is `0`.
```shell
>>> ChildGauge.integrate_inv_supply(1)
0
```
::::
### `integrate_inv_supply_of`
::::description[`ChildGauge.integrate_inv_supply_of(_user: address) -> uint256: view`]
The integrate_inv_supply_of variable is a mapping (HashMap[address, uint256]) that stores a user-specific cumulative measure of inverse supply up to the last checkpoint for each user. It is used to calculate the individual CRV emissions that a user is entitled to based on their participation in the gauge.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_user` | `address` | The user address to get the inverse supply for |
Returns: inverse supply of CRV at a given period for a user (`uint256`).
```vyper
# 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint
integrate_inv_supply_of: public(HashMap[address, uint256])
integrate_checkpoint_of: public(HashMap[address, uint256])
```
This example returns the inverse supply of CRV at the first period for `0x20a440aECf78c73d484B652C46d582B4D70906A8`, which is `0`.
```shell
>>> ChildGauge.integrate_inv_supply_of('0x20a440aECf78c73d484B652C46d582B4D70906A8')
0
```
::::
---
## RootGauge and VotingEscrow
### `root_gauge`
::::description[`ChildGauge.root_gauge() -> address: view`]
Getter for the root gauge address on Ethereum.
Returns: root gauge address (`address`).
```vyper
root_gauge: public(address)
```
```shell
>>> ChildGauge.root_gauge()
'0x12C3F630ec8f8A07C539b5F933e8E62F9b627396'
```
::::
### `set_root_gauge`
::::description[`ChildGauge.set_root_gauge(_root: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` or `manager` of the `ChildGaugeFactory`.
:::
Function to set the root gauge address in case something went wrong (e.g. between implementation updates).
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_root` | `address` | The root gauge address to set |
```vyper
root_gauge: public(address)
@external
def set_root_gauge(_root: address):
"""
@notice Set Root contract in case something went wrong (e.g. between implementation updates)
@param _root Root gauge to set
"""
assert msg.sender in [FACTORY.owner(), FACTORY.manager()]
assert _root != empty(address)
self.root_gauge = _root
```
```shell
>>> ChildGauge.root_gauge()
'0x12C3F630ec8f8A07C539b5F933e8E62F9b627396'
>>> ChildGauge.set_root_gauge('0x12C3F630ec8f8A07C539b5F933e8E62F9b627396')
>>> ChildGauge.root_gauge()
'0x12C3F630ec8f8A07C539b5F933e8E62F9b627396'
```
::::
### `voting_escrow`
::::description[`ChildGauge.voting_escrow() -> address: view`]
Getter for the voting escrow contract on the specific chain. If this variable is not set, boosting LP positions will not work. If boosting works, the `voting_escrow` variable will be set to a L2 Voting Escrow Oracle contract, which validates the user's veCRV balance from Ethereum mainnet. This value mirrors the voting escrow contract set on the `ChildGaugeFactory`. If the `ChildGaugeFactory` voting escrow contract is updated, the `ChildGauge` voting escrow contract can be updated by calling the `update_voting_escrow()` function.
Returns: voting escrow contract (`address`).
```vyper
voting_escrow: public(address)
```
```shell
>>> ChildGauge.voting_escrow()
'0x0000000000000000000000000000000000000000'
```
::::
### `update_voting_escrow`
::::description[`ChildGauge.update_voting_escrow()`]
Function to update the voting escrow contract to the voting escrow contract set in the factory. This function is callable by anyone.
```vyper
interface Factory:
def voting_escrow() -> address: view
@external
def update_voting_escrow():
"""
@notice Update the voting escrow contract in storage
"""
self.voting_escrow = FACTORY.voting_escrow()
```
This example shows the following: The gauge has been deployed without a voting escrow contract to be set in the `ChildGaugeFactory`. Therefore, the voting escrow address is `0x0000000000000000000000000000000000000000`. After the voting escrow contract has been set in the `ChildGaugeFactory`, the `update_voting_escrow()` function is called, and the voting escrow address is set to mirror the voting escrow contract set in the `ChildGaugeFactory`.
```shell
>>> ChildGauge.voting_escrow()
'0x0000000000000000000000000000000000000000'
>>> ChildGauge.update_voting_escrow()
>>> ChildGauge.voting_escrow()
'0xc73e8d8f7A68Fc9d67e989250484E57Ae03a5Da3'
```
::::
---
## Killing Gauges
### `is_killed`
::::description[`ChildGauge.is_killed() -> bool: view`]
Getter to check if the gauge is killed.
Returns: killed status (`bool`).
```vyper
is_killed: public(bool)
```
```shell
>>> ChildGauge.is_killed()
False
```
::::
### `set_killed`
::::description[`ChildGauge.set_killed(_is_killed: bool)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the `ChildGaugeFactory`.
:::
Function to set the killed status for the gauge.
| Input | Type | Description |
| ------------ | ------ | ----------- |
| `_is_killed` | `bool` | The killed status to set |
Emits: `SetKilled` event.
```vyper
is_killed: public(bool)
@external
def set_killed(_is_killed: bool):
"""
@notice Set the killed status for this contract
@dev Nothing happens, just stop emissions and that's it
@param _is_killed Killed status to set
"""
assert msg.sender == FACTORY.owner() # dev: only owner
self.is_killed = _is_killed
```
```shell
>>> ChildGauge.is_killed()
False
>>> ChildGauge.set_killed(True)
>>> ChildGauge.is_killed()
True
```
::::
---
## ERC20 and Other Methods
The contract inherits the ERC20 interface and follows the standard ERC20 methods. These methods are not further documented here. Some notable methods are documented below.
### `totalSupply`
::::description[`ChildGauge.totalSupply() -> uint256: view`]
Getter for the total supply of the gauge.
Returns: total supply (`uint256`).
```vyper
totalSupply: public(uint256)
```
```shell
>>> ChildGauge.totalSupply()
1000000000000000000000000
```
::::
### `factory`
::::description[`ChildGauge.factory() -> Factory: view`]
Getter for the `ChildGaugeFactory` contract.
Returns: `ChildGaugeFactory` contract (`Factory`).
```vyper
interface Factory:
def owner() -> address: view
def manager() -> address: view
def voting_escrow() -> address: view
def minted(_user: address, _gauge: address) -> uint256: view
def crv() -> ERC20: view
FACTORY: immutable(Factory)
```
```shell
>>> ChildGauge.factory()
'0x0B8D6B6CeFC7Aa1C2852442e518443B1b22e1C52'
```
::::
### `lp_token`
::::description[`ChildGauge.lp_token() -> address: view`]
Getter for the LP token address.
Returns: LP token contract (`address`).
```vyper
interface ERC20Extended:
def symbol() -> String[32]: view
lp_token: public(address)
@external
def initialize(_lp_token: address, _root: address, _manager: address):
assert self.lp_token == empty(address) # dev: already initialized
self.lp_token = _lp_token
self.root_gauge = _root
self.manager = _manager
self.voting_escrow = Factory(msg.sender).voting_escrow()
symbol: String[32] = ERC20Extended(_lp_token).symbol()
name: String[64] = concat("Curve.fi ", symbol, " Gauge Deposit")
self.name = name
self.symbol = concat(symbol, "-gauge")
self.period_timestamp[0] = block.timestamp
self.DOMAIN_SEPARATOR = keccak256(
_abi_encode(
EIP712_TYPEHASH,
keccak256(name),
keccak256(VERSION),
chain.id,
self
)
)
```
```shell
>>> ChildGauge.lp_token()
'0xF25E1dB1f0c7BD1a29761a1FcDaE187B8718CF18'
```
::::
### `version`
::::description[`ChildGauge.version() -> String[8]: view`]
Getter for the version of the gauge contract.
Returns: version (`String[8]`).
```vyper
VERSION: constant(String[8]) = "1.1.0"
@pure
@external
def version() -> String[8]:
"""
@notice Get the version of this gauge contract
"""
return VERSION
```
```shell
>>> ChildGauge.version()
"1.1.0"
```
::::
---
## Curve Crosschain Gauges
Due to the x-chain gauge system, Curve allows to deploy liquidity gauges on alternate chains which are eligible for receiving CRV emissions and boosts.
In order for a sidechain gauge to receive CRV emissions, the system uses a two-gauge approach:
- A **Root Gauge**, which is deployed on Ethereum and acts as the parent gauge for a child gauge deployed on other chains. This is the gauge that can be added to the `GaugeController` and is eligible to receive voting weight and therefore CRV emissions. Once a root gauge receives some weight and therefore CRV emissions, it can mint the according CRV emissions and transmit them to the child gauge on the target chain. All this is done in a permissionless way allowing anyone to transmit the CRV emissions to the child gauge.
- A **Child Gauge** containing the standard logic of a Curve liquidity gauge on Ethereum, which is deployed on alternate chains and acts as the child gauge for the root gauge on Ethereum.
---
## Smart Contracts
The cross-chain gauge factory requires components to be deployed both on Ethereum and on an alternate EVM compatible network.
Main contract for deploying root gauges on Ethereum. Also serves as a registry for finding deployed gauges and the bridge wrapper contracts used to bridge CRV emissions to alternate chains.
Main contract for deploying child gauges on alternate chains. Also serves as a registry for finding deployed gauges and as a pseudo CRV minter where users can claim CRV emissions.
The implementation used for root gauges deployed on Ethereum.
The implementation used for child gauges deployed on alternate chains.
Contracts used to bridge CRV emissions across chains. Bridge wrappers adhere to a specific interface and allow for a modular bridging system.
Contract used to transmit veCRV information across chains to a `L2 VotingEscrow Oracle`.
Contract used to store veCRV information on child chains.
---
## Boosting on Sidechains
Before reading this section, it is recommended to understand how boosting works in general.
:::warning[Crosschain Boosts]
This system is farily new and is not rolled out on every chain yet. Crosschain boosts only work if there is a `L2 VotingEscrow Oracle` set in the `ChildGaugeFactory` for the child chain.
:::
Because the `VotingEscrow`, where CRV are locked and user's veCRV informations are stored, is only deployed on Ethereum, a novel system was created to make crosschain boosts possible.
The idea of the system is pretty straight forward: an `Updater` contract on Ethereum queries the veCRV information of a user from the `VotingEscrow` contract on Ethereum and transmits the information to a `L2 VotingEscrow Oracle` on the child chain. This way, boosts on sidechains can be calculated using the veCRV information from Ethereum.
:::tip[L2 VotingEscrow Oracle Example for Fraxtal]
The [`Updater`](https://etherscan.io/address/0xc73e8d8f7A68Fc9d67e989250484E57Ae03a5Da3) contract on Ethereum makes use of the `update` function to query and transmit the veCRV information of a user from the [`VotingEscrow`](https://etherscan.io/address/0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2) on Ethereum to the [`L2 VotingEscrow Oracle`](https://fraxscan.com/address/0xc73e8d8f7A68Fc9d67e989250484E57Ae03a5Da3) on Fraxtal. For messaging, the [`Fraxtal: L1 Cross Domain Messenger Proxy`](https://etherscan.io/address/0x126bcc31bc076b3d515f60fbc81fdde0b0d542ed) is used to send the message. To relay the message, the [`Fraxtal: Cross Domain Messenger`](https://fraxscan.com/address/0x4200000000000000000000000000000000000007) is used.
```vyper
# @version 0.3.10
"""
@title Updater
"""
interface OVMMessenger:
def sendMessage(_target: address, _data: Bytes[1024], _gas_limit: uint32): nonpayable
interface OVMChain:
def enqueueL2GasPrepaid() -> uint32: view
interface VotingEscrow:
def epoch() -> uint256: view
def point_history(_idx: uint256) -> Point: view
def user_point_epoch(_user: address) -> uint256: view
def user_point_history(_user: address, _idx: uint256) -> Point: view
def locked(_user: address) -> LockedBalance: view
def slope_changes(_ts: uint256) -> int128: view
struct LockedBalance:
amount: int128
end: uint256
struct Point:
bias: int128
slope: int128
ts: uint256
blk: uint256
WEEK: constant(uint256) = 86400 * 7
VOTING_ESCROW: public(constant(address)) = 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2
ovm_chain: public(address) # CanonicalTransactionChain
ovm_messenger: public(address) # CrossDomainMessenger
@external
def __init__(_ovm_chain: address, _ovm_messenger: address):
self.ovm_chain = _ovm_chain
self.ovm_messenger = _ovm_messenger
@external
def update(_user: address = msg.sender, _gas_limit: uint32 = 0):
# https://community.optimism.io/docs/developers/bridge/messaging/#for-l1-%E2%87%92-l2-transactions
gas_limit: uint32 = _gas_limit
if gas_limit == 0:
gas_limit = OVMChain(self.ovm_chain).enqueueL2GasPrepaid()
epoch: uint256 = VotingEscrow(VOTING_ESCROW).epoch()
point_history: Point = VotingEscrow(VOTING_ESCROW).point_history(epoch)
user_point_epoch: uint256 = VotingEscrow(VOTING_ESCROW).user_point_epoch(_user)
user_point_history: Point = VotingEscrow(VOTING_ESCROW).user_point_history(_user, user_point_epoch)
locked: LockedBalance = VotingEscrow(VOTING_ESCROW).locked(_user)
start_time: uint256 = WEEK + (point_history.ts / WEEK) * WEEK
slope_changes: int128[12] = empty(int128[12])
for i in range(12):
slope_changes[i] = VotingEscrow(VOTING_ESCROW).slope_changes(start_time + WEEK * i)
OVMMessenger(self.ovm_messenger).sendMessage(
self,
_abi_encode(
_user,
epoch,
point_history,
user_point_epoch,
user_point_history,
locked,
slope_changes,
method_id=method_id("update(address,uint256,(int128,int128,uint256,uint256),uint256,(int128,int128,uint256,uint256),(int128,uint256),int128[12])")
),
gas_limit
)
```
:::
---
## Deploying a Sidechain Gauge
A sidechain gauge can be deployed by calling the `deploy_gauge` function of the `ChildGaugeFactory` on the respective chain. This creates a minimal proxy using Vyper’s built-in [`create_from_minimal_proxy`](https://docs.vyperlang.org/en/stable/built-in-functions.html#create_minimal_proxy_to) function, which points to the `ChildGauge` implementation and initializes the `ChildGauge` with the provided parameters, such as LP token, salt, and manager.
:::info[Deploying a RootGauge AFTER deploying a ChildGauge]
There is no specific order in which root and child gauges must be deployed, and deploying a root gauge is optional. It is perfectly fine to deploy only child gauges. In this case, the child gauge will not be linked to any root gauge and therefore will not be eligible to receive any CRV emissions (if the root gauge is added to the `GaugeController`).
It does not matter if a root gauge is deployed before or after the child gauge. However, to link the root gauge to the child gauge, the root gauge must be deployed using the same `salt` as the child gauge.
:::
Additionally, a sidechain gauge can also be deployed directly from the `RootGaugeFactory` on Ethereum. This is achieved using a `call_proxy`, which acts as an intermediary contract to facilitate cross-chain calls. **Currently, this feature is not enabled, and the `call_proxy` contract has not been set.**---
## Killing Sidechain Gauges
Killing a gauge essentially means cutting off all CRV emissions to the gauge by setting the inflation rate to 0.
Although each sidechain gauge has an `is_killed` variable and a `set_killed` function to modify its killed status, these do not affect the gauge directly. To kill sidechain gauges, the root gauge must be killed. This is done by setting the `is_killed` variable to `True` by calling the `set_killed` function. Only the `owner`, which is controlled by the CurveDAO and the EmergencyDAO of the `RootGaugeFactory`, can call this function.[^1]
[^1]: The `owner` of the `RootGaugeFactory` is set to a proxy contract controlled by the CurveDAO and EmergencyDAO.
---
## RootGaugeFactory
The `RootGaugeFactory` contract is used to deploy liquidity gauges on the Ethereum mainnet. These gauges can then be voted on to be added to the `GaugeController`. If successful, the gauges will be able to receive CRV emissions, which then can be bridged via a `Bridger` contract to the child chains `ChildGauge`.
:::vyper[`RootGaugeFactory.vy`]
The source code for the `RootGaugeFactory.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-xchain-factory/blob/master/contracts/RootGaugeFactory.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
The contract is deployed on :logos-ethereum: Ethereum at [`0x306A45a1478A000dC701A6e1f7a569afb8D9DCD6`](https://etherscan.io/address/0x306A45a1478A000dC701A6e1f7a569afb8D9DCD6).
```json
[{"name":"ChildUpdated","inputs":[{"name":"_chain_id","type":"uint256","indexed":true},{"name":"_new_bridger","type":"address","indexed":false},{"name":"_new_factory","type":"address","indexed":false},{"name":"_new_implementation","type":"address","indexed":false}],"anonymous":false,"type":"event"},{"name":"DeployedGauge","inputs":[{"name":"_implementation","type":"address","indexed":true},{"name":"_chain_id","type":"uint256","indexed":true},{"name":"_deployer","type":"address","indexed":true},{"name":"_salt","type":"bytes32","indexed":false},{"name":"_gauge","type":"address","indexed":false}],"anonymous":false,"type":"event"},{"name":"TransferOwnership","inputs":[{"name":"_old_owner","type":"address","indexed":false},{"name":"_new_owner","type":"address","indexed":false}],"anonymous":false,"type":"event"},{"name":"UpdateCallProxy","inputs":[{"name":"_old_call_proxy","type":"address","indexed":false},{"name":"_new_call_proxy","type":"address","indexed":false}],"anonymous":false,"type":"event"},{"name":"UpdateImplementation","inputs":[{"name":"_old_implementation","type":"address","indexed":false},{"name":"_new_implementation","type":"address","indexed":false}],"anonymous":false,"type":"event"},{"stateMutability":"nonpayable","type":"constructor","inputs":[{"name":"_call_proxy","type":"address"},{"name":"_owner","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"transmit_emissions","inputs":[{"name":"_gauge","type":"address"}],"outputs":[]},{"stateMutability":"payable","type":"function","name":"deploy_gauge","inputs":[{"name":"_chain_id","type":"uint256"},{"name":"_salt","type":"bytes32"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"nonpayable","type":"function","name":"deploy_child_gauge","inputs":[{"name":"_chain_id","type":"uint256"},{"name":"_lp_token","type":"address"},{"name":"_salt","type":"bytes32"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"deploy_child_gauge","inputs":[{"name":"_chain_id","type":"uint256"},{"name":"_lp_token","type":"address"},{"name":"_salt","type":"bytes32"},{"name":"_manager","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_child","inputs":[{"name":"_chain_id","type":"uint256"},{"name":"_bridger","type":"address"},{"name":"_child_factory","type":"address"},{"name":"_child_impl","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_implementation","inputs":[{"name":"_implementation","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_call_proxy","inputs":[{"name":"_call_proxy","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"commit_transfer_ownership","inputs":[{"name":"_future_owner","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"accept_transfer_ownership","inputs":[],"outputs":[]},{"stateMutability":"view","type":"function","name":"version","inputs":[],"outputs":[{"name":"","type":"string"}]},{"stateMutability":"view","type":"function","name":"call_proxy","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_bridger","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_child_factory","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_child_implementation","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_implementation","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_gauge","inputs":[{"name":"arg0","type":"uint256"},{"name":"arg1","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_gauge_count","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"is_valid_gauge","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"owner","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"future_owner","inputs":[],"outputs":[{"name":"","type":"address"}]}]
```
:::
---
## Deploying Gauges
The `RootGaugeFactory` allows the deployment of root gauges on Ethereum and child gauges on the child chains. Root gauges can only be deployed if there is a `bridger` contract set for the given chain ID, otherwise the chain is not supported.
:::info[Supported Chains]
If `get_bridger(chain_id)` returns a non-zero address, the chain is supported and a `RootGauge` can be deployed.
```vyper
>>> RootGaugeFactory.get_bridger(252) # fraxtal
'0x0199429171bcE183048dccf1d5546Ca519EA9717' # supported
>>> RootGaugeFactory.get_bridger(7700) # canto
'0x0000000000000000000000000000000000000000' # not supported
```
:::
### `deploy_gauge`
::::description[`RootGaugeFactory.deploy_gauge(_chain_id: uint256, _salt: bytes32) -> RootGauge`]
Function to deploy and initialize a new root gauge for a given chain ID. This function is `@payable` to allow for bridging costs. This function call reverts if there is no `bridger` contract set for the given `_chain_id`.
| Input | Type | Description |
| ----------- | --------- | ----------- |
| `_chain_id` | `uint256` | Chain ID of the child gauge |
| `_salt` | `bytes32` | Salt for the child gauge |
Returns: newly deployed gauge (`RootGauge`).
Emits: `DeployedGauge` event.
```vyper
event DeployedGauge:
_implementation: indexed(address)
_chain_id: indexed(uint256)
_deployer: indexed(address)
_salt: bytes32
_gauge: RootGauge
interface RootGauge:
def bridger() -> Bridger: view
def initialize(_bridger: Bridger, _chain_id: uint256, _child: address): nonpayable
def transmit_emissions(): nonpayable
call_proxy: public(CallProxy)
get_bridger: public(HashMap[uint256, Bridger])
get_child_factory: public(HashMap[uint256, address])
get_child_implementation: public(HashMap[uint256, address])
get_implementation: public(address)
get_gauge: public(HashMap[uint256, RootGauge[max_value(uint256)]])
get_gauge_count: public(HashMap[uint256, uint256])
is_valid_gauge: public(HashMap[RootGauge, bool])
@payable
@external
def deploy_gauge(_chain_id: uint256, _salt: bytes32) -> RootGauge:
"""
@notice Deploy a root liquidity gauge
@param _chain_id The chain identifier of the counterpart child gauge
@param _salt A value to deterministically deploy a gauge
"""
bridger: Bridger = self.get_bridger[_chain_id]
assert bridger != empty(Bridger) # dev: chain id not supported
implementation: address = self.get_implementation
salt: bytes32 = keccak256(_abi_encode(_chain_id, _salt))
gauge: RootGauge = RootGauge(create_minimal_proxy_to(
implementation,
value=msg.value,
salt=salt,
))
child: address = self._get_child(_chain_id, salt)
idx: uint256 = self.get_gauge_count[_chain_id]
self.get_gauge[_chain_id][idx] = gauge
self.get_gauge_count[_chain_id] = idx + 1
self.is_valid_gauge[gauge] = True
gauge.initialize(bridger, _chain_id, child)
log DeployedGauge(implementation, _chain_id, msg.sender, _salt, gauge)
return gauge
@internal
def _get_child(_chain_id: uint256, salt: bytes32) -> address:
"""
@dev zkSync address derivation is ignored, so need to set child address through a vote manually
"""
child_factory: address = self.get_child_factory[_chain_id]
child_impl: bytes20 = convert(self.get_child_implementation[_chain_id], bytes20)
assert child_factory != empty(address) # dev: child factory not set
assert child_impl != empty(bytes20) # dev: child implementation not set
gauge_codehash: bytes32 = keccak256(
concat(0x602d3d8160093d39f3363d3d373d3d3d363d73, child_impl, 0x5af43d82803e903d91602b57fd5bf3))
digest: bytes32 = keccak256(concat(0xFF, convert(child_factory, bytes20), salt, gauge_codehash))
return convert(convert(digest, uint256) & convert(max_value(uint160), uint256), address)
```
This example deploys a `RootGauge` for Fraxtal.
```shell
>>> RootGaugeFactory.deploy_gauge(252, '0x0000000000000000000000000000000000000000000000000000000000000000')
```
::::
### `deploy_child_gauge`
::::description[`RootGaugeFactory.deploy_child_gauge(_chain_id: uint256, _lp_token: address, _salt: bytes32, _manager: address = msg.sender)`]
:::warning[Important]
This function will only work if a `call_proxy` is set. Otherwise, the function will revert.
:::
Function to deploy a new child gauge on the child chain.
| Input | Type | Description |
| ----------- | --------- | ----------- |
| `_chain_id` | `uint256` | Chain ID of the child gauge |
| `_lp_token` | `address` | Address of the LP token |
| `_salt` | `bytes32` | Salt for the child gauge |
| `_manager` | `address` | Address of the manager |
```vyper
call_proxy: public(CallProxy)
get_bridger: public(HashMap[uint256, Bridger])
@external
def deploy_child_gauge(_chain_id: uint256, _lp_token: address, _salt: bytes32, _manager: address = msg.sender):
bridger: Bridger = self.get_bridger[_chain_id]
assert bridger != empty(Bridger) # dev: chain id not supported
self.call_proxy.anyCall(
self,
_abi_encode(
_lp_token,
_salt,
_manager,
method_id=method_id("deploy_gauge(address,bytes32,address)")
),
empty(address),
_chain_id
)
```
This example deploys a `ChildGauge` on Optimism for the `0xb757fc30bb2d96782188c45b6ebf20defe165ac7` LP token. `0x1234567890123456789012345678901234567890` is specified as the manager.
```shell
>>> RootGaugeFactory.deploy_child_gauge(10, '0xb757fc30bb2d96782188c45b6ebf20defe165ac7', '0x0000000000000000000000000000000000000000000000000000000000000000', '0x1234567890123456789012345678901234567890')
```
::::
---
## Transmitting Emissions
Once a root gauge has received emissions, they can be transmitted to the child gauge. This is done by calling the `transmit_emissions` function. Emissions can only be transmitted from the `RootGaugeFactory`.
Transmitting emissions is permissionless. Anyone can do it.
### `transmit_emissions`
::::description[`RootGaugeFactory.transmit_emissions(_gauge: RootGauge)`]
Function to mint and transmit emissions to the `ChildGauge` on the destination chain. This function is permissionless and can be called by anyone.
| Input | Type | Description |
| -------- | --------- | ----------- |
| `_gauge` | `RootGauge` | Root gauge to transmit emissions for |
```vyper
interface Bridger:
def check(_addr: address) -> bool: view
interface RootGauge:
def transmit_emissions(): nonpayable
@external
def transmit_emissions(_gauge: RootGauge):
"""
@notice Call `transmit_emissions` on a root gauge
@dev Entrypoint to request emissions for a child gauge.
The way that gauges work, this can also be called on the root
chain without a request.
"""
# in most cases this will return True
# for special bridges *cough cough Multichain, we can only do
# one bridge per tx, therefore this will verify msg.sender in [tx.origin, self.call_proxy]
assert _gauge.bridger().check(msg.sender)
_gauge.transmit_emissions()
```
This example transmits CRV emissions for the `RootGauge` at `0x6233394c3C466A45A505EFA4857489743168E9Fa` to the `ChildGauge` on Fraxtal.
```shell
>>> RootGaugeFactory.transmit_emissions('0x6233394c3C466A45A505EFA4857489743168E9Fa')
```
::::
### `get_bridger`
::::description[`RootGaugeFactory.get_bridger(_chain_id: uint256) -> Bridger: view`]
Getter for the bridger for a given chain ID. This contract is used to bridge CRV emissions to the `ChildGauge`.
| Input | Type | Description |
| ----------- | --------- | ----------- |
| `_chain_id` | `uint256` | Chain ID of the child gauge |
Returns: bridger (`Bridger`).
```vyper
interface Bridger:
def check(_addr: address) -> bool: view
get_bridger: public(HashMap[uint256, Bridger])
```
::::
---
## Gauge Information
The `RootGaugeFactory` contract also provides a few getters to retrieve information about the deployed `RootGauges`.
### `get_gauge`
::::description[`RootGaugeFactory.get_gauge(_chain_id: uint256, _idx: uint256) -> RootGauge: view`]
Getter for gauges on a given chain ID and index.
| Input | Type | Description |
| ----------- | --------- | ----------- |
| `_chain_id` | `uint256` | Chain ID of the child gauge |
| `_idx` | `uint256` | Index of the gauge |
Returns: gauge (`address`).
```vyper
get_gauge: public(HashMap[uint256, RootGauge[max_value(uint256)]])
```
::::
### `get_gauge_count`
::::description[`RootGaugeFactory.get_gauge_count(_chain_id: uint256) -> uint256: view`]
Getter to get the number of gauges for a given chain ID. This value is incremented by one for each new gauge deployed.
| Input | Type | Description |
| ----------- | --------- | ----------- |
| `_chain_id` | `uint256` | Chain ID of the child gauge |
Returns: number of gauges (`uint256`).
```vyper
get_gauge_count: public(HashMap[uint256, uint256])
```
::::
### `is_valid_gauge`
::::description[`RootGaugeFactory.is_valid_gauge(_gauge: RootGauge) -> bool: view`]
Getter to check if a gauge is valid.
| Input | Type | Description |
| -------- | --------- | ----------- |
| `_gauge` | `RootGauge` | Root gauge to check validity for |
Returns: `True` if the gauge is valid, `False` otherwise (`bool`).
```vyper
is_valid_gauge: public(HashMap[RootGauge, bool])
```
::::
---
## Child and Root Implementations and Factories
The `RootGaugeFactory` contract also provides a few getters to retrieve information about the deployed `ChildGauge` implementations and factories.
### `get_implementation`
::::description[`RootGaugeFactory.get_implementation() -> address: view`]
Getter for the `RootGauge` implementation contract. This implementation contract is used to deploy new `RootGauge` contracts using Vyper's built-in `create_minimal_proxy_to` function.
Returns: implementation address (`address`).
```vyper
get_implementation: public(address)
```
::::
### `set_implementation`
::::description[`RootGaugeFactory.set_implementation(_implementation: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
:::warning
Changing the implementation contract requires a change on all child factories.
:::
Function to set the implementation contract of the `RootGauge`.
| Input | Type | Description |
| ----------------- | ----------- | ----------- |
| `_implementation` | `address` | Address of the new implementation |
Emits: `UpdateImplementation` event.
```vyper
event UpdateImplementation:
_old_implementation: address
_new_implementation: address
get_implementation: public(address)
owner: public(address)
@external
def set_implementation(_implementation: address):
"""
@notice Set the implementation
@dev Changing implementation require change on all child factories
@param _implementation The address of the implementation to use
"""
assert msg.sender == self.owner # dev: only owner
log UpdateImplementation(self.get_implementation, _implementation)
self.get_implementation = _implementation
```
This example sets the `RootGauge` implementation to the address `0x6233394c3C466A45A505EFA4857489743168E9Fa`.
```shell
>>> RootGaugeFactory.get_implementation()
'0x0000000000000000000000000000000000000000'
>>> RootGaugeFactory.set_implementation('0x6233394c3C466A45A505EFA4857489743168E9Fa')
>>> RootGaugeFactory.get_implementation()
'0x6233394c3C466A45A505EFA4857489743168E9Fa'
```
::::
### `get_child_factory`
::::description[`RootGaugeFactory.get_child_factory(_chain_id: uint256) -> address: view`]
Getter for the child factory for a given chain ID.
| Input | Type | Description |
| ----------- | --------- | ----------- |
| `_chain_id` | `uint256` | Chain ID of the child gauge |
Returns: child factory address (`address`).
```vyper
get_child_factory: public(HashMap[uint256, address])
```
::::
### `get_child_implementation`
::::description[`RootGaugeFactory.get_child_implementation(_chain_id: uint256) -> address: view`]
Getter for the child implementation for a given chain ID.
| Input | Type | Description |
| ----------- | --------- | ----------- |
| `_chain_id` | `uint256` | Chain ID of the child gauge |
Returns: child implementation address (`address`).
```vyper
get_child_implementation: public(HashMap[uint256, address])
```
::::
### `set_child`
::::description[`RootGaugeFactory.set_child(_chain_id: uint256, _bridger: Bridger, _child_factory: address, _child_impl: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to set different child properties for a given chain ID such as the bridger contract, `ChildGaugeFactory` and `ChildGauge` implementation.
| Input | Type | Description |
| ----------- | --------- | ----------- |
| `_chain_id` | `uint256` | Chain ID of the child gauge |
| `_bridger` | `Bridger` | Bridger contract for the child gauge |
| `_child_factory` | `address` | Address of the new `ChildGaugeFactory` |
| `_child_impl` | `address` | Address of the new `ChildGauge` implementation |
Emits: `ChildUpdated` event.
```vyper
event ChildUpdated:
_chain_id: indexed(uint256)
_new_bridger: Bridger
_new_factory: address
_new_implementation: address
get_bridger: public(HashMap[uint256, Bridger])
get_child_factory: public(HashMap[uint256, address])
get_child_implementation: public(HashMap[uint256, address])
owner: public(address)
@external
def set_child(_chain_id: uint256, _bridger: Bridger, _child_factory: address, _child_impl: address):
"""
@notice Set the bridger for `_chain_id`
@param _chain_id The chain identifier to set the bridger for
@param _bridger The bridger contract to use
@param _child_factory Address of factory on L2 (needed in price derivation)
@param _child_impl Address of gauge implementation on L2 (needed in price derivation)
"""
assert msg.sender == self.owner # dev: only owner
log ChildUpdated(_chain_id, _bridger, _child_factory, _child_impl)
self.get_bridger[_chain_id] = _bridger
self.get_child_factory[_chain_id] = _child_factory
self.get_child_implementation[_chain_id] = _child_impl
```
This example sets the following properties for chain ID `252`:
- Bridger: `0x0199429171bcE183048dccf1d5546Ca519EA9717`
- ChildGaugeFactory: `0x0B8D6B6CeFC7Aa1C2852442e518443B1b22e1C52`
- ChildGauge implementation: `0x6A611215540555A7feBCB64CB0Ed11Ac90F165Af`
```shell
>>> RootGaugeFactory.set_child(252, '0x0199429171bcE183048dccf1d5546Ca519EA9717', '0x0B8D6B6CeFC7Aa1C2852442e518443B1b22e1C52', '0x6A611215540555A7feBCB64CB0Ed11Ac90F165Af')
```
::::
---
## Call Proxy
### `call_proxy`
::::description[`RootGaugeFactory.call_proxy() -> CallProxy: view`]
Getter to get the call proxy which is used for inter-chain communication. This variable is initially set at contract initialization and can be changed via the [`set_call_proxy`](#set_call_proxy) function.
Returns: call proxy (`CallProxy`).
```vyper
interface CallProxy:
def anyCall(
_to: address, _data: Bytes[1024], _fallback: address, _to_chain_id: uint256
): nonpayable
call_proxy: public(CallProxy)
@external
def __init__(_call_proxy: CallProxy, _owner: address):
self.call_proxy = _call_proxy
log UpdateCallProxy(empty(CallProxy), _call_proxy)
self.owner = _owner
log TransferOwnership(empty(address), _owner)
```
::::
### `set_call_proxy`
::::description[`RootGaugeFactory.set_call_proxy(_call_proxy: CallProxy)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to set the call proxy.
| Input | Type | Description |
| ------------- | ----------- | ----------------- |
| `_call_proxy` | `CallProxy` | Call proxy to set |
Emits: `UpdateCallProxy` event.
```vyper
event UpdateCallProxy:
_old_call_proxy: CallProxy
_new_call_proxy: CallProxy
call_proxy: public(CallProxy)
@external
def set_call_proxy(_call_proxy: CallProxy):
"""
@notice Set CallProxy
@param _call_proxy Contract to use for inter-chain communication
"""
assert msg.sender == self.owner
self.call_proxy = _call_proxy
log UpdateCallProxy(empty(CallProxy), _call_proxy)
```
This example sets the call proxy to `0x1234567890123456789012345678901234567890`.
```shell
>>> RootGaugeFactory.call_proxy()
'0x0000000000000000000000000000000000000000'
>>> RootGaugeFactory.set_call_proxy('0x1234567890123456789012345678901234567890')
>>> RootGaugeFactory.call_proxy()
'0x1234567890123456789012345678901234567890'
```
::::
---
## Contract Ownership
For contract ownership details, see [here](../../resources/curve-practices.md#commit--accept).
---
## Root Gauge Implementation
The `RootGauge` is a simplified liquidity gauge contract on Ethereum used for bridging CRV from Ethereum to a sidechain. This gauge can be, just like any other liquidity gauge, be added to the `GaugeController` and is then eligible to receive voting weight. If that is the case, it can [mint any new emissions and transmit](#checkpointing--crv-emissions) them to the child gauge on another chain using a [bridger contract](#bridger).
Root gauges are deployed from the `RootGaugeFactory` and makes use of Vyper's built-in [create_minimal_proxy_to](https://docs.vyperlang.org/en/stable/built-in-functions.html#create_minimal_proxy_to) function to create a EIP1167-compliant "minimal proxy contract" that duplicates the logic of the contract at target.
:::vyper[`RootGauge.vy`]
The source code for the `RootGauge.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-xchain-factory/blob/master/contracts/implementations/RootGauge.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
The contract is deployed on :logos-ethereum: Ethereum at [`0x96720942F9fF22eFd8611F696E5333Fe3671717a`](https://etherscan.io/address/0x96720942F9fF22eFd8611F696E5333Fe3671717a).
:::
---
## Initialization
Because the root gauges are deployed using a proxy pattern, they are automatically initialized directly after deployment.
### `initialize`
::::description[`RootGauge.initialize(_bridger: Bridger, _chain_id: uint256, _child: address)`]
Function to initialize the root gauge. Initializes the child gauge address, chain ID, bridger contract, and factory, aswell as sets the `inflation_params` and `last_period`. The function also sets the CRV token approval of the bridger contract to `max_value(uint256)`.
| Input | Type | Description |
| --------- | ---- | ----------- |
| `_bridger` | `Bridger` | The bridger contract |
| `_chain_id` | `uint256` | The chain ID |
| `_child` | `address` | The child gauge address |
```vyper
@external
def initialize(_bridger: Bridger, _chain_id: uint256, _child: address):
"""
@notice Proxy initialization method
"""
assert self.factory == empty(Factory) # dev: already initialized
self.child_gauge = _child
self.chain_id = _chain_id
self.bridger = _bridger
self.factory = Factory(msg.sender)
inflation_params: InflationParams = InflationParams({
rate: CRV.rate(),
finish_time: CRV.future_epoch_time_write()
})
assert inflation_params.rate != 0
self.inflation_params = inflation_params
self.last_period = block.timestamp / WEEK
CRV.approve(_bridger.address, max_value(uint256))
```
This example initializes a root gauge with a bridger contract on Arbitrum.
```shell
>>> RootGauge.initialize('0xceda55279fe22d256c4e6a6F2174C1588e94B2BB', 42161, '0x1234567890123456789012345678901234567896')
```
::::
---
## Checkpointing & CRV Emissions
### `user_checkpoint`
::::description[`RootGauge.user_checkpoint(_user: address) -> bool`]
Function to checkpoint a gauge and update the total emissions.
| Input | Type | Description |
| --------- | ---- | ----------- |
| `_user` | `address` | The user address. This parameter is vestigial and has no impact on the function |
Returns: true (`bool`).
```vyper
interface GaugeController:
def checkpoint_gauge(addr: address): nonpayable
def gauge_relative_weight(addr: address, time: uint256) -> uint256: view
last_period: public(uint256)
total_emissions: public(uint256)
@external
def user_checkpoint(_user: address) -> bool:
"""
@notice Checkpoint the gauge updating total emissions
@param _user Vestigial parameter with no impact on the function
"""
# the last period we calculated emissions up to (but not including)
last_period: uint256 = self.last_period
# our current period (which we will calculate emissions up to)
current_period: uint256 = block.timestamp / WEEK
# only checkpoint if the current period is greater than the last period
# last period is always less than or equal to current period and we only calculate
# emissions up to current period (not including it)
if last_period != current_period:
# checkpoint the gauge filling in any missing weight data
GAUGE_CONTROLLER.checkpoint_gauge(self)
params: InflationParams = self.inflation_params
emissions: uint256 = 0
# only calculate emissions for at most 256 periods since the last checkpoint
for i in range(last_period, last_period + 256):
if i == current_period:
# don't calculate emissions for the current period
break
period_time: uint256 = i * WEEK
weight: uint256 = GAUGE_CONTROLLER.gauge_relative_weight(self, period_time)
if period_time <= params.finish_time and params.finish_time < period_time + WEEK:
# calculate with old rate
emissions += weight * params.rate * (params.finish_time - period_time) / 10 ** 18
# update rate
params.rate = params.rate * RATE_DENOMINATOR / RATE_REDUCTION_COEFFICIENT
# calculate with new rate
emissions += weight * params.rate * (period_time + WEEK - params.finish_time) / 10 ** 18
# update finish time
params.finish_time += RATE_REDUCTION_TIME
# update storage
self.inflation_params = params
else:
emissions += weight * params.rate * WEEK / 10 ** 18
self.last_period = current_period
self.total_emissions += emissions
return True
```
```shell
>>> RootGauge.user_checkpoint('0x1234567890123456789012345678901234567896')
```
::::
### `transmit_emissions`
::::description[`RootGauge.transmit_emissions()`]
:::guard[Guarded Method]
This function is only callable by the `RootGaugeFactory`.
:::
Function to mint any new emissions and transmit them to the child gauge on another chain. Calling this function directly on the root gauge will not revert. The function to bridge emissions can only be called via the `RootGaugeFactory` using its `transmit_emissions(gauge)` function. The contract uses the `bridger` contract to bridge the emissions.
This source code example makes use of the Arbitrum Bridger Wrapper. The function to bridge to other chains can vary depending on the bridger contract used.
```vyper
interface Bridger:
def cost() -> uint256: view
def bridge(_token: CRV20, _destination: address, _amount: uint256): payable
CRV: immutable(CRV20)
GAUGE_CONTROLLER: immutable(GaugeController)
MINTER: immutable(Minter)
@external
def transmit_emissions():
"""
@notice Mint any new emissions and transmit across to child gauge
"""
assert msg.sender == self.factory.address # dev: call via factory
MINTER.mint(self)
minted: uint256 = CRV.balanceOf(self)
assert minted != 0 # dev: nothing minted
bridger: Bridger = self.bridger
bridger.bridge(CRV, self.child_gauge, minted, value=bridger.cost())
```
```shell
>>> RootGauge.transmit_emissions()
```
::::
### `integrate_fraction`
::::description[`RootGauge.integrate_fraction(_user: address) -> uint256: view`]
Function to query the total emissions a user is entitled to. Any value of `_user` other than the gauge address will return 0 (only the gauge itself if entitled to emissions as it is the one who mints and bridges them).
| Input | Type | Description |
| --------- | --------- | ------------ |
| `_user` | `address` | Address of the user |
Returns: The total emissions the user is entitled to (`uint256`).
```vyper
total_emissions: public(uint256)
@view
@external
def integrate_fraction(_user: address) -> uint256:
"""
@notice Query the total emissions `_user` is entitled to
@dev Any value of `_user` other than the gauge address will return 0
"""
if _user == self:
return self.total_emissions
return 0
```
```shell
>>> RootGauge.integrate_fraction('0x1234567890123456789012345678901234567896')
0
```
::::
### `inflation_params`
::::description[`RootGauge.inflation_params() -> InflationParams: view`]
Getter for the inflation parameters.
Returns: `InflationParams` struct containing the CRV emission [`rate`](../../curve-dao/crv-token.md#rate) and [`future_epoch_time_write()`](../../curve-dao/crv-token.md#future_epoch_time_write).
```vyper
struct InflationParams:
rate: uint256
finish_time: uint256
inflation_params: public(InflationParams)
```
```shell
>>> RootGauge.inflation_params()
{'rate': 1000000000000000000, 'finish_time': 1735689600}
```
::::
### `last_period`
::::description[`RootGauge.last_period() -> uint256: view`]
Getter for the last period.
Returns: last period (`uint256`).
```vyper
last_period: public(uint256)
```
```shell
>>> RootGauge.last_period()
1735689600
```
::::
### `total_emissions`
::::description[`RootGauge.total_emissions() -> uint256: view`]
Getter for the total emissions of the gauge. This value increases each time the gauge is checkpointed.
Returns: total emissions (`uint256`).
```vyper
total_emissions: public(uint256)
```
```shell
>>> RootGauge.total_emissions()
0
```
::::
---
## Bridger Contracts
The contract makes use of wrapper contracts around different bridging architectures to bridge CRV emissions to child gauges on other chains. These contracts are granted max approval when initialized in order for being able to transmit CRV tokens. The bridger contract used depends on the chain the child gauge is on. The `RootGaugeFactory` holds different bridger implementations for each chain.
If a bridger contract needs to be updated for whatever reason, this can only be done within the `RootGaugeFactory` using the `set_child` function. After the bridger has been updated, the `update_bridger()` function needs to be called on the specific gauge to update the bridger contract used by the gauge. This sets the CRV token approval of the "old" bridger to 0 and the new bridger to `max_value(uint256)`.
Source code for the `set_child` function, which is used to set the bridger for a specific chain ID.
```vyper
event ChildUpdated:
_chain_id: indexed(uint256)
_new_bridger: Bridger
_new_factory: address
_new_implementation: address
get_bridger: public(HashMap[uint256, Bridger])
get_child_factory: public(HashMap[uint256, address])
get_child_implementation: public(HashMap[uint256, address])
@external
def set_child(_chain_id: uint256, _bridger: Bridger, _child_factory: address, _child_impl: address):
"""
@notice Set the bridger for `_chain_id`
@param _chain_id The chain identifier to set the bridger for
@param _bridger The bridger contract to use
@param _child_factory Address of factory on L2 (needed in price derivation)
@param _child_impl Address of gauge implementation on L2 (needed in price derivation)
"""
assert msg.sender == self.owner # dev: only owner
log ChildUpdated(_chain_id, _bridger, _child_factory, _child_impl)
self.get_bridger[_chain_id] = _bridger
self.get_child_factory[_chain_id] = _child_factory
self.get_child_implementation[_chain_id] = _child_impl
```
### `bridger`
::::description[`RootGauge.bridger() -> Bridger: view`]
Getter for the bridger contract used by the gauge to bridge CRV emissions to the child gauge on another chain. The bridger contract is set during initialization and can only be updated within the `RootGaugeFactory`.
Returns: bridger contract (`address`).
```vyper
interface Bridger:
def cost() -> uint256: view
def bridge(_token: CRV20, _destination: address, _amount: uint256): payable
bridger: public(Bridger)
```
This example returns the bridger contract for transmitting CRV emissions from Ethereum to Arbitrum.
```shell
>>> RootGauge.bridger()
'0xceda55279fe22d256c4e6a6F2174C1588e94B2BB'
```
::::
### `update_bridger`
::::description[`RootGauge.update_bridger()`]
Function to update the bridger used by this contract. This function call will only have effect if the bridger implementation of the chain is updated. Bridger contracts should prevent bridging if ever updated, therefore the approval of the old bridger is set to 0 and the new bridger is set to `max_value(uint256)`. Function call is permissionless, anyone can call it.
```vyper
interface CRV20:
def approve(_account: address, _value: uint256): nonpayable
bridger: public(Bridger)
@external
def update_bridger():
"""
@notice Update the bridger used by this contract
@dev Bridger contracts should prevent bridging if ever updated
"""
# reset approval
bridger: Bridger = self.factory.get_bridger(self.chain_id)
CRV.approve(self.bridger.address, 0)
CRV.approve(bridger.address, max_value(uint256))
self.bridger = bridger
```
This function updates the `bridger` contract. Updating this variable is only possible when the bridger contract implementation within the `RootGaugeFactory` is updated.
```shell
>>> RootGauge.bridger()
'0xceda55279fe22d256c4e6a6F2174C1588e94B2BB'
>>> RootGaugeFactory.set_child(42161, '0x1234567890123456789012345678901234567896', '0x1234567890123456789012345678901234567896', '0x1234567890123456789012345678901234567896')
>>> RootGauge.update_bridger()
>>> RootGauge.bridger()
'0x1234567890123456789012345678901234567896'
```
::::
---
## Child Gauge
If a according child gauge is deployed with the same salt as the root gauge, the `child_gauge` variable will hold the address of the child gauge. Additionally, there is a function to set the child gauge in case something went wrong (e.g. between implementation updates or zkSync).
### `child_gauge`
::::description[`RootGauge.child_gauge() -> address: view`]
Getter for the corresponding child gauge on another chain.
Returns: child gauge contract (`address`).
```vyper
child_gauge: public(address)
```
```shell
>>> RootGauge.child_gauge()
'0xcde3Cdf332E35653A7595bA555c9fDBA3c78Ec04'
```
::::
### `set_child_gauge`
::::description[`RootGauge.set_child_gauge(_child: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the `RootGaugeFactory`.
:::
Function to set the child gauge in case something went wrong (e.g. between implementation updates or zkSync).
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_child` | `address` | The child gauge address |
```vyper
interface Factory:
def owner() -> address: view
child_gauge: public(address)
@external
def set_child_gauge(_child: address):
"""
@notice Set Child contract in case something went wrong (e.g. between implementation updates or zkSync)
@param _child Child gauge to set
"""
assert msg.sender == self.factory.owner()
assert _child != empty(address)
self.child_gauge = _child
```
```shell
>>> RootGauge.child_gauge()
'0xcde3Cdf332E35653A7595bA555c9fDBA3c78Ec04'
>>> RootGauge.set_child_gauge('0x1234567890123456789012345678901234567890')
>>> RootGauge.child_gauge()
'0x1234567890123456789012345678901234567890'
```
::::
### `chain_id`
::::description[`RootGauge.chain_id() -> uint256: view`]
Getter for the chain ID of the child gauge.
Returns: chain ID (`uint256`).
```vyper
chain_id: public(uint256)
```
This example returns the chain ID on which the corresponding child gauge is deployed.
```shell
>>> RootGauge.chain_id()
42161
```
::::
---
## Killing Root Gauges
Root gauges can be killed by the `owner` of the `RootGaugeFactory` to disable emissions of the specific gauge. Killed gauges will have their inflation rate be set to 0 and therefor restrict any minting of CRV emissions.
### `is_killed`
::::description[`RootGauge.is_killed() -> bool: view`]
Getter for the kill status of the gauge.
Returns: kill status (`bool`).
```vyper
is_killed: public(bool)
```
```shell
>>> RootGauge.is_killed()
False
```
::::
### `set_killed`
::::description[`RootGauge.set_killed(_is_killed: bool)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the `RootGaugeFactory`.
:::
Function to set the kill status of the gauge. If a gauge is killed, inflation params are modified accordingly to disable emissions. A gauge can be "unkilled" by setting the kill status to `False`, which restores the inflation params to their actual values.
| Input | Type | Description |
| --------- | ---- | ------------ |
| `_is_killed` | `bool` | The kill status |
```vyper
interface Factory:
def owner() -> address: view
interface CRV20:
def rate() -> uint256: view
def future_epoch_time_write() -> uint256: nonpayable
struct InflationParams:
rate: uint256
finish_time: uint256
last_period: public(uint256)
is_killed: public(bool)
@external
def set_killed(_is_killed: bool):
"""
@notice Set the gauge kill status
@dev Inflation params are modified accordingly to disable/enable emissions
"""
assert msg.sender == self.factory.owner()
if _is_killed:
self.inflation_params.rate = 0
else:
self.inflation_params = InflationParams({
rate: CRV.rate(),
finish_time: CRV.future_epoch_time_write()
})
self.last_period = block.timestamp / WEEK
self.is_killed = _is_killed
```
```shell
>>> RootGauge.set_killed(True)
>>> RootGauge.is_killed()
True
>>> RootGauge.inflation_params()
{'rate': 0, 'finish_time': 1735689600}
```
::::
---
## Other Methods
### `factory`
::::description[`RootGauge.factory() -> Factory: view`]
Getter for the `RootGaugeFactory` contract.
Returns: root gauge factory (`address`).
```vyper
factory: public(Factory)
```
```shell
>>> RootGauge.factory()
'0x306A45a1478A000dC701A6e1f7a569afb8D9DCD6'
```
::::
### `version`
::::description[`RootGauge.version() -> String[8]: view`]
Getter for the contract version.
Returns: contract version (`String[8]`).
```vyper
VERSION: constant(String[8]) = "1.0.0"
@pure
@external
def version() -> String[8]:
"""
@notice Get the version of this gauge
"""
return VERSION
```
```shell
>>> RootGauge.version()
'1.0.0'
```
::::
---
## Address Provider
The `AddressProvider` serves as the **entry point contract for Curve's various registries** and is deployed on all chains where Curve is operational. The contract holds the most important contract addresses.
:::vyper[`AddressProvider.vy`]
Source code of the `AddressProvider.vy` contract can be found on [GitHub](https://github.com/curvefi/metaregistry/blob/main/contracts/AddressProviderNG.vy). The contract is written using Vyper version 0.2.4.
A list of all deployed contracts can be found [here](../deployments.md).
```json
[{"name":"NewEntry","inputs":[{"name":"id","type":"uint256","indexed":true},{"name":"addr","type":"address","indexed":false},{"name":"description","type":"string","indexed":false}],"anonymous":false,"type":"event"},{"name":"EntryModified","inputs":[{"name":"id","type":"uint256","indexed":true},{"name":"version","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"EntryRemoved","inputs":[{"name":"id","type":"uint256","indexed":true}],"anonymous":false,"type":"event"},{"name":"CommitNewAdmin","inputs":[{"name":"admin","type":"address","indexed":true}],"anonymous":false,"type":"event"},{"name":"NewAdmin","inputs":[{"name":"admin","type":"address","indexed":true}],"anonymous":false,"type":"event"},{"stateMutability":"nonpayable","type":"constructor","inputs":[],"outputs":[]},{"stateMutability":"view","type":"function","name":"ids","inputs":[],"outputs":[{"name":"","type":"uint256[]"}]},{"stateMutability":"view","type":"function","name":"get_address","inputs":[{"name":"_id","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"nonpayable","type":"function","name":"add_new_id","inputs":[{"name":"_id","type":"uint256"},{"name":"_address","type":"address"},{"name":"_description","type":"string"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"add_new_ids","inputs":[{"name":"_ids","type":"uint256[]"},{"name":"_addresses","type":"address[]"},{"name":"_descriptions","type":"string[]"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"update_id","inputs":[{"name":"_id","type":"uint256"},{"name":"_new_address","type":"address"},{"name":"_new_description","type":"string"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"update_address","inputs":[{"name":"_id","type":"uint256"},{"name":"_address","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"update_description","inputs":[{"name":"_id","type":"uint256"},{"name":"_description","type":"string"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"remove_id","inputs":[{"name":"_id","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"remove_ids","inputs":[{"name":"_ids","type":"uint256[]"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"commit_transfer_ownership","inputs":[{"name":"_new_admin","type":"address"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"apply_transfer_ownership","inputs":[],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"revert_transfer_ownership","inputs":[],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"admin","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"future_admin","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"num_entries","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"check_id_exists","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"get_id_info","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"tuple","components":[{"name":"addr","type":"address"},{"name":"description","type":"string"},{"name":"version","type":"uint256"},{"name":"last_modified","type":"uint256"}]}]}]
```
:::
:::warning[Contract Upgradability]
The `AddressProvider` contract is managed by an `admin` who is currently an individual at Curve, rather than the Curve DAO[^1]. This **admin has the ability to update, add or remove new IDs** within the contract. When integrating this contract into systems or relying on it for critical components, it is essential to consider that these **IDs and their associated addresses can be modified at any time**.
[^1]: Reasoning: Due to the nature of the contract (it does not hold any user funds or has any monetary influence), it is not considered a crucial contract. It should only be used as a pure informational source. Additionally, the Curve ecosystem changes very rapidly and therefore requires fast updates for such a contract. Always putting up a DAO vote to change IDs would not be feasible.
:::
---
## Reading IDs
For the full mapping of IDs please see [`get_id_info`](#get_id_info).
*ID information is stored in a `struct`, containing an address, a detailed description, its version, and the timestamp marking its most recent modification:*
```shell
struct AddressInfo:
addr: address
description: String[256]
version: uint256
last_modified: uint256
```
:::colab[Google Colab Notebook]
A Google Colab notebook that provides a full mapping of IDs by iterating over all `ids` via calling the `get_id_info` can be found here: [Google Colab Notebook](https://colab.research.google.com/drive/1PnvfX5E_F7_VCsmkzHrN0_OiJNsUmx9w?usp=sharing)
:::
### `ids`
::::description[`AddressProvider.ids() -> DynArray[uint256, 1000]: view`]
Getter function for all the IDs of active registry items in the AddressProvider.
Returns: active ids (`DynArray[uint256, 1000]`).
```vyper
_ids: DynArray[uint256, 1000]
@view
@external
def ids() -> DynArray[uint256, 1000]:
"""
@notice returns IDs of active registry items in the AddressProvider.
@returns An array of IDs.
"""
_ids: DynArray[uint256, 1000] = []
for _id in self._ids:
if self.check_id_exists[_id]:
_ids.append(_id)
return _ids
```
This method returns all populated IDs.
::::
### `get_id_info`
::::description[`AddressProvider.get_id_info(arg0: uint256) -> tuple: view`]
Getter function to retrieve information about a specific ID.
| Input | Type | Description |
| ------ | --------- | ------------------------------ |
| `arg0` | `uint256` | ID to get the information for |
Returns: `AddressInfo` struct containing the addr (`address`), description (`String[256]`), version (`uint256`) and last_modified (`uint256`).
```vyper
struct AddressInfo:
addr: address
description: String[256]
version: uint256
last_modified: uint256
get_id_info: public(HashMap[uint256, AddressInfo])
```
This method returns the address of the contract, the description, the ID version (which is incremented by 1 each time the ID is updated), and the timestamp of the last modification. When calling the function for an unpopulated ID, it returns an empty `AddressInfo` struct.
::::
### `get_address`
::::description[`AddressProvider.get_address(_id: uint256) -> address: view`]
Getter for the contract address of an ID.
| Input | Type | Description |
| ------ | --------- | ---------------------------------- |
| `_id` | `uint256` | ID to get the contract address for |
Returns: contract (`address`).
```vyper
struct AddressInfo:
addr: address
description: String[256]
version: uint256
last_modified: uint256
get_id_info: public(HashMap[uint256, AddressInfo])
@view
@external
def get_address(_id: uint256) -> address:
"""
@notice Fetch the address associated with `_id`
@dev Returns empty(address) if `_id` has not been defined, or has been unset
@param _id Identifier to fetch an address for
@return Current address associated to `_id`
"""
return self.get_id_info[_id].addr
```
This method returns the address of an ID.
::::
### `check_id_exists`
::::description[`AddressProvider.check_id_exists(arg0: uint256) -> bool: view`]
Function to check if an ID exists.
| Input | Type | Description |
| ------ | --------- | ----------- |
| `arg0` | `uint256` | ID to check |
Returns: true or false (`bool`).
```vyper
check_id_exists: public(HashMap[uint256, bool])
```
This method checks if a certain ID exists.
::::
### `num_entries`
::::description[`AddressProvider.num_entries() -> uint256: view`]
Getter for the number of entries. The count increments by one upon calling `_add_new_id` and decreases by one upon calling `_remove_id`.
Returns: number of entries (`uint256`).
```vyper
num_entries: public(uint256)
```
This method returns the total number of IDs added to the `AddressProvider`.
::::
---
## Adding, Removing and Updating IDs
IDs can be added, removed, or adjusted by the `admin` of the contract.
:::warning[Contract Upgradability]
The `AddressProvider` contract is managed by an `admin` who is currently an individual at Curve, rather than the Curve DAO[^1]. This **admin has the ability to update, add or remove new IDs** within the contract. When integrating this contract into systems or relying on it for critical components, it is essential to consider that these **IDs and their associated addresses can be modified at any time**.
[^1]: Reasoning: Due to the nature of the contract (it does not hold any user funds or has any monetary influence), it is not considered a crucial contract. It should only be used as a pure informational source. Additionally, the Curve ecosystem changes very rapidly and therefore requires fast updates for such a contract. Always putting up a DAO vote to change IDs would not be feasible.
:::
### `update_id`
::::description[`AddressProvider.update_id(_id: uint256, _new_address: address, _new_description: String[64])`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to update the address and description of an ID.
| Input | Type | Description |
| ------------------ | ------------ | --------------- |
| `_id` | `uint256` | ID to update |
| `_new_address` | `address` | New address |
| `_new_description` | `String[64]` | New description |
Emits: `EntryModified` event.
```vyper
event EntryModified:
id: indexed(uint256)
version: uint256
@external
def update_id(
_id: uint256,
_new_address: address,
_new_description: String[64],
):
"""
@notice Update entries at an ID
@param _id Address assigned to the input _id
@param _new_address Address assigned to the _id
@param _new_description Human-readable description of the identifier
"""
assert msg.sender == self.admin # dev: admin-only function
assert self.check_id_exists[_id] # dev: id does not exist
# Update entry at _id:
self.get_id_info[_id].addr = _new_address
self.get_id_info[_id].description = _new_description
# Update metadata (version, update time):
self._update_entry_metadata(_id)
@internal
def _update_entry_metadata(_id: uint256):
version: uint256 = self.get_id_info[_id].version + 1
self.get_id_info[_id].version = version
self.get_id_info[_id].last_modified = block.timestamp
log EntryModified(_id, version)
```
This function updates the ID at index `0`.
```shell
>>> AddressProvider.update_id(0, "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E", "crvUSD Token")
```
::::
### `update_address`
::::description[`AddressProvider.update_address(_id: uint256, _address: address)`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to update the address of an ID.
| Input | Type | Description |
| ---------- | --------- | ---------------------------- |
| `_id` | `uint256` | ID to change the address for |
| `_address` | `address` | New address to change it to |
Emits: `EntryModified` event.
```vyper
event EntryModified:
id: indexed(uint256)
version: uint256
check_id_exists: public(HashMap[uint256, bool])
get_id_info: public(HashMap[uint256, AddressInfo])
@external
def update_address(_id: uint256, _address: address):
"""
@notice Set a new address for an existing identifier
@param _id Identifier to set the new address for
@param _address Address to set
"""
assert msg.sender == self.admin # dev: admin-only function
assert self.check_id_exists[_id] # dev: id does not exist
# Update address:
self.get_id_info[_id].addr = _address
# Update metadata (version, update time):
self._update_entry_metadata(_id)
@internal
def _update_entry_metadata(_id: uint256):
version: uint256 = self.get_id_info[_id].version + 1
self.get_id_info[_id].version = version
self.get_id_info[_id].last_modified = block.timestamp
log EntryModified(_id, version)
```
This example changes the address for ID 0.
```shell
>>> AddressProvider.update_address(0, "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E")
```
::::
### `update_description`
::::description[`AddressProvider.update_description(_id: uint256, _description: String[256])`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to update the description of an ID.
| Input | Type | Description |
| -------------- | ------------- | -------------------------------- |
| `_id` | `uint256` | ID to change the description for |
| `_description` | `String[256]` | New description |
Emits: `EntryModified` event.
```vyper
event EntryModified:
id: indexed(uint256)
version: uint256
check_id_exists: public(HashMap[uint256, bool])
get_id_info: public(HashMap[uint256, AddressInfo])
@external
def update_description(_id: uint256, _description: String[256]):
"""
@notice Update description for an existing _id
@param _id Identifier to set the new description for
@param _description New description to set
"""
assert msg.sender == self.admin # dev: admin-only function
assert self.check_id_exists[_id] # dev: id does not exist
# Update description:
self.get_id_info[_id].description = _description
# Update metadata (version, update time):
self._update_entry_metadata(_id)
@internal
def _update_entry_metadata(_id: uint256):
version: uint256 = self.get_id_info[_id].version + 1
self.get_id_info[_id].version = version
self.get_id_info[_id].last_modified = block.timestamp
log EntryModified(_id, version)
```
::::
### `add_new_id`
::::description[`AddressProvider.add_new_id(_id: uint256, _address: address, _description: String[64])`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to add a new registry item to the AddressProvider.
| Input | Type | Description |
| -------------- | ------------ | ----------------------------------------------- |
| `_id` | `uint256` | ID to add; Reverts if ID number is already used |
| `_address` | `address` | New address |
| `_description` | `String[64]` | New description |
Emits: `NewEntry` event.
```vyper
event NewEntry:
id: indexed(uint256)
addr: address
description: String[64]
@external
def add_new_id(
_id: uint256,
_address: address,
_description: String[64],
):
"""
@notice Enter a new registry item
@param _id ID assigned to the address
@param _address Address assigned to the ID
@param _description Human-readable description of the ID
"""
assert msg.sender == self.admin # dev: admin-only function
self._add_new_id(_id, _address, _description)
@internal
def _add_new_id(
_id: uint256,
_address: address,
_description: String[64]
):
assert not self.check_id_exists[_id] # dev: id exists
self.check_id_exists[_id] = True
self._ids.append(_id)
# Add entry:
self.get_id_info[_id] = AddressInfo(
{
addr: _address,
description: _description,
version: 1,
last_modified: block.timestamp,
}
)
self.num_entries += 1
log NewEntry(_id, _address, _description)
```
::::
### `add_new_ids`
::::description[`AddressProvider.add_new_ids(_ids: DynArray[uint256, 25], _addresses: DynArray[address, 25], _descriptions: DynArray[String[64], 25])`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to add multiple new registry items to the AddressProvider at once.
| Input | Type | Description |
| --------------- | -------------------------- | ------------------------------------------------ |
| `_ids` | `DynArray[uint256, 25]` | IDs to add; Reverts if ID number is already used |
| `_addresses` | `DynArray[address, 25]` | ID addresses |
| `_descriptions` | `DynArray[String[64], 25]` | ID descriptions |
Emits: `NewEntry` event.
```vyper
event NewEntry:
id: indexed(uint256)
addr: address
description: String[64]
@external
def add_new_ids(
_ids: DynArray[uint256, 25],
_addresses: DynArray[address, 25],
_descriptions: DynArray[String[64], 25],
):
"""
@notice Enter new registry items
@param _ids IDs assigned to addresses
@param _addresses Addresses assigned to corresponding IDs
@param _descriptions Human-readable description of each of the IDs
"""
assert msg.sender == self.admin # dev: admin-only function
# Check lengths
assert len(_ids) == len(_addresses)
assert len(_addresses) == len(_descriptions)
for i in range(len(_ids), bound=20):
self._add_new_id(
_ids[i],
_addresses[i],
_descriptions[i]
)
@internal
def _add_new_id(
_id: uint256,
_address: address,
_description: String[64]
):
assert not self.check_id_exists[_id] # dev: id exists
self.check_id_exists[_id] = True
self._ids.append(_id)
# Add entry:
self.get_id_info[_id] = AddressInfo(
{
addr: _address,
description: _description,
version: 1,
last_modified: block.timestamp,
}
)
self.num_entries += 1
log NewEntry(_id, _address, _description)
```
::::
### `remove_id`
::::description[`AddressProvider.remove_id(_id: uint256) -> bool`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to remove a registry item from the AddressProvider.
| Input | Type | Description |
| ----- | --------- | ------------ |
| `_id` | `uint256` | ID to remove |
Returns: true (`bool`).
Emits: `EntryRemoved` event.
```vyper
event EntryRemoved:
id: indexed(uint256)
@external
def remove_id(_id: uint256) -> bool:
"""
@notice Unset an existing identifier
@param _id Identifier to unset
@return bool success
"""
assert msg.sender == self.admin # dev: admin-only function
return self._remove_id(_id)
@internal
def _remove_id(_id: uint256) -> bool:
assert self.check_id_exists[_id] # dev: id does not exist
# Clear ID:
self.get_id_info[_id].addr = empty(address)
self.get_id_info[_id].last_modified = 0
self.get_id_info[_id].description = ''
self.get_id_info[_id].version = 0
self.check_id_exists[_id] = False
# Reduce num entries:
self.num_entries -= 1
# Emit 0 in version to notify removal of id:
log EntryRemoved(_id)
return True
```
::::
### `remove_ids`
::::description[`AddressProvider.remove_ids(_ids: DynArray[uint256, 20]) -> bool`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to remove multiple registry items from the AddressProvider at once.
| Input | Type | Description |
| ------ | ----------------------- | ------------- |
| `_ids` | `DynArray[uint256, 20]` | IDs to remove |
Returns: true (`bool`).
Emits: `EntryRemoved` event.
```vyper
event EntryRemoved:
id: indexed(uint256)
@external
def remove_ids(_ids: DynArray[uint256, 20]) -> bool:
"""
@notice Unset existing identifiers
@param _id DynArray of identifier to unset
@return bool success
"""
assert msg.sender == self.admin # dev: admin-only function
for _id in _ids:
assert self._remove_id(_id)
return True
@internal
def _remove_id(_id: uint256) -> bool:
assert self.check_id_exists[_id] # dev: id does not exist
# Clear ID:
self.get_id_info[_id].addr = empty(address)
self.get_id_info[_id].last_modified = 0
self.get_id_info[_id].description = ''
self.get_id_info[_id].version = 0
self.check_id_exists[_id] = False
# Reduce num entries:
self.num_entries -= 1
# Emit 0 in version to notify removal of id:
log EntryRemoved(_id)
return True
```
::::
---
## Contract Ownership
The ownership of the contract follows the classic two-step ownership model used across most Curve contracts.
### `admin`
::::description[`AddressProvider.admin() -> address: view`]
Getter for the admin of the contract. This address can add, remove or update ID's.
Returns: admin (`address`).
```vyper
admin: public(address)
@external
def __init__():
self.admin = tx.origin
```
::::
### `future_admin`
::::description[`AddressProvider.future_admin() -> address: view`]
Getter for the future admin of the contract.
Returns: future admin (`address`).
```vyper
future_admin: public(address)
```
::::
### `commit_transfer_ownership`
::::description[`AddressProvider.commit_transfer_ownership(_new_admin: address) -> bool`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to initiate a transfer of contract ownership.
| Input | Type | Description |
| ------------ | --------- | ------------------------------------ |
| `_new_admin` | `address` | Address to transfer the ownership to |
Returns: true (`bool`).
Emits: `CommitNewAdmin` event.
```vyper
event CommitNewAdmin:
admin: indexed(address)
future_admin: public(address)
@external
def commit_transfer_ownership(_new_admin: address) -> bool:
"""
@notice Initiate a transfer of contract ownership
@dev Once initiated, the actual transfer may be performed three days later
@param _new_admin Address of the new owner account
@return bool success
"""
assert msg.sender == self.admin # dev: admin-only function
self.future_admin = _new_admin
log CommitNewAdmin(_new_admin)
return True
```
::::
### `apply_transfer_ownership`
::::description[`AddressProvider.apply_transfer_ownership() -> bool`]
:::guard[Guarded Method]
This function can only be called by the `future_admin` of the contract.
:::
Function to finalize a transfer of contract ownership.
Returns: true (`bool`).
Emits: `NewAdmin` event.
```vyper
event NewAdmin:
admin: indexed(address)
admin: public(address)
future_admin: public(address)
@external
def apply_transfer_ownership() -> bool:
"""
@notice Finalize a transfer of contract ownership
@dev May only be called by the next owner
@return bool success
"""
assert msg.sender == self.future_admin # dev: admin-only function
new_admin: address = self.future_admin
self.admin = new_admin
log NewAdmin(new_admin)
return True
```
::::
### `revert_transfer_ownership`
::::description[`AddressProvider.revert_transfer_ownership() -> bool`]
:::guard[Guarded Method]
This function can only be called by the `admin` of the contract.
:::
Function to revert the transfer of contract ownership.
Returns: true (`bool`).
```vyper
admin: public(address)
future_admin: public(address)
@external
def revert_transfer_ownership() -> bool:
"""
@notice Revert a transfer of contract ownership
@dev May only be called by the current owner
@return bool success
"""
assert msg.sender == self.admin # dev: admin-only function
self.future_admin = empty(address)
return True
```
::::
---
## curve-api API
---
## curve-prices API
---
## Integrating LLAMMA
LLAMMA (Lending Liquidating AMM Algorithm) is Curve's band-based AMM used in **crvUSD** and **Curve Lending (Llamalend)** markets. Each lending market deploys its own LLAMMA AMM containing the collateral and borrowed asset. Unlike the other Curve AMMs which are general-purpose DEX pools, LLAMMA serves a dual role: it manages loan collateral liquidation and simultaneously provides tradeable liquidity that aggregators can route through.
Key properties:
- **2 coins** per AMM — always `coins[0]` = borrowed asset, `coins[1]` = collateral asset
- **Band-based liquidity** — similar to Uniswap V3 ticks, liquidity is distributed across discrete price bands
- **Not user-depositable** — liquidity comes from borrowers' collateral, not from LPs choosing to provide liquidity
- **The AMM is identical** across crvUSD and Curve Lending — same bytecode, same interface
- **No `exchange_received`** — uses standard `exchange()` with approval
:::info
LLAMMA AMMs are deployed per-market via two factories:
- **[crvUSD ControllerFactory](https://etherscan.io/address/0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC)** — for crvUSD borrowing markets (9 markets on mainnet)
- **[OneWayLendingFactory](https://etherscan.io/address/0xeA6876DDE9e3467564acBeE1Ed5bac88783205E0)** — for Curve Lending markets (46 markets on mainnet)
Both use the same AMM implementation — the interface is identical regardless of which factory deployed the market.
:::
---
## Market Discovery
### Via the crvUSD ControllerFactory
```solidity
// Number of crvUSD markets
factory.n_collaterals() → uint256
// Get AMM address by index
factory.amms(i: uint256) → address
// Get AMM for a specific collateral token
factory.get_amm(collateral: address, i: uint256) → address
// Get controller for a specific collateral token
factory.get_controller(collateral: address, i: uint256) → address
// Collateral token addresses
factory.collaterals(i: uint256) → address
```
### Via the OneWayLendingFactory (Curve Lending)
```solidity
// Number of lending markets
factory.market_count() → uint256
// Get AMM address by index
factory.amms(i: uint256) → address
// Get controller by index
factory.controllers(i: uint256) → address
// Get token addresses for a market
factory.borrowed_tokens(i: uint256) → address
factory.collateral_tokens(i: uint256) → address
factory.coins(vault_id: uint256) → address[2]
```
### Via the MetaRegistry
The [MetaRegistry](./meta-registry.md) also indexes LLAMMA pools. You can discover them alongside regular DEX pools:
```solidity
metaRegistry.find_pool_for_coins(_from: address, _to: address, i: uint256) → address
```
---
## Quoting Swap Amounts
```solidity
// How much of coin `j` will I get for `in_amount` of coin `i`?
amm.get_dy(i: uint256, j: uint256, in_amount: uint256) → uint256
// How much of coin `i` do I need to send to get `out_amount` of coin `j`?
amm.get_dx(i: uint256, j: uint256, out_amount: uint256) → uint256
// Get both input spent and output received for a given input
amm.get_dxdy(i: uint256, j: uint256, in_amount: uint256) → (uint256, uint256)
// Get both input needed and output received for a desired output
amm.get_dydx(i: uint256, j: uint256, out_amount: uint256) → (uint256, uint256)
```
The `get_dxdy` and `get_dydx` functions are useful for integrators because LLAMMA's band-based design means the actual input consumed may differ slightly from the requested amount — these functions return both values.
:::warning[Coin Indices]
- `coins[0]` = borrowed asset (e.g., crvUSD)
- `coins[1]` = collateral asset (e.g., WETH, wstETH, WBTC)
Indices use `uint256`.
:::
---
## Executing Swaps
### `exchange` — Standard Swap
Requires the caller to have approved the AMM to spend the input token.
```solidity
amm.exchange(
i: uint256, // index of input coin (0 or 1)
j: uint256, // index of output coin (0 or 1)
in_amount: uint256, // amount of input coin to swap
min_amount: uint256, // minimum output (slippage protection)
_for: address // recipient of output (defaults to msg.sender)
) → uint256[2] // [input_amount_spent, output_amount_received]
```
Note the return type: `uint256[2]` — returns **both** the actual input consumed and the output received. This differs from the other Curve AMMs which return only the output amount.
### `exchange_dy` — Swap by Output Amount
Specify the desired output amount instead of the input amount:
```solidity
amm.exchange_dy(
i: uint256, // index of input coin (0 or 1)
j: uint256, // index of output coin (0 or 1)
out_amount: uint256, // desired amount of output coin
max_amount: uint256, // maximum input willing to spend (slippage protection)
_for: address // recipient of output (defaults to msg.sender)
) → uint256[2] // [input_amount_spent, output_amount_received]
```
This is useful for aggregators that need to deliver an exact output amount.
---
## Fees
LLAMMA uses a **dynamic fee** that increases when the current price deviates from the oracle price. This incentivizes arbitrage to align the AMM price with the external market.
```solidity
// Current dynamic fee (1e18 precision, NOT 1e10 like DEX pools)
amm.dynamic_fee() → uint256
// Base fee parameter (1e18 precision)
amm.fee() → uint256
// Admin fee (fraction of fee going to admin, 1e18 precision)
amm.admin_fee() → uint256
```
:::warning[Fee Precision]
LLAMMA fees use **`1e18` precision**, unlike the DEX AMMs (Stableswap, Twocrypto, Tricrypto) which use `1e10`. A fee value of `6000000000000000` (6e15) means 0.6%.
:::
---
## Bands & Prices
LLAMMA's liquidity is organized into discrete price **bands** (similar to Uniswap V3 ticks). Each band covers a price range, and liquidity flows between bands as the price moves.
```solidity
// Current active band (where the price currently sits)
amm.active_band() → int256
// Active band accounting for empty bands that can be skipped
amm.active_band_with_skip() → int256
// Band boundaries
amm.min_band() → int256
amm.max_band() → int256
// Liquidity in a specific band
amm.bands_x(n: int256) → uint256 // borrowed asset (coins[0]) in band n
amm.bands_y(n: int256) → uint256 // collateral asset (coins[1]) in band n
// Check if bands can be skipped (empty bands with no liquidity)
amm.can_skip_bands(n_end: int256) → bool
```
### Band Prices
Each band has upper and lower price boundaries. There are two price systems — **current prices** (based on AMM state) and **oracle prices** (based on the external oracle):
```solidity
// Current price at the upper edge of band n
amm.p_current_up(n: int256) → uint256
// Current price at the lower edge of band n
amm.p_current_down(n: int256) → uint256
// Oracle-derived price at band boundaries
amm.p_oracle_up(n: int256) → uint256
amm.p_oracle_down(n: int256) → uint256
```
### Price Functions
```solidity
// External oracle price (from the market's price oracle contract)
amm.price_oracle() → uint256 // 1e18 precision
// Current AMM spot price
amm.get_p() → uint256
// Base price (initial price at deployment, adjusted by rate)
amm.get_base_price() → uint256
// Rate multiplier (accounts for accumulated interest)
amm.get_rate_mul() → uint256
// How much to trade to move the price to a target
amm.get_amount_for_price(p: uint256) → (uint256, bool)
// Returns: (amount, pump) — amount to trade, and direction
// pump=true means buying coins[1] (pushing price up)
```
---
## Understanding LLAMMA Liquidity
Unlike DEX pools where anyone can add/remove liquidity, LLAMMA liquidity comes exclusively from **borrowers' collateral**. When a user borrows against collateral:
1. Their collateral is deposited across a range of bands (4–50 bands, chosen at loan creation)
2. As the collateral price drops into a band's range, the AMM gradually converts collateral → borrowed asset (soft-liquidation)
3. If price recovers, the AMM converts back (de-liquidation)
For integrators, this means:
- **Liquidity availability depends on active loans** — bands may be empty if no borrowers have collateral in that price range
- **Liquidity is one-sided per band** — a fully soft-liquidated band contains only borrowed asset; an untouched band contains only collateral
- **The active band** is where trading actually happens — bands above are collateral-only, bands below are borrowed-asset-only
```solidity
// Check user position in the AMM
amm.read_user_tick_numbers(user: address) → int256[2] // [upper_band, lower_band]
amm.get_sum_xy(user: address) → uint256[2] // [total_x, total_y]
amm.get_xy(user: address) → uint256[][2] // per-band breakdown
amm.has_liquidity(user: address) → bool
```
---
## Contract Addresses (Ethereum Mainnet)
| Contract | Address |
|---|---|
| **crvUSD ControllerFactory** | [`0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC`](https://etherscan.io/address/0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC) |
| **OneWayLendingFactory** | [`0xeA6876DDE9e3467564acBeE1Ed5bac88783205E0`](https://etherscan.io/address/0xeA6876DDE9e3467564acBeE1Ed5bac88783205E0) |
| **AMM Implementation (crvUSD)** | [`0x2B7e624bdb839975d56D8428d9f6A4cf1160D3e9`](https://etherscan.io/address/0x2B7e624bdb839975d56D8428d9f6A4cf1160D3e9) |
| **AMM Implementation (Lending)** | [`0x0ec8e0c868541df59ceD49B39CC930C3a8DbD93a`](https://etherscan.io/address/0x0ec8e0c868541df59ceD49B39CC930C3a8DbD93a) |
:::tip
The two AMM implementations are **identical bytecode** — they were simply deployed separately by each factory. The interface is the same regardless of which factory created the market.
:::
:::tip
Both factories are deployed across many chains. Use the [AddressProvider](./address-provider.md) or check [Deployment Addresses](../deployments.md) for addresses on other networks.
:::
---
## Meta Registry
The `MetaRegistry` functions as a Curve Pool Registry Aggregator and offers an **on-chain API** for various properties of Curve pools by **consolidating different registries into a single contract**.
:::vyper[`MetaRegistry.vy and MetaRegistryL2.vy`]
The source code of the `MetaRegistry.vy` and `MetaRegistryL2.vy` contracts can be found on [GitHub](https://github.com/curvefi/metaregistry/tree/main/contracts).
Additionally, each `MetaRegistry` is integrated into the chain-specific [`AddressProvider`](./address-provider.md) at `ID = 7`. To get the most recent contract, users are advised to fetch it directly from the `AddressProvider`.
*For example, to query the `MetaRegistry` contract on Ethereum:*
```vyper
>>> AddressProvider.get_address(7)
'0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC'
```
A list of all deployed contracts can be found [here](../deployments.md).
```json
[{"name":"CommitNewAdmin","inputs":[{"name":"deadline","type":"uint256","indexed":true},{"name":"admin","type":"address","indexed":true}],"anonymous":false,"type":"event"},{"name":"NewAdmin","inputs":[{"name":"admin","type":"address","indexed":true}],"anonymous":false,"type":"event"},{"stateMutability":"nonpayable","type":"constructor","inputs":[{"name":"_address_provider","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"add_registry_handler","inputs":[{"name":"_registry_handler","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"update_registry_handler","inputs":[{"name":"_index","type":"uint256"},{"name":"_registry_handler","type":"address"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"get_registry_handlers_from_pool","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"address[10]"}]},{"stateMutability":"view","type":"function","name":"get_base_registry","inputs":[{"name":"registry_handler","type":"address"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"find_pool_for_coins","inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"find_pool_for_coins","inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"i","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"find_pools_for_coins","inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"}],"outputs":[{"name":"","type":"address[]"}]},{"stateMutability":"view","type":"function","name":"get_admin_balances","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"uint256[8]"}]},{"stateMutability":"view","type":"function","name":"get_admin_balances","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"uint256[8]"}]},{"stateMutability":"view","type":"function","name":"get_balances","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"uint256[8]"}]},{"stateMutability":"view","type":"function","name":"get_balances","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"uint256[8]"}]},{"stateMutability":"view","type":"function","name":"get_base_pool","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_base_pool","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_coin_indices","inputs":[{"name":"_pool","type":"address"},{"name":"_from","type":"address"},{"name":"_to","type":"address"}],"outputs":[{"name":"","type":"int128"},{"name":"","type":"int128"},{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"get_coin_indices","inputs":[{"name":"_pool","type":"address"},{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"int128"},{"name":"","type":"int128"},{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"get_coins","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"address[8]"}]},{"stateMutability":"view","type":"function","name":"get_coins","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"address[8]"}]},{"stateMutability":"view","type":"function","name":"get_decimals","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"uint256[8]"}]},{"stateMutability":"view","type":"function","name":"get_decimals","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"uint256[8]"}]},{"stateMutability":"view","type":"function","name":"get_fees","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"uint256[10]"}]},{"stateMutability":"view","type":"function","name":"get_fees","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"uint256[10]"}]},{"stateMutability":"view","type":"function","name":"get_gauge","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_gauge","inputs":[{"name":"_pool","type":"address"},{"name":"gauge_idx","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_gauge","inputs":[{"name":"_pool","type":"address"},{"name":"gauge_idx","type":"uint256"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_gauge_type","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"int128"}]},{"stateMutability":"view","type":"function","name":"get_gauge_type","inputs":[{"name":"_pool","type":"address"},{"name":"gauge_idx","type":"uint256"}],"outputs":[{"name":"","type":"int128"}]},{"stateMutability":"view","type":"function","name":"get_gauge_type","inputs":[{"name":"_pool","type":"address"},{"name":"gauge_idx","type":"uint256"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"int128"}]},{"stateMutability":"view","type":"function","name":"get_lp_token","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_lp_token","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_n_coins","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_n_coins","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_n_underlying_coins","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_n_underlying_coins","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_pool_asset_type","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_pool_asset_type","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_pool_from_lp_token","inputs":[{"name":"_token","type":"address"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_pool_from_lp_token","inputs":[{"name":"_token","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_pool_params","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"uint256[20]"}]},{"stateMutability":"view","type":"function","name":"get_pool_params","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"uint256[20]"}]},{"stateMutability":"view","type":"function","name":"get_pool_name","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"string"}]},{"stateMutability":"view","type":"function","name":"get_pool_name","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"string"}]},{"stateMutability":"view","type":"function","name":"get_underlying_balances","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"uint256[8]"}]},{"stateMutability":"view","type":"function","name":"get_underlying_balances","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"uint256[8]"}]},{"stateMutability":"view","type":"function","name":"get_underlying_coins","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"address[8]"}]},{"stateMutability":"view","type":"function","name":"get_underlying_coins","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"address[8]"}]},{"stateMutability":"view","type":"function","name":"get_underlying_decimals","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"uint256[8]"}]},{"stateMutability":"view","type":"function","name":"get_underlying_decimals","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"uint256[8]"}]},{"stateMutability":"view","type":"function","name":"get_virtual_price_from_lp_token","inputs":[{"name":"_token","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_virtual_price_from_lp_token","inputs":[{"name":"_token","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"is_meta","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"is_meta","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"is_registered","inputs":[{"name":"_pool","type":"address"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"is_registered","inputs":[{"name":"_pool","type":"address"},{"name":"_handler_id","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"pool_count","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"pool_list","inputs":[{"name":"_index","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"address_provider","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"owner","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_registry","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"registry_length","inputs":[],"outputs":[{"name":"","type":"uint256"}]}]
```
:::
*The contract utilizes `RegistryHandlers` interfaces to return data for most of the methods documented in this section:*
```shell
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def find_pool_for_coins(_from: address, _to: address, i: uint256 = 0) -> address: view
def get_admin_balances(_pool: address) -> uint256[MAX_COINS]: view
def get_balances(_pool: address) -> uint256[MAX_COINS]: view
def get_base_pool(_pool: address) -> address: view
def get_coins(_pool: address) -> address[MAX_COINS]: view
def get_coin_indices(_pool: address, _from: address, _to: address) -> (int128, int128, bool): view
def get_decimals(_pool: address) -> uint256[MAX_COINS]: view
def get_fees(_pool: address) -> uint256[10]: view
def get_gauges(_pool: address) -> (address[10], int128[10]): view
def get_lp_token(_pool: address) -> address: view
def get_n_coins(_pool: address) -> uint256: view
def get_n_underlying_coins(_pool: address) -> uint256: view
def get_pool_asset_type(_pool: address) -> uint256: view
def get_pool_from_lp_token(_lp_token: address) -> address: view
def get_pool_name(_pool: address) -> String[64]: view
def get_pool_params(_pool: address) -> uint256[20]: view
def get_underlying_balances(_pool: address) -> uint256[MAX_COINS]: view
def get_underlying_coins(_pool: address) -> address[MAX_COINS]: view
def get_underlying_decimals(_pool: address) -> uint256[MAX_COINS]: view
def is_meta(_pool: address) -> bool: view
def is_registered(_pool: address) -> bool: view
def pool_count() -> uint256: view
def pool_list(_index: uint256) -> address: view
def get_virtual_price_from_lp_token(_addr: address) -> uint256: view
def base_registry() -> address: view
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
---
## Finding Pools
Because the deployment of liquidity pools is permissionless, a significant number of pools are being deployed. Managing this vast array of pools can be challenging, and relying solely on a UI may not be the most effective and reliable approach. The `MetaRegistry` serves as an ideal tool for querying specific pools directly on-chain.
:::info[Understanding Base- and Metapool Logic]
The `MetaRegistry` considers metapools as well[^1]. For example, the [mkUSD/3CRV pool](https://etherscan.io/address/0x0CFe5C777A7438C9Dd8Add53ed671cEc7A5FAeE5) pairs `mkUSD` with the `3CRV` LP Token, which consists of `USDT`, `USDC`, and `DAI`. The contract identifies this logic and returns this pool e.g. when querying for `find_pools_for_coins(mkUSD, USDC)`, because mkUSD and USDC can be exchanged through this pool.
[^1]: Metapools are liquidity pools that pair a token against the LP token of another pool.
:::
*There are two key methods for querying pools containing two specific assets:*
- [`find_pools_for_coins`](#find_pools_for_coins): This function returns a list of all pools containing two specific tokens.
- [`find_pool_for_coins`](#find_pool_for_coins): This function returns a single pool containing two specific tokens, based on the input index from the list returned by `find_pools_for_coins`.
:::colab[Google Colab Notebook]
A guide on how to find liquidity pools which hold specific coins can be found [here](../integration/meta-registry.md#finding-pools).
A Jupyter notebook showcasing how to fetch pools directly from the blockchain, which contain two specific assets, can be found [here](https://colab.research.google.com/drive/1QsxqxQu7Um8gYPda30304W8ZcYbnbr1b?usp=sharing).
:::
### `find_pools_for_coins`
::::description[`MetaRegistry.find_pools_for_coins(_from: address, _to: address) -> DynArray[address, 1000]: view`]
Getter method for a list of pools that contain both the `_from` and `_to` tokens. It is designed to identify specific swap routes. The method returns all pools containing the specified assets, disregarding metrics such as total value locked (TVL) or other parameters.
| Input | Type | Description |
| ------- | --------- | ------------------------------ |
| `_from` | `address` | Address of coin to be sent |
| `_to` | `address` | Address of coin to be received |
Returns: pools (`DynArray[address, 1000]`).
```vyper
@view
@external
def find_pools_for_coins(_from: address, _to: address) -> DynArray[address, 1000]:
"""
@notice Find all pools that contain the input pair
@param _from Address of coin to be sent
@param _to Address of coin to be received
@return Pool addresses
"""
pools_found: DynArray[address, 1000]= empty(DynArray[address, 1000])
pool: address = empty(address)
registry: address = empty(address)
for registry_index in range(MAX_REGISTRIES):
registry = self.get_registry[registry_index]
if registry == empty(address):
break
for j in range(0, 65536):
pool = RegistryHandler(registry).find_pool_for_coins(_from, _to, j)
if pool == empty(address):
break
pools_found.append(pool)
return pools_found
@view
@external
def find_pool_for_coins(
_from: address, _to: address, i: uint256 = 0
) -> address:
"""
@notice Find the ith available pool containing the input pair
@param _from Address of coin to be sent
@param _to Address of coin to be received
@param i Index of the pool to return
@return Pool address
"""
pools_found: uint256 = 0
pool: address = empty(address)
registry: address = empty(address)
for registry_index in range(MAX_REGISTRIES):
registry = self.get_registry[registry_index]
if registry == empty(address):
break
for j in range(0, 65536):
pool = RegistryHandler(registry).find_pool_for_coins(_from, _to, j)
if pool == empty(address):
break
pools_found += 1
if pools_found > i:
return pool
return pool
```
In this example, we search for pools that include `crvUSD` and `ETH`. The function returns all pools including those two assets.
::::
### `find_pool_for_coins`
::::description[`MetaRegistry.find_pool_for_coins(_from: address, _to: address, i: uint256 = 0) -> address: view`]
Getter method for a pool that holds two coins (even if the pool is a metapool). The index in the query returns the index of the list of pools containing the two coins. The method returns all pools containing the specified assets, disregarding metrics such as total value locked (TVL) or other parameters.
| Input | Type | Description |
| ------- | --------- | ------------------------------ |
| `_from` | `address` | Address of coin to be sent |
| `_to` | `address` | Address of coin to be received |
| `i` | `uint256` | Index of the pool to return |
Returns: pool (`address`).
```vyper
@view
@external
def find_pool_for_coins(_from: address, _to: address, i: uint256 = 0) -> address:
"""
@notice Find the ith available pool containing the input pair
@param _from Address of coin to be sent
@param _to Address of coin to be received
@param i Index of the pool to return
@return Pool address
"""
pools_found: uint256 = 0
pool: address = empty(address)
registry: address = empty(address)
for registry_index in range(MAX_REGISTRIES):
registry = self.get_registry[registry_index]
if registry == empty(address):
break
for j in range(0, 65536):
pool = RegistryHandler(registry).find_pool_for_coins(_from, _to, j)
if pool == empty(address):
break
pools_found += 1
if pools_found > i:
return pool
return pool
```
In this example, we search for a single pool at index `i` which includes `crvUSD` and `ETH`. This method essentially returns the pools returned by `find_pools_for_coins`.
::::
---
## Pool Specific Information
All relevant pool and coin data for liquidity pools are stored in the `MetaRegistry`. This registry includes various functions that provide a wide range of data, such as pool balances, fees, decimals, and more.
### `pool_count`
::::description[`MetaRegistry.pool_count() -> uint256: view`]
Getter for the total number of pools registered in the `MetaRegistry`.
Returns: number of pools registered (`uint256`).
```vyper
# get registry/registry_handler by index, index starts at 0:
get_registry: public(HashMap[uint256, address])
registry_length: public(uint256)
@external
@view
def pool_count() -> uint256:
"""
@notice Return the total number of pools tracked by the metaregistry
@return uint256 The number of pools in the metaregistry
"""
total_pools: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
total_pools += RegistryHandler(handler).pool_count()
return total_pools
```
::::
### `pool_list`
::::description[`MetaRegistry.pool_list(_index: uint256) -> address: view`]
Getter for the pool at `_index`, with the index starting at `0`.
| Input | Type | Description |
| ------- | --------- | ----------------- |
| `_index` | `uint256` | Index of the pool |
Returns: pool (`address`).
```vyper
# get registry/registry_handler by index, index starts at 0:
get_registry: public(HashMap[uint256, address])
registry_length: public(uint256)
@external
@view
def pool_list(_index: uint256) -> address:
"""
@notice Return the pool at a given index in the metaregistry
@param _index The index of the pool in the metaregistry
@return The address of the pool at the given index
"""
pools_skip: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
count: uint256 = RegistryHandler(handler).pool_count()
if _index - pools_skip < count:
return RegistryHandler(handler).pool_list(_index - pools_skip)
pools_skip += count
return empty(address)
```
These examples essentially return the pools at index `0` and `1`, which are the first and second pools added to the `MetaRegistry`.
::::
### `get_pool_name`
::::description[`MetaRegistry.get_pool_name(_pool: address, _handler_id: uint256 = 0) -> String[64]: view`]
Getter for the name of a pool.
| Input | Type | Description |
| ------------- | --------- | ------------------- |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: name (`String[64]`).
```vyper
@external
@view
def get_pool_name(_pool: address, _handler_id: uint256 = 0) -> String[64]:
"""
@notice Get the given name for a pool
@param _pool Pool address
@return The name of a pool
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).get_pool_name(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `is_meta`
::::description[`MetaRegistry.is_meta(_pool: address, _handler_id: uint256 = 0) -> bool: view`]
Getter method to check if a pool is a metapool. Metapools are pools that pair a coin to a base pool.
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: true or false (`bool`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def is_meta(_pool: address) -> bool: view
@external
@view
def is_meta(_pool: address, _handler_id: uint256 = 0) -> bool:
"""
@notice Verify `_pool` is a metapool
@param _pool Pool address
@param _handler_id id of registry handler
@return True if `_pool` is a metapool
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).is_meta(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
An example is the [LUSD-3CRV](https://etherscan.io/address/0xed279fdd11ca84beef15af5d39bb4d4bee23f0ca) pool, which pairs [Liquity's](https://www.liquity.org/) [LUSD](https://etherscan.io/address/0x5f98805a4e8be255a32880fdec7f6728c6568ba0) against [3CRV](https://etherscan.io/address/0x6c3f90f043a72fa612cbac8115ee7e52bde6e490). 3CRV is a liquidity pool token that represents a share of a pool containing DAI, USDC, and USDT.
::::
### `get_base_pool`
::::description[`MetaRegistry.get_base_pool(_pool: address, _handler_id: uint256 = 0) -> address: view`]
Getter for the base pool of a metapool. This function can also be called on non-metapool pools; in that case, there is no base pool and the function will return `ZERO_ADDRESS`.
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: base pool (`address`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_base_pool(_pool: address) -> address: view
@external
@view
def get_base_pool(_pool: address, _handler_id: uint256 = 0) -> address:
"""
@notice Get the base pool for a given factory metapool
@dev Will return empty(address) if pool is not a metapool
@param _pool Metapool address
@param _handler_id id of registry handler
@return Address of base pool
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).get_base_pool(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
In the case of the LUSD-3CRV pool example, the function will return the 3pool as it is the base pool. When calling the same function for the 3pool itself, it returns `ZERO_ADDRESS` as it is a normal pool[^2].
[^2]: A base pool is also a regular pool.
::::
### `get_fees`
::::description[`MetaRegistry.get_fees(_pool: address, _handler_id: uint256 = 0) -> uint256[10]: view`]
Getter for the fee parameters that a Curve pool charges per swap. The fee data returned varies depending on the type of pool (see examples below).
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: fee parameters (`uint256[10]`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_fees(_pool: address) -> uint256[10]: view
@external
@view
def get_fees(_pool: address, _handler_id: uint256 = 0) -> uint256[10]:
"""
@notice Get pool fees
@dev Fees are expressed as integers
@param _pool Pool address
@param _handler_id id of registry handler
@return Pool fee as uint256 with 1e10 precision
Admin fee as 1e10 percentage of pool fee
Mid fee
Out fee
6 blank spots for future use cases
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).get_fees(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
**Stableswap** pools return the `fee` per swap and the `admin_fee` percentage. For the `3pool`, it shows that the pool charges 1 basis point per swap, 50% of which goes to the DAO. Stableswap-NG pools additionally return `offpeg_fee_multiplier`.
**Cryptoswap** pools return `fee`, `admin_fee` percentage, `mid_fee` and `out_fee`. The fee is the dynamic fee charged per swap, and ranges between `mid_fee` (balances in the pool are fully balanced) and the `out_fee` (balances in the pool are fully imbalanced).
::::
### `get_pool_params`
::::description[`MetaRegistry.get_pool_params(_pool: address, _handler_id: uint256 = 0) -> uint256[MAX_POOL_PARAMS]: view`]
Getter for the parameters of a pool. The parameters returned varies depending on the type of pool (see examples below).
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: parameters (`uint256[MAX_POOL_PARAMS]`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_pool_params(_pool: address) -> uint256[20]: view
@external
@view
def get_pool_params(_pool: address, _handler_id: uint256 = 0) -> uint256[MAX_POOL_PARAMS]:
"""
@notice Get the parameters of a pool
@param _pool Pool address
@param _handler_id id of registry handler
@return Pool parameters
"""
registry_handler: address = self._get_registry_handlers_from_pool(_pool)[_handler_id]
return RegistryHandler(registry_handler).get_pool_params(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
**Stableswap** pools return the amplification coefficient (`A`).
**Cryptoswap** pools return the amplification coefficient (`A`), `D` invariant, `gamma`, `allowed_extra_profit`, `fee_gamma`, `adjustment_step` and `ma_half_time`.
::::
### `get_lp_token`
::::description[`MetaRegistry.get_lp_token(_pool: address, _handler_id: uint256 = 0) -> address: view`]
Getter for the LP token of a pool.
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: LP token (`address`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_lp_token(_pool: address) -> address: view
@external
@view
def get_lp_token(_pool: address, _handler_id: uint256 = 0) -> address:
"""
@notice Get the address of the LP token of a pool
@param _pool Pool address
@param _handler_id id of registry handler
@return Address of the LP token
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).get_lp_token(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `get_pool_asset_type`
::::description[`MetaRegistry.get_pool_asset_type(_pool: address, _handler_id: uint256 = 0) -> uint256: view`]
Getter for the asset type of a pool according to: **`0 = USD`, `1 = ETH`, `2 = BTC`, `3 = Other`, `4 = CryptoPool`**. The asset type is only a property of StableSwap pools and is not enforced in CryptoSwap pools (which always return 4).
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: asset type (`uint256`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_pool_asset_type(_pool: address) -> uint256: view
@external
@view
def get_pool_asset_type(_pool: address, _handler_id: uint256 = 0) -> uint256:
"""
@notice Query the asset type of `_pool`
@param _pool Pool Address
@return The asset type as an unstripped string
@dev 0 : USD, 1: ETH, 2: BTC, 3: Other, 4: CryptoSwap
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).get_pool_asset_type(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `get_pool_from_lp_token`
::::description[`MetaRegistry.get_pool_from_lp_token(_token: address, _handler_id: uint256 = 0) -> address: view`]
Getter for the liquidity pool contract derived from an LP token.
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_token` | `address` | Address of the LP token |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: pool (`address`).
```vyper
@external
@view
def get_pool_from_lp_token(_token: address, _handler_id: uint256 = 0) -> address:
"""
@notice Get the pool associated with an LP token
@param _token LP token address
@return Pool address
"""
return self._get_pool_from_lp_token(_token)
@internal
@view
def _get_pool_from_lp_token(_token: address) -> address:
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
pool: address = RegistryHandler(handler).get_pool_from_lp_token(_token)
if pool != empty(address):
return pool
return empty(address)
```
::::
### `get_virtual_price_from_lp_token`
::::description[`MetaRegistry.get_virtual_price_from_lp_token(_token: address, _handler_id: uint256 = 0) -> uint256: view`]
Getter for a token's virtual price. The virtual price of any pool starts with a value of `1.0` and increases as the pool accrues fees. This number constantly increases for StableSwap pools unless the pool's amplification coefficient changes. For CryptoSwap pools, there are moments when the virtual price can decrease (e.g., admin fee claims, changes to the pool's parameters, etc.).
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_token` | `address` | Address of the LP token |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: virtual price of the LP token (`uint256`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_virtual_price_from_lp_token(_addr: address) -> uint256: view
@external
@view
def get_virtual_price_from_lp_token(_token: address, _handler_id: uint256 = 0) -> uint256:
"""
@notice Get the virtual price of a pool LP token
@param _token LP token address
@param _handler_id id of registry handler
@return uint256 Virtual price
"""
pool: address = self._get_pool_from_lp_token(_token)
registry_handler: address = self._get_registry_handlers_from_pool(pool)[_handler_id]
return RegistryHandler(registry_handler).get_virtual_price_from_lp_token(_token)
@internal
@view
def _get_pool_from_lp_token(_token: address) -> address:
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
pool: address = RegistryHandler(handler).get_pool_from_lp_token(_token)
if pool != empty(address):
return pool
return empty(address)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `is_registered`
::::description[`MetaRegistry.is_registered(_pool: address, _handler_id: uint256 = 0) -> bool: view`]
Function to check if a pool is registered in the `MetaRegistry`.
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: true or false (`bool`).
```vyper
@external
@view
def is_registered(_pool: address, _handler_id: uint256 = 0) -> bool:
"""
@notice Check if a pool is in the metaregistry using get_n_coins
@param _pool The address of the pool
@param _handler_id id of registry handler
@return A bool corresponding to whether the pool belongs or not
"""
return self._get_registry_handlers_from_pool(_pool)[_handler_id] != empty(address)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `get_gauge`
::::description[`MetaRegistry.get_gauge(_pool: address, gauge_idx: uint256 = 0, _handler_id: uint256 = 0) -> address: view`]
Getter for the liquidity gauge of a pool.
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `gauge_idx` | `uint256` | Index of the gauge; defaults to 0 |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: gauge address (`address`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_gauges(_pool: address) -> (address[10], int128[10]): view
@external
@view
def get_gauge(_pool: address, gauge_idx: uint256 = 0, _handler_id: uint256 = 0) -> address:
"""
@notice Get a single liquidity gauge contract associated with a pool
@param _pool Pool address
@param gauge_idx Index of gauge to return
@param _handler_id id of registry handler
@return Address of gauge
"""
registry_handler: RegistryHandler = RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id])
handler_output: address[10] = registry_handler.get_gauges(_pool)[0]
return handler_output[gauge_idx]
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `get_gauge_type`
::::description[`MetaRegistry.get_gauge_type(_pool: address, gauge_idx: uint256 = 0, _handler_id: uint256 = 0) -> int128: view`]
Getter for the gauge type of the gauge associated with a liquidity pool.
| Input | Type | Description |
| ------------ | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `gauge_idx` | `uint256` | Index of the gauge; defaults to 0 |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: gauge type (`int128`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_gauges(_pool: address) -> (address[10], int128[10]): view
@external
@view
def get_gauge_type(_pool: address, gauge_idx: uint256 = 0, _handler_id: uint256 = 0) -> int128:
"""
@notice Get gauge_type of a single liquidity gauge contract associated with a pool
@param _pool Pool address
@param gauge_idx Index of gauge to return
@param _handler_id id of registry handler
@return Address of gauge
"""
registry_handler: RegistryHandler = RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id])
handler_output: int128[10] = registry_handler.get_gauges(_pool)[1]
return handler_output[gauge_idx]
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `get_coins`
::::description[`MetaRegistry.get_coins(_pool: address, _handler_id: uint256 = 0) -> address[MAX_COINS]: view`]
Getter method for the coins in a pool. If the pool is a metapool, the method returns the LP token of the base pool, not the underlying coins. To additionally return the underlying coins, see: [`get_underlying_coins`](#get_underlying_coins).
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: coins (`address[MAX_COINS]`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_coins(_pool: address) -> address[MAX_COINS]: view
@external
@view
def get_coins(_pool: address, _handler_id: uint256 = 0) -> address[MAX_COINS]:
"""
@notice Get the coins within a pool
@dev For metapools, these are the wrapped coin addresses
@param _pool Pool address
@param _handler_id id of registry handler
@return List of coin addresses
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).get_coins(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `get_n_coins`
::::description[`MetaRegistry.get_n_coins(_pool: address, _handler_id: uint256 = 0) -> uint256: view`]
Getter for the number of coins in a pool. If the pool is a metapool, the method returns `2`, the meta- and base pool token. To additionally return the number of coins including the underlying ones from the base pool, see: [`get_n_underlying_coins`](#get_underlying_coins).
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: number of coins (`uint256`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_n_coins(_pool: address) -> uint256: view
@external
@view
def get_n_coins(_pool: address, _handler_id: uint256 = 0) -> uint256:
"""
@notice Get the number of coins in a pool
@dev For metapools, it is tokens + wrapping/lending token (no underlying)
@param _pool Pool address
@return Number of coins
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).get_n_coins(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `get_decimals`
::::description[`MetaRegistry.get_decimals(_pool: address, _handler_id: uint256 = 0) -> uint256[MAX_COINS]: view`]
Getter for the decimals of the coins in a pool. If the pool is a metapool, the method returns the decimals of the meta- and base pool token. To additionally return the decimals of the underlying coin from the base pool, see: [`get_underlying_decimals`](#get_underlying_decimals).
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: coin decimals (`uint256[MAX_COINS]`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_decimals(_pool: address) -> uint256[MAX_COINS]: view
@external
@view
def get_decimals(_pool: address, _handler_id: uint256 = 0) -> uint256[MAX_COINS]:
"""
@notice Get decimal places for each coin within a pool
@dev For metapools, these are the wrapped coin decimal places
@param _pool Pool address
@param _handler_id id of registry handler
@return uint256 list of decimals
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).get_decimals(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `get_balances`
::::description[`MetaRegistry.get_balances(_pool: address, _handler_id: uint256 = 0) -> uint256[MAX_COINS]: view`]
Getter for the coin balances in a pool. If the pool is a metapool, the method returns the balances of the meta- and base pool tokens. To additionally return the balances of the underlying coins from the base pool, see: [`get_underlying_balances`](#get_underlying_balances).
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: balances (`uint256[MAX_COINS]`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_balances(_pool: address) -> uint256[MAX_COINS]: view
@external
@view
def get_balances(_pool: address, _handler_id: uint256 = 0) -> uint256[MAX_COINS]:
"""
@notice Get balances for each coin within a pool
@dev For metapools, these are the wrapped coin balances
@param _pool Pool address
@param _handler_id id of registry handler
@return uint256 list of balances
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).get_balances(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `get_underlying_coins`
::::description[`MetaRegistry.get_underlying_coins(_pool: address, _handler_id: uint256 = 0) -> address[MAX_COINS]: view`]
Getter for all coins in a pool, including the underlying ones. For non-metapools, it returns the same value as [`get_coins`](#get_coins).
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: underlying coins (`address[MAX_COINS]`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_underlying_coins(_pool: address) -> address[MAX_COINS]: view
@external
@view
def get_underlying_coins(_pool: address, _handler_id: uint256 = 0) -> address[MAX_COINS]:
"""
@notice Get the underlying coins within a pool
@dev For non-metapools, returns the same value as `get_coins`
@param _pool Pool address
@param _handler_id id of registry handler
@return List of coin addresses
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).get_underlying_coins(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `get_n_underlying_coins`
::::description[`MetaRegistry.get_n_underlying_coins(_pool: address, _handler_id: uint256 = 0) -> uint256: view`]
Getter for the number of coins in a pool, including the underlying ones. For non-metapools, it returns the same value as [`get_n_coins`](#get_n_coins).
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: number of coins (`uint256`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_n_underlying_coins(_pool: address) -> uint256: view
@external
@view
def get_n_underlying_coins(_pool: address, _handler_id: uint256 = 0) -> uint256:
"""
@notice Get the number of underlying coins in a pool
@dev For non-metapools, returns the same as get_n_coins
@param _pool Pool address
@return Number of coins
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).get_n_underlying_coins(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `get_underlying_decimals`
::::description[`MetaRegistry.get_underlying_decimals(_pool: address, _handler_id: uint256 = 0) -> uint256[MAX_COINS]: view`]
Getter for the decimals of the coins in a pool, including those for the underlying ones. For non-metapools, it returns the same value as [`get_decimals`](#get_decimals).
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: coin decimals (`uint256[MAX_COINS]`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_underlying_decimals(_pool: address) -> uint256[MAX_COINS]: view
@external
@view
def get_underlying_decimals(_pool: address, _handler_id: uint256 = 0) -> uint256[MAX_COINS]:
"""
@notice Get decimal places for each underlying coin within a pool
@dev For non-metapools, returns the same value as `get_decimals`
@param _pool Pool address
@param _handler_id id of registry handler
@return uint256 list of decimals
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).get_underlying_decimals(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `get_underlying_balances`
::::description[`MetaRegistry.get_underlying_balances(_pool: address, _handler_id: uint256 = 0) -> uint256[MAX_COINS]: view`]
Getter method for the coin balances in a pool, including those for the underlying ones. For non-metapools, it returns the same value as [`get_balances`](#get_balances).
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: coin balances (`uint256[MAX_COINS]`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_underlying_balances(_pool: address) -> uint256[MAX_COINS]: view
@external
@view
def get_underlying_balances(_pool: address, _handler_id: uint256 = 0) -> uint256[MAX_COINS]:
"""
@notice Get balances for each underlying coin within a pool
@dev For non-metapools, returns the same value as `get_balances`
@param _pool Pool address
@param _handler_id id of registry handler
@return uint256 List of underlying balances
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).get_underlying_balances(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `get_admin_balances`
::::description[`MetaRegistry.get_admin_balances(_pool: address, _handler_id: uint256 = 0) -> uint256[MAX_COINS]: view`]
Getter for the pool's admin balances. The admin balances are essentially the fees that can be claimed and paid out to veCRV holders.
| Input | Type | Description |
| ------------- | --------- | ------------------------------------------ |
| `_pool` | `address` | Address of the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: admin balances (`uint256[MAX_COINS]`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_admin_balances(_pool: address) -> uint256[MAX_COINS]: view
@external
@view
def get_admin_balances(_pool: address, _handler_id: uint256 = 0) -> uint256[MAX_COINS]:
"""
@notice Get the current admin balances (uncollected fees) for a pool
@dev _handler_id < 1 if pool is registry in one handler, more than 0 otherwise
@param _pool Pool address
@param _handler_id id of registry handler
@return List of uint256 admin balances
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).get_admin_balances(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
::::
### `get_coin_indices`
::::description[`MetaRegistry.get_coin_indices(_pool: address, _from: address, _to: address, _handler_id: uint256 = 0) -> (int128, int128, bool): view`]
Getter method that converts coin addresses to indices.
| Input | Type | Description |
| ------------- | --------- | ---------------------------------------------- |
| `_pool` | `address` | Address of the pool |
| `_from` | `address` | Coin address to be used as `i` within the pool |
| `_to` | `address` | Coin address to be used as `j` within the pool |
| `_handler_id` | `uint256` | ID of the `RegistryHandler`; defaults to 0 |
Returns: index for `_from` (`int128`), index for `_to` (`int128`) and whether the market a metapool or not (`bool`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def get_coin_indices(_pool: address, _from: address, _to: address) -> (int128, int128, bool): view
@view
@external
def get_coin_indices(_pool: address, _from: address, _to: address, _handler_id: uint256 = 0) -> (int128, int128, bool):
"""
@notice Convert coin addresses to indices for use with pool methods
@param _pool Pool address
@param _from Coin address to be used as `i` within a pool
@param _to Coin address to be used as `j` within a pool
@param _handler_id id of registry handler
@return from index, to index, is the market underlying ?
"""
return RegistryHandler(self._get_registry_handlers_from_pool(_pool)[_handler_id]).get_coin_indices(_pool, _from, _to)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise "no registry"
return pool_registry_handler
```
The first example checks the index of `DAI` and `USDC` within the 3pool. The second one checks the index of `LUSD` and `USDC` within the `LUSD<>3CRV` pool.
::::
---
## Handlers and Registries
The `MetaRegistry` makes use of `Handlers`, which are essentially wrappers around other contracts (mostly Pool Factories) to ensure ABI compatibility with the contract itself.
New handlers can be added or existing ones can be updated by the [`owner`](#owner) of the contract.
*To fetch registry information from the contract, the following methods can be used:*
- `get_registry_length`: Returns the total number of registries added.
- `get_registry`: Fetches single registries.
- `get_base_registry`: Returns the "base registry" of a handler.
- `get_registry_handlers_from_pool`: Fetches the handler from pools.
:::colab[Google Colab Notebook]
A Google Colab notebook showcasing how to query registries or add/update them can be found [here ↗](https://colab.research.google.com/drive/1wFvIeNKpKhy58xkGSfKw0XzEPnwn9Zym?usp=sharing).
:::
### `get_registry_handlers_from_pool`
::::description[`MetaRegistry.get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]: view`]
Getter for the `RegistryHandler` that a pool has been registered in. Usually, each pool is registered in a single registry.
| Input | Type | Description |
| ------------- | --------- | ------------------- |
| `_pool` | `address` | Address of the pool |
Returns: `RegistryHandler` (`address[MAX_REGISTRIES]`).
```vyper
@external
@view
def get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get the registry handlers associated with a pool
@param _pool Pool address
@return List of registry handlers
"""
return self._get_registry_handlers_from_pool(_pool)
@internal
@view
def _get_registry_handlers_from_pool(_pool: address) -> address[MAX_REGISTRIES]:
"""
@notice Get registry handler that handles the registry api for a pool
@dev sometimes a factory pool can be registered in a manual registry
because of this, we always take the last registry a pool is
registered in and not the first, as manual registries are first
and factories come later
@param _pool address of the pool
@return registry_handlers: address[MAX_REGISTRIES]
"""
pool_registry_handler: address[MAX_REGISTRIES] = empty(address[MAX_REGISTRIES])
c: uint256 = 0
for i in range(MAX_REGISTRIES):
if i == self.registry_length:
break
handler: address = self.get_registry[i]
if RegistryHandler(handler).is_registered(_pool):
pool_registry_handler[c] = handler
c += 1
if pool_registry_handler[0] == empty(address):
raise("no registry")
return pool_registry_handler
```
::::
### `get_base_registry`
::::description[`MetaRegistry.get_base_registry(registry_handler: address) -> address: view`]
Getter for the `BaseRegistry` associated with a `RegistryHandler`.
| Input | Type | Description |
| ------------------ | --------- | -------------------------- |
| `registry_handler` | `address` | `RegistryHandler` contract |
Returns: `BaseRegistry` (`address`).
```vyper
# registry and registry handlers are considered to be the same here.
# registry handlers are just wrapper contracts that simplify/fix underlying registries
# for integrating it into the Metaregistry.
interface RegistryHandler:
def base_registry() -> address: view
@external
@view
def get_base_registry(registry_handler: address) -> address:
"""
@notice Get the registry associated with a registry handler
@param registry_handler Registry Handler address
@return Address of base registry
"""
return RegistryHandler(registry_handler).base_registry()
```
::::
### `get_registry`
::::description[`MetaRegistry.get_registry(arg0: uint256) -> address: view`]
Getter for the `RegistryHandler` at index `arg0`. New handlers can be added via the [`add_registry_handler`](#add_registry_handler) function.
| Input | Type | Description |
| ------ | --------- | --------------------- |
| `arg0` | `uint256` | Index (starts at `0`) |
Returns: `Registry` (`address`).
```vyper
# get registry/registry_handler by index, index starts at 0:
get_registry: public(HashMap[uint256, address])
registry_length: public(uint256)
```
::::
### `registry_length`
::::description[`MetaRegistry.registry_length() -> uint256: view`]
Getter for the registry length, essentially how many registries have been added to the `MetaRegistry`. This variable is incremented by one when adding a new registry.
Returns: number of registries added (`uint256`).
```vyper
# get registry/registry_handler by index, index starts at 0:
get_registry: public(HashMap[uint256, address])
registry_length: public(uint256)
```
::::
### `address_provider`
::::description[`MetaRegistry.address_provider() -> address: view`]
Getter for the `AddressProvider` contract.
Returns: `AddressProvider` (`address`).
```vyper
address_provider: public(AddressProvider)
@external
def __init__(_address_provider: address):
self.address_provider = AddressProvider(_address_provider)
self.owner = AddressProvider(_address_provider).admin()
```
::::
---
## Adding and Updating Registries
New registries can be added by the `owner` of the contract using the [`add_registry_handler`](#add_registry_handler) function. Existing ones can be updated using the [`update_registry_handler`](#update_registry_handler) function.
:::colab[Google Colab Notebook]
A Google Colab notebook showcasing how to query registries or add/update them can be found [:logos-googlecolab: here](https://colab.research.google.com/drive/1wFvIeNKpKhy58xkGSfKw0XzEPnwn9Zym?usp=sharing).
:::
### `owner`
::::description[`MetaRegistry.owner() -> address: view`]
Getter for the owner of the contract, who can perform owner-guarded functions.
Returns: owner (`address`).
```vyper
owner: public(address)
@external
def __init__(_address_provider: address):
self.address_provider = AddressProvider(_address_provider)
self.owner = AddressProvider(_address_provider).admin()
```
::::
### `add_registry_handler`
::::description[`MetaRegistry.add_registry_handler(_registry_handler: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to add a `RegistryHandler` to the `MetaRegistry`.
| Input | Type | Description |
| ------------------- | --------- | -------------------------------- |
| `_registry_handler` | `address` | `RegistryHandler` to add |
```vyper
@external
def add_registry_handler(_registry_handler: address):
"""
@notice Adds a registry from the address provider entry
@param _registry_handler Address of the handler contract
"""
assert msg.sender == self.owner # dev: only owner
self._update_single_registry(self.registry_length, _registry_handler)
@internal
def _update_single_registry(_index: uint256, _registry_handler: address):
assert _index <= self.registry_length
if _index == self.registry_length:
self.registry_length += 1
self.get_registry[_index] = _registry_handler
```
::::
### `update_registry_handler`
::::description[`MetaRegistry.update_registry_handler(_index: uint256, _registry_handler: address)`]
:::guard[Guarded Method]
This function is only callable by the `owner` of the contract.
:::
Function to update an already existing `RegistryHandler` with a new one.
| Input | Type | Description |
| ------------------- | --------- | ------------------------------------------------- |
| `_index` | `uint256` | Index of the registry according to `get_registry` |
| `_registry_handler` | `address` | address of the new handler contract |
```vyper
@external
def update_registry_handler(_index: uint256, _registry_handler: address):
"""
@notice Updates the contract used to handle a registry
@param _index The index of the registry in get_registry
@param _registry_handler Address of the new handler contract
"""
assert msg.sender == self.owner # dev: only owner
assert _index < self.registry_length
self._update_single_registry(_index, _registry_handler)
@internal
def _update_single_registry(_index: uint256, _registry_handler: address):
assert _index <= self.registry_length
if _index == self.registry_length:
self.registry_length += 1
self.get_registry[_index] = _registry_handler
```
::::
---
## Integration Docs
This section is targeted at external third parties interested in integrating Curve into their systems. Curve provides various contracts that simplify the integration process, making the lives of integrators much easier and more efficient.
*For integrators, the following contracts may be of great use:*
The `AddressProvider` contract acts as an **entry point for Curve's various registries**, deployed across all chains where Curve infrastructure is present. It maps all relevant Curve contracts across different chains.
The `MetaRegistry` contract serves as a Curve Finance Pool Registry Aggregator, providing an on-chain API that consolidates various properties of Curve pools by **integrating multiple registries into a single contract**.
The `RateProvider` contract **provides exchange rates for token swaps** using different Curve AMMs that are recognized within the `MetaRegistry`.
---
## Guides
*Below are some basic guides and examples. More will be added soon.*
### Fetching Pools
[Discover how to check on-chain pools containing two specific assets.](./meta-registry.md#finding-pools)
---
## Rate Provider
The `RateProvider` contract is designed to provide rates for token swaps.
:::vyper[`RateProvider.vy`]
The source code of the `RateProvider.vy` contract can be found on [GitHub](https://github.com/curvefi/metaregistry/blob/main/contracts/RateProvider.vy).
Additionally, each `RateProvider` contract is **integrated into the chain-specific [`AddressProvider`](./address-provider.md) at `ID = 18`**. To get the **most recent contract, users are advised to fetch it directly from the `AddressProvider`**.
*For example, to query the `RateProvider` contract on Ethereum:*
```vyper
>>> AddressProvider.get_address(18)
'0xA834f3d23749233c9B61ba723588570A1cCA0Ed7'
```
A list of all deployed contracts can be found [here](../deployments.md).
```json
[{"stateMutability":"nonpayable","type":"constructor","inputs":[{"name":"address_provider","type":"address"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"get_quotes","inputs":[{"name":"source_token","type":"address"},{"name":"destination_token","type":"address"},{"name":"amount_in","type":"uint256"}],"outputs":[{"name":"","type":"tuple[]","components":[{"name":"source_token_index","type":"uint256"},{"name":"dest_token_index","type":"uint256"},{"name":"is_underlying","type":"bool"},{"name":"amount_out","type":"uint256"},{"name":"pool","type":"address"},{"name":"source_token_pool_balance","type":"uint256"},{"name":"dest_token_pool_balance","type":"uint256"},{"name":"pool_type","type":"uint8"}]}]},{"stateMutability":"view","type":"function","name":"get_aggregated_rate","inputs":[{"name":"source_token","type":"address"},{"name":"destination_token","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"version","inputs":[],"outputs":[{"name":"","type":"string"}]},{"stateMutability":"view","type":"function","name":"ADDRESS_PROVIDER","inputs":[],"outputs":[{"name":"","type":"address"}]}]
```
:::
The contract has a [`get_quotes`](#get_quotes) method which fetches and returns exchange rates for specified token pairs. These quotes are only sourced from Curve AMM pools. The contract strictly relies on the `Metaregistry` contract as it fetches rates only from pools picked up by it[^1]. Additionally, there is a [`get_aggregated_rate`](#get_aggregated_rate) method which returns a weighted aggregated rate.
The logic of the contract is to identify the pool type used to facilitate the desired swap and then use the corresponding ABI, which essentially calls the `get_dy` or `get_dy_underlying` function to fetch the rates.
[^1]: All old liquidity pools are integrated into the `Metaregistry`. Newly deployed ones are automatically picked up. Therefore, all pools *should* be included.
```vyper
STABLESWAP_META_ABI: constant(String[64]) = "get_dy_underlying(int128,int128,uint256)"
STABLESWAP_ABI: constant(String[64]) = "get_dy(int128,int128,uint256)"
CRYPTOSWAP_ABI: constant(String[64]) = "get_dy(uint256,uint256,uint256)"
```
---
### `get_quotes`
::::description[`CurveRateProvider.get_quotes(source_token: address, destination_token: address, amount_in: uint256) -> DynArray[Quote, MAX_QUOTES]: view`]
Getter method which returns quotes for a specified `source_token` compared to a `destination_token` based on the input amount `amount_in`.
| Input | Type | Description |
| ------------------- | --------- | ---------------------------- |
| `source_token` | `address` | Source token. |
| `destination_token` | `address` | Destination token. |
| `amount_in` | `uint256` | Amount of tokens the provided rate is based on. |
Returns: a dynamic array of `Quote` structs containing quote data for each pool (`DynArray[Quote, MAX_QUOTES]`).
- `source_token_index (uint256)`: Index of the input token in the pool.
- `dest_token_index (uint256)`: Index of the output token in the pool.
- `is_underlying (bool)`: Indicates if a metapool is involved.
- `amount_out (uint256)`: Amount of the destination token to be received.
- `pool (address)`: Liquidity pool address from which the rate is provided.
- `source_token_pool_balance (uint256)`: Source token balance within the pool.
- `dest_token_pool_balance (uint256)`: Destination token balance within the pool.
- `pool_type (uint8)`: Type of pool: `0 = Stableswap`, `1 = Cryptoswap`, `2 = LLAMMA`
```vyper
struct Quote:
source_token_index: uint256
dest_token_index: uint256
is_underlying: bool
amount_out: uint256
pool: address
source_token_pool_balance: uint256
dest_token_pool_balance: uint256
pool_type: uint8 # 0 for stableswap, 1 for cryptoswap, 2 for LLAMMA.
interface AddressProvider:
def get_address(id: uint256) -> address: view
interface Metaregistry:
def find_pools_for_coins(source_coin: address, destination_coin: address) -> DynArray[address, 1000]: view
def get_coin_indices(_pool: address, _from: address, _to: address) -> (int128, int128, bool): view
def get_underlying_balances(_pool: address) -> uint256[MAX_COINS]: view
ADDRESS_PROVIDER: public(immutable(AddressProvider))
METAREGISTRY_ID: constant(uint256) = 7
STABLESWAP_META_ABI: constant(String[64]) = "get_dy_underlying(int128,int128,uint256)"
STABLESWAP_ABI: constant(String[64]) = "get_dy(int128,int128,uint256)"
CRYPTOSWAP_ABI: constant(String[64]) = "get_dy(uint256,uint256,uint256)"
@external
@view
def get_quotes(source_token: address, destination_token: address, amount_in: uint256) -> DynArray[Quote, MAX_QUOTES]:
return self._get_quotes(source_token, destination_token, amount_in)
@internal
@view
def _get_quotes(source_token: address, destination_token: address, amount_in: uint256) -> DynArray[Quote, MAX_QUOTES]:
quotes: DynArray[Quote, MAX_QUOTES] = []
metaregistry: Metaregistry = Metaregistry(ADDRESS_PROVIDER.get_address(METAREGISTRY_ID))
pools: DynArray[address, 1000] = metaregistry.find_pools_for_coins(source_token, destination_token)
if len(pools) == 0:
return quotes
# get pool types for each pool
for pool in pools:
# is it a stableswap pool? are the coin pairs part of a metapool?
pool_type: uint8 = self._get_pool_type(pool, metaregistry)
# get coin indices
i: int128 = 0
j: int128 = 0
is_underlying: bool = False
(i, j, is_underlying) = metaregistry.get_coin_indices(pool, source_token, destination_token)
# get balances
balances: uint256[MAX_COINS] = metaregistry.get_underlying_balances(pool)
dyn_balances: DynArray[uint256, MAX_COINS] = []
for bal in balances:
if bal > 0:
dyn_balances.append(bal)
# skip if pool is too small
if 0 in dyn_balances:
continue
# do a get_dy call and only save quote if call does not bork; use correct abi (in128 vs uint256)
quote: uint256 = self._get_pool_quote(i, j, amount_in, pool, pool_type, is_underlying)
# check if get_dy works and if so, append quote to dynarray
if quote > 0 and len(quotes) < MAX_QUOTES:
quotes.append(
Quote(
{
source_token_index: convert(i, uint256),
dest_token_index: convert(j, uint256),
is_underlying: is_underlying,
amount_out: quote,
pool: pool,
source_token_pool_balance: balances[i],
dest_token_pool_balance: balances[j],
pool_type: pool_type
}
)
)
return quotes
@internal
@view
def _get_pool_quote(
i: int128,
j: int128,
amount_in: uint256,
pool: address,
pool_type: uint8,
is_underlying: bool
) -> uint256:
success: bool = False
response: Bytes[32] = b""
method_abi: Bytes[4] = b""
# choose the right abi:
if pool_type == 0 and is_underlying:
method_abi = method_id(STABLESWAP_META_ABI)
elif pool_type == 0 and not is_underlying:
method_abi = method_id(STABLESWAP_ABI)
else:
method_abi = method_id(CRYPTOSWAP_ABI)
success, response = raw_call(
pool,
concat(
method_abi,
convert(i, bytes32),
convert(j, bytes32),
convert(amount_in, bytes32),
),
max_outsize=32,
revert_on_failure=False,
is_static_call=True
)
if success:
return convert(response, uint256)
return 0
@internal
@view
def _get_pool_type(pool: address, metaregistry: Metaregistry) -> uint8:
# 0 for stableswap, 1 for cryptoswap, 2 for LLAMMA.
success: bool = False
response: Bytes[32] = b""
# check if cryptoswap
success, response = raw_call(
pool,
method_id("allowed_extra_profit()"),
max_outsize=32,
revert_on_failure=False,
is_static_call=True
)
if success:
return 1
# check if llamma
success, response = raw_call(
pool,
method_id("get_rate_mul()"),
max_outsize=32,
revert_on_failure=False,
is_static_call=True
)
if success:
return 2
return 0
```
This example shows the quotes when swapping 1000 `CRV` for `asdCRV`. The `get_quotes` method returns two `Quote` structs because there are two pools that can facilitate the trade:
::::
### `get_aggregated_rate`
::::description[`CurveRateProvider.get_aggregated_rate(source_token: address, destination_token: address) -> uint256: view`]
Getter for the weighted aggregated rate of all quotes from the `source_token` to the `destination_token`. The calculations are based on an input amount of 1 unit of the source token. The aggregated rate is calculated as follows:
1. For each quote, the balances of the source and destination tokens in the pool are normalized to a scale of 18 decimals.
2. The total balance is computed by summing the normalized balances of the source and destination tokens across all pools.
3. The weight for each quote is determined by the proportion of its normalized pool balance to the total balance. The weighted average is then computed by summing the product of each quote's output amount and its weight.
| Input | Type | Description |
| ------------------- | --------- | ---------------------------- |
| `source_token` | `address` | Source token. |
| `destination_token` | `address` | Destination token. |
Returns: aggregated rate (`uint256`).
```vyper
@external
@view
def get_aggregated_rate(source_token: address, destination_token: address) -> uint256:
amount_in: uint256 = 10**convert(ERC20Detailed(source_token).decimals(), uint256)
quotes: DynArray[Quote, MAX_QUOTES] = self._get_quotes(source_token, destination_token, amount_in)
return self.weighted_average_quote(
convert(ERC20Detailed(source_token).decimals(), uint256),
convert(ERC20Detailed(destination_token).decimals(), uint256),
quotes,
)
@internal
@pure
def weighted_average_quote(
source_token_decimals: uint256,
dest_token_decimals: uint256,
quotes: DynArray[Quote, MAX_QUOTES]
) -> uint256:
num_quotes: uint256 = len(quotes)
# Calculate total balance with normalization
total_balance: uint256 = 0
for i in range(num_quotes, bound=MAX_QUOTES):
source_balance_normalized: uint256 = quotes[i].source_token_pool_balance * 10**(18 - source_token_decimals)
dest_balance_normalized: uint256 = quotes[i].dest_token_pool_balance * 10**(18 - dest_token_decimals)
total_balance += source_balance_normalized + dest_balance_normalized
# Calculate weighted sum with normalization
weighted_avg: uint256 = 0
for i in range(num_quotes, bound=MAX_QUOTES):
source_balance_normalized: uint256 = quotes[i].source_token_pool_balance * 10**(18 - source_token_decimals)
dest_balance_normalized: uint256 = quotes[i].dest_token_pool_balance * 10**(18 - dest_token_decimals)
pool_balance_normalized: uint256 = source_balance_normalized + dest_balance_normalized
weight: uint256 = (pool_balance_normalized * 10**18) / total_balance # Use 18 decimal places for precision
weighted_avg += weight * quotes[i].amount_out / 10**18
return weighted_avg
```
::::
### `version`
::::description[`CurveRateProvider.version() -> String[8]: view`]
Getter for the version of the rate provider contract.
Returns: contract version (`String[8]`).
```vyper
version: public(constant(String[8])) = "1.0.0"
```
::::
### `ADDRESS_PROVIDER`
::::description[`CurveRateProvider.ADDRESS_PROVIDER() -> address: view`]
Getter for the address provider contract. This variable is set when initializing the contract and cannot be changed afterward. Documentation for the address provider can be found [here](../integration/address-provider.md).
Returns: address provider contract (`address`).
```vyper
interface AddressProvider:
def get_address(id: uint256) -> address: view
ADDRESS_PROVIDER: public(immutable(AddressProvider))
@external
def __init__(address_provider: address):
ADDRESS_PROVIDER = AddressProvider(address_provider)
```
::::
---
## Integrating Stableswap-NG
Stableswap-NG is Curve's AMM for trading **pegged and correlated assets** — stablecoins (USDC/USDT), liquid staking tokens (wstETH/ETH), yield-bearing tokens (sDAI), and other assets expected to trade near a fixed ratio. Pools support 2–8 coins.
There are two pool types:
- **Plain pools** — all coins are top-level ERC-20 tokens (e.g., USDC/USDT/DAI)
- **Metapools** — one coin paired against an existing Curve base pool's LP token (e.g., newStable/3CRV), enabling swaps with all underlying base pool tokens
Both share the same swap interface. Metapools add `exchange_underlying()` for direct swaps into base pool tokens.
:::info
All Stableswap-NG pools are deployed via the **[Factory](https://etherscan.io/address/0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf)** (`0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf`). The Factory uses blueprint patterns — pool contracts are deployed as minimal proxies of stored implementations.
:::
---
## Pool Discovery
### Via the Factory
The Factory maintains a registry of all deployed Stableswap-NG pools.
```solidity
// Get total number of pools
factory.pool_count() → uint256
// Get pool address by index
factory.pool_list(i: uint256) → address
// Find a pool for a specific token pair
// Use i=0 for first match, i=1 for second, etc.
factory.find_pool_for_coins(_from: address, _to: address, i: uint256) → address
// Get pool metadata
factory.get_coins(pool: address) → address[]
factory.get_n_coins(pool: address) → uint256
factory.get_balances(pool: address) → uint256[]
factory.get_decimals(pool: address) → uint256[]
factory.is_meta(pool: address) → bool
factory.get_pool_asset_types(pool: address) → uint8[]
factory.get_implementation_address(pool: address) → address
```
### Via the MetaRegistry
The [MetaRegistry](./meta-registry.md) aggregates pools across all Curve Factory types (Stableswap-NG, Twocrypto-NG, Tricrypto-NG, and legacy). If you want to find the best pool for a pair regardless of AMM type, use the MetaRegistry:
```solidity
metaRegistry.find_pool_for_coins(_from: address, _to: address, i: uint256) → address
```
---
## Quoting Swap Amounts
### On the Pool
Every pool exposes quote functions directly:
```solidity
// How much `j` will I get for `dx` of `i`?
pool.get_dy(i: int128, j: int128, dx: uint256) → uint256
// How much `i` do I need to get `dy` of `j`?
pool.get_dx(i: int128, j: int128, dy: uint256) → uint256
```
For **metapools**, you can also quote swaps through to the underlying base pool tokens:
```solidity
pool.get_dy_underlying(i: int128, j: int128, dx: uint256) → uint256
pool.get_dx_underlying(i: int128, j: int128, dy: uint256) → uint256
```
:::warning[Coin Indices]
Stableswap-NG uses **`int128`** for coin indices in swap and quote functions (not `uint256`). This is a legacy convention. Indices start at `0`.
For metapools, `coins(0)` is the metapool's own coin, and `coins(1)` is the base pool LP token. When using `exchange_underlying()`, the indices span all underlying tokens: index `0` is the metapool coin, and indices `1..N` map to the base pool's coins.
:::
### Via the Views Contract
The Views contract (`0xFF53042865dF617de4bB871bD0988E7B93439cCF`) provides the same quoting functions but parameterized by pool address. This is useful for off-chain integrations where you want a single contract to query any pool:
```solidity
views.get_dy(i: int128, j: int128, dx: uint256, pool: address) → uint256
views.get_dx(i: int128, j: int128, dy: uint256, pool: address) → uint256
views.get_dy_underlying(i: int128, j: int128, dx: uint256, pool: address) → uint256
views.get_dx_underlying(i: int128, j: int128, dy: uint256, pool: address) → uint256
views.dynamic_fee(i: int128, j: int128, pool: address) → uint256
views.calc_token_amount(amounts: uint256[], is_deposit: bool, pool: address) → uint256
views.calc_withdraw_one_coin(burn_amount: uint256, i: int128, pool: address) → uint256
```
The Views contract address is stored in the Factory and can be queried via `factory.views_implementation()`.
---
## Executing Swaps
### `exchange` — Standard Swap
The primary swap function. Requires the caller to have approved the pool to spend the input token.
```solidity
pool.exchange(
i: int128, // index of input coin
j: int128, // index of output coin
_dx: uint256, // amount of input coin to swap
_min_dy: uint256, // minimum output (slippage protection)
_receiver: address // recipient of output (defaults to msg.sender)
) → uint256 // actual output amount
```
**Flow:**
1. Caller approves pool for `_dx` of `coins(i)`
2. Call `exchange()`
3. Pool transfers `_dx` from caller, sends output to `_receiver`
### `exchange_received` — Approval-Free Swap
Designed for **aggregators and smart contract integrators**. Instead of the pool pulling tokens via `transferFrom`, the caller sends tokens to the pool first, and the pool calculates the swap based on its balance change.
```solidity
pool.exchange_received(
i: int128, // index of input coin
j: int128, // index of output coin
_dx: uint256, // expected amount of input coin (already sent)
_min_dy: uint256, // minimum output (slippage protection)
_receiver: address // recipient of output (defaults to msg.sender)
) → uint256 // actual output amount
```
**Flow:**
1. Transfer `_dx` of `coins(i)` directly to the pool address
2. Call `exchange_received()`
3. Pool detects the balance increase, executes the swap, sends output to `_receiver`
This saves one ERC-20 approval and is gas-efficient when chaining swaps across multiple protocols (e.g., Uniswap → Curve in a single aggregator route).
:::warning
`exchange_received` does **not** work with rebasing tokens that have fee-on-transfer, as the actual amount received by the pool may differ from `_dx`. The pool checks `balanceOf(self) - stored_balance >= _dx` — if the received amount is less due to a transfer fee, the call reverts.
:::
:::abstract[Article]
For a deeper dive into `exchange_received`, including efficiency benefits, security considerations, and practical integration examples, see: [How to Do Cheaper, Approval-Free Swaps](https://blog.curvemonitor.com/posts/exchange-received/).
:::
### `exchange_underlying` — Metapool Underlying Swap
Only available on **metapools**. Swaps directly between the metapool coin and any coin in the underlying base pool, without the caller needing to interact with the base pool separately.
```solidity
pool.exchange_underlying(
i: int128, // index of input coin (0 = meta coin, 1..N = base pool coins)
j: int128, // index of output coin
_dx: uint256, // amount of input coin
_min_dy: uint256, // minimum output
_receiver: address // recipient (defaults to msg.sender)
) → uint256 // actual output amount
```
---
## Fees
Stableswap-NG uses **dynamic fees** that increase when the pool is imbalanced (off-peg). This incentivizes arbitrage to restore the peg.
```solidity
// Base fee (in 1e10 precision, e.g., 4000000 = 0.04% = 4bps)
pool.fee() → uint256
// Off-peg fee multiplier
pool.offpeg_fee_multiplier() → uint256
// Actual fee for a specific swap pair (accounts for current pool state)
pool.dynamic_fee(i: int128, j: int128) → uint256
```
The dynamic fee formula scales between the base `fee` (when balanced) up to `fee * offpeg_fee_multiplier / 1e10` (when heavily imbalanced). The fee precision is `1e10`, so to get the fee as a percentage: `fee / 1e10 * 100`.
Admin fees are hardcoded at **50%** of trading fees — these go to the protocol (Curve DAO / fee receiver). The remaining 50% accrues to LPs via the pool's virtual price.
---
## Token Handling & Asset Types
Stableswap-NG supports four asset types. This matters for integrators because it affects how token balances and rates are calculated internally:
| Asset Type | ID | Description | Rate Source | Examples |
|---|---|---|---|---|
| **Standard** | `0` | Regular ERC-20 | `1e18` (no rate adjustment) | USDC, USDT, DAI |
| **Oracle** | `1` | Token with an exchange rate oracle | External oracle contract | wstETH, cbETH, rETH |
| **Rebasing** | `2` | Token whose balance changes automatically | `balanceOf()` tracked directly | stETH |
| **ERC4626** | `3` | Tokenized vault with `convertToAssets` | `convertToAssets(1e(decimals))` | sDAI |
Query a pool's asset types:
```solidity
factory.get_pool_asset_types(pool: address) → uint8[]
```
Query the internal rates used for balancing:
```solidity
pool.stored_rates() → uint256[]
```
For **oracle-type tokens** (type `1`), the rate is fetched from an external oracle contract using a method ID specified at pool deployment. This rate is used to normalize token values so the invariant can treat them as equivalent.
---
## Oracles
Each Stableswap-NG pool provides built-in **exponential moving average (EMA)** oracles. For a full technical deep-dive, see the [Stableswap-NG Oracle documentation](../amm/stableswap-ng/pools/oracles.md).
```solidity
// EMA price oracle for coin i relative to coin 0
pool.price_oracle(i: uint256) → uint256 // 1e18 precision
// Last raw price (spot price from most recent trade)
pool.last_price(i: uint256) → uint256
// Current spot price (calculated from current balances)
pool.get_p(i: uint256) → uint256
// EMA of the D invariant (useful for LP token pricing)
pool.D_oracle() → uint256
```
The EMA is calculated as: `EMA = last_spot × (1 - α) + prev_EMA × α`, where `α = e^(-Δt / ma_exp_time)`. The smoothing window `ma_exp_time` is set at pool deployment (default ~866 seconds for prices). A separate `D_ma_time` (default ~62,324 seconds) controls the D oracle.
**Update behavior:**
- EMA values update **at most once per block** — multiple swaps in the same block only update the spot price, not the EMA
- Spot prices (`last_price`) are **capped at `2 × 1e18`** before entering the EMA to limit manipulation impact
- `price_oracle()` and `D_oracle()` are triggered by swaps, `add_liquidity`, `remove_liquidity_one_coin`, and `remove_liquidity_imbalance` — but **not** by balanced `remove_liquidity` (which doesn't change prices, though D oracle is still updated)
:::info
The index `i` for oracle functions uses `uint256` (not `int128`), and represents the coin index relative to `coins(0)`. For a 2-coin pool, `price_oracle(0)` gives the price of `coins(1)` in terms of `coins(0)`.
:::
---
## Useful Pool Getters
```solidity
// Token addresses
pool.coins(i: uint256) → address
// Pool balances
pool.balances(i: uint256) → uint256
pool.get_balances() → uint256[]
// Amplification parameter (controls concentration around peg)
pool.A() → uint256
// Virtual price of LP token (increases monotonically as fees accrue)
pool.get_virtual_price() → uint256
// Total LP token supply (pool itself is the LP token, ERC-20 compliant)
pool.totalSupply() → uint256
// Internal rates (for asset type normalization)
pool.stored_rates() → uint256[]
```
---
## Deployments & Pool Implementations
Stableswap-NG is deployed across many chains. Select a chain below to view contract addresses and pool implementations.
Implementations have been upgraded over time, but **pools are immutable once deployed** — a pool keeps the implementation it was deployed with forever. The Factory's `set_pool_implementations()` only affects newly deployed pools. All implementations share the same ABI interface, so integrators can use a single interface regardless of which implementation a pool uses.
The Factory stores separate implementations for plain pools and metapools (`pool_implementations(idx)` and `metapool_implementations(idx)`). Use `factory.is_meta(pool)` to determine the pool type.
:::info[Live Contract & Implementation Explorer]
This tool fetches contract addresses and pool implementations directly from the on-chain Factory contract. It queries `math_implementation()`, `views_implementation()`, and `gauge_implementation()` for infrastructure contracts, then scans all deployed pools via `pool_list()` and `get_implementation_address()` to discover which implementation each pool uses. All calls are batched via [Multicall3](https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11) for efficiency. Results are cached in your browser — click **Refresh** to fetch newly deployed pools. You can optionally provide a custom RPC URL if the default public endpoint is unreliable.
:::
---
## Integrating Tricrypto-NG
Tricrypto-NG is Curve's AMM for trading **three volatile, uncorrelated assets** — the canonical example being ETH/BTC/USDT. Like Twocrypto-NG, it uses the **Cryptoswap invariant** with automatic liquidity concentration and rebalancing, but extended to three coins.
Key properties:
- **3 coins** per pool (always exactly 3)
- **Auto-rebalancing** — two internal `price_scale` values track coin prices relative to `coins[0]`
- **Native ETH support** — pools can wrap/unwrap WETH automatically via `use_eth` and `exchange_underlying`
- **The pool contract is the LP token** — ERC-20 compliant
- **No `exchange_received`** — uses `exchange_extended` with a callback pattern instead
:::info
All Tricrypto-NG pools are deployed via the **[Factory](https://etherscan.io/address/0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963)** (`0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963`). The Factory uses blueprint patterns — pool contracts are deployed as minimal proxies of stored implementations.
:::
---
## Pool Discovery
### Via the Factory
```solidity
// Get total number of pools
factory.pool_count() → uint256
// Get pool address by index
factory.pool_list(i: uint256) → address
// Find a pool for a specific token pair
factory.find_pool_for_coins(_from: address, _to: address, i: uint256) → address
// Get pool metadata
factory.get_coins(pool: address) → address[3]
factory.get_decimals(pool: address) → uint256[3]
factory.get_balances(pool: address) → uint256[3]
factory.get_coin_indices(pool: address, _from: address, _to: address) → (uint256, uint256)
factory.get_gauge(pool: address) → address
factory.get_market_counts(coin_a: address, coin_b: address) → uint256
```
### Via the MetaRegistry
The [MetaRegistry](./meta-registry.md) aggregates pools across all Curve Factory types:
```solidity
metaRegistry.find_pool_for_coins(_from: address, _to: address, i: uint256) → address
```
---
## Quoting Swap Amounts
### On the Pool
```solidity
// How much of coin `j` will I get for `dx` of coin `i`?
pool.get_dy(i: uint256, j: uint256, dx: uint256) → uint256
// How much of coin `i` do I need to send to get `dy` of coin `j`?
pool.get_dx(i: uint256, j: uint256, dy: uint256) → uint256
```
Indices are `uint256` — values `0`, `1`, or `2`.
### Via the Views Contract
The Views contract (`0x064253915b8449fdEFac2c4A74aA9fdF56691a31`) provides pool-parameterized quotes and fee calculation helpers:
```solidity
// Standard quotes
views.get_dy(i: uint256, j: uint256, dx: uint256, swap: address) → uint256
views.get_dx(i: uint256, j: uint256, dy: uint256, swap: address) → uint256
// LP token calculations
views.calc_token_amount(amounts: uint256[3], deposit: bool, swap: address) → uint256
views.calc_withdraw_one_coin(token_amount: uint256, i: uint256, swap: address) → uint256
// Fee calculations (returns fee amount separately)
views.calc_fee_get_dy(i: uint256, j: uint256, dx: uint256, swap: address) → uint256
views.calc_fee_withdraw_one_coin(token_amount: uint256, i: uint256, swap: address) → uint256
views.calc_fee_token_amount(amounts: uint256[3], deposit: bool, swap: address) → uint256
```
The Views contract address is stored in the Factory and can be queried via `factory.views_implementation()`.
---
## Executing Swaps
### `exchange` — Standard Swap
Requires the caller to have approved the pool to spend the input token.
```solidity
pool.exchange(
i: uint256, // index of input coin (0, 1, or 2)
j: uint256, // index of output coin (0, 1, or 2)
dx: uint256, // amount of input coin to swap
min_dy: uint256, // minimum output (slippage protection)
use_eth: bool, // if true, wraps/unwraps native ETH (default: false)
receiver: address // recipient of output (defaults to msg.sender)
) → uint256 // actual output amount
```
The `use_eth` parameter controls native ETH handling:
- **`use_eth = false`** (default): swap operates on WETH like any other ERC-20
- **`use_eth = true`**: if sending ETH, pass it as `msg.value` instead of approving WETH; if receiving ETH, the pool unwraps WETH and sends native ETH to the receiver
### `exchange_underlying` — Native ETH Swap
A convenience function that always uses native ETH (equivalent to `exchange` with `use_eth = true`):
```solidity
pool.exchange_underlying(
i: uint256, // index of input coin
j: uint256, // index of output coin
dx: uint256, // amount of input coin
min_dy: uint256, // minimum output
receiver: address // recipient (defaults to msg.sender)
) → uint256 // actual output amount
```
### `exchange_extended` — Callback Swap
Designed for **aggregators and advanced integrators**. Supports a callback pattern where the pool calls back to the sender to request tokens:
```solidity
pool.exchange_extended(
i: uint256, // index of input coin
j: uint256, // index of output coin
dx: uint256, // amount of input coin
min_dy: uint256, // minimum output
use_eth: bool, // native ETH handling
sender: address, // address to pull tokens from
receiver: address, // recipient of output
cb: bytes32 // callback identifier (0x00 = no callback)
) → uint256 // actual output amount
```
When `cb` is set to a non-zero value, the pool executes a callback to the `sender` address before pulling tokens, allowing the sender to source the input tokens just-in-time (e.g., from a flash loan or another pool).
:::warning[No `exchange_received`]
Unlike Stableswap-NG and Twocrypto-NG, Tricrypto-NG does **not** have `exchange_received()`. For approval-free swaps, use `exchange_extended` with the callback mechanism, or pre-approve the pool.
:::
---
## Fees
Tricrypto-NG uses the same **dynamic fee model** as Twocrypto-NG — two fee levels that blend based on pool imbalance:
```solidity
// Current effective fee (1e10 precision)
pool.fee() → uint256
// Fee bounds
pool.mid_fee() → uint256 // fee when trading near internal price (lower)
pool.out_fee() → uint256 // fee when trading far from internal price (higher)
// Fee blending parameter
pool.fee_gamma() → uint256
// Calculate fee for a given pool state
pool.fee_calc(xp: uint256[3]) → uint256
```
Fee precision is `1e10` — to get a percentage: `fee / 1e10 * 100`.
Unlike Twocrypto-NG where admin fees are claimed internally, Tricrypto-NG has an explicit admin fee claim function:
```solidity
pool.claim_admin_fees() // callable by anyone
```
---
## Price Scale & Rebalancing
Tricrypto-NG tracks two price scales — one for each of `coins[1]` and `coins[2]` relative to `coins[0]`:
```solidity
// Internal price scale — prices the pool concentrates liquidity around
pool.price_scale(k: uint256) → uint256 // k=0: coins[1]/coins[0], k=1: coins[2]/coins[0]
// Last traded prices
pool.last_prices(k: uint256) → uint256
// EMA price oracle (manipulation-resistant)
pool.price_oracle(k: uint256) → uint256
// Price of LP token in terms of coins[0]
pool.lp_price() → uint256
```
The rebalancing parameters are the same as Twocrypto-NG:
```solidity
pool.adjustment_step() → uint256
pool.allowed_extra_profit() → uint256
pool.ma_time() → uint256
```
---
## Oracles
Each Tricrypto-NG pool provides built-in **exponential moving average (EMA)** oracles for two price pairs. For a full technical deep-dive, see the [Tricrypto-NG Oracle documentation](../amm/tricrypto-ng/pools/oracles.md).
```solidity
// EMA price of coins[k+1] in terms of coins[0]
pool.price_oracle(k: uint256) → uint256 // k=0 or k=1, 1e18 precision
// Last traded prices (spot)
pool.last_prices(k: uint256) → uint256
// LP token price in terms of coins[0]
// Formula: 3 × virtual_price × cbrt(price_oracle[0] × price_oracle[1]) / 1e24
pool.lp_price() → uint256
```
The EMA is calculated as: `EMA = last_spot × (1 - α) + prev_EMA × α`, where `α = e^(-Δt / ma_time)`. The smoothing window `ma_time` defaults to ~600 seconds. Spot prices are **capped at `2 × price_scale[k]`** per coin before entering the EMA.
**Update behavior:**
- Both price oracles share a **single timestamp** and update together, at most once per block
- Triggered by swaps, `add_liquidity`, and `remove_liquidity_one_coin` — but **not** by balanced `remove_liquidity`
:::info
Unlike Twocrypto-NG where `price_oracle()` takes no arguments, Tricrypto-NG's `price_oracle(k)` takes an index `k` because there are two independent price ratios to track (3 coins → 2 price pairs relative to `coins[0]`).
:::
---
## Useful Pool Getters
```solidity
// Token addresses (immutable)
pool.coins(i: uint256) → address // i = 0, 1, or 2
// Pool balances (raw token amounts)
pool.balances(i: uint256) → uint256
// Token precisions (decimal normalization)
pool.precisions() → uint256[3]
// Amplification and gamma
pool.A() → uint256
pool.gamma() → uint256
// Virtual price (increases as fees accrue)
pool.get_virtual_price() → uint256
// D invariant
pool.D() → uint256
// Total LP supply
pool.totalSupply() → uint256
// Version
pool.version() → String // "v2.0.0"
```
---
## Deployments & Pool Implementations
Tricrypto-NG is deployed across many chains. Select a chain below to view contract addresses and pool implementations.
All pools share the same interface. The Factory stores the current implementation blueprint at `pool_implementations(0)`. Pools are immutable once deployed — if the implementation is upgraded, only newly deployed pools use the new version.
:::info[Live Contract & Implementation Explorer]
This tool fetches contract addresses and pool implementations directly from the on-chain Factory contract. It queries `math_implementation()`, `views_implementation()`, and `gauge_implementation()` for infrastructure contracts, and scans `pool_implementations(idx)` for active pool blueprints. All calls are batched via [Multicall3](https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11) for efficiency. Results are cached in your browser — click **Refresh** to re-fetch. You can optionally provide a custom RPC URL if the default public endpoint is unreliable.
**Note:** Unlike Stableswap-NG, the Tricrypto-NG Factory does not expose a `get_implementation_address(pool)` getter — the implementation used by each individual pool cannot be queried on-chain. The tool can only show which implementations are currently set in the Factory, not which implementation a specific pool was deployed with.
:::
---
## Integrating Twocrypto-NG
Twocrypto-NG is Curve's AMM for trading **volatile and uncorrelated asset pairs** — e.g., ETH/USDC, TOKEN/ETH, or any two tokens that are not expected to maintain a fixed price ratio. It uses the **Cryptoswap invariant**, which automatically concentrates liquidity around the current market price and rebalances as prices move.
Key properties:
- **2 coins** per pool (always exactly 2)
- **Auto-rebalancing** — the pool's internal `price_scale` adjusts to track the market price, keeping liquidity concentrated without LP intervention
- **The pool contract is the LP token** — ERC-20 compliant, no separate LP token contract
- **Admin fees are hardcoded at 50%** of trading fees, claimed internally (no external claim function)
:::info
All Twocrypto-NG pools are deployed via the **[Factory](https://etherscan.io/address/0x98EE851a00abeE0d95D08cF4CA2BdCE32aeaAF7F)** (`0x98EE851a00abeE0d95D08cF4CA2BdCE32aeaAF7F`). The Factory uses blueprint patterns — pool contracts are deployed as minimal proxies of stored implementations.
:::
---
## Pool Discovery
### Via the Factory
The Factory maintains a registry of all deployed Twocrypto-NG pools.
```solidity
// Get total number of pools
factory.pool_count() → uint256
// Get pool address by index (returns from a dynamic array)
factory.pool_list(i: uint256) → address
// Find a pool for a specific token pair
// Use i=0 for first match, i=1 for second, etc.
factory.find_pool_for_coins(_from: address, _to: address, i: uint256) → address
// Get pool metadata
factory.get_coins(pool: address) → address[2]
factory.get_decimals(pool: address) → uint256[2]
factory.get_balances(pool: address) → uint256[2]
factory.get_gauge(pool: address) → address
factory.get_implementation(pool: address) → address
```
### Via the MetaRegistry
The [MetaRegistry](./meta-registry.md) aggregates pools across all Curve Factory types. If you want to find the best pool for a pair regardless of AMM type:
```solidity
metaRegistry.find_pool_for_coins(_from: address, _to: address, i: uint256) → address
```
---
## Quoting Swap Amounts
### On the Pool
```solidity
// How much of coin `j` will I get for `dx` of coin `i`?
pool.get_dy(i: uint256, j: uint256, dx: uint256) → uint256
// How much of coin `i` do I need to send to get `dy` of coin `j`?
pool.get_dx(i: uint256, j: uint256, dy: uint256) → uint256
```
:::warning[Coin Indices]
Twocrypto-NG uses **`uint256`** for coin indices (unlike Stableswap-NG which uses `int128`). Indices are `0` and `1`.
:::
### Via the Views Contract
The Views contract (`0x07CdEBF81977E111B08C126DEFA07818d0045b80`) provides the same quoting functions parameterized by pool address, plus fee calculation helpers:
```solidity
// Standard quotes
views.get_dy(i: uint256, j: uint256, dx: uint256, swap: address) → uint256
views.get_dx(i: uint256, j: uint256, dy: uint256, swap: address) → uint256
// LP token calculations
views.calc_token_amount(amounts: uint256[2], deposit: bool, swap: address) → uint256
views.calc_withdraw_one_coin(token_amount: uint256, i: uint256, swap: address) → uint256
// Fee calculations (returns fee amount separately)
views.calc_fee_get_dy(i: uint256, j: uint256, dx: uint256, swap: address) → uint256
views.calc_fee_withdraw_one_coin(token_amount: uint256, i: uint256, swap: address) → uint256
views.calc_fee_token_amount(amounts: uint256[2], deposit: bool, swap: address) → uint256
```
The Views contract address is stored in the Factory and can be queried via `factory.views_implementation()`.
---
## Executing Swaps
### `exchange` — Standard Swap
Requires the caller to have approved the pool to spend the input token.
```solidity
pool.exchange(
i: uint256, // index of input coin (0 or 1)
j: uint256, // index of output coin (0 or 1)
dx: uint256, // amount of input coin to swap
min_dy: uint256, // minimum output (slippage protection)
receiver: address // recipient of output (defaults to msg.sender)
) → uint256 // actual output amount
```
### `exchange_received` — Approval-Free Swap
Designed for **aggregators and smart contract integrators**. The caller sends tokens to the pool first, then calls `exchange_received()` — the pool detects the balance increase and executes the swap.
```solidity
pool.exchange_received(
i: uint256, // index of input coin (0 or 1)
j: uint256, // index of output coin (0 or 1)
dx: uint256, // expected amount of input coin (already sent)
min_dy: uint256, // minimum output (slippage protection)
receiver: address // recipient of output (defaults to msg.sender)
) → uint256 // actual output amount
```
**Flow:**
1. Transfer `dx` of `coins[i]` directly to the pool address
2. Call `exchange_received()`
3. Pool detects the balance increase, executes the swap, sends output to `receiver`
This saves one ERC-20 approval and is gas-efficient when chaining swaps across multiple protocols.
:::abstract[Article]
For a deeper dive into `exchange_received`, including efficiency benefits, security considerations, and practical integration examples, see: [How to Do Cheaper, Approval-Free Swaps](https://blog.curvemonitor.com/posts/exchange-received/).
:::
---
## Fees
Twocrypto-NG uses a **dynamic fee model** with two fee levels that blend based on how far the trade pushes the pool away from its internal price scale:
```solidity
// Current effective fee for the pool's state (1e10 precision)
pool.fee() → uint256
// The two fee bounds
pool.mid_fee() → uint256 // fee when trading near the internal price (lower)
pool.out_fee() → uint256 // fee when trading far from the internal price (higher)
// Fee blending parameter
pool.fee_gamma() → uint256
// Calculate fee for a given pool state
pool.fee_calc(xp: uint256[2]) → uint256
```
The fee precision is `1e10` — to get a percentage: `fee / 1e10 * 100`. For example, a `fee()` return value of `3000000` means 0.03% (3 bps).
The dynamic fee interpolates between `mid_fee` and `out_fee` based on how imbalanced the pool is relative to its price scale. Trades that push the pool further from equilibrium pay closer to `out_fee`; trades that bring it back pay closer to `mid_fee`.
Admin fees are **hardcoded at 50%** and are claimed internally — there is no external function to claim admin fees. Fees are collected when liquidity is removed single-sidedly via `remove_liquidity_one_coin()`.
---
## Price Scale & Rebalancing
Unlike Stableswap (which assumes assets trade near 1:1), Twocrypto-NG tracks the market price of `coins[1]` relative to `coins[0]` via an internal **price scale**:
```solidity
// Internal price scale — the price the pool concentrates liquidity around
pool.price_scale() → uint256 // 1e18 precision
// Last traded price
pool.last_prices() → uint256
// EMA price oracle (manipulation-resistant)
pool.price_oracle() → uint256
// Price of LP token in terms of coins[0]
pool.lp_price() → uint256
```
The pool continuously adjusts `price_scale` toward `price_oracle` based on profits. This rebalancing is controlled by:
```solidity
pool.adjustment_step() → uint256 // minimum price scale adjustment
pool.allowed_extra_profit() → uint256 // profit threshold before rebalancing
pool.ma_time() → uint256 // EMA oracle half-time (seconds)
```
---
## Oracles
Each Twocrypto-NG pool provides built-in **exponential moving average (EMA)** oracles. For a full technical deep-dive, see the [Twocrypto-NG Oracle documentation](../amm/twocrypto-ng/pools/oracles.md).
```solidity
// EMA price of coins[1] in terms of coins[0]
pool.price_oracle() → uint256 // 1e18 precision
// Last traded price (spot, from most recent swap)
pool.last_prices() → uint256
// LP token price in terms of coins[0]
// Formula: 2 × virtual_price × sqrt(price_oracle) / 1e18
pool.lp_price() → uint256
```
The EMA is calculated as: `EMA = last_spot × (1 - α) + prev_EMA × α`, where `α = e^(-Δt / ma_time)`. The smoothing window `ma_time` defaults to ~601 seconds. Spot prices are **capped at `2 × price_scale`** before entering the EMA to limit manipulation.
**Update behavior:**
- EMA values update **at most once per block**
- Triggered by swaps, `add_liquidity`, and `remove_liquidity_one_coin` — but **not** by balanced `remove_liquidity`
- The pool also maintains an **XCP oracle** (`xcp_oracle()`) representing estimated TVL, with a longer smoothing window (~62,324 seconds)
:::info
Unlike Stableswap-NG where `price_oracle(i)` takes an index parameter, Twocrypto-NG's `price_oracle()` takes **no arguments** — it always returns the price of `coins[1]` relative to `coins[0]` (since there are only 2 coins).
:::
---
## Pool Parameters
Cryptoswap pools have more parameters than Stableswap pools. Key ones for integrators:
```solidity
// Amplification and gamma — control curve shape
pool.A() → uint256 // amplification parameter
pool.gamma() → uint256 // controls the width of the concentrated liquidity region
// Virtual price (increases monotonically as fees accrue, useful for LP pricing)
pool.get_virtual_price() → uint256
// D invariant
pool.D() → uint256
// Token precisions (scaling factors for decimal normalization)
pool.precisions() → uint256[2]
```
---
## Useful Pool Getters
```solidity
// Token addresses
pool.coins(i: uint256) → address // i = 0 or 1
// Pool balances (raw token amounts)
pool.balances(i: uint256) → uint256
// Total LP token supply
pool.totalSupply() → uint256
// Version
pool.version() → String // "v2.1.0"
```
---
## Deployments & Pool Implementations
Twocrypto-NG is deployed across many chains. Select a chain below to view contract addresses and pool implementations.
All pools share the same interface. The Factory stores the current implementation blueprint at `pool_implementations(0)`. Pools are immutable once deployed — if the implementation is upgraded, only newly deployed pools use the new version.
:::info[Live Contract & Implementation Explorer]
This tool fetches contract addresses and pool implementations directly from the on-chain Factory contract. It queries `math_implementation()`, `views_implementation()`, and `gauge_implementation()` for infrastructure contracts, and scans `pool_implementations(idx)` for active pool blueprints. All calls are batched via [Multicall3](https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11) for efficiency. Results are cached in your browser — click **Refresh** to re-fetch. You can optionally provide a custom RPC URL if the default public endpoint is unreliable.
**Note:** Unlike Stableswap-NG, the Twocrypto-NG Factory does not expose a `get_implementation_address(pool)` getter — the implementation used by each individual pool cannot be queried on-chain. The tool can only show which implementations are currently set in the Factory, not which implementation a specific pool was deployed with.
:::
---
## LLAMMA and Controller
:::vyper[`AMM.vy` & `Controller.vy`]
The source code for both contracts, `AMM.vy` and `Controller.vy`, can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/tree/lending/contracts). The contracts are written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
:::
Llamalend uses **Controller V3** — the same blueprint that powers the latest crvUSD mint markets (weETH, cbBTC, LBTC). The Controller and AMM contracts originated in the crvUSD system and were extended for lending use. For a full version history including the evolution from crvUSD V1 through V3, see the [crvUSD Overview](../../crvusd/overview.md#controller--amm-versions).
Because Curve Lending operates very similarly to the system for minting crvUSD, both `Controller.vy` and `AMM.vy` can be used for lending markets. To ensure full compatibility with both systems, **several modifications were made in V3**:
The core contract `AMM.vy` (LLAMMA) **remains exactly the same** as in the crvUSD system. No changes were needed for lending.
The Controller has been modified for lending with changes to **token decimal handling**, **fee collection**, and **ETH transfer behavior**.
---
## Changes to Controller.vy
The changes made to the codebase of the Controller contract are mainly under-the-hood changes, which do not significantly affect how users interact with the contract. External functions like `create_loan`, `repay`, etc., work the same way as before.
*The following changes have been made:*
- The Controller now has the **ability to handle not only 18-digit tokens** (like crvUSD) but also **tokens with any number of digits**. For this, there were multiple changes to **ensure rounding always rounds up in favor of the existing borrowers**.
- The **`collect_fees()` method will not work in lending**. Admin fees are set to zero, and all the interest will go to the vault depositors. Moreover, AMM admin fees cannot be charged: their claim would fail too. The system is designed to make money on fees generated by crvUSD itself.
- The contract that creates the Controller has `collateral_token()` and `borrowed_token()` public methods instead of a `stablecoin()` method. This keeps the code clean and understandable when a stablecoin is collateral, not the borrowed asset. However, compatibility with the `stablecoin()` method is preserved.
- **Transfers of native ETH are removed for safety**. To enhance safety with unknown variables, automatic wrapping of ETH is disabled permanently.
---
## CryptoFromPoolVault
This oracle contract takes the **price oracle from a Curve liquidity pool and applies the redemption of the vault token to it**. This is often used when having **ERC-4626 Vault tokens** with `pricePerShare`, `convertToAsset`, or other similar functions which essentially return the price of one vault token compared to the underlying assets. The first oracle contracts were deployed without considering the [aggregated price of crvUSD](../../crvusd/oracles/price-aggregator.md), but experience has shown that it makes sense to include this value in the calculation. The respective differences are documented in the relevant sections.
These kinds of oracle contracts **need to be deployed manually**, as there is currently no `Factory` to do so.
:::vyper[`CryptoFromPoolVault.vy`]
The source code for the `CryptoFromPoolVault.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/price_oracles/CryptoFromPoolVault.vy). A variant that includes the aggregated crvUSD price, [`CryptoFromPoolVaultWAgg.vy`](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/price_oracles/CryptoFromPoolVaultWAgg.vy), is also available. The contracts are written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
:::warning[Oracle Suitability]
`CryptoFromPoolVaultWAgg.vy` is only suitable for vaults which cannot be affected by [donation attacks](https://mixbytes.io/blog/overview-of-the-inflation-attack).
:::
:::
:::danger[Oracle Immutability]
The oracle contracts are fully immutable. Once deployed, they cannot change any parameters, stop the price updates, or alter the pools used to calculate the prices. However, because the contract relies on other pools, it's important to keep in mind that changing parameters in the pool, such as the periodicity of the oracle, can influence these oracle contracts. All relevant data required for the oracle to function is passed into the `__init__` function during the deployment of the contract.
```vyper
@external
def __init__(
pool: Pool,
N: uint256,
borrowed_ix: uint256,
collateral_ix: uint256,
vault: Vault
):
assert borrowed_ix != collateral_ix
assert borrowed_ix < N
assert collateral_ix < N
POOL = pool
N_COINS = N
BORROWED_IX = borrowed_ix
COLLATERAL_IX = collateral_ix
VAULT = vault
no_argument: bool = False
if N == 2:
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pool.address,
_abi_encode(empty(uint256), method_id=method_id("price_oracle(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
no_argument = True
NO_ARGUMENT = no_argument
self.cached_price_per_share = VAULT.pricePerShare()
self.cached_timestamp = block.timestamp
```
```vyper
@external
def __init__(
pool: Pool,
N: uint256,
borrowed_ix: uint256,
collateral_ix: uint256,
vault: Vault,
agg: StableAggregator
):
assert borrowed_ix != collateral_ix
assert borrowed_ix < N
assert collateral_ix < N
POOL = pool
N_COINS = N
BORROWED_IX = borrowed_ix
COLLATERAL_IX = collateral_ix
VAULT = vault
AGG = agg
no_argument: bool = False
if N == 2:
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pool.address,
_abi_encode(empty(uint256), method_id=method_id("price_oracle(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
no_argument = True
NO_ARGUMENT = no_argument
```
:::
---
## Oracle Price
The oracle price is calculated by taking the `price_oracle` of a Curve pool and then adjusting it by the redemption rate of a vault, using methods such as `convertToAssets`, `pricePerShare` or really any other equvalent function which returns the rate of the vault token and the underlying asset.
:::example[Example]
Let's take a look at the [sDOLA/crvUSD lending market](https://lend.curve.fi/#/ethereum/markets/one-way-market-17/create), which uses the `CryptoFromPoolVaultWAgg.vy` code.
The [oracle contract](https://etherscan.io/address/0x002688C4296A2C4d800F271fe6F01741111B09Be) fetches the `price_oracle` of the [DOLA <> crvUSD stableswap-ng pool](https://etherscan.io/address/0x8272E1A3dBef607C04AA6e5BD3a1A134c8ac063B#readContract#F9) and then adjusts this value by the redemption rate obtained from the [`convertToAssets`](https://etherscan.io/address/0xb45ad160634c528Cc3D2926d9807104FA3157305#readContract#F7) method of the [sDOLA vault](https://etherscan.io/address/0xb45ad160634c528Cc3D2926d9807104FA3157305).
:::
Additionally, the `CryptoFromPoolVault.vy` contract has a **built-in mechanism that considers a certain maximum speed of price change within the vault** when calculating the oracle price. This feature is not included in the `CryptoFromPoolVaultWAgg.vy` oracle contract.
*The formula to calculate the applied redemption rate is the following:*
$$\min \left( \text{pricePerShare}, \frac{\text{cached\_price\_per\_share} \times (10^{18} + \text{PPS\_MAX\_SPEED} \times (\text{block.timestamp} - \text{cached\_timestamp}))}{10^{18}} \right)$$
In this example, `pricePerShare` is used, but it can really be any equivalent method that returns the redemption rate of the vault token with respect to its underlying token.
`cached_price_per_share` and `cached_timestamp` are internal variables that are updated whenever the `price_w` function is called. The first value is set to the current redemption rate within the vault at the block when the function is called, and the second value to the current timestamp (`block.timestamp`).
```vyper
PPS_MAX_SPEED: constant(uint256) = 10**16 / 60 # Max speed of pricePerShare change
cached_price_per_share: public(uint256)
cached_timestamp: public(uint256)
@internal
@view
def _pps() -> uint256:
return min(VAULT.pricePerShare(), self.cached_price_per_share * (10**18 + PPS_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18)
@internal
def _pps_w() -> uint256:
pps: uint256 = min(VAULT.pricePerShare(), self.cached_price_per_share * (10**18 + PPS_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18)
self.cached_price_per_share = pps
self.cached_timestamp = block.timestamp
return pps
```
### `price`
::::description[`CryptoFromPoolVault.price() -> uint256: view`]
Getter for the price of the collateral asset denominated against the borrowed token and applying the conversion rate from a vault.
Returns: oracle price (`uint256`).
The `CryptoFromPoolVault.vy` oracle contract does not take the aggregated price of crvUSD from the [`PriceAggregator.vy` contract](../../crvusd/oracles/price-aggregator.md) into account. Experience has shown that it makes sense to include this value in the oracle calculations. This is implemented in the `CryptoFromPoolVaultWAgg.vy` oracle contract.
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
interface Pool:
def price_oracle(i: uint256 = 0) -> uint256: view # Universal method!
interface StableAggregator:
def price() -> uint256: view
def price_w() -> uint256: nonpayable
def stablecoin() -> address: view
interface Vault:
def convertToAssets(shares: uint256) -> uint256: view
POOL: public(immutable(Pool))
BORROWED_IX: public(immutable(uint256))
COLLATERAL_IX: public(immutable(uint256))
N_COINS: public(immutable(uint256))
NO_ARGUMENT: public(immutable(bool))
VAULT: public(immutable(Vault))
AGG: public(immutable(StableAggregator))
PPS_MAX_SPEED: constant(uint256) = 10**16 / 60 # Max speed of pricePerShare change
cached_price_per_share: public(uint256)
cached_timestamp: public(uint256)
@external
@view
def price() -> uint256:
return self._raw_price(self._pps())
@internal
@view
def _raw_price(pps: uint256) -> uint256:
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT:
p: uint256 = POOL.price_oracle()
if COLLATERAL_IX > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX > 0:
p_borrowed = POOL.price_oracle(BORROWED_IX - 1)
if COLLATERAL_IX > 0:
p_collateral = POOL.price_oracle(COLLATERAL_IX - 1)
return p_collateral * pps / p_borrowed
@internal
@view
def _pps() -> uint256:
return min(VAULT.pricePerShare(), self.cached_price_per_share * (10**18 + PPS_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18)
```
```vyper
interface Pool:
def price_oracle(i: uint256 = 0) -> uint256: view # Universal method!
interface StableAggregator:
def price() -> uint256: view
def price_w() -> uint256: nonpayable
def stablecoin() -> address: view
interface Vault:
def convertToAssets(shares: uint256) -> uint256: view
POOL: public(immutable(Pool))
BORROWED_IX: public(immutable(uint256))
COLLATERAL_IX: public(immutable(uint256))
N_COINS: public(immutable(uint256))
NO_ARGUMENT: public(immutable(bool))
VAULT: public(immutable(Vault))
AGG: public(immutable(StableAggregator))
@external
@view
def price() -> uint256:
return self._raw_price() * AGG.price() / 10**18
@internal
@view
def _raw_price() -> uint256:
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT:
p: uint256 = POOL.price_oracle()
if COLLATERAL_IX > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX > 0:
p_borrowed = POOL.price_oracle(BORROWED_IX - 1)
if COLLATERAL_IX > 0:
p_collateral = POOL.price_oracle(COLLATERAL_IX - 1)
return p_collateral * VAULT.convertToAssets(10**18) / p_borrowed
```
```shell
>>> CryptoFromPoolVault.price()
1046959880335267532
```
::::
### `price_w`
::::description[`CryptoFromPoolVault.price_w() -> uint256`]
This function calculates and writes the price while updating `cached_rate` and `cached_timestamp`. This method is called whenever the `_exchange` function is called within the AMM contract of the lending market.
Returns: oracle price (`uint256`).
The `CryptoFromPoolVault.vy` oracle contract does not take the aggregated price of crvUSD from the [`PriceAggregator.vy` contract](../../crvusd/oracles/price-aggregator.md) into account. Experience has shown that it makes sense to include this value in the oracle calculations. This is implemented in the `CryptoFromPoolVaultWAgg.vy` oracle contract.
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
interface Pool:
def price_oracle(i: uint256 = 0) -> uint256: view # Universal method!
interface StableAggregator:
def price() -> uint256: view
def price_w() -> uint256: nonpayable
def stablecoin() -> address: view
interface Vault:
def convertToAssets(shares: uint256) -> uint256: view
POOL: public(immutable(Pool))
BORROWED_IX: public(immutable(uint256))
COLLATERAL_IX: public(immutable(uint256))
N_COINS: public(immutable(uint256))
NO_ARGUMENT: public(immutable(bool))
VAULT: public(immutable(Vault))
AGG: public(immutable(StableAggregator))
PPS_MAX_SPEED: constant(uint256) = 10**16 / 60 # Max speed of pricePerShare change
cached_price_per_share: public(uint256)
cached_timestamp: public(uint256)
@external
def price_w() -> uint256:
return self._raw_price(self._pps_w())
@internal
@view
def _raw_price(pps: uint256) -> uint256:
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT:
p: uint256 = POOL.price_oracle()
if COLLATERAL_IX > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX > 0:
p_borrowed = POOL.price_oracle(BORROWED_IX - 1)
if COLLATERAL_IX > 0:
p_collateral = POOL.price_oracle(COLLATERAL_IX - 1)
return p_collateral * pps / p_borrowed
@internal
def _pps_w() -> uint256:
pps: uint256 = min(VAULT.pricePerShare(), self.cached_price_per_share * (10**18 + PPS_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18)
self.cached_price_per_share = pps
self.cached_timestamp = block.timestamp
return pps
```
```vyper
interface Pool:
def price_oracle(i: uint256 = 0) -> uint256: view # Universal method!
interface StableAggregator:
def price() -> uint256: view
def price_w() -> uint256: nonpayable
def stablecoin() -> address: view
interface Vault:
def convertToAssets(shares: uint256) -> uint256: view
POOL: public(immutable(Pool))
BORROWED_IX: public(immutable(uint256))
COLLATERAL_IX: public(immutable(uint256))
N_COINS: public(immutable(uint256))
NO_ARGUMENT: public(immutable(bool))
VAULT: public(immutable(Vault))
AGG: public(immutable(StableAggregator))
@external
def price_w() -> uint256:
return self._raw_price() * AGG.price_w() / 10**18
@internal
@view
def _raw_price() -> uint256:
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT:
p: uint256 = POOL.price_oracle()
if COLLATERAL_IX > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX > 0:
p_borrowed = POOL.price_oracle(BORROWED_IX - 1)
if COLLATERAL_IX > 0:
p_collateral = POOL.price_oracle(COLLATERAL_IX - 1)
return p_collateral * VAULT.convertToAssets(10**18) / p_borrowed
```
```shell
>>> CryptoFromPoolVault.price_w()
1046959880335267532
```
::::
---
## Contract Info Methods
### `VAULT`
::::description[`CryptoFromPoolVault.VAULT() -> address: view`]
Getter for the vault contract from which the redemption rate (`convertToAssets` or similar functions) is fetched. This value is immutable and set at contract initialization.
Returns: vault contract (`address`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
VAULT: public(immutable(Vault))
```
```vyper
VAULT: public(immutable(Vault))
```
```shell
>>> CryptoFromPoolVault.VAULT()
'0xb45ad160634c528Cc3D2926d9807104FA3157305'
```
::::
### `POOL`
::::description[`CryptoFromPoolVault.POOL() -> address: view`]
Getter for the liquidity pool used to fetch the `price_oracle`.
Returns: pool contract (`address`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
POOL: public(immutable(Pool))
```
```vyper
POOL: public(immutable(Pool))
```
```shell
>>> CryptoFromPoolVault.POOL()
'0x8272E1A3dBef607C04AA6e5BD3a1A134c8ac063B'
```
::::
### `BORROWED_IX`
::::description[`CryptoFromPoolVault.BORROWED_IX() -> uint256: view`]
Getter for the coin index of the borrowed token within the pool from which `price_oracle` is fetched. This value is immutable and set at contract initialization.
Returns: coin index (`uint256`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
BORROWED_IX: public(immutable(uint256))
```
```vyper
BORROWED_IX: public(immutable(uint256))
```
`BORROWED_IX` of `1` means the borrowed token in the pool, from which the price oracle value is fetched, is at coin index `1` (`Pool.coins(1)`).
```shell
>>> CryptoFromPoolVaultWAgg.BORROWED_IX()
1
```
::::
### `COLLATERAL_IX`
::::description[`CryptoFromPoolVault.COLLATERAL_IX() -> uint256: view`]
Getter for the coin index of the collateral token within the pool from which `price_oracle` is fetched. This value is immutable and set at contract initialization.
Returns: coin index (`uint256`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
COLLATERAL_IX: public(immutable(uint256))
```
```vyper
COLLATERAL_IX: public(immutable(uint256))
```
`COLLATERAL_IX` of `0` means the borrowed token in the pool, from which the price oracle value is fetched, is at coin index `0` (`Pool.coins(0)`).
```shell
>>> CryptoFromPoolVaultWAgg.COLLATERAL_IX()
0
```
::::
### `N_COINS`
::::description[`CryptoFromPoolVault.N_COINS() -> uint256: view`]
Getter for the number of coins in `POOL`.
Returns: number of coins (`uint256`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
N_COINS: public(immutable(uint256))
```
```vyper
N_COINS: public(immutable(uint256))
```
```shell
>>> CryptoFromPoolVault.N_COINS()
2
```
::::
### `NO_ARGUMENT`
::::description[`CryptoFromPoolVault.NO_ARGUMENT() -> bool: view`]
Getter for the `NO_ARGUMENT` storage variable. This is an additional variable to ensure the correct price oracle is fetched from a `POOL`. This value is immutable and set at contract initialization.
Returns: true or false (`bool`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
NO_ARGUMENT: public(immutable(bool))
```
```shell
>>> CryptoFromPoolVault.NO_ARGUMENT()
'True'
```
::::
### `AGG`
::::description[`CryptoFromPoolVaultWAgg.AGG() -> address: view`]
:::info
This `AGG` storage variable is only used within the `CryptoFromPoolVaultWAgg` contracts.
:::
Getter for the crvUSD `PriceAggregator` contract. This value is immutable and set at contract initialization.
Returns: `PriceAggregator` (`address`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
interface StableAggregator:
def price() -> uint256: view
def price_w() -> uint256: nonpayable
def stablecoin() -> address: view
AGG: public(immutable(StableAggregator))
```
```shell
>>> CryptoFromPoolVaultWAgg.AGG()
'0x18672b1b0c623a30089A280Ed9256379fb0E4E62'
```
::::
---
## CryptoFromPool
Oracle contract for a collateral token that **fetches its price from a single Curve pool**. The first oracle contracts were deployed without considering the [aggregated price of crvUSD](../../crvusd/oracles/price-aggregator.md), but experience showed that it makes sense to include this value in the calculation. The respective differences are documented in the relevant sections.
:::vyper[`CryptoFromPool.vy`]
The source code for the `CryptoFromPool.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/price_oracles/CryptoFromPool.vy). A variant that includes the aggregated crvUSD price, [`CryptoFromPoolWAgg.vy`](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/price_oracles/CryptoFromPoolWAgg.vy), is also available. The contracts are written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
:::
The [`OneWayLendingFactory.vy`](./oneway-factory.md) has a [`create_from_pool`](./oneway-factory.md#create_from_pool) method which deploys the full lending market infrastucture along with a price oracle using a [`stableswap-ng`](../../amm/stableswap-ng/pools/oracles.md), [`twocrypto-ng`](../../amm/twocrypto-ng/overview.md) or [`tricrypto-ng`](../../amm/tricrypto-ng/pools/oracles.md) pool. These pools all have a suitable exponential moving-average (EMA) oracle, which can be used in lending markets.
:::danger[Oracle Immutability]
The oracle contracts are fully immutable. Once deployed, they cannot change any parameters, stop the price updates, or alter the pools used to calculate the prices. All relevant data required for the oracle to function is passed into the `__init__` function during the deployment of the contract.
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
@external
def __init__(
pool: Pool,
N: uint256,
borrowed_ix: uint256,
collateral_ix: uint256
):
assert borrowed_ix != collateral_ix
assert borrowed_ix < N
assert collateral_ix < N
POOL = pool
N_COINS = N
BORROWED_IX = borrowed_ix
COLLATERAL_IX = collateral_ix
no_argument: bool = False
if N == 2:
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pool.address,
_abi_encode(empty(uint256), method_id=method_id("price_oracle(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
no_argument = True
NO_ARGUMENT = no_argument
```
```vyper
@external
def __init__(
pool: Pool,
N: uint256,
borrowed_ix: uint256,
collateral_ix: uint256,
agg: StableAggregator
):
assert borrowed_ix != collateral_ix
assert borrowed_ix < N
assert collateral_ix < N
POOL = pool
N_COINS = N
BORROWED_IX = borrowed_ix
COLLATERAL_IX = collateral_ix
AGG = agg
no_argument: bool = False
if N == 2:
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pool.address,
_abi_encode(empty(uint256), method_id=method_id("price_oracle(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
no_argument = True
NO_ARGUMENT = no_argument
```
:::
:::example[Example: CRV long market]
In the CRV short market, `CRV` serves as the collateral token, while `crvUSD` is the borrowable token. This lending market utilizes the price oracle sourced from the [TriCRV liquidity pool](https://etherscan.io/address/0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14).
When calling the `create_from_pool` function, the code automatically checks the index of the tokens within the liquidity pool. Subsequently, it passes these values as constructor arguments during the creation of the oracle contract from the blueprint implementation.
```vyper
# the following arguments will be passed into the `__init__` function:
pool = '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14'
N = 3
borrow_ix = 0 # crvUSD
collateral_ix = 2 # CRV
```
:::
---
## Oracle Price
### `price`
::::description[`CryptoFromPool.price() -> uint256`]
Getter function for the price. For example, in a lending market using `CRV` as collateral and `crvUSD` as the borrowable token, it returns the price of `CRV` relative to `crvUSD`. Conversely, in the inverse market scenario, it returns the price of `crvUSD` relative to `CRV`. This function is view-only and does not modify the state. For contracts applying the aggregated crvUSD price, it essentially multiplies the collateral price with the aggregated crvUSD price.
Returns: price (`uint256`).
The `CryptoFromPool.vy` oracle contract does not take the aggregated price of crvUSD from the [`PriceAggregator.vy` contract](../../crvusd/oracles/price-aggregator.md) into account. Experience has shown that it makes sense to include this value in the oracle calculations. This is implemented in the `CryptoFromPoolWAgg.vy` oracle contract.
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
@external
@view
def price() -> uint256:
return self._raw_price()
@internal
@view
def _raw_price() -> uint256:
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT:
p: uint256 = POOL.price_oracle()
if COLLATERAL_IX > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX > 0:
p_borrowed = POOL.price_oracle(BORROWED_IX - 1)
if COLLATERAL_IX > 0:
p_collateral = POOL.price_oracle(COLLATERAL_IX - 1)
return p_collateral * 10**18 / p_borrowed
```
```vyper
interface StableAggregator:
def price() -> uint256: view
def price_w() -> uint256: nonpayable
def stablecoin() -> address: view
AGG: public(immutable(StableAggregator))
@external
@view
def price() -> uint256:
return self._raw_price() * AGG.price() / 10**18
@internal
@view
def _raw_price() -> uint256:
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT:
p: uint256 = POOL.price_oracle()
if COLLATERAL_IX > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX > 0:
p_borrowed = POOL.price_oracle(BORROWED_IX - 1)
if COLLATERAL_IX > 0:
p_collateral = POOL.price_oracle(COLLATERAL_IX - 1)
return p_collateral * 10**18 / p_borrowed
```
```shell
>>> CryptoFromPool.price()
458009543343504151
```
::::
### `price_w`
::::description[`CryptoFromPool.price_w() -> uint256`]
Function to return the price and update the state of the blockchain. This function is called whenever the `_exchange` function from the LLAMMA is called. For contracts applying the aggregated crvUSD price, it essentially multiplies the collateral price with the aggregated crvUSD price.
Returns: price (`uint256`).
The `CryptoFromPool.vy` oracle contract does not take the aggregated price of crvUSD from the [`PriceAggregator.vy` contract](../../crvusd/oracles/price-aggregator.md) into account. Experience has shown that it makes sense to include this value in the oracle calculations. This is implemented in the `CryptoFromPoolWAgg.vy` oracle contract.
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
@external
def price_w() -> uint256:
return self._raw_price()
@internal
@view
def _raw_price() -> uint256:
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT:
p: uint256 = POOL.price_oracle()
if COLLATERAL_IX > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX > 0:
p_borrowed = POOL.price_oracle(BORROWED_IX - 1)
if COLLATERAL_IX > 0:
p_collateral = POOL.price_oracle(COLLATERAL_IX - 1)
return p_collateral * 10**18 / p_borrowed
```
```vyper
interface StableAggregator:
def price() -> uint256: view
def price_w() -> uint256: nonpayable
def stablecoin() -> address: view
AGG: public(immutable(StableAggregator))
@external
def price_w() -> uint256:
return self._raw_price() * AGG.price_w() / 10**18
@internal
@view
def _raw_price() -> uint256:
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT:
p: uint256 = POOL.price_oracle()
if COLLATERAL_IX > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX > 0:
p_borrowed = POOL.price_oracle(BORROWED_IX - 1)
if COLLATERAL_IX > 0:
p_collateral = POOL.price_oracle(COLLATERAL_IX - 1)
return p_collateral * 10**18 / p_borrowed
```
```vyper hl_lines="3 22"
@internal
def _price_oracle_w() -> uint256[2]:
p: uint256[2] = self.limit_p_o(price_oracle_contract.price_w())
self.prev_p_o_time = block.timestamp
self.old_p_o = p[0]
self.old_dfee = p[1]
return p
@internal
def _exchange(i: uint256, j: uint256, amount: uint256, minmax_amount: uint256, _for: address, use_in_amount: bool) -> uint256[2]:
"""
@notice Exchanges two coins, callable by anyone
@param i Input coin index
@param j Output coin index
@param amount Amount of input/output coin to swap
@param minmax_amount Minimal/maximum amount to get as output/input
@param _for Address to send coins to
@param use_in_amount Whether input or output amount is specified
@return Amount of coins given in and out
"""
assert (i == 0 and j == 1) or (i == 1 and j == 0), "Wrong index"
p_o: uint256[2] = self._price_oracle_w() # Let's update the oracle even if we exchange 0
if amount == 0:
return [0, 0]
...
```
```shell
>>> CryptoFromPool.price_w()
458009543343504151
```
::::
---
## Contract Info Methods
### `POOL`
::::description[`CryptoFromPool.POOL() -> address: view`]
Getter for the liquidity pool from which the oracle price is fetched.
Returns: liquidity pool (`address`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
POOL: public(immutable(Pool))
@external
def __init__(
pool: Pool,
N: uint256,
borrowed_ix: uint256,
collateral_ix: uint256
):
assert borrowed_ix != collateral_ix
assert borrowed_ix < N
assert collateral_ix < N
POOL = pool
N_COINS = N
BORROWED_IX = borrowed_ix
COLLATERAL_IX = collateral_ix
no_argument: bool = False
if N == 2:
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pool.address,
_abi_encode(empty(uint256), method_id=method_id("price_oracle(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
no_argument = True
NO_ARGUMENT = no_argument
```
```shell
>>> CryptoFromPool.POOL()
'0x4eBdF703948ddCEA3B11f675B4D1Fba9d2414A14'
```
::::
### `N_COINS`
::::description[`CryptoFromPool.N_COINS() -> uint256: view`]
Getter for the total number of coins in the liquidity pool.
Returns: coins count (`uint256`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
N_COINS: public(immutable(uint256))
@external
def __init__(
pool: Pool,
N: uint256,
borrowed_ix: uint256,
collateral_ix: uint256
):
assert borrowed_ix != collateral_ix
assert borrowed_ix < N
assert collateral_ix < N
POOL = pool
N_COINS = N
BORROWED_IX = borrowed_ix
COLLATERAL_IX = collateral_ix
no_argument: bool = False
if N == 2:
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pool.address,
_abi_encode(empty(uint256), method_id=method_id("price_oracle(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
no_argument = True
NO_ARGUMENT = no_argument
```
```shell
>>> CryptoFromPool.N_COINS()
3
```
::::
### `BORROWED_IX`
::::description[`CryptoFromPool.BORROWED_IX() -> uint256: view`]
Getter for the index of the borrowed coin in the liquidity pool from which the price oracle is taken from.
Returns: coin index (`uint256`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
BORROWED_IX: public(immutable(uint256))
@external
def __init__(
pool: Pool,
N: uint256,
borrowed_ix: uint256,
collateral_ix: uint256
):
assert borrowed_ix != collateral_ix
assert borrowed_ix < N
assert collateral_ix < N
POOL = pool
N_COINS = N
BORROWED_IX = borrowed_ix
COLLATERAL_IX = collateral_ix
no_argument: bool = False
if N == 2:
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pool.address,
_abi_encode(empty(uint256), method_id=method_id("price_oracle(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
no_argument = True
NO_ARGUMENT = no_argument
```
```shell
>>> CryptoFromPool.BORROWED_IX()
0
```
::::
### `COLLATERAL_IX`
::::description[`CryptoFromPool.COLLATERAL_IX() -> uint256: view`]
Getter for the index of the collateral coin in the liquidity pool from which the price oracle is taken from.
Returns: coin index (`uint256`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
COLLATERAL_IX: public(immutable(uint256))
@external
def __init__(
pool: Pool,
N: uint256,
borrowed_ix: uint256,
collateral_ix: uint256
):
assert borrowed_ix != collateral_ix
assert borrowed_ix < N
assert collateral_ix < N
POOL = pool
N_COINS = N
BORROWED_IX = borrowed_ix
COLLATERAL_IX = collateral_ix
no_argument: bool = False
if N == 2:
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pool.address,
_abi_encode(empty(uint256), method_id=method_id("price_oracle(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
no_argument = True
NO_ARGUMENT = no_argument
```
```shell
>>> CryptoFromPool.COLLATERAL_IX()
2
```
::::
### `NO_ARGUMENT`
::::description[`CryptoFromPool.NO_ARGUMENT() -> bool: view`]
Getter for the `NO_ARGUMENT` storage variable. This is an additional variable to ensure the correct price oracle is fetched from a pool with more than two coins.
The variable is set to `false` if the pool from which the price oracle is taken has only two coins.
Returns: true or false (`bool`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
NO_ARGUMENT: public(immutable(bool))
@external
def __init__(
pool: Pool,
N: uint256,
borrowed_ix: uint256,
collateral_ix: uint256
):
assert borrowed_ix != collateral_ix
assert borrowed_ix < N
assert collateral_ix < N
POOL = pool
N_COINS = N
BORROWED_IX = borrowed_ix
COLLATERAL_IX = collateral_ix
no_argument: bool = False
if N == 2:
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pool.address,
_abi_encode(empty(uint256), method_id=method_id("price_oracle(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
no_argument = True
NO_ARGUMENT = no_argument
```
```shell
>>> CryptoFromPool.NO_ARGUMENT()
'False'
```
::::
### `AGG`
::::description[`CryptoFromPoolWAgg.AGG() -> address: view`]
:::info
This `AGG` storage variable is only used within the `CryptoFromPoolWAgg` contracts.
:::
Getter for the crvUSD `PriceAggregator` contract. This value is immutable and set at contract initialization.
Returns: `PriceAggregator` (`address`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
interface StableAggregator:
def price() -> uint256: view
def price_w() -> uint256: nonpayable
def stablecoin() -> address: view
AGG: public(immutable(StableAggregator))
```
```shell
>>> CryptoFromPoolWAgg.AGG()
'0x18672b1b0c623a30089A280Ed9256379fb0E4E62'
```
::::
---
## Arbitrum
In addition to the aforementioned functions, oracle contracts on Arbitrum use a [Chainlink Uptime Feed Oracle](https://arbiscan.io/address/0xFdB631F5EE196F0ed6FAa767959853A9F217697D) to monitor and validate any potential downtime of the [sequencer](https://docs.arbitrum.io/sequencer).
Should the internal `_raw_price` function, responsible for fetching the price, encounter an indication from the uptime oracle that the Arbitrum sequencer is presently offline, or if it has experienced recent downtime and the `DOWNTIME_WAIT` period of 3988 seconds has not yet elapsed, it will revert.
```vyper
interface ChainlinkOracle:
def latestRoundData() -> ChainlinkAnswer: view
struct ChainlinkAnswer:
roundID: uint80
answer: int256
startedAt: uint256
updatedAt: uint256
answeredInRound: uint80
@internal
@view
def _raw_price() -> uint256:
# Check that we had no downtime
cl_answer: ChainlinkAnswer = ChainlinkOracle(CHAINLINK_UPTIME_FEED).latestRoundData()
assert cl_answer.answer == 0, "Sequencer is down"
assert block.timestamp >= cl_answer.startedAt + DOWNTIME_WAIT, "Wait after downtime"
...
```
### `CHAINLINK_UPTIME_FEED`
::::description[`CryptoFromPool.CHAINLINK_UPTIME_FEED() -> address: view`]
Getter for the `ChainlinkUptimeFeed` contract.
Returns: uptime feed contract (`address`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
CHAINLINK_UPTIME_FEED: public(constant(address)) = 0xFdB631F5EE196F0ed6FAa767959853A9F217697D
```
```shell
>>> CryptoFromPool.CHAINLINK_UPTIME_FEED()
'0xFdB631F5EE196F0ed6FAa767959853A9F217697D'
```
::::
### `DOWNTIME_WAIT`
::::description[`CryptoFromPool.DOWNTIME_WAIT() -> uint256: view`]
Getter for the required time to wait after the sequencer was down.
Returns: time to wait (`uint256`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
DOWNTIME_WAIT: public(constant(uint256)) = 3988 # 866 * log(100) s
```
```shell
>>> CryptoFromPool.DOWNTIME_WAIT()
3988
```
::::
---
## CryptoFromPoolsRate
This oracle contract **chains together two oracles from two different Curve liquidity pools and optionally applies `stored_rates` to tokens with an existing rate oracle**. By chaining oracles together, it facilitates the creation of lending oracle contracts without requiring the collateral asset to be paired directly against crvUSD. The first oracle contracts were deployed without considering the [aggregated price of crvUSD](../../crvusd/oracles/price-aggregator.md), but experience has shown that it makes sense to include this value in the calculation. The respective differences are documented in the relevant sections.
These kinds of oracle contracts **need to be deployed manually**, as there is currently no `Factory` to do so.
:::vyper[`CryptoFromPoolsRate.vy`]
The source code for the `CryptoFromPoolsRate.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/price_oracles/CryptoFromPoolsRate.vy). A variant that includes the aggregated crvUSD price, [`CryptoFromPoolsRateWAgg.vy`](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/price_oracles/CryptoFromPoolsRateWAgg.vy), is also available. The contracts are written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
:::
:::danger[Oracle Immutability]
The oracle contracts are fully immutable. Once deployed, they cannot change any parameters, stop the price updates, or alter the pools used to calculate the prices. All relevant data required for the oracle to function is passed into the `__init__` function during the deployment of the contract.
```vyper
@external
def __init__(
pools: DynArray[Pool, MAX_POOLS],
borrowed_ixs: DynArray[uint256, MAX_POOLS],
collateral_ixs: DynArray[uint256, MAX_POOLS]
):
POOLS = pools
pool_count: uint256 = 0
no_arguments: DynArray[bool, MAX_POOLS] = empty(DynArray[bool, MAX_POOLS])
use_rates: DynArray[bool, MAX_POOLS] = empty(DynArray[bool, MAX_POOLS])
for i in range(MAX_POOLS):
if i == len(pools):
assert i != 0, "Wrong pool counts"
pool_count = i
break
# Find N
N: uint256 = 0
for j in range(MAX_COINS + 1):
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pools[i].address,
_abi_encode(j, method_id=method_id("coins(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
assert j != 0, "No coins(0)"
N = j
break
assert borrowed_ixs[i] != collateral_ixs[i]
assert borrowed_ixs[i] < N
assert collateral_ixs[i] < N
# Init variables for raw call
success: bool = False
# Check and record if pool requires coin id in argument or no
if N == 2:
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pools[i].address,
_abi_encode(empty(uint256), method_id=method_id("price_oracle(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
no_arguments.append(True)
else:
no_arguments.append(False)
else:
no_arguments.append(False)
res: Bytes[1024] = empty(Bytes[1024])
success, res = raw_call(pools[i].address, method_id("stored_rates()"), max_outsize=1024, is_static_call=True, revert_on_failure=False)
stored_rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
if success and len(res) > 0:
stored_rates = _abi_decode(res, DynArray[uint256, MAX_COINS])
u: bool = False
for r in stored_rates:
if r != 10**18:
u = True
use_rates.append(u)
```
```vyper
@external
def __init__(
pools: DynArray[Pool, MAX_POOLS],
borrowed_ixs: DynArray[uint256, MAX_POOLS],
collateral_ixs: DynArray[uint256, MAX_POOLS],
agg: StableAggregator
):
POOLS = pools
pool_count: uint256 = 0
no_arguments: DynArray[bool, MAX_POOLS] = empty(DynArray[bool, MAX_POOLS])
use_rates: DynArray[bool, MAX_POOLS] = empty(DynArray[bool, MAX_POOLS])
AGG = agg
for i in range(MAX_POOLS):
if i == len(pools):
assert i != 0, "Wrong pool counts"
pool_count = i
break
# Find N
N: uint256 = 0
for j in range(MAX_COINS + 1):
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pools[i].address,
_abi_encode(j, method_id=method_id("coins(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
assert j != 0, "No coins(0)"
N = j
break
assert borrowed_ixs[i] != collateral_ixs[i]
assert borrowed_ixs[i] < N
assert collateral_ixs[i] < N
# Init variables for raw call
success: bool = False
# Check and record if pool requires coin id in argument or no
if N == 2:
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pools[i].address,
_abi_encode(empty(uint256), method_id=method_id("price_oracle(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
if not success:
no_arguments.append(True)
else:
no_arguments.append(False)
else:
no_arguments.append(False)
res: Bytes[1024] = empty(Bytes[1024])
success, res = raw_call(pools[i].address, method_id("stored_rates()"), max_outsize=1024, is_static_call=True, revert_on_failure=False)
stored_rates: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS])
if success and len(res) > 0:
stored_rates = _abi_decode(res, DynArray[uint256, MAX_COINS])
u: bool = False
for r in stored_rates:
if r != 10**18:
u = True
use_rates.append(u)
NO_ARGUMENT = no_arguments
BORROWED_IX = borrowed_ixs
COLLATERAL_IX = collateral_ixs
if pool_count == 0:
pool_count = MAX_POOLS
POOL_COUNT = pool_count
USE_RATES = use_rates
```
:::
---
## Oracle Price
The price is determined by combining two different oracle prices. When necessary, `stored_rates` are used to adjust the final computed price from these combined oracles.
:::example[Example]
For example, suppose the oracle price of Token/ETH is 0.05, and the oracle price of ETH/crvUSD is 1000. The computed price of Token/crvUSD would then be calculated as follows:
$\text{price} = 0.05 \times 1000 = 50$
This calculation indicates that one Token is equivalent to 50 crvUSD.
:::
### `price`
::::description[`CryptoFromPoolsRate.price() -> uint256`]
Getter for the price of the collateral denominated against the borrowed token. E.g. a market with pufETH as collateral and crvUSD borrowable, the price will return the pufETH price with regard to crvUSD.
Returns: oracle price (`uint256`).
The `CryptoFromPoolsRate.vy` oracle contract does not take the aggregated price of crvUSD from the [`PriceAggregator.vy` contract](../../crvusd/oracles/price-aggregator.md) into account. Experience has shown that it makes sense to include this value in the oracle calculations. This is implemented in the `CryptoFromPoolsRateWAgg.vy` oracle contract.
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
@external
@view
def price() -> uint256:
return self._unscaled_price() * self._stored_rate()[0] / 10**18
@internal
@view
def _unscaled_price() -> uint256:
_price: uint256 = 10**18
for i in range(MAX_POOLS):
if i >= POOL_COUNT:
break
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT[i]:
p: uint256 = POOLS[i].price_oracle()
if COLLATERAL_IX[i] > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX[i] > 0:
p_borrowed = POOLS[i].price_oracle(unsafe_sub(BORROWED_IX[i], 1))
if COLLATERAL_IX[i] > 0:
p_collateral = POOLS[i].price_oracle(unsafe_sub(COLLATERAL_IX[i], 1))
_price = _price * p_collateral / p_borrowed
return _price
@internal
@view
def _stored_rate() -> (uint256, bool):
use_rates: bool = False
rate: uint256 = 0
rate, use_rates = self._raw_stored_rate()
if not use_rates:
return rate, use_rates
cached_rate: uint256 = self.cached_rate
if cached_rate == 0 or cached_rate == rate:
return rate, use_rates
if rate > cached_rate:
return min(rate, cached_rate * (10**18 + RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18), use_rates
else:
return max(rate, cached_rate * (10**18 - min(RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp), 10**18)) / 10**18), use_rates
@internal
@view
def _raw_stored_rate() -> (uint256, bool):
rate: uint256 = 10**18
use_rates: bool = False
for i in range(MAX_POOLS):
if i == POOL_COUNT:
break
if USE_RATES[i]:
use_rates = True
rates: DynArray[uint256, MAX_COINS] = POOLS[i].stored_rates()
rate = rate * rates[COLLATERAL_IX[i]] / rates[BORROWED_IX[i]]
return rate, use_rates
```
```vyper
interface StableAggregator:
def price() -> uint256: view
def price_w() -> uint256: nonpayable
def stablecoin() -> address: view
AGG: public(immutable(StableAggregator))
@external
@view
def price() -> uint256:
return self._unscaled_price() * self._stored_rate()[0] / 10**18 * AGG.price() / 10**18
@internal
@view
def _unscaled_price() -> uint256:
_price: uint256 = 10**18
for i in range(MAX_POOLS):
if i >= POOL_COUNT:
break
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT[i]:
p: uint256 = POOLS[i].price_oracle()
if COLLATERAL_IX[i] > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX[i] > 0:
p_borrowed = POOLS[i].price_oracle(unsafe_sub(BORROWED_IX[i], 1))
if COLLATERAL_IX[i] > 0:
p_collateral = POOLS[i].price_oracle(unsafe_sub(COLLATERAL_IX[i], 1))
_price = _price * p_collateral / p_borrowed
return _price
@internal
@view
def _stored_rate() -> (uint256, bool):
use_rates: bool = False
rate: uint256 = 0
rate, use_rates = self._raw_stored_rate()
if not use_rates:
return rate, use_rates
cached_rate: uint256 = self.cached_rate
if cached_rate == 0 or cached_rate == rate:
return rate, use_rates
if rate > cached_rate:
return min(rate, cached_rate * (10**18 + RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18), use_rates
else:
return max(rate, cached_rate * (10**18 - min(RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp), 10**18)) / 10**18), use_rates
@internal
@view
def _raw_stored_rate() -> (uint256, bool):
rate: uint256 = 10**18
use_rates: bool = False
for i in range(MAX_POOLS):
if i == POOL_COUNT:
break
if USE_RATES[i]:
use_rates = True
rates: DynArray[uint256, MAX_COINS] = POOLS[i].stored_rates()
rate = rate * rates[COLLATERAL_IX[i]] / rates[BORROWED_IX[i]]
return rate, use_rates
```
```shell
>>> CryptoFromPoolsRate.price()
3110715001971074513929
```
::::
### `price_w`
::::description[`CryptoFromPoolsRate.price_w() -> uint256`]
This function calculates and writes the price while updating `cached_rate` and `cached_timestamp`. It is invoked whenever the `_exchange` function is called within the AMM contract of the lending market.
Returns: oracle price (`uint256`).
The `CryptoFromPoolsRate.vy` oracle contract does not take the aggregated price of crvUSD from the [`PriceAggregator.vy` contract](../../crvusd/oracles/price-aggregator.md) into account. Experience has shown that it makes sense to include this value in the oracle calculations. This is implemented in the `CryptoFromPoolsRateWAgg.vy` oracle contract.
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
@external
def price_w() -> uint256:
return self._unscaled_price() * self._stored_rate_w() / 10**18
@internal
@view
def _unscaled_price() -> uint256:
_price: uint256 = 10**18
for i in range(MAX_POOLS):
if i >= POOL_COUNT:
break
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT[i]:
p: uint256 = POOLS[i].price_oracle()
if COLLATERAL_IX[i] > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX[i] > 0:
p_borrowed = POOLS[i].price_oracle(unsafe_sub(BORROWED_IX[i], 1))
if COLLATERAL_IX[i] > 0:
p_collateral = POOLS[i].price_oracle(unsafe_sub(COLLATERAL_IX[i], 1))
_price = _price * p_collateral / p_borrowed
return _price
@internal
def _stored_rate_w() -> uint256:
rate: uint256 = 0
use_rates: bool = False
rate, use_rates = self._stored_rate()
if use_rates:
self.cached_rate = rate
self.cached_timestamp = block.timestamp
return rate
@internal
@view
def _stored_rate() -> (uint256, bool):
use_rates: bool = False
rate: uint256 = 0
rate, use_rates = self._raw_stored_rate()
if not use_rates:
return rate, use_rates
cached_rate: uint256 = self.cached_rate
if cached_rate == 0 or cached_rate == rate:
return rate, use_rates
if rate > cached_rate:
return min(rate, cached_rate * (10**18 + RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18), use_rates
else:
return max(rate, cached_rate * (10**18 - min(RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp), 10**18)) / 10**18), use_rates
```
```vyper
interface StableAggregator:
def price() -> uint256: view
def price_w() -> uint256: nonpayable
def stablecoin() -> address: view
AGG: public(immutable(StableAggregator))
@external
def price_w() -> uint256:
return self._unscaled_price() * self._stored_rate_w() / 10**18 * AGG.price_w() / 10**18
@internal
@view
def _unscaled_price() -> uint256:
_price: uint256 = 10**18
for i in range(MAX_POOLS):
if i >= POOL_COUNT:
break
p_borrowed: uint256 = 10**18
p_collateral: uint256 = 10**18
if NO_ARGUMENT[i]:
p: uint256 = POOLS[i].price_oracle()
if COLLATERAL_IX[i] > 0:
p_collateral = p
else:
p_borrowed = p
else:
if BORROWED_IX[i] > 0:
p_borrowed = POOLS[i].price_oracle(unsafe_sub(BORROWED_IX[i], 1))
if COLLATERAL_IX[i] > 0:
p_collateral = POOLS[i].price_oracle(unsafe_sub(COLLATERAL_IX[i], 1))
_price = _price * p_collateral / p_borrowed
return _price
@internal
def _stored_rate_w() -> uint256:
rate: uint256 = 0
use_rates: bool = False
rate, use_rates = self._stored_rate()
if use_rates:
self.cached_rate = rate
self.cached_timestamp = block.timestamp
return rate
@internal
@view
def _stored_rate() -> (uint256, bool):
use_rates: bool = False
rate: uint256 = 0
rate, use_rates = self._raw_stored_rate()
if not use_rates:
return rate, use_rates
cached_rate: uint256 = self.cached_rate
if cached_rate == 0 or cached_rate == rate:
return rate, use_rates
if rate > cached_rate:
return min(rate, cached_rate * (10**18 + RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18), use_rates
else:
return max(rate, cached_rate * (10**18 - min(RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp), 10**18)) / 10**18), use_rates
```
```shell
>>> CryptoFromPoolsRate.price_w()
3110715001971074513929
```
::::
---
## Rates
The oracle contract utilizes the `stored_rates` from a stableswap pool and considers these rates accordingly. The application of these rates is governed by the `USE_RATES` variable. If set to `true`, the rates are applied; if set to `false`, they are not.
```vyper
RATE_MAX_SPEED: constant(uint256) = 10**16 / 60 # Max speed of Rate change
@internal
@view
def _stored_rate() -> (uint256, bool):
use_rates: bool = False
rate: uint256 = 0
rate, use_rates = self._raw_stored_rate()
if not use_rates:
return rate, use_rates
cached_rate: uint256 = self.cached_rate
if cached_rate == 0 or cached_rate == rate:
return rate, use_rates
if rate > cached_rate:
return min(rate, cached_rate * (10**18 + RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18), use_rates
else:
return max(rate, cached_rate * (10**18 - min(RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp), 10**18)) / 10**18), use_rates
```
Based on the values of `rate` and `cached_rate`, specific calculations are required to account for changes in rates:
- **If the `cached_rate` is 0**(indicating that no rates need to be applied) **or equal to the actual `rate`**(meaning no rate changes have occurred since the last update), there is no need to do additional calculations to obtain the `rate` value.
- **If `rate > cached_rate`**, the rate is calculated as follows:
$\text{rate} = \min\left(\text{rate}, \frac{\text{cached_rate} \times \left(10^{18} + \text{RATE_MAX_SPEED} \times (\text{block.timestamp} - \text{cached_timestamp})\right)}{10^{18}}\right)$
This calculation aims to smoothly increase the rate, capping it at a calculated maximum based on the `RATE_MAX_SPEED` and the time elapsed since the last cache update.
- **In all other cases**, where the rate has decreased, it is calculated as follows:
$\text{rate} = \max\left(\text{rate}, \frac{\text{cached_rate} \times \left(10^{18} - \min\left(\text{RATE_MAX_SPEED} \times (\text{block.timestamp} - \text{cached_timestamp}), 10^{18}\right)\right)}{10^{18}}\right)$
This formula ensures that the rate does not decrease too rapidly, with the reduction bounded by a minimum that considers the `RATE_MAX_SPEED` and the elapsed time.
### `stored_rate`
::::description[`CryptoFromPoolsRate.stored_rate() -> uint256: view`]
Getter for the stored rates fetched from a stableswap pool.
The `stored_rate` is calculated the following:
$$\text{rate} = 10^{18} \cdot \frac{\text{stored\_rates[collateral\_ix]}}{\text{stored\_rates[borrowed\_ix]}}$$
Returns: stored rate (`uint256`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
@external
@view
def stored_rate() -> uint256:
return self._stored_rate()[0]
@internal
@view
def _stored_rate() -> (uint256, bool):
use_rates: bool = False
rate: uint256 = 0
rate, use_rates = self._raw_stored_rate()
if not use_rates:
return rate, use_rates
cached_rate: uint256 = self.cached_rate
if cached_rate == 0 or cached_rate == rate:
return rate, use_rates
if rate > cached_rate:
return min(rate, cached_rate * (10**18 + RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp)) / 10**18), use_rates
else:
return max(rate, cached_rate * (10**18 - min(RATE_MAX_SPEED * (block.timestamp - self.cached_timestamp), 10**18)) / 10**18), use_rates
@internal
@view
def _raw_stored_rate() -> (uint256, bool):
rate: uint256 = 10**18
use_rates: bool = False
for i in range(MAX_POOLS):
if i == POOL_COUNT:
break
if USE_RATES[i]:
use_rates = True
rates: DynArray[uint256, MAX_COINS] = POOLS[i].stored_rates()
rate = rate * rates[COLLATERAL_IX[i]] / rates[BORROWED_IX[i]]
return rate, use_rates
```
```shell
>>> StableSwap.stored_rates()
1009207839112594800, 1166115485922357109
>>> CryptoFromPoolsRate.stored_rate()
865444161659808698 # calculated via: 1009207839112594800 / 1166115485922357109
```
::::
### `cached_rate`
::::description[`CryptoFromPoolsRate.cached_rate() -> uint256: view`]
Getter for the cached rate. This value is updated whenever the `price_w` method is called.
Returns: cached rate (`uint256`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
cached_rate: public(uint256)
@external
def price_w() -> uint256:
return self._unscaled_price() * self._stored_rate_w() / 10**18
@internal
def _stored_rate_w() -> uint256:
rate: uint256 = 0
use_rates: bool = False
rate, use_rates = self._stored_rate()
if use_rates:
self.cached_rate = rate
self.cached_timestamp = block.timestamp
return rate
```
```shell
>>> CryptoFromPoolsRate.cached_rate()
865388487987562874
```
::::
### `cached_timestamp`
::::description[`CryptoFromPoolsRate.cached_timestamp() -> uint256: view`]
Getter for the cached timestamp. This value is updated whenever the `price_w` method is called.
Returns: cached timestamp (`uint256`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
cached_timestamp: public(uint256)
@external
def price_w() -> uint256:
return self._unscaled_price() * self._stored_rate_w() / 10**18
@internal
def _stored_rate_w() -> uint256:
rate: uint256 = 0
use_rates: bool = False
rate, use_rates = self._stored_rate()
if use_rates:
self.cached_rate = rate
self.cached_timestamp = block.timestamp
return rate
```
```shell
>>> CryptoFromPoolsRate.cached_timestamp()
1714877987
```
::::
### `USE_RATES`
::::description[`CryptoFromPoolsRate.USE_RATES(arg0: uint256) -> bool: view`]
Getter method to check whether the pool at index `arg0` uses rates or not. Pool indices are fetched via [`POOLS`](#pools).
Returns: whether to apply rates or not (`bool`).
| Input | Type | Description |
| ------ | --------- | ----------- |
| `arg0` | `uint256` | Pool index. |
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
USE_RATES: public(immutable(DynArray[bool, MAX_POOLS]))
```
```shell
>>> CryptoFromPoolsRate.USE_RATES(0) # checks for pufETH/wETH pool
'true' # true, because the `stored_rates` of this pool are used
>>> CryptoFromPoolsRate.USE_RATES(0) # checks for tryLSD pool
'false' # false, because no rates are used
```
::::
---
## Contract Info Methods
### `POOLS`
::::description[`CryptoFromPoolsRate.POOLS(arg0: uint256) -> address: view`]
Getter for the liquidity pools used in the oracle contract.
Returns: pool contract (`address`).
| Input | Type | Description |
| ------ | --------- | ----------- |
| `arg0` | `uint256` | Pool index. |
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
POOLS: public(immutable(DynArray[Pool, MAX_POOLS]))
```
```shell
>>> CryptoFromPoolsRate.POOLS(0)
'0xEEda34A377dD0ca676b9511EE1324974fA8d980D'
>>> CryptoFromPoolsRate.POOLS(1)
'0x2889302a794dA87fBF1D6Db415C1492194663D13'
```
::::
### `POOL_COUNT`
::::description[`CryptoFromPoolsRate.POOL_COUNT() -> uint256: view`]
Getter for the total amount of pools used in the oracle contract.
Returns: amount of pools (`uint256`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
POOL_COUNT: public(immutable(uint256))
```
```shell
>>> CryptoFromPoolsRate.POOL_COUNT()
2
```
::::
### `BORROWED_IX`
::::description[`CryptoFromPoolsRate.BORROWED_IX(arg0: uint256) -> uint256: view`]
Getter for the index of the borrowed token in the chain together pools. If the oracle contract is for an asset that has a rate, this method will return the coin indices of the "base asset". E.g., for pufETH, this method will return the index of wstETH in the pools and later, when calculating the price of pufETH, the rates are applied.
Returns: coin index (`uint256`).
| Input | Type | Description |
| ------ | --------- | ----------- |
| `arg0` | `uint256` | Pool index to check `BORROWED_IX` for. |
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
BORROWED_IX: public(immutable(DynArray[uint256, MAX_POOLS]))
```
```shell
>>> CryptoFromPoolsRate.BORROWED_IX(0)
1 # wstETH
>>> CryptoFromPoolsRate.BORROWED_IX(1)
0 # wstETH
```
::::
### `COLLATERAL_IX`
::::description[`CryptoFromPoolsRate.COLLATERAL_IX(arg0: uint256) -> uint256: view`]
Getter for the index of the collateral token within the pool.
Returns: coin index (`uint256`).
| Input | Type | Description |
|-------|-----------|-------------------------------------------|
| `arg0` | `uint256` | Pool index to check `COLLATERAL_IX` for. |
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
COLLATERAL_IX: public(immutable(DynArray[uint256, MAX_POOLS]))
```
```shell
>>> CryptoFromPoolsRate.COLLATERAL_IX(0)
0 # pufETH
>>> CryptoFromPoolsRate.COLLATERAL_IX(1)
2 # wstETH
```
::::
### `NO_ARGUMENT`
::::description[`CryptoFromPoolsRate.NO_ARGUMENT(arg0: uint256) -> bool: view`]
Getter for the `NO_ARGUMENT` storage variable. This is an additional variable to ensure the correct price oracle is fetched from a pool.
Returns: true or false (`bool`).
| Input | Type | Description |
| ------ | --------- | ----------- |
| `arg0` | `uint256` | Pool index. |
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
NO_ARGUMENT: public(immutable(DynArray[bool, MAX_POOLS]))
```
```shell
>>> CryptoFromPoolsRate.NO_ARGUMENT(0)
'false'
>>> CryptoFromPoolsRate.NO_ARGUMENT(1)
'false'
```
::::
### `AGG`
::::description[`CryptoFromPoolsRate.AGG() -> address: view`]
:::info
This `AGG` storage variable is only used within the `CryptoFromPoolsRateWAgg` contracts.
:::
Getter for the crvUSD `PriceAggregator` contract. This value is immutable and set at contract initialization.
Returns: `PriceAggregator` (`address`).
The following source code includes all changes up to commit hash [86cae3a](https://github.com/curvefi/curve-stablecoin/tree/86cae3a89f2138122be428b3c060cc75fa1df1b0); any changes made after this commit are not included.
```vyper
interface StableAggregator:
def price() -> uint256: view
def price_w() -> uint256: nonpayable
def stablecoin() -> address: view
AGG: public(immutable(StableAggregator))
```
```shell
>>> CryptoFromPoolsRate.AGG()
'0x18672b1b0c623a30089A280Ed9256379fb0E4E62'
```
::::
---
## Building Leverage
There are multiple ways on how to create automated leverage for lending markets:
- v1: using the [Curve Pools](../../crvusd/leverage/leverage-zap.md)
- v2: using the [1inch Router](../../crvusd/leverage/leverage-zap-1inch.md)
- v3: using the [Odos Router](../../crvusd/leverage/llamalend-odos-leverage-zap.md)
:::warning[Warning]
The possibility of creating leverage and the usage of the contract above is dependent on the implementation contract of the `Controller` contract of the market.
For more information on the leverage feature, please refer to the [Leverage](../../crvusd/leverage/llamalend-odos-leverage-zap.md) section of the crvUSD documentation.
:::
---
## Monetary Policies: Overview
Lending markets use monetary policies to determine interest rates. Each market has its own policy contract.
*Currently, there are two different kinds of policy contracts in use:*
Semi-logarithmic monetary policy based on the utilization of lending markets.
A monetary policy that follows the rate of crvUSD mint markets based on the utilization of the market.
---
## OneWay Lending Factory
A one-way lending market is a **non-rehypothecating** market where one token is considered the collateral token and another token is the borrow token. This means the **deposited collateral cannot be lent out** but can only be used as collateral.
:::vyper[`OneWayLendingFactory.vy`]
The source code for the `OneWayLendingFactory.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/lending/contracts/lending/OneWayLendingFactory.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
:::
*Later on, two-way lending markets will be established, allowing the collateral provided to be lent out and used as liquidity to borrow.*
---
## Creating Lending Markets
A lending market **must always include crvUSD, either as collateral or as the borrowable token**.
*There are two ways to create lending markets:*
- **`create`**: This method involves creating a vault and its accompanying contracts using an **external user-supplied price oracle**.
- **`create_from_pool`**: This method involves creating a vault and its accompanying contracts using an **existing oraclized Curve pool as a price oracle**.
:::info[Finding Optimal Parameters]
To find optimal values for the parameters, check out: https://github.com/curvefi/llamma-simulator.
*Regarding rates:* Minimum and maximum borrow rates for lending markets default to `min_default_borrow_rate` and `max_default_borrow_rate` if input values for `min_borrow_rate` and `max_borrow_rate` are set to zero. If custom values are used, they need to be within the range of `MIN_RATE` and `MAX_RATE`.
:::
### `create`
::::description[`OneWayLendingVaultFactory.create(borrowed_token: address, collateral_token: address, A: uint256, fee: uint256, loan_discount: uint256, liquidation_discount: uint256, price_oracle: address, name: String[64], min_borrow_rate: uint256 = 0, max_borrow_rate: uint256 = 0) -> Vault`]
Function to create a new vault using a user-supplied price oracle contract.
| Input | Type | Description |
|------------------------|---------------|-------------|
| `borrowed_token` | `address` | Token which is being borrowed. |
| `collateral_token` | `address` | Token used as collateral. |
| `A` | `uint256` | Amplification coefficient. Band size is ~1/A. |
| `fee` | `uint256` | Fee for swaps in the AMM. |
| `loan_discount` | `uint256` | Maximum discount. LTV = sqrt(((A - 1) / A) **4) - loan_discount. |
| `liquidation_discount` | `uint256` | Liquidation discount. LT = sqrt(((A - 1) / A) **4) - liquidation_discount |
| `price_oracle` | `address` | Custom price oracle contract. |
| `name` | `String[64]` | Name of the vault. |
| `min_borrow_rate` | `uint256` | Custom minimum borrow rate; if not set will default to `min_default_borrow_rate` |
| `max_borrow_rate` | `uint256` | Custom maximum borrow rate; if not set will default to `max_default_borrow_rate` |
Returns: vault (`address`).
Emits: `NewVault` event.
```vyper
event NewVault:
id: indexed(uint256)
collateral_token: indexed(address)
borrowed_token: indexed(address)
vault: address
controller: address
amm: address
price_oracle: address
monetary_policy: address
@external
@nonreentrant('lock')
def create(
borrowed_token: address,
collateral_token: address,
A: uint256,
fee: uint256,
loan_discount: uint256,
liquidation_discount: uint256,
price_oracle: address,
name: String[64],
min_borrow_rate: uint256 = 0,
max_borrow_rate: uint256 = 0
) -> Vault:
"""
@notice Creation of the vault using user-supplied price oracle contract
@param borrowed_token Token which is being borrowed
@param collateral_token Token used for collateral
@param A Amplification coefficient: band size is ~1/A
@param fee Fee for swaps in AMM (for ETH markets found to be 0.6%)
@param loan_discount Maximum discount. LTV = sqrt(((A - 1) / A) **4) - loan_discount
@param liquidation_discount Liquidation discount. LT = sqrt(((A - 1) / A) **4) - liquidation_discount
@param price_oracle Custom price oracle contract
@param name Human-readable market name
@param min_borrow_rate Custom minimum borrow rate (otherwise min_default_borrow_rate)
@param max_borrow_rate Custom maximum borrow rate (otherwise max_default_borrow_rate)
"""
return self._create(borrowed_token, collateral_token, A, fee, loan_discount, liquidation_discount,
price_oracle, name, min_borrow_rate, max_borrow_rate)
@internal
def _create(
borrowed_token: address,
collateral_token: address,
A: uint256,
fee: uint256,
loan_discount: uint256,
liquidation_discount: uint256,
price_oracle: address,
name: String[64],
min_borrow_rate: uint256,
max_borrow_rate: uint256
) -> Vault:
"""
@notice Internal method for creation of the vault
"""
assert borrowed_token != collateral_token, "Same token"
assert borrowed_token == STABLECOIN or collateral_token == STABLECOIN
vault: Vault = Vault(create_minimal_proxy_to(self.vault_impl))
min_rate: uint256 = self.min_default_borrow_rate
max_rate: uint256 = self.max_default_borrow_rate
if min_borrow_rate > 0:
min_rate = min_borrow_rate
if max_borrow_rate > 0:
max_rate = max_borrow_rate
assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates"
monetary_policy: address = create_from_blueprint(
self.monetary_policy_impl, borrowed_token, min_rate, max_rate, code_offset=3)
controller: address = empty(address)
amm: address = empty(address)
controller, amm = vault.initialize(
self.amm_impl, self.controller_impl,
borrowed_token, collateral_token,
A, fee,
price_oracle,
monetary_policy,
loan_discount, liquidation_discount
)
market_count: uint256 = self.market_count
log NewVault(market_count, collateral_token, borrowed_token, vault.address, controller, amm, price_oracle, monetary_policy)
self.vaults[market_count] = vault
self.amms[market_count] = AMM(amm)
self._vaults_index[vault] = market_count + 2**128
self.names[market_count] = name
self.market_count = market_count + 1
token: address = borrowed_token
if borrowed_token == STABLECOIN:
token = collateral_token
market_count = self.token_market_count[token]
self.token_to_vaults[token][market_count] = vault
self.token_market_count[token] = market_count + 1
ERC20(borrowed_token).approve(amm, max_value(uint256))
ERC20(collateral_token).approve(amm, max_value(uint256))
return vault
```
```shell
>>> OneWayLendingVaultFactory.create(
"0xf939e0a03fb07f59a73314e73794be0e57ac1b4e", # borrowed_token
"0x8f22779662ad253844013d8e99eccb4d80e31417", # collateral_token
50, # A
6000000000000000, # fee
140000000000000000, # loan_discount
110000000000000000, # liquidation_discount
external price oracle, # price_oracle
"bobrCRV-long", # name
0, # min_borrow_rate
1) # max_borrow_rate
'0xE16D806c4198955534d4EB10E4861Ea94557602E' # returns address of the created vault
```
::::
### `create_from_pool`
::::description[`OneWayLendingVaultFactory.create_from_pool(borrowed_token: address, collateral_token: address, A: uint256, fee: uint256, loan_discount: uint256, liquidation_discount: uint256, pool: address, name: String[64], min_borrow_rate: uint256 = 0, max_borrow_rate: uint256 = 0) -> Vault`]
:::warning[Valid Pool Oracles]
Only oracles from stableswap-ng, twocrypto-ng, and tricrypto-ng pools are valid. Oracles from other pools may not be manipulation resistant and therefore should not be used.
:::
Function to create a new vault using a existing oraclized Curve pool as the price oracle.
| Input | Type | Description |
|------------------------|---------------|-------------|
| `borrowed_token` | `address` | Token which is being borrowed. |
| `collateral_token` | `address` | Token used as collateral. |
| `A` | `uint256` | Amplification coefficient. Band size is ~1/A. |
| `fee` | `uint256` | Fee for swaps in the AMM. |
| `loan_discount` | `uint256` | Maximum discount. LTV = sqrt(((A - 1) / A) **4) - loan_discount. |
| `liquidation_discount` | `uint256` | Liquidation discount. LT = sqrt(((A - 1) / A) **4) - liquidation_discount |
| `pool` | `address` | Curve tricrypto-ng, twocrypto-ng or stableswap-ng pool which has non-manipulatable `price_oracle()`. Must contain both collateral_token and borrowed_token. |
| `name` | `String[64]` | Name of the vault. |
| `min_borrow_rate` | `uint256` | Custom minimum borrow rate; if not set will default to `min_default_borrow_rate` |
| `max_borrow_rate` | `uint256` | Custom maximum borrow rate; if not set will default to `max_default_borrow_rate` |
Returns: vault (`address`).
Emits: `NewVault` event.
```vyper
event NewVault:
id: indexed(uint256)
collateral_token: indexed(address)
borrowed_token: indexed(address)
vault: address
controller: address
amm: address
price_oracle: address
monetary_policy: address
@external
@nonreentrant('lock')
def create_from_pool(
borrowed_token: address,
collateral_token: address,
A: uint256,
fee: uint256,
loan_discount: uint256,
liquidation_discount: uint256,
pool: address,
name: String[64],
min_borrow_rate: uint256 = 0,
max_borrow_rate: uint256 = 0
) -> Vault:
"""
@notice Creation of the vault using existing oraclized Curve pool as a price oracle
@param borrowed_token Token which is being borrowed
@param collateral_token Token used for collateral
@param A Amplification coefficient: band size is ~1/A
@param fee Fee for swaps in AMM (for ETH markets found to be 0.6%)
@param loan_discount Maximum discount. LTV = sqrt(((A - 1) / A) **4) - loan_discount
@param liquidation_discount Liquidation discount. LT = sqrt(((A - 1) / A) **4) - liquidation_discount
@param pool Curve tricrypto-ng, twocrypto-ng or stableswap-ng pool which has non-manipulatable price_oracle().
Must contain both collateral_token and borrowed_token.
@param name Human-readable market name
@param min_borrow_rate Custom minimum borrow rate (otherwise min_default_borrow_rate)
@param max_borrow_rate Custom maximum borrow rate (otherwise max_default_borrow_rate)
"""
# Find coins in the pool
borrowed_ix: uint256 = 100
collateral_ix: uint256 = 100
N: uint256 = 0
for i in range(10):
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
pool,
_abi_encode(i, method_id=method_id("coins(uint256)")),
max_outsize=32, is_static_call=True, revert_on_failure=False)
coin: address = convert(res, address)
if not success or coin == empty(address):
break
N += 1
if coin == borrowed_token:
borrowed_ix = i
elif coin == collateral_token:
collateral_ix = i
if collateral_ix == 100 or borrowed_ix == 100:
raise "Tokens not in pool"
price_oracle: address = create_from_blueprint(
self.pool_price_oracle_impl, pool, N, borrowed_ix, collateral_ix, code_offset=3)
return self._create(borrowed_token, collateral_token, A, fee, loan_discount, liquidation_discount,
price_oracle, name, min_borrow_rate, max_borrow_rate)
@internal
def _create(
borrowed_token: address,
collateral_token: address,
A: uint256,
fee: uint256,
loan_discount: uint256,
liquidation_discount: uint256,
price_oracle: address,
name: String[64],
min_borrow_rate: uint256,
max_borrow_rate: uint256
) -> Vault:
"""
@notice Internal method for creation of the vault
"""
assert borrowed_token != collateral_token, "Same token"
assert borrowed_token == STABLECOIN or collateral_token == STABLECOIN
vault: Vault = Vault(create_minimal_proxy_to(self.vault_impl))
min_rate: uint256 = self.min_default_borrow_rate
max_rate: uint256 = self.max_default_borrow_rate
if min_borrow_rate > 0:
min_rate = min_borrow_rate
if max_borrow_rate > 0:
max_rate = max_borrow_rate
assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates"
monetary_policy: address = create_from_blueprint(
self.monetary_policy_impl, borrowed_token, min_rate, max_rate, code_offset=3)
controller: address = empty(address)
amm: address = empty(address)
controller, amm = vault.initialize(
self.amm_impl, self.controller_impl,
borrowed_token, collateral_token,
A, fee,
price_oracle,
monetary_policy,
loan_discount, liquidation_discount
)
market_count: uint256 = self.market_count
log NewVault(market_count, collateral_token, borrowed_token, vault.address, controller, amm, price_oracle, monetary_policy)
self.vaults[market_count] = vault
self.amms[market_count] = AMM(amm)
self._vaults_index[vault] = market_count + 2**128
self.names[market_count] = name
self.market_count = market_count + 1
token: address = borrowed_token
if borrowed_token == STABLECOIN:
token = collateral_token
market_count = self.token_market_count[token]
self.token_to_vaults[token][market_count] = vault
self.token_market_count[token] = market_count + 1
ERC20(borrowed_token).approve(amm, max_value(uint256))
ERC20(collateral_token).approve(amm, max_value(uint256))
return vault
```
```shell
>>> OneWayLendingVaultFactory.create_from_pool(
"0xf939e0a03fb07f59a73314e73794be0e57ac1b4e", # borrowed_token
"0x8f22779662ad253844013d8e99eccb4d80e31417", # collateral_token
50, # A
6000000000000000, # fee
140000000000000016, # loan_discount
110000000000000000, # liquidation_discount
"0x9fee65d5a627e73212989c8bbedc5fa5cae3821f", # pool to use oracle from
"bobrCRV-long", # name
0, # min_borrow_rate
1) # max_borrow_rate
'0xE16D806c4198955534d4EB10E4861Ea94557602E' # returns address of the created vault
```
::::
## Deploying Gauges
Just like pools, vaults can have liquidity gauges. Once they are added to the `GaugeController` by the DAO, they are eligible to receive CRV emissions.
### `deploy_gauge`
::::description[`OneWayLendingVaultFactory.deploy_gauge(_vault: Vault) -> address`]
Function to deploy a liquidity gauge for a vault.
| Input | Type | Description |
|----------|-----------|---------------------------------------|
| `_vault` | `address` | Vault address to deploy the gauge for.|
Returns: gauge (`address`).
Emits: `LiquidityGaugeDeployed` event.
```vyper
event LiquidityGaugeDeployed:
vault: address
gauge: address
@external
def deploy_gauge(_vault: Vault) -> address:
"""
@notice Deploy a liquidity gauge for a vault
@param _vault Vault address to deploy a gauge for
@return Address of the deployed gauge
"""
ix: uint256 = self._vaults_index[_vault]
assert ix != 0, "Unknown vault"
ix -= 2**128
assert self.gauges[ix] == empty(address), "Gauge already deployed"
implementation: address = self.gauge_impl
assert implementation != empty(address), "Gauge implementation not set"
gauge: address = create_from_blueprint(implementation, _vault, code_offset=3)
self.gauges[ix] = gauge
log LiquidityGaugeDeployed(_vault.address, gauge)
return gauge
```
```shell
In [1]: OneWayLendingVaultFactory.deploy_gauge("0xE16D806c4198955534d4EB10E4861Ea94557602E")
Out [1]: '0xACEBA186aDF691245dfb20365B48DB87DEA7b98F' # returns address of deployed gauge
```
::::
---
## Rates
The Factory has a `MIN_RATE` and `MAX_RATE`. These variables are constants and can not be changed. The minimum rate is 0.1%, the maximum rate is 1000%.
Additionally, the Factory has two variables, `min_default_borrow_rate` and `max_default_borrow_rate`, which are used as default values when creating new lending markets.
If no value is given when deploying a new market, the default rates are applied. Default rates can be changed by the `admin` via the `set_default_rates` method.
**Rates are denominated in seconds and have a base unit of 1e18**.
*To get the annualized value, do:*
$$\text{Annualized Rate} = \text{rate} \times 86400 \times 365$$
:::colab[Google Colab Notebook]
A notebook on how to change default borrow rates and how to calculate annualized rates can be found here: [https://colab.research.google.com/drive/1mQV5yDyBqZrVSIOweP2g1Qu3WWjsgZtv?usp=sharing](https://colab.research.google.com/drive/1mQV5yDyBqZrVSIOweP2g1Qu3WWjsgZtv?usp=sharing).
:::
### `MIN_RATE`
::::description[`OneWayLendingVaultFactory.MIN_RATE() -> uint256: view`]
Getter for the minimum rate a one-way lending vault can have. This variable is a constant and can therefore not be changed.
Returns: minimum rate (`uint256`).
```vyper
MIN_RATE: public(constant(uint256)) = 10**15 / (365 * 86400) # 0.1%
```
```shell
In [1]: OneWayLendingVaultFactory.MIN_RATE()
Out [1]: 31709791 # 0.1%
```
::::
### `MAX_RATE`
::::description[`OneWayLendingVaultFactory.MAX_RATE() -> uint256: view`]
Getter for the maximum rate a one-way lending vault can have. This variable is a constant and can therefore not be changed.
Returns: maximum rate (`uint256`).
```vyper
MAX_RATE: public(constant(uint256)) = 10**19 / (365 * 86400) # 1000%
```
```shell
In [1]: OneWayLendingVaultFactory.MAX_RATE()
Out [1]: 317097919837 # 1000%
```
::::
### `min_default_borrow_rate`
::::description[`OneWayLendingVaultFactory.min_default_borrow_rate() -> uint256: view`]
Getter for the minimum default borrow rate which is used when creating a new vault. The minimum borrow rate is charged when the utilization is 0. This parameter can be changed via the `set_default_rates` function.
Returns: minimum default borrow rate (`uint256`).
```vyper
min_default_borrow_rate: public(uint256)
@external
@nonreentrant('lock')
def set_default_rates(min_rate: uint256, max_rate: uint256):
"""
@notice Change min and max default borrow rates for creating new markets
@param min_rate Minimal borrow rate (0 utilization)
@param max_rate Maxumum borrow rate (100% utilization)
"""
assert msg.sender == self.admin
assert min_rate >= MIN_RATE
assert max_rate <= MAX_RATE
assert max_rate >= min_rate
self.min_default_borrow_rate = min_rate
self.max_default_borrow_rate = max_rate
log SetDefaultRates(min_rate, max_rate)
```
```shell
In [1]: OneWayLendingVaultFactory.min_default_borrow_rate()
Out [1]: 158548959 # 0.5%
```
::::
### `max_default_borrow_rate`
::::description[`OneWayLendingVaultFactory.max_default_borrow_rate() -> uint256: view`]
Getter for the maximum default borrow rate which is used when creating a new vault. The maximum borrow rate is charged when the utilization is 100%. This parameter can be changed via the `set_default_rates` function.
Returns: maximum default borrow rate (`uint256`).
```vyper
max_default_borrow_rate: public(uint256)
@external
@nonreentrant('lock')
def set_default_rates(min_rate: uint256, max_rate: uint256):
"""
@notice Change min and max default borrow rates for creating new markets
@param min_rate Minimal borrow rate (0 utilization)
@param max_rate Maxumum borrow rate (100% utilization)
"""
assert msg.sender == self.admin
assert min_rate >= MIN_RATE
assert max_rate <= MAX_RATE
assert max_rate >= min_rate
self.min_default_borrow_rate = min_rate
self.max_default_borrow_rate = max_rate
log SetDefaultRates(min_rate, max_rate)
```
```shell
In [1]: OneWayLendingVaultFactory.max_default_borrow_rate()
Out [1]: 15854895991 # 50%
```
::::
### `set_default_rates`
::::description[`OneWayLendingVaultFactory.set_default_rates(min_rate: uint256, max_rate: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set new values for the maximum (`max_default_borrow_rate`) and minimum (`min_default_borrow_rate`) default borrow rates.
| Input | Type | Description |
| ---------- | ---------- | -------------------------------- |
| `min_rate` | `uint256` | New minimum default borrow rate. |
| `max_rate` | `uint256` | New maximum default borrow rate. |
Emits: `SetDefaultRates` event.
```vyper
event SetDefaultRates:
min_rate: uint256
max_rate: uint256
min_default_borrow_rate: public(uint256)
max_default_borrow_rate: public(uint256)
@external
@nonreentrant('lock')
def set_default_rates(min_rate: uint256, max_rate: uint256):
"""
@notice Change min and max default borrow rates for creating new markets
@param min_rate Minimal borrow rate (0 utilization)
@param max_rate Maxumum borrow rate (100% utilization)
"""
assert msg.sender == self.admin
assert min_rate >= MIN_RATE
assert max_rate <= MAX_RATE
assert max_rate >= min_rate
self.min_default_borrow_rate = min_rate
self.max_default_borrow_rate = max_rate
log SetDefaultRates(min_rate, max_rate)
```
```shell
In [1]: OneWayLendingVaultFactory.min_default_borrow_rate()
Out [1]: 158548959
In [2]: OneWayLendingVaultFactory.max_default_borrow_rate()
Out [2]: 15854895991
In [3]: OneWayLendingVaultFactory.set_default_rates(168548959, 16854895991)
In [4]: OneWayLendingVaultFactory.min_default_borrow_rate()
Out [4]: 168548959
In [5]: OneWayLendingVaultFactory.max_default_borrow_rate()
Out [5]: 16854895991
```
::::
---
## Implementations
The implementations of the Factory can be upgraded by the `admin`, which is the Curve DAO.
:::colab[Google Colab Notebook]
A notebook on how to change implementations using the `set_implementations` function can be found here: [https://colab.research.google.com/drive/1r3Vhb28Wy8iX_YRBNpfnwjzS4dKuMADf?usp=sharing](https://colab.research.google.com/drive/1r3Vhb28Wy8iX_YRBNpfnwjzS4dKuMADf?usp=sharing)
:::
### `controller_impl`
::::description[`OneWayLendingVaultFactory.controller_impl() -> address: view`]
Getter for the controller implementation.
Returns: controller implementation (`address`).
```vyper
controller_impl: public(address)
```
```shell
In [1]: OneWayLendingVaultFactory.controller_impl()
Out [1]: '0x5473B1BcBbC45d38d8fBb50a18a73aFb8B0637A7'
```
::::
### `amm_impl`
::::description[`OneWayLendingVaultFactory.amm_impl() -> address: view`]
Getter for the amm implementation.
Returns: amm implementation (`address`).
```vyper
amm_impl: public(address)
```
```shell
In [1]: OneWayLendingVaultFactory.amm_impl()
Out [1]: '0x4f37395BdFbE3A0dca124ad3C9DbFe6A6cbc31D6'
```
::::
### `vault_imp`
::::description[`OneWayLendingVaultFactory.vault_imp() -> address: view`]
Getter for the vault implementation.
Returns: vault implementation (`address`).
```vyper
vault_impl: public(address)
```
```shell
In [1]: OneWayLendingVaultFactory.vault_imp()
Out [1]: '0x596F8E49acE6fC8e09B561972360DC216f1c2A1f'
```
::::
### `pool_price_oracle_impl`
::::description[`OneWayLendingVaultFactory.pool_price_oracle_impl() -> address: view`]
Getter for the price oracle implementation when creating lending markets from pools.
Returns: pool price oracle implementation (`address`).
```vyper
pool_price_oracle_impl: public(address)
```
```shell
In [1]: OneWayLendingVaultFactory.pool_price_oracle_impl()
Out [1]: '0x9164e210d123e6566DaF113136a73684C4AB01e2'
```
::::
### `monetary_policy_impl`
::::description[`OneWayLendingVaultFactory.monetary_policy_impl() -> address: view`]
Getter for the monetary policy implementation.
Returns: monetary policy implementation (`address`).
```vyper
monetary_policy_impl: public(address)
```
```shell
In [1]: OneWayLendingVaultFactory.monetary_policy_impl()
Out [1]: '0xa7E98815c0193E01165720C3abea43B885ae67FD'
```
::::
### `gauge_impl`
::::description[`OneWayLendingVaultFactory.gauge_impl() -> address: view`]
Getter for the gauge implementation.
Returns: gauge implementation (`address`).
```vyper
gauge_impl: public(address)
```
```shell
In [1]: OneWayLendingVaultFactory.gauge_impl()
Out [1]: '0x00B71A425Db7C8B65a46CF39c23A188e10A2DE99'
```
::::
### `set_implementations`
::::description[`OneWayLendingVaultFactory.set_implementations(controller: address, amm: address, vault: address, pool_price_oracle: address, monetary_policy: address, gauge: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to set new implementations. If a certain implementation should not be changed, `ZERO_ADDRESS` can be used as a placeholder.
Emits: `SetImplementations` event.
| Input | Type | Description |
| ------------------- | --------- | ----------- |
| `controller` | `address` | New controller implementation. |
| `amm` | `address` | New amm implementation. |
| `vault` | `address` | New vault implementation. |
| `pool_price_oracle` | `address` | New pool price oracle implementation. |
| `monetary_policy` | `address` | New monetary policy implementation. |
| `gauge` | `address` | New gauge implementation. |
```vyper
event SetImplementations:
amm: address
controller: address
vault: address
price_oracle: address
monetary_policy: address
gauge: address
# Implementations which can be changed by governance
amm_impl: public(address)
controller_impl: public(address)
vault_impl: public(address)
pool_price_oracle_impl: public(address)
monetary_policy_impl: public(address)
gauge_impl: public(address)
@external
@nonreentrant('lock')
def set_implementations(controller: address, amm: address, vault: address,
pool_price_oracle: address, monetary_policy: address, gauge: address):
"""
@notice Set new implementations (blueprints) for controller, amm, vault, pool price oracle and monetary polcy.
Doesn't change existing ones
@param controller Address of the controller blueprint
@param amm Address of the AMM blueprint
@param vault Address of the Vault template
@param pool_price_oracle Address of the pool price oracle blueprint
@param monetary_policy Address of the monetary policy blueprint
@param gauge Address for gauge implementation blueprint
"""
assert msg.sender == self.admin
if controller != empty(address):
self.controller_impl = controller
if amm != empty(address):
self.amm_impl = amm
if vault != empty(address):
self.vault_impl = vault
if pool_price_oracle != empty(address):
self.pool_price_oracle_impl = pool_price_oracle
if monetary_policy != empty(address):
self.monetary_policy_impl = monetary_policy
if gauge != empty(address):
self.gauge_impl = gauge
log SetImplementations(amm, controller, vault, pool_price_oracle, monetary_policy, gauge)
```
```shell
>>> soon
```
::::
---
## Contract Ownership
The Factory contract is owned by the DAO ([CurveOwnershipAdmin](https://etherscan.io/address/0x40907540d8a6C65c637785e8f8B742ae6b0b9968)). Ownership can be transferred using the `set_admin` function.
### `admin`
::::description[`OneWayLendingVaultFactory.admin -> address: view`]
Getter for the admin of the Factory.
Returns: admin (`address`).
```vyper
admin: public(address)
```
```shell
In [1]: OneWayLendingVaultFactory.admin()
Out [1]: '0x40907540d8a6C65c637785e8f8B742ae6b0b9968'
```
::::
### `set_admin`
::::description[`OneWayLendingVaultFactory.set_admin(admin: address)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to change the contract ownership by setting a new admin.
| Input | Type | Description |
| ---------- | ------ | ----------- |
| `admin` | `address` | New admin address. |
Emits: `SetAdmin` event.
```vyper
event SetAdmin:
admin: address
admin: public(address)
@external
@nonreentrant('lock')
def set_admin(admin: address):
"""
@notice Set admin of the factory (should end up with DAO)
@param admin Address of the admin
"""
assert msg.sender == self.admin
self.admin = admin
log SetAdmin(admin)
```
```shell
In [1]: OneWayLendingVaultFactory.admin()
Out [1]: '0x40907540d8a6C65c637785e8f8B742ae6b0b9968'
In [2]: OneWayLendingVaultFactory.set_admin("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
In [3]: OneWayLendingVaultFactory.admin()
Out [3]: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
```
::::
---
## Contract Info Methods
Most information is queried based on vault indices. The first deployed vault is vault index 0, second one index 1, etc.
*To get the index of a certain vault:*
```shell
>>> OneWayLendingVaultFactory.vaults_index("0x67A18c18709C09D48000B321c6E1cb09F7181211")
1
```
### `vaults_index`
::::description[`OneWayLendingVaultFactory.vaults_index(vault: Vault) -> uint256: view`]
Getter for the vault index within the factory by using the vault address.
Returns: vault index (`uint256`).
| Input | Type | Description |
| ---------- | ------ | ----------- |
| `vault` | `address` | Vault address to get the index for. |
```vyper
_vaults_index: HashMap[Vault, uint256]
@view
@external
def vaults_index(vault: Vault) -> uint256:
return self._vaults_index[vault] - 2**128
```
```shell
In [1]: OneWayLendingVaultFactory.vaults_index('0x67A18c18709C09D48000B321c6E1cb09F7181211')
Out [1]: 1
```
::::
### `vaults`
::::description[`OneWayLendingVaultFactory.vaults(arg0: uint256) -> address: view`]
Getter for the vault at index `arg0`.
Returns: vault (`address`).
| Input | Type | Description |
|-------|----------|----------------|
| `n` | `uint256` | Vault index. |
```vyper
vaults: public(Vault[10**18])
```
```shell
In [1]: OneWayLendingVaultFactory.vaults(0)
Out [1]: '0xE21C518a09b26Bf65B16767B97249385f12780d9'
In [2]: OneWayLendingVaultFactory.vaults(0)
Out [2]: '0x67A18c18709C09D48000B321c6E1cb09F7181211'
```
::::
### `controllers`
::::description[`OneWayLendingVaultFactory.controllers(n: uint256) -> address: view`]
Getter for the controller of the vault at index `n`. This variable holds all controllers of vaults deployed through this factory.
Returns: controller (`address`).
| Input | Type | Description |
|-------|----------|---------------|
| `n` | `uint256` | Vault index. |
```vyper
interface Vault:
def amm() -> address: view
vaults: public(Vault[10**18])
@view
@external
def controllers(n: uint256) -> address:
return self.vaults[n].controller()
```
```shell
In [1]: OneWayLendingVaultFactory.controllers(0)
Out [1]: '0x5E657c5227A596a860621C5551c9735d8f4A8BE3'
In [2]: OneWayLendingVaultFactory.controllers(1)
Out [2]: '0x7443944962D04720f8c220C0D25f56F869d6EfD4'
```
::::
### `amms`
::::description[`OneWayLendingVaultFactory.amms(n: uint256) -> address: view`]
Getter for the AMM of the vault at index `n`. This variable holds all AMMs of vaults deployed through this factory.
Returns: AMM (`address`).
| Input | Type | Description |
|-------|----------|---------------|
| `n` | `uint256` | Vault index. |
```vyper
amms: public(AMM[10**18])
```
```shell
In [1]: OneWayLendingVaultFactory.amms(0)
Out [1]: '0x0167B8a9A3959E698A3e3BCaFe829878FfB709e3'
In [2]: OneWayLendingVaultFactory.amms(1)
Out [2]: '0xafC1ab86045Cb2a07C23399dbE64b56D1B8B3239'
```
::::
### `borrowed_tokens`
::::description[`OneWayLendingVaultFactory.borrowed_tokens(n: uint256) -> address: view`]
Getter for the borrow token for the vault at index `n`. This variable holds all borrowable tokens of vaults deployed through this factory.
Returns: borrowable token (`address`).
| Input | Type | Description |
|-------|----------|---------------|
| `n` | `uint256` | Vault index. |
```vyper
interface Vault:
def borrowed_token() -> address: view
vaults: public(Vault[10**18])
@view
@external
def borrowed_tokens(n: uint256) -> address:
return self.vaults[n].borrowed_token()
```
```shell
In [1]: OneWayLendingVaultFactory.borrowed_tokens(0)
Out [1]: '0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'
In [2]: OneWayLendingVaultFactory.borrowed_tokens(2)
Out [2]: '0xD533a949740bb3306d119CC777fa900bA034cd52'
```
::::
### `collateral_tokens`
::::description[`OneWayLendingVaultFactory.collateral_tokens(n: uint256) -> address: view`]
Getter for the collateral token for the vault at index `n`. This variable holds all collateral tokens of vaults deployed through this factory.
Returns: collateral token (`address`).
| Input | Type | Description |
|-------|----------|---------------|
| `n` | `uint256` | Vault index. |
```vyper
interface Vault:
def collateral_token() -> address: view
vaults: public(Vault[10**18])
@view
@external
def collateral_tokens(n: uint256) -> address:
return self.vaults[n].collateral_token()
```
```shell
In [1]: OneWayLendingVaultFactory.collateral_tokens(0)
Out [1]: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0'
In [2]: OneWayLendingVaultFactory.collateral_tokens(1)
Out [2]: '0xD533a949740bb3306d119CC777fa900bA034cd52'
```
::::
### `price_oracles`
::::description[`OneWayLendingVaultFactory.price_oracles(n: uint256) -> address: view`]
Getter for the price oracle contracts for the vault at index `n`. This variable holds all price oracles of vaults deployed through this factory.
Returns: price oracle (`address`).
| Input | Type | Description |
|-------|----------|---------------|
| `n` | `uint256` | Vault index. |
```vyper
interface Vault:
def price_oracle() -> address: view
vaults: public(Vault[10**18])
@view
@external
def price_oracles(n: uint256) -> address:
return self.vaults[n].price_oracle()
```
```shell
In [1]: OneWayLendingVaultFactory.price_oracles(0)
Out [1]: '0xDf1B41413EafcCfC6E98BB905feaeB271d307aF3'
In [2]: OneWayLendingVaultFactory.price_oracles(1)
Out [2]: '0xc17B0451E6d8C0f71297d0f174590632BE81163c'
```
::::
### `monetary_policies`
::::description[`OneWayLendingVaultFactory.monetary_policies(n: uint256) -> address: view`]
Getter for the monetary policy contracts for the vault at index `n`. This variable holds all monetary policies of vaults deployed through this factory.
Returns: monetary policy (`address`).
| Input | Type | Description |
|-------|----------|---------------|
| `n` | `uint256` | Vault index. |
```vyper
interface Vault:
def controller() -> address: view
interface Controller:
def monetary_policy() -> address: view
vaults: public(Vault[10**18])
@view
@external
def monetary_policies(n: uint256) -> address:
return Controller(self.vaults[n].controller()).monetary_policy()
```
```shell
In [1]: OneWayLendingVaultFactory.monetary_policies(0)
Out [1]: '0xfd8eF79883815D6771FC986D43E3Dce60ea33726'
In [2]: OneWayLendingVaultFactory.monetary_policies(1)
Out [2]: '0x5c79C4cFE9D77B3d2385E119fADb4F8ff8c08294'
```
::::
### `gauge_for_vault`
::::description[`OneWayLendingVaultFactory.gauge_for_vault(_vault: Vault) -> address: view`]
Getter for the liquidity gauge of `vault`.
Returns: gauge (`address`).
| Input | Type | Description |
|----------|-----------|---------------------------------------|
| `_vault` | `address` | Vault address to get the gauge for. |
```vyper
_vaults_index: HashMap[Vault, uint256]
gauges: public(address[10**18])
@view
@external
def gauge_for_vault(_vault: Vault) -> address:
return self.gauges[self._vaults_index[_vault] - 2**128]
```
```shell
In [1]: OneWayLendingVaultFactory.gauge_for_vault("0x67A18c18709C09D48000B321c6E1cb09F7181211")
Out [1]: '0xAA90BE8bd52aeA49314dFc6e385e21A4e9c4ea0c'
```
::::
### `coins`
::::description[`OneWayLendingVaultFactory.coins(vault_id: uint256) -> address[2]: view`]
Getter for the borrow and collateral token of `vault_id`.
Returns: borrow and collateral token (`address[2]`).
| Input | Type | Description |
|-------|----------|---------------|
| `vault_id` | `uint256` | Vault index. |
```vyper
interface Vault:
def borrowed_token() -> address: view
def collateral_token() -> address: view
vaults: public(Vault[10**18])
@external
@view
def coins(vault_id: uint256) -> address[2]:
vault: Vault = self.vaults[vault_id]
return [vault.borrowed_token(), vault.collateral_token()]
```
```shell
In [1]: OneWayLendingVaultFactory.coins(1)
Out [1]: [Address('0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'),
Address('0xD533a949740bb3306d119CC777fa900bA034cd52')]
```
::::
### `STABLECOIN`
::::description[`OneWayLendingVaultFactory.STABLECOIN() -> address: view`]
Getter for the crvUSD token. Only crvUSD-containing lending vaults are possible.
Returns: crvUSD (`address`).
```vyper
STABLECOIN: public(immutable(address))
@external
def __init__(
stablecoin: address,
amm: address,
controller: address,
vault: address,
pool_price_oracle: address,
monetary_policy: address,
gauge: address,
admin: address):
"""
@notice Factory which creates one-way lending vaults (e.g. collateral is non-borrowable)
@param stablecoin Address of crvUSD. Only crvUSD-containing markets are allowed
@param amm Address of AMM implementation
@param controller Address of Controller implementation
@param pool_price_oracle Address of implementation for price oracle factory (prices from pools)
@param monetary_policy Address for implementation of monetary policy
@param gauge Address for gauge implementation
@param admin Admin address (DAO)
"""
STABLECOIN = stablecoin
...
```
```shell
In [1]: OneWayLendingVaultFactory.STABLECOIN()
Out [1]: '0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'
```
::::
### `market_count`
::::description[`OneWayLendingVaultFactory.():`]
Getter for the total market count. This value represents the total number of lending vaults created through this factory. This value is incremented by 1 whenever the internal `_create` function is called.
Returns: market count (`uint256`).
```vyper
market_count: public(uint256)
@internal
def _create(
borrowed_token: address,
collateral_token: address,
A: uint256,
fee: uint256,
loan_discount: uint256,
liquidation_discount: uint256,
price_oracle: address,
name: String[64],
min_borrow_rate: uint256,
max_borrow_rate: uint256
) -> Vault:
"""
@notice Internal method for creation of the vault
"""
...
market_count: uint256 = self.market_count
self.market_count = market_count + 1
...
```
```shell
In [1]: OneWayLendingVaultFactory.market_count()
Out [1]: 3
```
::::
### `token_to_vaults`
::::description[`OneWayLendingVaultFactory.token_to_vaults(arg0: address, arg1: uint256) -> address: view`]
Getter for the vault at index `arg1` which includes coin `arg0`.
Returns: vault (`address`).
| Input | Type | Description |
|-------|----------|---------------|
| `arg0`| `address` | Token address.|
| `arg1`| `uint256` | Vault index. |
```vyper
token_to_vaults: public(HashMap[address, Vault[10**18]])
```
```shell
In [1]: OneWayLendingVaultFactory.token_to_vaults("0xD533a949740bb3306d119CC777fa900bA034cd52", 0)
Out [1]: '0x67A18c18709C09D48000B321c6E1cb09F7181211'
In [2]: OneWayLendingVaultFactory.token_to_vaults("0xD533a949740bb3306d119CC777fa900bA034cd52", 1)
Out [2]: '0x044aC5160e5A04E09EBAE06D786fc151F2BA5ceD'
```
::::
### `token_market_count`
::::description[`OneWayLendingVaultFactory.token_market_count(arg0: address) -> uint256: view`]
Getter for the amount of markets coin `arg0` is in.
Returns: number of markets (`uint256`).
| Input | Type | Description |
|-------|----------|---------------|
| `arg0`| `address` | Token address.|
```vyper
token_market_count: public(HashMap[address, uint256])
@internal
def _create(
borrowed_token: address,
collateral_token: address,
A: uint256,
fee: uint256,
loan_discount: uint256,
liquidation_discount: uint256,
price_oracle: address,
name: String[64],
min_borrow_rate: uint256,
max_borrow_rate: uint256
) -> Vault:
"""
@notice Internal method for creation of the vault
"""
...
token: address = borrowed_token
if borrowed_token == STABLECOIN:
token = collateral_token
market_count = self.token_market_count[token]
self.token_to_vaults[token][market_count] = vault
self.token_market_count[token] = market_count + 1
...
```
```shell
In [1]: OneWayLendingVaultFactory.token_market_count("0xD533a949740bb3306d119CC777fa900bA034cd52")
Out [1]: 2
In [2]: OneWayLendingVaultFactory.token_market_count("0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E")
Out [2]: 0 # market count of crvusd will always return 0, because the token is included in every vault (market)
```
::::
### `gauges`
::::description[`OneWayLendingVaultFactory.gauges(arg0: uint256) -> address: view`]
Getter for the gauge of the vault at index `arg0`.
Returns: gauge (`address`).
| Input | Type | Description |
|-------|----------|--------------|
| `arg0`| `uint256` | Vault index. |
```vyper
gauges: public(address[10**18])
```
```shell
In [1]: OneWayLendingVaultFactory.gauges(0)
Out [1]: '0x3742aCa9ad8655d2d3eab5569eF1BdB4C5d52e5D'
```
::::
### `names`
::::description[`OneWayLendingVaultFactory.names(arg0: uint256) -> String[64]: view`]
Getter for the name of the vault at index `arg0`.
Returns: name (`String[64]`).
| Input | Type | Description |
|-------|----------|--------------|
| `arg0`| `uint256` | Vault index. |
```vyper
names: public(HashMap[uint256, String[64]])
```
```shell
In [1]: OneWayLendingVaultFactory.names(0)
Out [1]: 'wstETH-long'
In [2]: OneWayLendingVaultFactory.names(1)
Out [2]: 'CRV-long'
```
::::
---
## Lending Oracles: Overview
*There are two main contracts for lending oracles:*
EMA oracle for collateral tokens using **a single Curve pool to fetch the price oracle from**. The `OneWayLendingFactory` can automatically deploy this kind of oracle when deploying a new market.
EMA oracle for collateral tokens using **multiple different Curve pool oracles chained together**. This oracle contract can also make use of `stored_rates` from `stableswap-ng` pools.
EMA oracle for collateral tokens using **a single Curve pool to fetch the price oracle, which is then adjusted by the redemption rate of a vault**.
---
## Oracle Examples
This sections aims to give examples on the various oracle contract combinations, focusing on [`CryptoFromPool.vy`](../contracts/crypto-from-pool.md) and [`CryptoFromPoolsRate.vy`](../contracts/crypto-from-pools-rate.md).
## Single Curve-Pool Oracle (e.g. CRV)
The [oracle contract](https://etherscan.io/address/0xE0a4C53408f5ACf3246c83b9b8bD8d36D5ee38B8) for the CRV market fetches the price oracle from a single Curve pool, the [triCRV pool](https://etherscan.io/address/0x4eBdF703948ddCEA3B11f675B4D1Fba9d2414A14) consisting of crvUSD, wETH and wBTC. This oracle can even be deployed automatically using the [`create_from_pool`](../contracts/oneway-factory.md#create_from_pool) method on the [`OneWayLendingFactory`](../contracts/oneway-factory.md).
*The `CryptoFromPool.vy` contract is specifically designed for these types of oracles. Full documentation is available [here](../contracts/crypto-from-pool.md).*
## Chained Oracles without Rates (FXN)
This [oracle contract](https://arbiscan.io/address/0xbB82bf9a0C6739c0bacFdFFbcE3D2Ec4AA97970E) utilizes two Curve pool oracles to derive the price of the [FXN token](https://arbiscan.io/address/0x179F38f78346F5942E95C5C59CB1da7F55Cf7CAd) relative to the crvUSD token. Importantly, this oracle does not apply any conversion rates; it strictly uses the raw prices provided by the oracles.
*The `CryptoFromPoolsRate.vy` contract is specifically designed for these types of oracles. Full documentation is available [here](../contracts/crypto-from-pools-rate.md).*
*To obtain the FXN token price, we use the following two Curve pool oracles:*
1. [`FXN/ETH pool`](https://arbiscan.io/address/0x5f0985A8aAd85e82fD592a23Cc0501e4345fb18c), which consists of `ETH` and `FXN`.
2. [`tricrypto-crvUSD pool`](https://arbiscan.io/address/0x82670f35306253222F8a165869B28c64739ac62e), which consists of `crvUSD`, `wBTC`, and `wETH`.
The price oracle from the first pool determines the price of FXN relative to ETH. The oracle from the second pool computes the price of wETH in terms of crvUSD. By combining these two prices, we can calculate the final price of FXN relative to crvUSD.[^1]
[^1]: The `price_oracle` method in each pool always returns prices relative to the token at index 0 within the pool. For example, in the tricrypto-crvUSD pool, crvUSD is at index 0, wBTC at index 1, and wETH at index 2. Thus, `price_oracle(0)` returns the price of wBTC with respect to crvUSD, and `price_oracle(1)` returns the price of wETH with respect to crvUSD. More on oracles can be found [here](../../amm/tricrypto-ng/pools/oracles.md).
*Let's consider some actual values:*
- `price_oracle` of FXN/ETH = 43130436331749331
- `price_oracle` of ETH/crvUSD = 3011786169374663706441
*Calculating the final price:*
$$\text{price} = \frac{\text{FXN/ETH} \times \text{ETH/crvUSD}}{10^{18}}$$
$$\text{price} = \frac{43130436331749331 \times 3011786169374663706441}{10^{18}} = 129899651623057139817$$
*This final value represents the price of FXN in terms of crvUSD by chaining together two oracles. All values are based on a scale of 1e18; hence, 129899651623057139817 would approximate to 129.71 crvUSD per FXN token.*
## Chained Oracles with Rates (pufETH)
The [oracle contract](https://etherscan.io/address/0xb08eB288C57a37bC82238168ad96e15975602cd9) for the pufETH lending market integrates two Curve pool oracles and applies the `stored_rates` from the [pufETH/wstETH pool](https://etherscan.io/address/0xeeda34a377dd0ca676b9511ee1324974fa8d980d) due to the nature of the tokens.
*The `CryptoFromPoolsRate.vy` contract is specifically designed for these types of oracles. Full documentation is available [here](../contracts/crypto-from-pools-rate.md).*
The pufETH/wstETH exchange rate is nearly 1:1. We take this exchange rate and multiply it by the wstETH/crvUSD rate obtained from the tryLSD pool. This calculation provides the price of pufETH in terms of crvUSD. **Note:**This is not the actual price of pufETH due to the operational mechanics of stableswap-ng pools. To ascertain the accurate and final price of pufETH, we must apply the `stored_rates`.
*The final price of pufETH is calculated as follows:*
1. Retrieve the pufETH/wstETH exchange rate (e.g., 0.99, where 1 pufETH is equivalent to 0.99 wstETH).
2. Obtain the wstETH price with respect to crvUSD from the tryLSD pool.
3. Multiply these values to calculate the oracle price of pufETH in terms of crvUSD.
4. To derive the complete price, apply the `stored_rates` from the stableswap pool, as provided by the oracle contract.
:::info[`stored_rates`]
Specific tokens have a rate which is denominated against another asset. For example, wstETH has a rate against stETH as the token can always be redeemed for a certain amount of stETH based on the rate. At origin, wstETH and stETH were 1:1, but as time passed and wstETH earned yield, the underlying amount of stETH increased. So, for example, after 1 year, 1 wstETH would be worth 1.1 stETH. Therefore, the rate would be 1.1 and is stored in the `stored_rates` variable in the stableswap pool.
The same applies to ERC4626 tokens like pufETH with a `convertToAssets` method. This kind of rate is also stored in the `stored_rates` variable.
The stableswap pool uses these rates to ensure accurate calculations when, for example, exchanging tokens or adding liquidity.
:::
---
## Secondary Monetary Policy
The `SecondaryMonetaryPolicy` contract calculates borrow rates based on the utilization in a lending market. It uses parameters derived from the target utilization and ratios at 0% and 100% utilization to define a hyperbolic dependency. The rate is dynamically adjusted based on the current utilization and the rate from the AMM (Automated Market Maker), which mints crvUSD.
This design ensures that when the target utilization is met, the borrow rate in the lending market matches the borrow rate of the minting market. At 0% utilization, the rate is defined as $\alpha \times \text{rate}\_{\text{AMM}}$ and at 100% utilization as $\beta \times \text{rate}\_{\text{AMM}}$.
:::vyper[`SecondaryMonetaryPolicy.vy`]
The source code for the `SecondaryMonetaryPolicy.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/contracts/mpolicies/SecondaryMonetaryPolicy.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
:::
---
## Calculations
:::colab[Google Colab Notebook]
An interactive Google Colab notebook that plots the interest rate depending on utilization can be found here: [https://colab.research.google.com/drive/1lU0SWtvQoJHNe7pLiKD33nYBKacljhck?usp=sharing](https://colab.research.google.com/drive/1lU0SWtvQoJHNe7pLiKD33nYBKacljhck?usp=sharing).
:::
### Borrow Rate
The formula for calculating the borrow rate is as follows:
$$\text{rate} = \text{rate}_{\text{AMM}} \left( r_{\text{minf}} + \frac{A}{u_{\text{inf}} - \text{utilization}} \right) + \text{shift}$$
$\text{shift}$ is an additional value which shifts the entire rate curve up or down by a specified amount.[^1]
[^1]: This kind of rate shift is rarely used but is applied, for example, in the wstETH lending market. The `SecondaryMonetaryPolicy` of that market does not follow the wstETH mint market but follows the wETH mint market instead, with a +4% shift applied to the rate. This is done because the "more fair" interest rate is the wETH rate plus the staking rate (which is approximately 4%).
### Parameters
Depending on **target utilization ($u_0$)**, **rate ratio at 0% utilization ($\alpha$)**, and **rate ratio at 100% utilization ($\beta$)**, the coefficients for the hyperbolic dependency are calculated as follows:
$$u_{\text{inf}} = \frac{(\beta - 1) \times u_0}{(\beta - 1) \times u_0 - (1 - u_0) \times (1 - \alpha)}$$
$$A = (1 - \alpha) \times (u_{\text{inf}} - u_0) \times \frac{u_{\text{inf}}}{u_0}$$
$$r_{\text{minf}} = \alpha - \frac{A}{u_{\text{inf}}}$$
*Where:*
- $u_0 = \text{target utilization}$
- $\alpha = \text{low ratio}$
- $\beta = \text{high ratio}$
Alpha ($\alpha$) and Beta ($\beta$) essentially determine how the borrow rate scales with utilization. For example:
- Alpha ($\alpha$): This is the ratio of the borrowing rate to the AMM rate at 0% utilization. If you set $\alpha$ to 1%, it means that when the utilization is 0%, the borrowing rate will be 1% of the rate provided by the AMM.
- Beta ($\beta$): This is the ratio of the borrowing rate to the AMM rate at 100% utilization. If you set $\beta$ to 50%, it means that when the utilization is 100%, the borrowing rate will be 50% of the rate provided by the AMM.
:::info[Setting Parameters]
`target_utilization`, `low_ratio`, and `high_ratio` are set when deploying the contract. The values can later only be changed by the `admin` of the contract. For more, see here: [`set_parameters`](#set_parameters).
Also, the `A` parameter has nothing to do with the amplification coefficient used in Curve AMMs.
:::
---
## Rates
**The rate values are based on 1e18 and NOT annualized.**
*To calculate the Borrow APR (annualized):*
$$\text{borrowAPR} = \frac{\text{rate} \cdot 365 \cdot 86400}{10^{18}}$$
Rate calculations occur within the MonetaryPolicy contract. The rate is regularly updated by the internal `_save_rate` method in the Controller. This happens whenever a new loan is initiated (`_create_loan`), collateral is either added (`add_collateral`) or removed (`remove_collateral`), additional debt is incurred (`borrow_more` and `borrow_more_extended`), debt is repaid (`repay`, `repay_extended`), or a loan undergoes liquidation (`_liquidate`).
```vyper
@internal
def _save_rate():
"""
@notice Save current rate
"""
rate: uint256 = min(self.monetary_policy.rate_write(), MAX_RATE)
AMM.set_rate(rate)
```
```vyper
struct Parameters:
u_inf: uint256
A: uint256
r_minf: uint256
parameters: public(Parameters)
@external
def rate_write(_for: address = msg.sender) -> uint256:
return self.calculate_rate(_for, 0, 0)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
p: Parameters = self.parameters
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
u: uint256 = 0
if total_reserves > 0:
u = convert(total_debt * 10**18 / total_reserves, uint256)
r0: uint256 = AMM.rate()
return r0 * p.r_minf / 10**18 + p.A * r0 / (p.u_inf - u) + p.shift
```
```vyper
@external
@nonreentrant('lock')
def set_rate(rate: uint256) -> uint256:
"""
@notice Set interest rate. That affects the dependence of AMM base price over time
@param rate New rate in units of int(fraction * 1e18) per second
@return rate_mul multiplier (e.g. 1.0 + integral(rate, dt))
"""
assert msg.sender == self.admin
rate_mul: uint256 = self._rate_mul()
self.rate_mul = rate_mul
self.rate_time = block.timestamp
self.rate = rate
log SetRate(rate, rate_mul, block.timestamp)
return rate_mul
```
### `rate`
::::description[`SecondaryMonetaryPolicy.rate(_for: address = msg.sender) -> uint256: view`]
Getter for the borrow rate for a specific lending market.
Returns: rate (`uint256`).
| Input | Type | Description |
| ------- | --------- | ---------------------------------------------------------------------------------------------------- |
| `_for` | `address` | Contract to calculate the rate for. Defaults to `msg.sender`, as the caller of the function is usually the Controller. |
```vyper
@view
@external
def rate(_for: address = msg.sender) -> uint256:
return self.calculate_rate(_for, 0, 0)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
p: Parameters = self.parameters
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
u: uint256 = 0
if total_reserves > 0:
u = convert(total_debt * 10**18 / total_reserves, uint256)
r0: uint256 = AMM.rate()
return r0 * p.r_minf / 10**18 + p.A * r0 / (p.u_inf - u) + p.shift
```
```shell
>>> soon
```
::::
### `future_rate`
::::description[`SecondaryMonetaryPolicy.future_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256: view`]
Function to calculate the future borrow rate for a lending market given a specific change in reserves and debt.
Returns: future borrow rate (`uint256`).
| Input | Type | Description |
| ------------ | --------- | ------------------------ |
| `_for` | `address` | Controller contract to calculate the future rate for. |
| `d_reserves` | `int256` | Change in reserve assets.|
| `d_debt` | `int256` | Change in debt. |
```vyper
@view
@external
def future_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
return self.calculate_rate(_for, d_reserves, d_debt)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
p: Parameters = self.parameters
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
u: uint256 = 0
if total_reserves > 0:
u = convert(total_debt * 10**18 / total_reserves, uint256)
r0: uint256 = AMM.rate()
return r0 * p.r_minf / 10**18 + p.A * r0 / (p.u_inf - u) + p.shift
```
```shell
>>> soon
```
::::
### `rate_write`
::::description[`SecondaryMonetaryPolicy.rate_write(_for: address = msg.sender) -> uint256`]
Function to calculate the rate of a lending market, similar to the `rate` method. However, the key difference is that this function updates the rate and therefore changes the state of the blockchain. This method is usually called by the Controller.
Returns: updated rate (`uint256`).
| Input | Type | Description |
| ------- | --------- | ---------------------------------------------------------------------------------------------------- |
| `_for` | `address` | Contract to calculate the rate for. Defaults to `msg.sender`, as the caller of the function is usually the Controller. |
```vyper
@external
def rate_write(_for: address = msg.sender) -> uint256:
return self.calculate_rate(_for, 0, 0)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
p: Parameters = self.parameters
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
u: uint256 = 0
if total_reserves > 0:
u = convert(total_debt * 10**18 / total_reserves, uint256)
r0: uint256 = AMM.rate()
return r0 * p.r_minf / 10**18 + p.A * r0 / (p.u_inf - u) + p.shift
```
```shell
>>> soon
```
::::
---
## Parameters
The contract includes a `Parameters` struct that holds values essential for the hyperbolic dependency model used in borrow rate calculations. This struct consists of `u_inf`, `A`, `r_minf`, and `shift`, which are derived from the **target utilization ($u_0$)**, the rate ratio at **0% utilization ($\alpha$)**, and the rate ratio at **maximum utilization ($\beta$)**. These parameters are initially computed using the internal `get_params` function during contract initialization and are recalculated whenever new parameter values are set through the [`set_parameters`](#set_parameters) method. This struct and the associated calculations ensure the borrow rates adjust dynamically based on fund utilization.
```vyper
struct Parameters:
u_inf: uint256
A: uint256
r_minf: uint256
shift: uint256
@internal
def get_params(u_0: uint256, alpha: uint256, beta: uint256, rate_shift: uint256) -> Parameters:
p: Parameters = empty(Parameters)
p.u_inf = (beta - 10**18) * u_0 / (((beta - 10**18) * u_0 - (10**18 - u_0) * (10**18 - alpha)) / 10**18)
p.A = (10**18 - alpha) * p.u_inf / 10**18 * (p.u_inf - u_0) / u_0
p.r_minf = alpha - p.A * 10**18 / p.u_inf
p.shift = rate_shift
return p
```
For parameter calculations see [here](#parameters).
---
### `parameters`
::::description[`SecondaryMonetaryPolicy.parameters() -> tuple: view`]
Getter for the parameters of the monetary policy. These parameters can be changed by the admin of the contract using the `set_parameters` function. This function does NOT return the `target_rate` ($u_0$), `low_ratio` ($\alpha$), or `high_ratio` ($\beta$), but rather the derived parameters based on those values.
Returns: u_inf (`uint256`), A (`uint256`), r_minf (`uint256`) and shift (`uint256`).
```vyper
struct Parameters:
u_inf: uint256
A: uint256
r_minf: uint256
shift: uint256
parameters: public(Parameters)
```
```shell
>>> SecondaryMonetaryPolicy.parameters() # mp for BTC lending market (follows wBTC mint market)
1046153846153846153, 120710059171597632, 384615384615384617, 0
>>> SecondaryMonetaryPolicy.parameters() # mp for wstETH lending market (follows wETH mint market)
1046153846153846153, 120710059171597632, 384615384615384617, 1268391679
```
:::note[Added shift in wstETH Lending Market]
The `SecondaryMonetaryPolicy` for the wstETH market includes a shift of 1268391679, because this policy follows the ETH mint market and adds this additional shift to the interest rate curve as it is more fair to use the ETH rate + staking rate:
$shift = \frac{1268391679 \times 365 \times 86400}{10^{18}} = 0.04$
:::
::::
### `set_parameters`
::::description[`SecondaryMonetaryPolicy.set_parameters(target_utilization: uint256, low_ratio: uint256, high_ratio: uint256, rate_shift: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the contract.
:::
Function to update the rate of a lending market.
| Input | Type | Description |
| -------------------- | --------- | ------------------------------------------------------------------------------------- |
| `target_utilization` | `uint256` | Target ratio of the market utilization. Needs to be between 1% and 99%, usually set to 80%. |
| `low_ratio` | `uint256` | Low ratio. Needs to be higher than 1%. |
| `high_ratio` | `uint256` | High ratio. Needs to be lower than 100%. |
| `rate_shift` | `uint256` | Value by which the rate curve is shifted. |
Emits: `SetParameters` event.
```vyper
event SetParameters:
u_inf: uint256
A: uint256
r_minf: uint256
shift: uint256
struct Parameters:
u_inf: uint256
A: uint256
r_minf: uint256
shift: uint256
MIN_UTIL: constant(uint256) = 10**16
MAX_UTIL: constant(uint256) = 99 * 10**16
MIN_LOW_RATIO: constant(uint256) = 10**16
MAX_HIGH_RATIO: constant(uint256) = 100 * 10**18
MAX_RATE_SHIFT: constant(uint256) = 100 * 10**18
parameters: public(Parameters)
@external
def set_parameters(target_utilization: uint256, low_ratio: uint256, high_ratio: uint256, rate_shift: uint256):
"""
@param target_utilization Utilization at which borrow rate is the same as in AMM
@param low_ratio Ratio rate/target_rate at 0% utilization
@param high_ratio Ratio rate/target_rate at 100% utilization
@param rate_shift Shift all the rate curve by this rate
"""
assert msg.sender == FACTORY.admin()
assert target_utilization >= MIN_UTIL
assert target_utilization <= MAX_UTIL
assert low_ratio >= MIN_LOW_RATIO
assert high_ratio <= MAX_HIGH_RATIO
assert low_ratio < high_ratio
assert rate_shift <= MAX_RATE_SHIFT
p: Parameters = self.get_params(target_utilization, low_ratio, high_ratio, rate_shift)
self.parameters = p
log SetParameters(p.u_inf, p.A, p.r_minf, p.shift)
@internal
def get_params(u_0: uint256, alpha: uint256, beta: uint256, rate_shift: uint256) -> Parameters:
p: Parameters = empty(Parameters)
p.u_inf = (beta - 10**18) * u_0 / (((beta - 10**18) * u_0 - (10**18 - u_0) * (10**18 - alpha)) / 10**18)
p.A = (10**18 - alpha) * p.u_inf / 10**18 * (p.u_inf - u_0) / u_0
p.r_minf = alpha - p.A * 10**18 / p.u_inf
p.shift = rate_shift
return p
```
```shell
>>> soon
```
::::
---
## Contract Info Methods
### `AMM`
::::description[`SecondaryMonetaryPolicy.AMM() -> address: view`]
Getter for the AMM contract (used for minting crvUSD), which is used for rate comparison.
Returns: AMM contract (`address`).
```vyper
AMM: public(immutable(IAMM))
@external
def __init__(factory: Factory, amm: IAMM, borrowed_token: ERC20,
target_utilization: uint256, low_ratio: uint256, high_ratio: uint256, rate_shift: uint256):
"""
@param factory Factory contract
@param amm AMM to take borrow rate from as a basis
@param borrowed_token Borrowed token in the market (e.g. crvUSD)
@param target_utilization Utilization at which borrow rate is the same as in AMM
@param low_ratio Ratio rate/target_rate at 0% utilization
@param high_ratio Ratio rate/target_rate at 100% utilization
@param rate_shift Shift all the rate curve by this rate
"""
assert target_utilization >= MIN_UTIL
assert target_utilization <= MAX_UTIL
assert low_ratio >= MIN_LOW_RATIO
assert high_ratio <= MAX_HIGH_RATIO
assert low_ratio < high_ratio
assert rate_shift <= MAX_RATE_SHIFT
FACTORY = factory
AMM = amm
BORROWED_TOKEN = borrowed_token
p: Parameters = self.get_params(target_utilization, low_ratio, high_ratio, rate_shift)
self.parameters = p
log SetParameters(p.u_inf, p.A, p.r_minf, p.shift)
```
```shell
>>> SecondaryMonetaryPolicy.AMM()
'0xE0438Eb3703bF871E31Ce639bd351109c88666ea'
```
::::
### `BORROWED_TOKEN`
::::description[`SecondaryMonetaryPolicy.BORROWED_TOKEN() -> address: view`]
Getter for the token borrowed from the lending market.
Returns: token contract (`address`).
```vyper
BORROWED_TOKEN: public(immutable(ERC20))
@external
def __init__(factory: Factory, amm: IAMM, borrowed_token: ERC20,
target_utilization: uint256, low_ratio: uint256, high_ratio: uint256, rate_shift: uint256):
"""
@param factory Factory contract
@param amm AMM to take borrow rate from as a basis
@param borrowed_token Borrowed token in the market (e.g. crvUSD)
@param target_utilization Utilization at which borrow rate is the same as in AMM
@param low_ratio Ratio rate/target_rate at 0% utilization
@param high_ratio Ratio rate/target_rate at 100% utilization
@param rate_shift Shift all the rate curve by this rate
"""
assert target_utilization >= MIN_UTIL
assert target_utilization <= MAX_UTIL
assert low_ratio >= MIN_LOW_RATIO
assert high_ratio <= MAX_HIGH_RATIO
assert low_ratio < high_ratio
assert rate_shift <= MAX_RATE_SHIFT
FACTORY = factory
AMM = amm
BORROWED_TOKEN = borrowed_token
p: Parameters = self.get_params(target_utilization, low_ratio, high_ratio, rate_shift)
self.parameters = p
log SetParameters(p.u_inf, p.A, p.r_minf, p.shift)
```
```shell
>>> SecondaryMonetaryPolicy.BORROWED_TOKEN()
'0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'
```
::::
### `FACTORY`
::::description[`SecondaryMonetaryPolicy.FACTORY() -> address: view`]
Getter for the lending factory contract.
Returns: factory contract (`address`).
```vyper
FACTORY: public(immutable(Factory))
@external
def __init__(factory: Factory, amm: IAMM, borrowed_token: ERC20,
target_utilization: uint256, low_ratio: uint256, high_ratio: uint256, rate_shift: uint256):
"""
@param factory Factory contract
@param amm AMM to take borrow rate from as a basis
@param borrowed_token Borrowed token in the market (e.g. crvUSD)
@param target_utilization Utilization at which borrow rate is the same as in AMM
@param low_ratio Ratio rate/target_rate at 0% utilization
@param high_ratio Ratio rate/target_rate at 100% utilization
@param rate_shift Shift all the rate curve by this rate
"""
assert target_utilization >= MIN_UTIL
assert target_utilization <= MAX_UTIL
assert low_ratio >= MIN_LOW_RATIO
assert high_ratio <= MAX_HIGH_RATIO
assert low_ratio < high_ratio
assert rate_shift <= MAX_RATE_SHIFT
FACTORY = factory
AMM = amm
BORROWED_TOKEN = borrowed_token
p: Parameters = self.get_params(target_utilization, low_ratio, high_ratio, rate_shift)
self.parameters = p
log SetParameters(p.u_inf, p.A, p.r_minf, p.shift)
```
```shell
>>> SecondaryMonetaryPolicy.FACTORY()
'0xeA6876DDE9e3467564acBeE1Ed5bac88783205E0'
```
::::
---
## Semi-Log Monetary Policy
The **borrow rate**in the semi-logarithmic MonetaryPolicy contract is **intricately linked to the utilization ratio of the lending markets**. This ratio plays a crucial role in determining the cost of borrowing, with its value ranging between 0 and 1. At a **utilization rate of 0**, indicating no borrowed assets, the borrowing **rate aligns with the minimum threshold, `min_rate`**. Conversely, a **utilization rate of 1**, where all available assets are borrowed, escalates the **borrowing rate to its maximum limit, `max_rate`**.
:::vyper[`SemilogMonetaryPolicy.vy`]
The source code for the `SemilogMonetaryPolicy.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/lending/contracts/mpolicies/SemilogMonetaryPolicy.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
:::
*The function for the rate is as simple as:*
$$\text{rate} = \text{rate}_{\text{min}} \cdot \left(\frac{\text{rate}_{\text{max}}}{\text{rate}_{\text{min}}}\right)^{\text{utilization}}$$
| Variable | Description |
| :------: | ----------- |
| $\text{rate}_{\text{min}}$ | `MonetaryPolicy.min_rate()` |
| $\text{rate}_{\text{max}}$ | `MonetaryPolicy.max_rate()` |
| $\text{utilization}$ | `Utilization of the lending market. What ratio of the provided assets are borrowed?` |
---
*A graph to display the interplay between `min_rate`, `max_rate`, and market utilization:[^1]*
[^1]: For simplicity, the minimum input value is set at 1% and the maximum at 100%. In reality, these values can range from 0.01% to 1000%.
---
## Rates
**The rate values are based on 1e18 and NOT annualized.**
*To calculate the Borrow APR:*
$$\text{borrowAPR} = \frac{\text{rate} \cdot 365 \cdot 86400}{10^{18}}$$
Rate calculations occur within the MonetaryPolicy contract. The rate is regularly updated by the internal `_save_rate` method in the Controller. This happens whenever a new loan is initiated (`_create_loan`), collateral is either added (`add_collateral`) or removed (`remove_collateral`), additional debt is incurred (`borrow_more` and `borrow_more_extended`), debt is repaid (`repay`, `repay_extended`), or a loan undergoes liquidation (`_liquidate`).
```vyper
@internal
def _save_rate():
"""
@notice Save current rate
"""
rate: uint256 = min(self.monetary_policy.rate_write(), MAX_RATE)
AMM.set_rate(rate)
```
```vyper
log_min_rate: public(int256)
log_max_rate: public(int256)
@internal
@external
def rate_write(_for: address = msg.sender) -> uint256:
return self.calculate_rate(_for, 0, 0)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
if total_debt == 0:
return self.min_rate
else:
log_min_rate: int256 = self.log_min_rate
log_max_rate: int256 = self.log_max_rate
return self.exp(total_debt * (log_max_rate - log_min_rate) / total_reserves + log_min_rate)
```
```vyper
@external
@nonreentrant('lock')
def set_rate(rate: uint256) -> uint256:
"""
@notice Set interest rate. That affects the dependence of AMM base price over time
@param rate New rate in units of int(fraction * 1e18) per second
@return rate_mul multiplier (e.g. 1.0 + integral(rate, dt))
"""
assert msg.sender == self.admin
rate_mul: uint256 = self._rate_mul()
self.rate_mul = rate_mul
self.rate_time = block.timestamp
self.rate = rate
log SetRate(rate, rate_mul, block.timestamp)
return rate_mul
```
### `rate`
::::description[`SemiLogMonetaryPolicy.rate(_for: address = msg.sender) -> uint256`]
Getter for the borrow rate for a specific lending market.
Returns: rate (`uint256`).
| Input | Type | Description |
| ---------- | --------- | -------------- |
| `_for` | `address` | Controller contract; Defaults to `msg.sender`, because the caller of the function is usually the Controller. |
```vyper
@view
@external
def rate(_for: address = msg.sender) -> uint256:
return self.calculate_rate(_for, 0, 0)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
if total_debt == 0:
return self.min_rate
else:
log_min_rate: int256 = self.log_min_rate
log_max_rate: int256 = self.log_max_rate
return self.exp(total_debt * (log_max_rate - log_min_rate) / total_reserves + log_min_rate)
```
```shell
In [1]: SemilogMonetaryPolicy.rate("0x7443944962D04720f8c220C0D25f56F869d6EfD4")
Out [1]: 6113754953
```
::::
### `future_rate`
::::description[`SemiLogMonetaryPolicy.future_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256`]
Function to calculate the future borrow rate for a lending market given a specific change of reserves and debt.
Returns: future borrow rate (`uint256`).
| Input | Type | Description |
| ------------ | --------- | -------------- |
| `_for` | `address` | Controller address. |
| `d_reserves` | `int256` | Change of reserve asset. |
| `d_debt` | `int256` | Change of debt. |
```vyper
@view
@external
def future_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
return self.calculate_rate(_for, d_reserves, d_debt)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
if total_debt == 0:
return self.min_rate
else:
log_min_rate: int256 = self.log_min_rate
log_max_rate: int256 = self.log_max_rate
return self.exp(total_debt * (log_max_rate - log_min_rate) / total_reserves + log_min_rate)
```
```shell
In [1]: SemilogMonetaryPolicy.future_rate(controller.address, 0, 10000000000000000000000)
Out [1]: 7882992245
```
::::
### `rate_write`
::::description[`SemiLogMonetaryPolicy.rate_write(_for: address = msg.sender) -> uint256`]
Function to update the rate of a lending market.
Returns: rate (`uint256`).
```vyper
@external
def rate_write(_for: address = msg.sender) -> uint256:
return self.calculate_rate(_for, 0, 0)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
if total_debt == 0:
return self.min_rate
else:
log_min_rate: int256 = self.log_min_rate
log_max_rate: int256 = self.log_max_rate
return self.exp(total_debt * (log_max_rate - log_min_rate) / total_reserves + log_min_rate)
```
```shell
In [1]: SemilogMonetaryPolicy.rate_write()
Out [1]: 6113754953
```
::::
---
## Changing Rates
Rates within the MonetaryPolicy contract can only be **changed by the `admin` of the lending factory**, which is the Curve DAO.
*A short overview of the different parameters:*
| Variable | Description |
| :------------: | ----------------------------------------------------- |
| `min_rate` | Current minimum rate set within the MP contract. |
| `max_rate` | Current maximum rate set within the MP contract. |
| `log_min_rate` | Logarithm ln() function of `min_rate`, based on log2. |
| `log_max_rate` | Logarithm ln() function of `max_rate`, based on log2. |
| `MIN_RATE` | Absolute minimum rate settable. |
| `MAX_RATE` | Absolute maximum rate settable. |
### `set_rates`
::::description[`SemiLogMonetaryPolicy.set_rates(min_rate: uint256, max_rate: uint256)`]
:::guard[Guarded Methods]
This function can only be called by the `admin` of `FACTORY`.
:::
Function to set new values for `min_rate` and `max_rate`, and consequently `log_min_rate` and `log_max_rate` as well. New rate values can be chosen quite deliberately, but need to be **within the bounds of `MIN_RATE` and `MAX_RATE`**:
- `MIN_RATE = 31709791 (0.01%)`
- `MAX_RATE = 317097919837 (1000%)`
| Input | Type | Description |
| ---------- | --------- | -------------- |
| `min_rate` | `uint256` | New value for the minimum rate. |
| `max_rate` | `uint256` | New value for the maximum rate. |
Emits: `SetRates` event.
```vyper
event SetRates:
min_rate: uint256
max_rate: uint256
min_rate: public(uint256)
max_rate: public(uint256)
log_min_rate: public(int256)
log_max_rate: public(int256)
@external
def set_rates(min_rate: uint256, max_rate: uint256):
assert msg.sender == FACTORY.admin()
assert max_rate >= min_rate
assert min_rate >= MIN_RATE
assert max_rate <= MAX_RATE
if min_rate != self.min_rate:
self.log_min_rate = self.ln_int(min_rate)
if max_rate != self.max_rate:
self.log_max_rate = self.ln_int(max_rate)
self.min_rate = min_rate
self.max_rate = max_rate
log SetRates(min_rate, max_rate)
```
```shell
In [1]: SemilogMonetaryPolicy.min_rate()
Out [1]: 158548959
In [2]: SemilogMonetaryPolicy.set_rates(31709791, 317097919837)
In [3]: SemilogMonetaryPolicy.min_rate()
Out [3]: 31709791
```
::::
### `min_rate`
::::description[`SemiLogMonetaryPolicy.min_rate() -> uint256: view`]
Getter for the current minimum borrow rate. This value is set to the input given for `min_default_borrow_rate` when [creating a new market](./oneway-factory.md#creating-lending-markets). The rate is charged when utilization is 0 and can be changed by the admin of the lending factory.
Returns: minimum interest rate (`uint256`).
```vyper
min_rate: public(uint256)
@external
def __init__(borrowed_token: ERC20, min_rate: uint256, max_rate: uint256):
assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates"
BORROWED_TOKEN = borrowed_token
self.min_rate = min_rate
self.max_rate = max_rate
self.log_min_rate = self.ln_int(min_rate)
self.log_max_rate = self.ln_int(max_rate)
FACTORY = Factory(msg.sender)
```
```shell
In [1]: SemilogMonetaryPolicy.min_rate()
Out [1]: 158548959
```
::::
### `max_rate`
::::description[`SemiLogMonetaryPolicy.max_rate() -> uint256: view`]
Getter for the current maximum borrow rate. This value is set to the input given for `max_default_borrow_rate` when [creating a new market](./oneway-factory.md#creating-lending-markets). The rate is charged when utilization is 1 and can be changed by the admin of the lending factory.
Returns: maximum interest rate (`uint256`).
```vyper
max_rate: public(uint256)
@external
def __init__(borrowed_token: ERC20, min_rate: uint256, max_rate: uint256):
assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates"
BORROWED_TOKEN = borrowed_token
self.min_rate = min_rate
self.max_rate = max_rate
self.log_min_rate = self.ln_int(min_rate)
self.log_max_rate = self.ln_int(max_rate)
FACTORY = Factory(msg.sender)
```
```shell
In [1]: SemilogMonetaryPolicy.max_rate()
Out [1]: 15854895991
```
::::
### `log_min_rate`
::::description[`SemiLogMonetaryPolicy.log_min_rate() -> int256: view`]
Getter for the logarithm ln() function of `min_rate`, based on log2.
Returns: semi-log minimum rate (`int256`).
```vyper
log_min_rate: public(int256)
@external
def __init__(borrowed_token: ERC20, min_rate: uint256, max_rate: uint256):
assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates"
BORROWED_TOKEN = borrowed_token
self.min_rate = min_rate
self.max_rate = max_rate
self.log_min_rate = self.ln_int(min_rate)
self.log_max_rate = self.ln_int(max_rate)
FACTORY = Factory(msg.sender)
@internal
@pure
def ln_int(_x: uint256) -> int256:
"""
@notice Logarithm ln() function based on log2. Not very gas-efficient but brief
"""
# adapted from: https://medium.com/coinmonks/9aef8515136e
# and vyper log implementation
# This can be much more optimal but that's not important here
x: uint256 = _x
if _x < 10**18:
x = 10**36 / _x
res: uint256 = 0
for i in range(8):
t: uint256 = 2**(7 - i)
p: uint256 = 2**t
if x >= p * 10**18:
x /= p
res += t * 10**18
d: uint256 = 10**18
for i in range(59): # 18 decimals: math.log2(10**18) == 59.7
if (x >= 2 * 10**18):
res += d
x /= 2
x = x * x / 10**18
d /= 2
# Now res = log2(x)
# ln(x) = log2(x) / log2(e)
result: int256 = convert(res * 10**18 / 1442695040888963328, int256)
if _x >= 10**18:
return result
else:
return -result
```
```shell
In [1]: SemilogMonetaryPolicy.log_min_rate()
Out [1]: -22564957680717876419
```
::::
### `log_max_rate`
::::description[`SemiLogMonetaryPolicy.log_max_rate() -> int256: view`]
Getter for the logarithm ln() function of `max_rate`, based on log2.
Returns: semi-log maximum rate (`int256`).
```vyper
log_max_rate: public(int256)
@external
def __init__(borrowed_token: ERC20, min_rate: uint256, max_rate: uint256):
assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates"
BORROWED_TOKEN = borrowed_token
self.min_rate = min_rate
self.max_rate = max_rate
self.log_min_rate = self.ln_int(min_rate)
self.log_max_rate = self.ln_int(max_rate)
FACTORY = Factory(msg.sender)
@internal
@pure
def ln_int(_x: uint256) -> int256:
"""
@notice Logarithm ln() function based on log2. Not very gas-efficient but brief
"""
# adapted from: https://medium.com/coinmonks/9aef8515136e
# and vyper log implementation
# This can be much more optimal but that's not important here
x: uint256 = _x
if _x < 10**18:
x = 10**36 / _x
res: uint256 = 0
for i in range(8):
t: uint256 = 2**(7 - i)
p: uint256 = 2**t
if x >= p * 10**18:
x /= p
res += t * 10**18
d: uint256 = 10**18
for i in range(59): # 18 decimals: math.log2(10**18) == 59.7
if (x >= 2 * 10**18):
res += d
x /= 2
x = x * x / 10**18
d /= 2
# Now res = log2(x)
# ln(x) = log2(x) / log2(e)
result: int256 = convert(res * 10**18 / 1442695040888963328, int256)
if _x >= 10**18:
return result
else:
return -result
```
```shell
In [1]: SemilogMonetaryPolicy.log_max_rate()
Out [1]: -17959787488990232781
```
::::
### `MIN_RATE`
::::description[`SemiLogMonetaryPolicy.MIN_RATE() -> uint256: view`]
Getter for the lowest possible rate for the MonetaryPolicy. When setting new rates via `set_rates()`, `MIN_RATE` is the lowest possible value. This variable is a constant and therefore cannot be changed.
Returns: absolute minimum rate (`uint256`).
```vyper
MIN_RATE: public(constant(uint256)) = 10**15 / (365 * 86400) # 0.1%
```
```shell
In [1]: SemilogMonetaryPolicy.MIN_RATE()
Out [1]: 31709791
```
::::
### `MAX_RATE`
::::description[`SemiLogMonetaryPolicy.MAX_RATE() -> uint256: view`]
Getter for the highest possible rate for the MonetaryPolicy. When setting new rates via `set_rates()`, `MAX_RATE` is the highest possible value. This variable is a constant and therefore cannot be changed.
Returns: absolute maximum rate (`uint256`).
```vyper
MAX_RATE: public(constant(uint256)) = 10**19 / (365 * 86400) # 1000%
```
```shell
In [1]: SemilogMonetaryPolicy.MAX_RATE()
Out [1]: 317097919837
```
::::
---
## Contract Info Methods
### `BORROWED_TOKEN`
::::description[`SemiLogMonetaryPolicy.BORROWED_TOKEN() -> address: view`]
Getter for the borrowed token. This is a immutable variable and is set at deployment (`__init__()`).
Returns: borrowable token from the lending market (`address`).
```vyper
BORROWED_TOKEN: public(immutable(ERC20))
@external
def __init__(borrowed_token: ERC20, min_rate: uint256, max_rate: uint256):
assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates"
BORROWED_TOKEN = borrowed_token
self.min_rate = min_rate
self.max_rate = max_rate
self.log_min_rate = self.ln_int(min_rate)
self.log_max_rate = self.ln_int(max_rate)
FACTORY = Factory(msg.sender)
```
```shell
In [1]: SemilogMonetaryPolicy.BORROWED_TOKEN()
Out [1]: '0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'
```
::::
### `FACTORY`
::::description[`SemiLogMonetaryPolicy.FACTORY() -> address: view`]
Getter for the Factory contract. This is a immutable variable and is set at deployment (`__init__()`).
Returns: Factory (`address`).
```vyper
FACTORY: public(immutable(Factory))
@external
def __init__(borrowed_token: ERC20, min_rate: uint256, max_rate: uint256):
assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates"
BORROWED_TOKEN = borrowed_token
self.min_rate = min_rate
self.max_rate = max_rate
self.log_min_rate = self.ln_int(min_rate)
self.log_max_rate = self.ln_int(max_rate)
FACTORY = Factory(msg.sender)
```
```shell
In [1]: SemilogMonetaryPolicy.FACTORY()
Out [1]: '0xc67a44D958eeF0ff316C3a7c9E14FB96f6DedAA3'
```
::::
---
## Vault
The vault is an **implementation of a [ERC-4626](https://ethereum.org/developers/docs/standards/tokens/erc-4626)** vault which **deposits the underlying asset into the controller** and **tracks the progress of the fees earned**.
:::vyper[`Vault.vy`]
The source code for the `Vault.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/lending/contracts/lending/Vault.vy). The contract is written using [Vyper](https://github.com/vyperlang/vyper) version `0.3.10`.
:::
ERC-4626 vaults are **yield-bearing**, meaning the shares received when depositing assets increase in value due to the interest earned from lending out assets. The share balance does not increase, only its value. **Shares are transferable**.
It is a proxy contract (EIP1167-compliant) duplicating the logic of the factory's vault implementation contract. Upon initialization it also creates the market's AMM and Controller using blueprint contracts.
Function which initializes a vault and creates the corresponding Controller and AMM from their blueprint implementations.
```vyper
@external
def initialize(
amm_impl: address,
controller_impl: address,
borrowed_token: ERC20,
collateral_token: ERC20,
A: uint256,
fee: uint256,
price_oracle: PriceOracle, # Factory makes from template if needed, deploying with a from_pool()
monetary_policy: address, # Standard monetary policy set in factory
loan_discount: uint256,
liquidation_discount: uint256
) -> (address, address):
"""
@notice Initializer for vaults
@param amm_impl AMM implementation (blueprint)
@param controller_impl Controller implementation (blueprint)
@param borrowed_token Token which is being borrowed
@param collateral_token Token used for collateral
@param A Amplification coefficient: band size is ~1/A
@param fee Fee for swaps in AMM (for ETH markets found to be 0.6%)
@param price_oracle Already initialized price oracle
@param monetary_policy Already initialized monetary policy
@param loan_discount Maximum discount. LTV = sqrt(((A - 1) / A) **4) - loan_discount
@param liquidation_discount Liquidation discount. LT = sqrt(((A - 1) / A) **4) - liquidation_discount
"""
assert self.borrowed_token.address == empty(address)
self.borrowed_token = borrowed_token
self.collateral_token = collateral_token
self.price_oracle = price_oracle
assert A >= MIN_A and A <= MAX_A, "Wrong A"
assert fee <= MAX_FEE, "Fee too high"
assert fee >= MIN_FEE, "Fee too low"
assert liquidation_discount >= MIN_LIQUIDATION_DISCOUNT, "Liquidation discount too low"
assert loan_discount <= MAX_LOAN_DISCOUNT, "Loan discount too high"
assert loan_discount > liquidation_discount, "need loan_discount>liquidation_discount"
p: uint256 = price_oracle.price() # This also validates price oracle ABI
assert p > 0
assert price_oracle.price_w() == p
A_ratio: uint256 = 10**18 * A / (A - 1)
borrowed_precision: uint256 = 10**(18 - borrowed_token.decimals())
amm: address = create_from_blueprint(
amm_impl,
borrowed_token.address, borrowed_precision,
collateral_token.address, 10**(18 - collateral_token.decimals()),
A, isqrt(A_ratio * 10**18), self.ln_int(A_ratio),
p, fee, ADMIN_FEE, price_oracle.address,
code_offset=3)
controller: address = create_from_blueprint(
controller_impl,
empty(address), monetary_policy, loan_discount, liquidation_discount, amm,
code_offset=3)
AMM(amm).set_admin(controller)
self.amm = AMM(amm)
self.controller = Controller(controller)
self.factory = Factory(msg.sender)
# ERC20 set up
self.precision = borrowed_precision
borrowed_symbol: String[32] = borrowed_token.symbol()
self.name = concat(NAME_PREFIX, borrowed_symbol)
# Symbol must be String[32], but we do String[34]. It doesn't affect contracts which read it (they will truncate)
# However this will be changed as soon as Vyper can *properly* manipulate strings
self.symbol = concat(SYMBOL_PREFIX, borrowed_symbol)
# No events because it's the only market we would ever create in this contract
return controller, amm
```
The Vault itself does not hold any tokens, as the deposited tokens are forwarded to the Controller contract where it can be borrowed from.
*Unlike standard ERC4626 methods, it also has:*
- `borrow_apr()`
- `lend_apr()`
- `pricePerShare()`
Additionally, methods like `mint()`, `deposit()`, `redeem()`, and `withdraw()` can have the receiver address not specified. In such cases, the receiver address defaults to `msg.sender`.
---
## Depositing Assets and Minting Shares
:::colab[Google Colab Notebook]
A google colab notebook on how to use the `deposit` and `mint` functions can be found here: [https://colab.research.google.com/drive/1Qj9nOk5TYXp6j6go3VIh6--r5VILnoo9?usp=sharing](https://colab.research.google.com/drive/1Qj9nOk5TYXp6j6go3VIh6--r5VILnoo9?usp=sharing).
:::
*Two methods for obtaining shares directly from an ERC4626 Vault:*
- **Deposit**: A lender deposits a specified amount of the underlying (borrowable) token into the vault. In exchange, the user receives an equivalent number of shares.
- **Mint**: A lender specifies the desired number of shares to receive and deposits the required amount of the underlying (borrowable) asset to mint these shares. This method allows a user to obtain a precise number of shares.
Because shares are transferable, a user can also acquire shares by means other than depositing assets and minting shares.
:::tip
A newer version of the vault contract allows for setting a maximum supply of assets that can be deposited into the vault. This change was introduced in commit [`cb08681`](https://github.com/curvefi/curve-stablecoin/tree/cb08681ae940f8ff57889e79dce5a23212f0dc19) and [`46fdec2`](https://github.com/curvefi/curve-stablecoin/tree/46fdec2561cc32f43f0e8c1080429a9f3f984e60). When using newer vaults, `mint` and `deposit` functions will check if the maximum supply is exceeded and revert if it is.
:::
### `deposit`
::::description[`Vault.deposit(assets: uint256, receiver: address = msg.sender) -> uint256`]
Function to deposit a specified number of assets of the underlying token (`borrowed_token`) into the vault and mint the corresponding amount of shares to `receiver`. There is no cap when depositing assets into the vault - as many token as desired can be deposited into it.
| Input | Type | Description |
|-------------|-----------|-----------------------------------------------------------------------|
| `assets` | `uint256` | Amount of assets to deposit. |
| `receiver` | `address` | Receiver of the minted shares. Defaults to `msg.sender`. |
Returns: minted shares (`uint256`).
Emits: `Deposit`, `Transfer`, and `SetRate` events.
```vyper
event Transfer:
sender: indexed(address)
receiver: indexed(address)
value: uint256
event Deposit:
sender: indexed(address)
owner: indexed(address)
assets: uint256
shares: uint256
@external
@nonreentrant('lock')
def deposit(assets: uint256, receiver: address = msg.sender) -> uint256:
"""
@notice Deposit assets in return for whatever number of shares corresponds to the current conditions
@param assets Amount of assets to deposit
@param receiver Receiver of the shares who is optional. If not specified - receiver is the sender
"""
controller: Controller = self.controller
total_assets: uint256 = self._total_assets()
assert total_assets + assets >= MIN_ASSETS, "Need more assets"
to_mint: uint256 = self._convert_to_shares(assets, True, total_assets)
assert self.borrowed_token.transferFrom(msg.sender, controller.address, assets, default_return_value=True)
self._mint(receiver, to_mint)
controller.save_rate()
log Deposit(msg.sender, receiver, assets, to_mint)
return to_mint
@internal
def _mint(_to: address, _value: uint256):
self.balanceOf[_to] += _value
self.totalSupply += _value
log Transfer(empty(address), _to, _value)
@internal
@view
def _total_assets() -> uint256:
# admin fee should be accounted for here when enabled
self.controller.check_lock()
return self.borrowed_token.balanceOf(self.controller.address) + self.controller.total_debt()
@internal
@view
def _convert_to_shares(assets: uint256, is_floor: bool = True,
_total_assets: uint256 = max_value(uint256)) -> uint256:
total_assets: uint256 = _total_assets
if total_assets == max_value(uint256):
total_assets = self._total_assets()
precision: uint256 = self.precision
numerator: uint256 = (self.totalSupply + DEAD_SHARES) * assets * precision
denominator: uint256 = (total_assets * precision + 1)
if is_floor:
return numerator / denominator
else:
return (numerator + denominator - 1) / denominator
```
```vyper
interface MonetaryPolicy:
def rate_write() -> uint256: nonpayable
monetary_policy: public(MonetaryPolicy)
@external
@nonreentrant('lock')
def save_rate():
"""
@notice Save current rate
"""
self._save_rate()
@internal
def _save_rate():
"""
@notice Save current rate
"""
rate: uint256 = min(self.monetary_policy.rate_write(), MAX_RATE)
AMM.set_rate(rate)
```
```vyper
log_min_rate: public(int256)
log_max_rate: public(int256)
@external
def rate_write(_for: address = msg.sender) -> uint256:
return self.calculate_rate(_for, 0, 0)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
if total_debt == 0:
return self.min_rate
else:
log_min_rate: int256 = self.
log_max_rate: int256 = self.log_max_rate
return self.exp(total_debt * (log_max_rate - log_min_rate) / total_reserves + log_min_rate)
```
```vyper
event SetRate:
rate: uint256
rate_mul: uint256
time: uint256
@external
@nonreentrant('lock')
def set_rate(rate: uint256) -> uint256:
"""
@notice Set interest rate. That affects the dependence of AMM base price over time
@param rate New rate in units of int(fraction * 1e18) per second
@return rate_mul multiplier (e.g. 1.0 + integral(rate, dt))
"""
assert msg.sender == self.admin
rate_mul: uint256 = self._rate_mul()
self.rate_mul = rate_mul
self.rate_time = block.timestamp
self.rate = rate
log SetRate(rate, rate_mul, block.timestamp)
return rate_mul
@internal
@view
def _rate_mul() -> uint256:
"""
@notice Rate multiplier which is 1.0 + integral(rate, dt)
@return Rate multiplier in units where 1.0 == 1e18
"""
return unsafe_div(self.rate_mul * (10**18 + self.rate * (block.timestamp - self.rate_time)), 10**18)
```
```shell
In [1]: Vault.balanceOf(trader)
Out [1]: 0
In [2]: Vault.deposit(1000000000000000000)
In [3]: Vault.balanceOf(trader)
Out [3]: 997552662404145514069
```
::::
### `maxDeposit`
::::description[`Vault.maxDeposit(receiver: address) -> uint256: view`]
Getter for the maximum amount of assets `receiver` can deposit. Essentially equals to `max_value(uint256)`.
Returns: maximum depositable assets (`uint256`).
| Input | Type | Description |
|-------------|-----------|----------------------|
| `receiver` | `address` | Address of the user. |
```vyper
balanceOf: public(HashMap[address, uint256])
@external
@view
def maxDeposit(receiver: address) -> uint256:
"""
@notice Maximum amount of assets which a given user can deposit (inf)
"""
return self.borrowed_token.balanceOf(receiver)
```
```shell
>>> Vault.maxDeposit("0x7a16fF8270133F063aAb6C9977183D9e72835428"):
should return borrowed_token.balanceOf("0x7a16fF8270133F063aAb6C9977183D9e72835428")
```
::::
### `previewDeposit`
::::description[`Vault.previewDeposit(assets: uint256) -> uint256: view`]
Function to simulate the effects of depositing `assets` into the vault based on the current state.
Returns: amount of shares to be received (`uint256`).
| Input | Type | Description |
|-----------|-----------|------------------------|
| `assets` | `uint256` | Number of assets to deposit. |
```vyper
@external
@view
@nonreentrant('lock')
def previewDeposit(assets: uint256) -> uint256:
"""
@notice Returns the amount of shares which can be obtained upon depositing assets
"""
return self._convert_to_shares(assets)
@internal
@view
def _convert_to_shares(assets: uint256, is_floor: bool = True,
_total_assets: uint256 = max_value(uint256)) -> uint256:
total_assets: uint256 = _total_assets
if total_assets == max_value(uint256):
total_assets = self._total_assets()
precision: uint256 = self.precision
numerator: uint256 = (self.totalSupply + DEAD_SHARES) * assets * precision
denominator: uint256 = (total_assets * precision + 1)
if is_floor:
return numerator / denominator
else:
return (numerator + denominator - 1) / denominator
```
```shell
>>> Vault.previewDeposit(1000000000000000000): # depositing 1 crvusd
998709265069121019738 # shares to receive
```
::::
### `mint`
::::description[`Vault.mint(shares: uint256, receiver: address = msg.sender) -> uint256`]
Function to mint a specific amount of shares (`shares`) to `receiver` by depositing the necessary number of assets into the vault.
| Input | Type | Description |
|-------------|-----------|------------------------------------------------------------------|
| `shares` | `uint256` | Number of shares to be minted. |
| `receiver` | `address` | Receiver of the minted shares. Defaults to `msg.sender`. |
Returns: amount of assets deposited (`uint256`).
Emits: `Deposit`, `Transfer`, and `SetRate` events.
```vyper
event Transfer:
sender: indexed(address)
receiver: indexed(address)
value: uint256
event Deposit:
sender: indexed(address)
owner: indexed(address)
assets: uint256
shares: uint256
@external
@nonreentrant('lock')
def mint(shares: uint256, receiver: address = msg.sender) -> uint256:
"""
@notice Mint given amount of shares taking whatever number of assets it requires
@param shares Number of sharess to mint
@param receiver Optional receiver for the shares. If not specified - it's the sender
"""
controller: Controller = self.controller
total_assets: uint256 = self._total_assets()
assets: uint256 = self._convert_to_assets(shares, False, total_assets)
assert total_assets + assets >= MIN_ASSETS, "Need more assets"
assert self.borrowed_token.transferFrom(msg.sender, controller.address, assets, default_return_value=True)
self._mint(receiver, shares)
controller.save_rate()
log Deposit(msg.sender, receiver, assets, shares)
return assets
@internal
def _mint(_to: address, _value: uint256):
self.balanceOf[_to] += _value
self.totalSupply += _value
log Transfer(empty(address), _to, _value)
@internal
@view
def _total_assets() -> uint256:
# admin fee should be accounted for here when enabled
self.controller.check_lock()
return self.borrowed_token.balanceOf(self.controller.address) + self.controller.total_debt()
@internal
@view
def _convert_to_assets(shares: uint256, is_floor: bool = True,
_total_assets: uint256 = max_value(uint256)) -> uint256:
total_assets: uint256 = _total_assets
if total_assets == max_value(uint256):
total_assets = self._total_assets()
precision: uint256 = self.precision
numerator: uint256 = shares * (total_assets * precision + 1)
denominator: uint256 = (self.totalSupply + DEAD_SHARES) * precision
if is_floor:
return numerator / denominator
else:
return (numerator + denominator - 1) / denominator
```
```vyper
interface MonetaryPolicy:
def rate_write() -> uint256: nonpayable
monetary_policy: public(MonetaryPolicy)
@external
@nonreentrant('lock')
def save_rate():
"""
@notice Save current rate
"""
self._save_rate()
@internal
def _save_rate():
"""
@notice Save current rate
"""
rate: uint256 = min(self.monetary_policy.rate_write(), MAX_RATE)
AMM.set_rate(rate)
```
```vyper
log_min_rate: public(int256)
log_max_rate: public(int256)
@external
def rate_write(_for: address = msg.sender) -> uint256:
return self.calculate_rate(_for, 0, 0)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
if total_debt == 0:
return self.min_rate
else:
log_min_rate: int256 = self.log_min_rate
log_max_rate: int256 = self.log_max_rate
return self.exp(total_debt * (log_max_rate - log_min_rate) / total_reserves + log_min_rate)
```
```vyper
event SetRate:
rate: uint256
rate_mul: uint256
time: uint256
@external
@nonreentrant('lock')
def set_rate(rate: uint256) -> uint256:
"""
@notice Set interest rate. That affects the dependence of AMM base price over time
@param rate New rate in units of int(fraction * 1e18) per second
@return rate_mul multiplier (e.g. 1.0 + integral(rate, dt))
"""
assert msg.sender == self.admin
rate_mul: uint256 = self._rate_mul()
self.rate_mul = rate_mul
self.rate_time = block.timestamp
self.rate = rate
log SetRate(rate, rate_mul, block.timestamp)
return rate_mul
@internal
@view
def _rate_mul() -> uint256:
"""
@notice Rate multiplier which is 1.0 + integral(rate, dt)
@return Rate multiplier in units where 1.0 == 1e18
"""
return unsafe_div(self.rate_mul * (10**18 + self.rate * (block.timestamp - self.rate_time)), 10**18)
```
```shell
In [1]: Vault.balanceOf(trader)
Out [1]: 997552662404145514069
In [2]: Vault.mint(100000000000000000000)
In [3]: Vault.balanceOf(trader)
Out [3]: 1097552662404145514069
```
::::
### `maxMint`
::::description[`Vault.maxMint(receiver: address) -> uint256: view`]
Getter for the maximum amount of shares a user can mint. Essentially equals to `max_value(uint256)`.
Returns: maximum mintable shares (`uint256`).
| Input | Type | Description |
|------------|-----------|-------------------|
| `receiver` | `address` | Address of the user. |
```vyper
@external
@view
def maxMint(receiver: address) -> uint256:
"""
@notice Return maximum amount of shares which a given user can mint (inf)
"""
return max_value(uint256)
@internal
@view
def _convert_to_shares(assets: uint256, is_floor: bool = True,
_total_assets: uint256 = max_value(uint256)) -> uint256:
total_assets: uint256 = _total_assets
if total_assets == max_value(uint256):
total_assets = self._total_assets()
precision: uint256 = self.precision
numerator: uint256 = (self.totalSupply + DEAD_SHARES) * assets * precision
denominator: uint256 = (total_assets * precision + 1)
if is_floor:
return numerator / denominator
else:
return (numerator + denominator - 1) / denominator
```
```shell
>>> Vault.maxMint("0x7a16fF8270133F063aAb6C9977183D9e72835428"):
119831204184300884951118160092
```
::::
### `previewMint`
::::description[`Vault.previewMint(shares: uint256) -> uint256: view`]
Function to simulate the number of assets required to mint a specified amount of shares (`shares`) given the current state of the vault.
Returns: Number of assets required (`uint256`).
| Input | Type | Description |
|-----------|-----------|-------------------------|
| `shares` | `uint256` | Number of shares to mint. |
```vyper
@external
@view
@nonreentrant('lock')
def previewMint(shares: uint256) -> uint256:
"""
@notice Calculate the amount of assets which is needed to exactly mint the given amount of shares
"""
return self._convert_to_assets(shares, False)
@internal
@view
def _convert_to_assets(shares: uint256, is_floor: bool = True,
_total_assets: uint256 = max_value(uint256)) -> uint256:
total_assets: uint256 = _total_assets
if total_assets == max_value(uint256):
total_assets = self._total_assets()
precision: uint256 = self.precision
numerator: uint256 = shares * (total_assets * precision + 1)
denominator: uint256 = (self.totalSupply + DEAD_SHARES) * precision
if is_floor:
return numerator / denominator
else:
return (numerator + denominator - 1) / denominator
```
```shell
>>> Vault.previewMint(1000000000000000000): # mint 1 share
1001291061639566 # assets needed deposit to mint 1 share
```
::::
### `convertToShares`
::::description[`Vault.convertToShares(assets: uint256) -> uint256: view`]
Function to calculate the amount of shares received for a given amount of `assets` provided.
Returns: amount of shares received (`uint256`).
| Input | Type | Description |
|----------|-----------|-------------------------------|
| `assets` | `uint256` | Amount of assets to convert. |
```vyper
@external
@view
@nonreentrant('lock')
def convertToShares(assets: uint256) -> uint256:
"""
@notice Returns the amount of shares which the Vault would exchange for the given amount of shares provided
"""
return self._convert_to_shares(assets)
@internal
@view
def _convert_to_shares(assets: uint256, is_floor: bool = True,
_total_assets: uint256 = max_value(uint256)) -> uint256:
total_assets: uint256 = _total_assets
if total_assets == max_value(uint256):
total_assets = self._total_assets()
precision: uint256 = self.precision
numerator: uint256 = (self.totalSupply + DEAD_SHARES) * assets * precision
denominator: uint256 = (total_assets * precision + 1)
if is_floor:
return numerator / denominator
else:
return (numerator + denominator - 1) / denominator
```
```shell
>>> Vault.convertToShares(1000000000000000000):
998709265069121019738
```
::::
### `maxSupply`
::::description[`Vault.maxSupply() -> uint256: view`]
Getter for the maximum amount of assets that can be supplied to the vault. This function is only available in a newer version of the vault contract.
Returns: maximum supply (`uint256`).
```vyper
event SetMaxSupply:
max_supply: uint256
maxSupply: public(uint256)
@external
def set_max_supply(max_supply: uint256):
"""
@notice Set maximum depositable supply
"""
assert msg.sender == self.factory.admin() or msg.sender == self.factory.address
self.maxSupply = max_supply
log SetMaxSupply(max_supply)
```
```shell
>>> Vault.maxSupply()
1500000000000000000000000 # 15m of the underlying asset
```
::::
### `set_max_supply`
::::description[`Vault.set_max_supply(max_supply: uint256)`]
:::guard[Guarded Method]
This function is only callable by the `admin` of the factory.
:::
Function to set the maximum amount of assets that can be supplied to the vault. This function is only available in a newer version of the vault contract.
Emits: `SetMaxSupply` event.
| Input | Type | Description |
|------------|-----------|-------------------------------|
| `max_supply` | `uint256` | Maximum amount of assets to set. |
```vyper
event SetMaxSupply:
max_supply: uint256
maxSupply: public(uint256)
@external
def set_max_supply(max_supply: uint256):
"""
@notice Set maximum depositable supply
"""
assert msg.sender == self.factory.admin() or msg.sender == self.factory.address
self.maxSupply = max_supply
log SetMaxSupply(max_supply)
```
```shell
>>> Vault.maxSupply()
1500000000000000000000000 # 15m of the underlying asset
```
::::
---
## Withdrawing Assets and Redeeming Shares
:::colab[Google Colab Notebook]
A Google Colab notebook on how to use the `withdraw` and `mint` functions, as well as how shares are priced, can be found here: [https://colab.research.google.com/drive/1Ta69fsIc7zmtjFlQ94a8MDYYLeo4GJJI?usp=sharing](https://colab.research.google.com/drive/1Ta69fsIc7zmtjFlQ94a8MDYYLeo4GJJI?usp=sharing).
:::
*Two ways to retrieve the underlying asset directly from the according ERC4626 Vault:*
- **Withdraw**: A user withdraws a predefined number of the underlying asset and burns the corresponding amount of shares. This action reduces the user's shares in exchange for the underlying asset.
- **Redeem**: A user redeems (and burns) a predefined number of shares to receive the corresponding amount of the underlying assets. This process decreases the shares owned by the user while increasing their holding of the underlying asset.
:::warning[Withdrawing and Redeeming Assets When Utilization is High]
Withdrawing assets (or redeeming shares) from the vault is generally always possible. However, there might be cases where a lending market has very high utilization, which could hinder the withdrawal of assets. For example, if a market has a utilization rate of 100%, meaning every supplied asset in the vault is borrowed, then a user cannot redeem their vault shares for assets. They would need to wait for the utilization rate to go down.
To prevent this scenario, the borrow rate is based on the utilization rate of the lending market. If the utilization reaches 100%, this would cause the interest rate to skyrocket to the maximum value, incentivizing either the borrowers to repay debt or lenders to supply more assets to the vault.
:::
### `withdraw`
::::description[`Vault.withdraw(assets: uint256, receiver: address = msg.sender, owner: address = msg.sender) -> uint256`]
Function to withdraw `assets` from `owner` to the `receiver` and burn the corresponding amount of shares.
| Input | Type | Description |
| ---------- | -------- | -------------------------------------------------- |
| `assets` | `uint256` | Amount of assets to withdraw. |
| `receiver` | `address` | Receiver of the withdrawn assets. Defaults to `msg.sender`. |
| `owner` | `address` | Address of whose shares to burn. Defaults to `msg.sender`. |
Returns: shares withdrawn (`uint256`).
Emits: `Withdraw`, `Transfer`, and `SetRate` events.
```vyper
event Transfer:
sender: indexed(address)
receiver: indexed(address)
value: uint256
event Withdraw:
sender: indexed(address)
receiver: indexed(address)
owner: indexed(address)
assets: uint256
shares: uint256
@external
@nonreentrant('lock')
def withdraw(assets: uint256, receiver: address = msg.sender, owner: address = msg.sender) -> uint256:
"""
@notice Withdraw given amount of asset and burn the corresponding amount of vault shares
@param assets Amount of assets to withdraw
@param receiver Receiver of the assets (optional, sender if not specified)
@param owner Owner who's shares the caller takes. Only can take those if owner gave the approval to the sender. Optional
"""
total_assets: uint256 = self._total_assets()
assert total_assets - assets >= MIN_ASSETS or total_assets == assets, "Need more assets"
shares: uint256 = self._convert_to_shares(assets, False, total_assets)
if owner != msg.sender:
allowance: uint256 = self.allowance[owner][msg.sender]
if allowance != max_value(uint256):
self._approve(owner, msg.sender, allowance - shares)
controller: Controller = self.controller
self._burn(owner, shares)
assert self.borrowed_token.transferFrom(controller.address, receiver, assets, default_return_value=True)
controller.save_rate()
log Withdraw(msg.sender, receiver, owner, assets, shares)
return shares
@internal
def _burn(_from: address, _value: uint256):
self.balanceOf[_from] -= _value
self.totalSupply -= _value
log Transfer(_from, empty(address), _value)
@internal
@view
def _total_assets() -> uint256:
# admin fee should be accounted for here when enabled
self.controller.check_lock()
return self.borrowed_token.balanceOf(self.controller.address) + self.controller.total_debt()
@internal
@view
def _convert_to_shares(assets: uint256, is_floor: bool = True,
_total_assets: uint256 = max_value(uint256)) -> uint256:
total_assets: uint256 = _total_assets
if total_assets == max_value(uint256):
total_assets = self._total_assets()
precision: uint256 = self.precision
numerator: uint256 = (self.totalSupply + DEAD_SHARES) * assets * precision
denominator: uint256 = (total_assets * precision + 1)
if is_floor:
return numerator / denominator
else:
return (numerator + denominator - 1) / denominator
```
```vyper
interface MonetaryPolicy:
def rate_write() -> uint256: nonpayable
monetary_policy: public(MonetaryPolicy)
@external
@nonreentrant('lock')
def save_rate():
"""
@notice Save current rate
"""
self._save_rate()
@internal
def _save_rate():
"""
@notice Save current rate
"""
rate: uint256 = min(self.monetary_policy.rate_write(), MAX_RATE)
AMM.set_rate(rate)
```
```vyper
log_min_rate: public(int256)
log_max_rate: public(int256)
@external
def rate_write(_for: address = msg.sender) -> uint256:
return self.calculate_rate(_for, 0, 0)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
if total_debt == 0:
return self.min_rate
else:
log_min_rate: int256 = self.log_min_rate
log_max_rate: int256 = self.log_max_rate
return self.exp(total_debt * (log_max_rate - log_min_rate) / total_reserves + log_min_rate)
```
```vyper
event SetRate:
rate: uint256
rate_mul: uint256
time: uint256
@external
@nonreentrant('lock')
def set_rate(rate: uint256) -> uint256:
"""
@notice Set interest rate. That affects the dependence of AMM base price over time
@param rate New rate in units of int(fraction * 1e18) per second
@return rate_mul multiplier (e.g. 1.0 + integral(rate, dt))
"""
assert msg.sender == self.admin
rate_mul: uint256 = self._rate_mul()
self.rate_mul = rate_mul
self.rate_time = block.timestamp
self.rate = rate
log SetRate(rate, rate_mul, block.timestamp)
return rate_mul
@internal
@view
def _rate_mul() -> uint256:
"""
@notice Rate multiplier which is 1.0 + integral(rate, dt)
@return Rate multiplier in units where 1.0 == 1e18
"""
return unsafe_div(self.rate_mul * (10**18 + self.rate * (block.timestamp - self.rate_time)), 10**18)
```
```shell
In [1]: Vault.balanceOf(trader)
Out [1]: 1097552662404145514069
In [2]: crvusd.balanceOf(trader)
Out [2]: 999998899754665824864192
In [3]: Vault.withdraw(1000000000000000000)
In [4]: Vault.balanceOf(trader)
Out [4]: 99999999999999999999
In [5]: crvusd.balanceOf(trader)
Out [5]: 999999899754665824864192
```
::::
### `maxWithdraw`
::::description[`Vault.maxWithdraw(owner: address) -> uint256: view`]
Getter for the maximum amount of assets withdrawable by `owner`.
Returns: withdrawable assets (`uint256`).
| Input | Type | Description |
|---------|----------|------------------------------------|
| `owner` | `address` | Address of the user to withdraw from. |
```vyper
@external
@view
@nonreentrant('lock')
def maxWithdraw(owner: address) -> uint256:
"""
@notice Maximum amount of assets which a given user can withdraw. Aware of both user's balance and available liquidity
"""
return min(
self._convert_to_assets(self.balanceOf[owner]),
self.borrowed_token.balanceOf(self.controller.address))
@internal
@view
def _convert_to_assets(shares: uint256, is_floor: bool = True,
_total_assets: uint256 = max_value(uint256)) -> uint256:
total_assets: uint256 = _total_assets
if total_assets == max_value(uint256):
total_assets = self._total_assets()
precision: uint256 = self.precision
numerator: uint256 = shares * (total_assets * precision + 1)
denominator: uint256 = (self.totalSupply + DEAD_SHARES) * precision
if is_floor:
return numerator / denominator
else:
return (numerator + denominator - 1) / denominator
```
```shell
>>> Vault.maxWithdraw("0x7a16fF8270133F063aAb6C9977183D9e72835428")
45917295006116605730466
```
::::
### `previewWithdraw`
::::description[`Vault.previewWithdraw(assets: uint256) -> uint256: view`]
Function to simulate the amount of shares getting burned when withdrawing `assets`.
Returns: number of shares burned (`uint256`).
| Input | Type | Description |
|---------|-----------|----------------------------------|
| `assets` | `uint256` | Number of assets to withdraw. |
```vyper
@external
@view
@nonreentrant('lock')
def previewWithdraw(assets: uint256) -> uint256:
"""
@notice Calculate number of shares which gets burned when withdrawing given amount of asset
"""
assert assets <= self.borrowed_token.balanceOf(self.controller.address)
return self._convert_to_shares(assets, False)
@internal
@view
def _convert_to_shares(assets: uint256, is_floor: bool = True,
_total_assets: uint256 = max_value(uint256)) -> uint256:
total_assets: uint256 = _total_assets
if total_assets == max_value(uint256):
total_assets = self._total_assets()
precision: uint256 = self.precision
numerator: uint256 = (self.totalSupply + DEAD_SHARES) * assets * precision
denominator: uint256 = (total_assets * precision + 1)
if is_floor:
return numerator / denominator
else:
return (numerator + denominator - 1) / denominator
```
```shell
>>> Vault.previewWithdraw(1000000000000000000): # withdrawing 1 crvusd
998540201056049850914 # shares to burn (approx. 988)
```
::::
### `redeem`
::::description[`Vault.redeem(shares: uint256, receiver: address = msg.sender, owner: address = msg.sender) -> uint256`]
Function to redeem (and burn) `shares` from `owner` and send the received assets to `receiver`. Shares are burned when they are redeemed.
| Input | Type | Description |
|-----------|-----------|----------------------------------------------------|
| `shares` | `uint256` | Amount of shares to redeem. |
| `receiver`| `address` | Receiver of the shares. Defaults to `msg.sender`. |
| `owner` | `address` | Address of whose shares to burn. Defaults to `msg.sender`. |
Returns: assets received (`uint256`).
Emits: `Withdraw`, `Transfer`, and `SetRate` events.
```vyper
event Transfer:
sender: indexed(address)
receiver: indexed(address)
value: uint256
event Withdraw:
sender: indexed(address)
receiver: indexed(address)
owner: indexed(address)
assets: uint256
shares: uint256
@external
@nonreentrant('lock')
def redeem(shares: uint256, receiver: address = msg.sender, owner: address = msg.sender) -> uint256:
"""
@notice Burn given amount of shares and give corresponding assets to the user
@param shares Amount of shares to burn
@param receiver Optional receiver of the assets
@param owner Optional owner of the shares. Can only redeem if owner gave approval to the sender
"""
if owner != msg.sender:
allowance: uint256 = self.allowance[owner][msg.sender]
if allowance != max_value(uint256):
self._approve(owner, msg.sender, allowance - shares)
total_assets: uint256 = self._total_assets()
assets_to_redeem: uint256 = self._convert_to_assets(shares, True, total_assets)
if total_assets - assets_to_redeem < MIN_ASSETS:
if shares == self.totalSupply:
# This is the last withdrawal, so we can take everything
assets_to_redeem = total_assets
else:
raise "Need more assets"
self._burn(owner, shares)
controller: Controller = self.controller
assert self.borrowed_token.transferFrom(controller.address, receiver, assets_to_redeem, default_return_value=True)
controller.save_rate()
log Withdraw(msg.sender, receiver, owner, assets_to_redeem, shares)
return assets_to_redeem
@internal
def _burn(_from: address, _value: uint256):
self.balanceOf[_from] -= _value
self.totalSupply -= _value
log Transfer(_from, empty(address), _value)
@internal
@view
def _total_assets() -> uint256:
# admin fee should be accounted for here when enabled
self.controller.check_lock()
return self.borrowed_token.balanceOf(self.controller.address) + self.controller.total_debt()
@internal
@view
def _convert_to_shares(assets: uint256, is_floor: bool = True,
_total_assets: uint256 = max_value(uint256)) -> uint256:
total_assets: uint256 = _total_assets
if total_assets == max_value(uint256):
total_assets = self._total_assets()
precision: uint256 = self.precision
numerator: uint256 = (self.totalSupply + DEAD_SHARES) * assets * precision
denominator: uint256 = (total_assets * precision + 1)
if is_floor:
return numerator / denominator
else:
return (numerator + denominator - 1) / denominator
```
```vyper
interface MonetaryPolicy:
def rate_write() -> uint256: nonpayable
monetary_policy: public(MonetaryPolicy)
@external
@nonreentrant('lock')
def save_rate():
"""
@notice Save current rate
"""
self._save_rate()
@internal
def _save_rate():
"""
@notice Save current rate
"""
rate: uint256 = min(self.monetary_policy.rate_write(), MAX_RATE)
AMM.set_rate(rate)
```
```vyper
log_min_rate: public(int256)
log_max_rate: public(int256)
@external
def rate_write(_for: address = msg.sender) -> uint256:
return self.calculate_rate(_for, 0, 0)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
if total_debt == 0:
return self.min_rate
else:
log_min_rate: int256 = self.log_min_rate
log_max_rate: int256 = self.log_max_rate
return self.exp(total_debt * (log_max_rate - log_min_rate) / total_reserves + log_min_rate)
```
```vyper
event SetRate:
rate: uint256
rate_mul: uint256
time: uint256
@external
@nonreentrant('lock')
def set_rate(rate: uint256) -> uint256:
"""
@notice Set interest rate. That affects the dependence of AMM base price over time
@param rate New rate in units of int(fraction * 1e18) per second
@return rate_mul multiplier (e.g. 1.0 + integral(rate, dt))
"""
assert msg.sender == self.admin
rate_mul: uint256 = self._rate_mul()
self.rate_mul = rate_mul
self.rate_time = block.timestamp
self.rate = rate
log SetRate(rate, rate_mul, block.timestamp)
return rate_mul
@internal
@view
def _rate_mul() -> uint256:
"""
@notice Rate multiplier which is 1.0 + integral(rate, dt)
@return Rate multiplier in units where 1.0 == 1e18
"""
return unsafe_div(self.rate_mul * (10**18 + self.rate * (block.timestamp - self.rate_time)), 10**18)
```
```shell
In [1]: Vault.balanceOf(trader)
Out [1]: 99999999999999999999
In [2]: crvusd.balanceOf(trader)
Out [2]: 999999899754665824864192
In [3]: Vault.redeem(99999999999999999999)
In [4]: Vault.balanceOf(trader)
Out [4]: 0
In [5]: crvusd.balanceOf(trader)
Out [5]: 999999999999999999999998
```
::::
### `maxRedeem`
::::description[`Vault.maxRedeem(owner: address) -> uint256: view`]
Getter for the maximum redeemable shares from `owner`.
Returns: maximum redeemable shares (`uint256`).
| Input | Type | Description |
|---------|-----------|---------------------------------------|
| `owner` | `address` | Address of the user to redeem shares from. |
```vyper
@external
@view
@nonreentrant('lock')
def maxRedeem(owner: address) -> uint256:
"""
@notice Calculate maximum amount of shares which a given user can redeem
"""
return min(
self._convert_to_shares(self.borrowed_token.balanceOf(self.controller.address), False),
self.balanceOf[owner])
@internal
@view
def _convert_to_shares(assets: uint256, is_floor: bool = True,
_total_assets: uint256 = max_value(uint256)) -> uint256:
total_assets: uint256 = _total_assets
if total_assets == max_value(uint256):
total_assets = self._total_assets()
precision: uint256 = self.precision
numerator: uint256 = (self.totalSupply + DEAD_SHARES) * assets * precision
denominator: uint256 = (total_assets * precision + 1)
if is_floor:
return numerator / denominator
else:
return (numerator + denominator - 1) / denominator
```
```shell
>>> Vault.maxRedeem("0x7a16fF8270133F063aAb6C9977183D9e72835428")
45836614069469292514157944
```
::::
### `previewRedeem`
::::description[`Vault.previewRedeem(shares: uint256) -> uint256: view`]
Function to simulate the number of assets received when redeeming (burning) `shares`.
Returns: obtainable assets (`uint256`).
| Input | Type | Description |
|----------|-----------|----------------------------------|
| `shares` | `uint256` | Number of shares to redeem. |
```vyper
@external
@view
@nonreentrant('lock')
def previewRedeem(shares: uint256) -> uint256:
"""
@notice Calculate the amount of assets which can be obtained by redeeming the given amount of shares
"""
if self.totalSupply == 0:
assert shares == 0
return 0
else:
assets_to_redeem: uint256 = self._convert_to_assets(shares)
assert assets_to_redeem <= self.borrowed_token.balanceOf(self.controller.address)
return assets_to_redeem
@internal
@view
def _convert_to_assets(shares: uint256, is_floor: bool = True,
_total_assets: uint256 = max_value(uint256)) -> uint256:
total_assets: uint256 = _total_assets
if total_assets == max_value(uint256):
total_assets = self._total_assets()
precision: uint256 = self.precision
numerator: uint256 = shares * (total_assets * precision + 1)
denominator: uint256 = (self.totalSupply + DEAD_SHARES) * precision
if is_floor:
return numerator / denominator
else:
return (numerator + denominator - 1) / denominator
```
```shell
>>> Vault.previewRedeem(1000000000000000000):
1001293138709678
```
::::
### `convertToAssets`
::::description[`Vault.convertToAssets(shares: uint256) -> uint256: view`]
Function to calculate the amount of assets received when converting `shares` to assets.
Returns: amount of assets received (`uint256`).
| Input | Type | Description |
|----------|-----------|--------------------------------------|
| `shares` | `uint256` | Amount of shares to convert to assets. |
```vyper
@external
@view
@nonreentrant('lock')
def convertToAssets(shares: uint256) -> uint256:
"""
@notice Returns the amount of assets that the Vault would exchange for the amount of shares provided
"""
return self._convert_to_assets(shares)
@internal
@view
def _convert_to_assets(shares: uint256, is_floor: bool = True,
_total_assets: uint256 = max_value(uint256)) -> uint256:
total_assets: uint256 = _total_assets
if total_assets == max_value(uint256):
total_assets = self._total_assets()
precision: uint256 = self.precision
numerator: uint256 = shares * (total_assets * precision + 1)
denominator: uint256 = (self.totalSupply + DEAD_SHARES) * precision
if is_floor:
return numerator / denominator
else:
return (numerator + denominator - 1) / denominator
```
```shell
>>> Vault.convertToAssets(1000000000000000000):
1001293138709678
```
::::
---
## Interest Rates
Interest rates within lending markets are intricately linked to the market's utilization rate. Essentially, as market utilization increases, so too do the interest rates. This dynamic relationship underscores the market's demand-supply equilibrium, directly influencing the cost of borrowing and the returns on lending.
The vault contract has two public methods, named `borrow_apr` and `lend_apr`. These methods are designed to compute and return the **annualized rates for borrowing and lending**, respectively, standardized to a base of 1e18.
- **Borrow Rate:** This is the interest rate charged on the amount borrowed by a user.
- **Lend Rate:** Conversely, this rate represents the yield a user earns by lending their assets to the vault.
```shell
In [1]: Vault.borrow_apr()
Out [1]: 352629439534800000 # -> 0.352 -> 35.2%
In [2]: Vault.lend_apr()
Out [2]: 248442280773618417 # -> 0.248 -> 24.8%
```
More on rates and when they are updated here: [SemiLog Monetary Policy](./semilog-mp.md)
---
*The formula to calculate the annual percentage rate (APR) for borrowing is outlined as follows:*
$$\text{borrowAPR} = \frac{\text{rate} \cdot 365 \cdot 86400}{10^{18}}$$
*The APR for lending is directly linked to the APR for borrowing, defined by:*
$$\text{lendAPR} = \text{borrowAPR} \cdot \text{utilization}$$
*Additionally, the utilization ratio is determined by the following:*[^1]
$$\text{utilization} = \frac{\text{debt}}{\text{totalAssets}}$$
[^1]: This ratio represents the proportion of borrowed assets (debt) to the total assets supplied in the vault. It's a key metric that reflects the level of asset utilization within the vault. Borrowed assets, or debt, are obtained through the `total_debt` method from the Controller, while the `totalAssets` method within the Vault provides the value of total assets supplied.
---
### `borrow_apr`
::::description[`Vault.borrow_apr() -> uint256: view`]
Getter for the annualized borrow APR. The user pays this rate on the assets borrowed.
Returns: borrow rate (`uint256`).
```vyper
interface AMM:
def set_admin(_admin: address): nonpayable
def rate() -> uint256: view
amm: public(AMM)
@external
@view
@nonreentrant('lock')
def borrow_apr() -> uint256:
"""
@notice Borrow APR (annualized and 1e18-based)
"""
return self.amm.rate() * (365 * 86400)
```
```vyper
rate: public(uint256)
```
```shell
>>> Vault.borrow_apr():
152933173055280000 # 15.29%
```
::::
### `lend_apr`
::::description[`Vault.lend_apr() -> uint256: view`]
Getter for the annualized lending APR. The value is based on the utilization is awarded to the user for supplying underlying asset (`borrowed_token`) to the vault.
Returns: lending rate (`uint256`).
```vyper
interface AMM:
def set_admin(_admin: address): nonpayable
def rate() -> uint256: view
amm: public(AMM)
@external
@view
@nonreentrant('lock')
def lend_apr() -> uint256:
"""
@notice Lending APR (annualized and 1e18-based)
"""
debt: uint256 = self.controller.total_debt()
if debt == 0:
return 0
else:
return self.amm.rate() * (365 * 86400) * debt / self._total_assets()
@internal
@view
def _total_assets() -> uint256:
# admin fee should be accounted for here when enabled
self.controller.check_lock()
return self.borrowed_token.balanceOf(self.controller.address) + self.controller.total_debt()
```
```vyper
rate: public(uint256)
```
```shell
>>> Vault.lend_apr():
113600673360849488 # 11.36%
```
::::
---
## Contract Info Methods
### `asset`
::::description[`Vault.asset() -> ERC20: view`]
Getter for the underlying asset used by the vault, which is the `borrowed_token`.
Returns: underlying asset (`address`).
```vyper
@external
@view
def asset() -> ERC20:
"""
@notice Asset which is the same as borrowed_token
"""
return self.borrowed_token
```
```shell
>>> Vault.asset():
'0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'
```
::::
### `totalAssets`
::::description[`Vault.totalAssets() -> uint256: view`]
Getter for the total amount of the underlying asset (`borrowed_token`) held by the vault. These are the total assets that can be lent out.
Returns: total assets in the vault (`uint256`).
```vyper
@external
@view
@nonreentrant('lock')
def totalAssets() -> uint256:
"""
@notice Total assets which can be lent out or be in reserve
"""
return self._total_assets()
@internal
@view
def _total_assets() -> uint256:
# admin fee should be accounted for here when enabled
self.controller.check_lock()
return self.borrowed_token.balanceOf(self.controller.address) + self.controller.total_debt()
```
```shell
>>> Vault.totalAssets():
181046847949654671685165
```
::::
### `pricePerShare`
::::description[`Vault.pricePerShare(is_floor: bool = True) -> uint256: view`]
Getter for the price of one share in asset tokens.
Returns: asset price per share (`uint256`).
| Input | Type | Description |
|------------|--------|-------------|
| `is_floor` | `bool` | - |
```vyper
@external
@view
@nonreentrant('lock')
def pricePerShare(is_floor: bool = True) -> uint256:
"""
@notice Method which shows how much one pool share costs in asset tokens if they are normalized to 18 decimals
"""
supply: uint256 = self.totalSupply
if supply == 0:
return 10**18 / DEAD_SHARES
else:
precision: uint256 = self.precision
numerator: uint256 = 10**18 * (self._total_assets() * precision + 1)
denominator: uint256 = (supply + DEAD_SHARES)
pps: uint256 = 0
if is_floor:
pps = numerator / denominator
else:
pps = (numerator + denominator - 1) / denominator
assert pps > 0
return pps
@internal
@view
def _total_assets() -> uint256:
# admin fee should be accounted for here when enabled
self.controller.check_lock()
return self.borrowed_token.balanceOf(self.controller.address) + self.controller.total_debt()
```
```shell
>>> Vault.pricePerShare(true):
1001291278001035
>>> Vault.pricePerShare(false):
1001292100174622
```
::::
### `admin`
::::description[`Vault.admin() -> address: view`]
Getter for the admin of the vault.
Returns: admin (`address`).
```vyper
interface Factory:
def admin() -> address: view
@external
@view
def admin() -> address:
return self.factory.admin()
```
```vyper
admin: public(address)
```
```shell
>>> Vault.admin():
'0x40907540d8a6C65c637785e8f8B742ae6b0b9968'
```
::::
### `borrowed_token`
::::description[`Vault.borrowed_token() -> address: view`]
Getter for the borrowable token in the vault.
Returns: borrowable token (`address`).
```vyper
borrowed_token: public(ERC20)
```
```shell
>>> Vault.borrowed_token():
'0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'
```
::::
### `collateral_token`
::::description[`Vault.collateral_token() -> address: view`]
Getter for the collateral token of the lending market which is deposited into the AMM.
Returns: collateral token (`address`).
```vyper
collateral_token: public(ERC20)
```
```shell
>>> Vault.collateral_token():
'0xD533a949740bb3306d119CC777fa900bA034cd52'
```
::::
### `price_oracle`
::::description[`Vault.price_oracle() -> address: view`]
Getter for the price oracle contract used in the vault.
Returns: oracle (`address`).
```vyper
price_oracle: public(PriceOracle)
```
```shell
>>> Vault.price_oracle():
'0xc17B0451E6d8C0f71297d0f174590632BE81163c'
```
::::
### `amm`
::::description[`Vault.amm() -> address: view`]
Getter for the AMM of the vault.
Returns: AMM (`address`).
```vyper
amm: public(AMM)
```
```shell
>>> Vault.amm():
'0xafC1ab86045Cb2a07C23399dbE64b56D1B8B3239'
```
::::
### `controller`
::::description[`Vault.controller() -> address: view`]
Getter for the Controller of the vault.
Returns: Controller (`address`).
```vyper
controller: public(Controller)
```
```shell
>>> Vault.controller():
'0x7443944962D04720f8c220C0D25f56F869d6EfD4'
```
::::
### `factory`
::::description[`Vault.factory() -> address: view`]
Getter for the Factory of the vault.
Returns: Factory (`address`).
```vyper
factory: public(Factory)
```
```shell
>>> Vault.factory():
'0xc67a44D958eeF0ff316C3a7c9E14FB96f6DedAA3'
```
::::
---
## Curve Lending: Overview
Curve lending allows the **creation of permissionless lending/borrowing markets to borrow crvUSD against any token, or to borrow any token against crvUSD in an isolated mode**, powered by **LLAMMA** for soft-liquidations. All markets are **isolated** from each other and do not intertwine.
The **borrowable liquidity is provided by willing lenders**through [Vaults](./contracts/vault.md), which are [ERC4626](https://ethereum.org/en/developers/docs/standards/tokens/erc-4626/) contracts with some additional methods for convenience.
:::deploy[Contract Source & Deployment]
Lending-related deployments can be found [here](../deployments.md).
Source code for all lending-relevant contracts is available on [GitHub](https://github.com/curvefi/curve-stablecoin/tree/lending).
:::
## Overview
*The entire system is similar to the one for minting crvUSD. Every lending market has an individual **Controller**, **LLAMMA**, and **Vault**.*
The **Controller** is some sort of on-chain interface. Most user actions, such as *creating or repaying loans or managing existing ones*, are done through this contract.
The **LLAMMA** is an AMM that holds the collateral assets. This is where the magic around *soft-liquidations* happens. Full documentation can be found [here](../crvusd/amm.md).
The **Vault** is where willing *lenders provide assets to be borrowed*. The contract does not actually hold any borrowable assets; they are held by the Controller.
---
## LLAMMA and Controller
Because Curve Lending operates very similarly to the system for minting crvUSD, both `Controller.vy` and `AMM.vy` (LLAMMA) can be used for lending markets. To ensure full compatibility with both systems, **several modifications have been made to their codebases**:
[→ More here](./contracts/controller-llamma.md)
## Vault
The Vault is an **implementation of the ERC4626 vault which deposits assets into the Controller contract** and tracks the **progress of fees earned**. It is a standard factory (non-blueprint) contract that also creates the AMM and Controller using `initialize()`.
Function which initializes a vault and creates the corresponding Controller and AMM contract from their blueprint implementations.
```vyper
@external
def initialize(
amm_impl: address,
controller_impl: address,
borrowed_token: ERC20,
collateral_token: ERC20,
A: uint256,
fee: uint256,
price_oracle: PriceOracle, # Factory makes from template if needed, deploying with a from_pool()
monetary_policy: address, # Standard monetary policy set in factory
loan_discount: uint256,
liquidation_discount: uint256
) -> (address, address):
"""
@notice Initializer for vaults
@param amm_impl AMM implementation (blueprint)
@param controller_impl Controller implementation (blueprint)
@param borrowed_token Token which is being borrowed
@param collateral_token Token used for collateral
@param A Amplification coefficient: band size is ~1/A
@param fee Fee for swaps in AMM (for ETH markets found to be 0.6%)
@param price_oracle Already initialized price oracle
@param monetary_policy Already initialized monetary policy
@param loan_discount Maximum discount. LTV = sqrt(((A - 1) / A) **4) - loan_discount
@param liquidation_discount Liquidation discount. LT = sqrt(((A - 1) / A) **4) - liquidation_discount
"""
assert self.borrowed_token.address == empty(address)
self.borrowed_token = borrowed_token
self.collateral_token = collateral_token
self.price_oracle = price_oracle
assert A >= MIN_A and A <= MAX_A, "Wrong A"
assert fee <= MAX_FEE, "Fee too high"
assert fee >= MIN_FEE, "Fee too low"
assert liquidation_discount >= MIN_LIQUIDATION_DISCOUNT, "Liquidation discount too low"
assert loan_discount <= MAX_LOAN_DISCOUNT, "Loan discount too high"
assert loan_discount > liquidation_discount, "need loan_discount>liquidation_discount"
p: uint256 = price_oracle.price() # This also validates price oracle ABI
assert p > 0
assert price_oracle.price_w() == p
A_ratio: uint256 = 10**18 * A / (A - 1)
borrowed_precision: uint256 = 10**(18 - borrowed_token.decimals())
amm: address = create_from_blueprint(
amm_impl,
borrowed_token.address, borrowed_precision,
collateral_token.address, 10**(18 - collateral_token.decimals()),
A, isqrt(A_ratio * 10**18), self.ln_int(A_ratio),
p, fee, ADMIN_FEE, price_oracle.address,
code_offset=3)
controller: address = create_from_blueprint(
controller_impl,
empty(address), monetary_policy, loan_discount, liquidation_discount, amm,
code_offset=3)
AMM(amm).set_admin(controller)
self.amm = AMM(amm)
self.controller = Controller(controller)
self.factory = Factory(msg.sender)
# ERC20 set up
self.precision = borrowed_precision
borrowed_symbol: String[32] = borrowed_token.symbol()
self.name = concat(NAME_PREFIX, borrowed_symbol)
# Symbol must be String[32], but we do String[34]. It doesn't affect contracts which read it (they will truncate)
# However this will be changed as soon as Vyper can *properly* manipulate strings
self.symbol = concat(SYMBOL_PREFIX, borrowed_symbol)
# No events because it's the only market we would ever create in this contract
return controller, amm
```
[→ More here](./contracts/vault.md)
## OneWay Lending Factory
The factory allows the **permissionless creation of borrowing/lending markets without rehypothecation**, meaning the collateral asset cannot be lent out. A distinctive feature is its ability to generate markets from Curve pools with a `price_oracle()` method, eliminating the need for a separate price oracle. Nonetheless, these pools must adhere to one of the following standards:
- [`stableswap-ng`](../amm/stableswap-ng/overview.md)
- [`tricrypto-ng`](../amm/tricrypto-ng/overview.md)
- [`twocrypto-ng`](../amm/twocrypto-ng/overview.md)
[→ More here](./contracts/oneway-factory.md)
## Oracles
Curve lending markets use **EMA oracles** as price sources to value the underlying collaterals. There are **multiple different oracles in use**. For example, one version uses the `price_oracle` of a single Curve pool, while another version uses an oracle contract that chains together multiple price oracles from different liquidity pools.
[→ More here](./contracts/oracle-overview.md)
## Monetary Policies
Lending markets use a semi-log monetary policy for lending markets where the **borrow rate does not depend on the price of crvUSD**but just on the **utilization of the market**.
[→ More here](./contracts/mp-overview.md)
---
## Ownership in Curve Contracts
Smart contracts often have owner-guarded functions which only allow specific addresses to call certain functions. These are typically used for administrative operations like modifying liquidity pool parameters, updating fee receivers, or changing critical contract settings.
:::info[Ownership Agents]
To make ownership work along with the DAO governance, Curve makes use of `OwnershipAgents` on Ethereum, aswell as on other L1 or L2s to handle governance actions.
The main `OwnershipAgent` is deployed at [`0x40907540d8a6C65c637785e8f8B742ae6b0b9968`](https://etherscan.io/address/0x40907540d8a6C65c637785e8f8B742ae6b0b9968) on Ethereum. Deployments on other chains can be found [here](../deployments.md).
:::
Curve strives to remain as decentralized as possible, with most administrative operations controlled by the DAO. However, there are some exceptions where DAO control may not be optimal:
Contracts like the `AddressProvider` or `MetaRegistry` that:
- Don't hold any user assets
- Only provide on-chain information
- Require frequent maintenance and quick updates
In these specific cases, ownership may be delegated to specialized administrative addresses.
---
## Ownership Transfer Patterns
Curve uses different ownership transfer patterns in its contracts. The most common ones are described below.
## `commit + accept`
The implementation of the commit + accept pattern might vary slightly. Nontheless, the general idea is the same.
```vyper
owner: public(address)
future_owner: public(address)
event TransferOwnership:
_old_owner: address
_new_owner: address
@external
def commit_transfer_ownership(_future_owner: address):
"""
@notice Transfer ownership to `_future_owner`
@param _future_owner The account to commit as the future owner
"""
assert msg.sender == self.owner # dev: only owner
self.future_owner = _future_owner
@external
def accept_transfer_ownership():
"""
@notice Accept the transfer of ownership
@dev Only the committed future owner can call this function
"""
assert msg.sender == self.future_owner # dev: only future owner
log TransferOwnership(self.owner, msg.sender)
self.owner = msg.sender
```
The ownership transfer mechanism implements a secure two-step process that prevents accidental or malicious ownership transfers. At its core, the implementation revolves around two state variables: `owner` and `future_owner`, both public addresses that track the current and prospective contract owners respectively.
The transfer process begins when the current owner initiates a transfer by calling `commit_transfer_ownership`. This function takes a single parameter - the address of the intended new owner - and stores it in the `future_owner` state variable. Importantly, this function can only be called by the current owner, enforced through an assertion check at the start of the function.
Once the transfer is committed, the second phase of the transfer can begin. The designated `future_owner` must actively accept the ownership by calling `accept_transfer_ownership`. This function performs its own security check, ensuring that only the committed `future_owner` can call it. Upon successful execution, it updates the `owner` state variable to the new address and emits a `TransferOwnership` event that logs both the old and new owner addresses.
This two-step process provides several security benefits. First, it prevents ownership transfers due to accidental input of wrong addresses, as the intended recipient must actively accept the role. Second, it ensures that the new owner has control of their address and can actually interact with the contract before the transfer is complete. The process also leaves a clear on-chain trail through the emitted event, making ownership transfers transparent and auditable.
:::colab[Google Colab Notebook]
A simple Google Colab notebook that simulates the commit + accept pattern can be found here: [ Google Colab Notebook](https://colab.research.google.com/drive/10cEFQbHxuXFyzi7CnmL3tdsXaQ4GxaeJ?usp=sharing).
:::
---
## `commit + apply`
The implementation of the commit + apply pattern might vary slightly. Nontheless, the general idea is the same.
```vyper
event CommitOwnership:
admin: address
event ApplyOwnership:
admin: address
admin: public(address)
future_admin: public(address)
@external
def commit_transfer_ownership(addr: address):
"""
@notice Transfer ownership of GaugeController to `addr`
@param addr Address to have ownership transferred to
"""
assert msg.sender == self.admin # dev: admin only
self.future_admin = addr
log CommitOwnership(addr)
@external
def apply_transfer_ownership():
"""
@notice Apply pending ownership transfer
"""
assert msg.sender == self.admin # dev: admin only
_admin: address = self.future_admin
assert _admin != ZERO_ADDRESS # dev: admin not set
self.admin = _admin
log ApplyOwnership(_admin)
```
This implementation presents another variation of the two-step ownership transfer pattern, commonly used in Curve's contracts, particularly in the GaugeController. Instead of requiring the future owner to accept the transfer, this pattern allows the current admin to both initiate and complete the transfer process.
The mechanism uses two state variables: `admin` (instead of owner) and `future_admin`, following the same principle of separating the current and prospective contract administrators. The process is tracked through two distinct events: `CommitOwnership` and `ApplyOwnership`, providing clear on-chain visibility of the transfer stages.
The transfer process begins with the current admin calling `commit_transfer_ownership`, specifying the address of the intended new administrator. This function sets the `future_admin` state variable and emits a `CommitOwnership` event. Unlike the accept pattern, this function includes a crucial security check ensuring that only the current admin can initiate the transfer.
The second phase involves calling `apply_transfer_ownership`, which finalizes the transfer. This function includes several important security features:
- Only the current admin can execute the transfer
- The function verifies that `future_admin` is not set to the zero address
- Upon successful execution, it updates the `admin` state and emits an `ApplyOwnership` event
This implementation differs from the accept pattern in a key aspect: the current admin maintains full control throughout the entire process, rather than requiring action from the future admin. While this provides more flexibility for the current admin, it also means extra care must be taken to ensure the new admin address is correct and capable of managing the contract.
The zero-address check in the apply function serves as an additional safety mechanism, preventing transfers to invalid addresses that could permanently lock the contract's administrative functions.
:::colab[Google Colab Notebook]
A simple Google Colab notebook that simulates the commit + apply pattern can be found here: [ Google Colab Notebook](https://colab.research.google.com/drive/1KV25arJ-P4UrscHOx8wjdCaD9H4a9tHx?usp=sharing).
:::
---
## `snekmate`
[Snekmate](https://github.com/pcaversaccio/snekmate) is a collection of production-grade, secure Vyper smart contract building blocks maintained by [pcaversaccio](https://github.com/pcaversaccio). It provides reusable modules for common patterns including ownership management, token standards (ERC-20, ERC-721, ERC-1155, ERC-4626), and various utility functions.
Snekmate's **authentication modules** offer ready-to-use implementations of the ownership patterns described above:
| Module | Description |
|---|---|
| `ownable` | Single-step ownership transfer via `transfer_ownership` |
| `ownable_2step` | Two-step ownership transfer matching Curve's **commit + accept** pattern |
| `access_control` | Multi-role-based access control with admin-managed role assignments |
### `ownable_2step`
The `ownable_2step` module implements a two-step ownership transfer mechanism functionally equivalent to Curve's commit + accept pattern. The current owner initiates a transfer with `transfer_ownership`, setting a `pending_owner`. The pending owner must then call `accept_ownership` to finalize the transfer.
```vyper
from snekmate.auth import ownable_2step as ow
initializes: ow
@deploy
def __init__():
ow.__init__()
@external
def protected_function():
ow._check_owner()
# only the owner can execute this
...
```
### Installation
Snekmate can be installed via multiple package managers:
```bash
# PyPI
pip install snekmate
# Foundry
forge install pcaversaccio/snekmate
# npm / yarn / pnpm
npm install --save-dev snekmate
```
:::info
For full documentation, module references, and examples, see the [**snekmate GitHub repository**](https://github.com/pcaversaccio/snekmate).
:::
---
## Mathematical Derivations
This page contains the mathematical derivations for the Newton's method implementations used in Curve's AMM contracts.
---
## StableSwap Derivations
The StableSwap invariant is defined as:
$$
A n^n \sum x_i + D = A D n^n + \frac{D^{n+1}}{n^n \prod x_i}
$$
Where:
- $A$ is the amplification coefficient
- $n$ is the number of coins
- $x_i$ is the amount of the $i$-th coin
- $D$ is the invariant (i.e. the total virtual balance when all coins are equal)
---
## Newton’s Method for Solving $D$
We derive Newton's iteration for solving $D$ given $\{x_i\}_{i=1}^n$ and $A$.
Start with:
$$
f(D) = A n^n \sum x_i + D(1 - A n^n) - \frac{D^{n+1}}{n^n \prod x_i} = 0
$$
Or equivalently:
$$
f(D) = \frac{D^{n+1}}{n^n \prod x_i} - D(1 - A n^n) - A n^n \sum x_i
$$
Taking the derivative:
$$
f'(D) = \frac{(n + 1) D^n}{n^n \prod x_i} + A n^n - 1
$$
### Newton Iteration
Using Newton’s formula:
$$
D_{\text{new}} = D - \frac{f(D)}{f'(D)} = \frac{D f'(D) - f(D)}{f'(D)}
$$
Substituting $f(D)$ and $f'(D)$:
$$
\begin{aligned}
D_{\text{new}} &= \frac{D \left(\frac{(n+1) D^n}{n^n \prod x_i} + A n^n - 1\right) - \left(\frac{D^{n+1}}{n^n \prod x_i} - D(1 - A n^n) - A n^n \sum x_i \right)}{\frac{(n+1) D^n}{n^n \prod x_i} + A n^n - 1} \\
&= \frac{\frac{(n+1) D^{n+1}}{n^n \prod x_i} + A n^n D - D - \frac{D^{n+1}}{n^n \prod x_i} + D - A n^n D + A n^n \sum x_i}{\frac{(n+1) D^n}{n^n \prod x_i} + A n^n - 1} \\
&= \frac{\frac{n D^{n+1}}{n^n \prod x_i} + A n^n \sum x_i}{\frac{(n+1) D^n}{n^n \prod x_i} + A n^n - 1}
\end{aligned}
$$
This corresponds to the `newton_D` function in `math.vy`.
---
## Newton’s Method for Solving $x_j$
Now derive Newton’s iteration for solving $x_j$, given $\{x_i\}_{i \neq j}$, $A$, and $D$.
Start with the invariant:
$$
A n^n \sum x_i + D = A D n^n + \frac{D^{n+1}}{n^n \prod x_i}
$$
Isolate $x_j$:
$$
A n^n \left(x_j + \sum_{i \neq j} x_i\right) + D = A D n^n + \frac{D^{n+1}}{n^n (x_j \prod_{i \neq j} x_i)}
$$
Multiply both sides by $x_j$:
$$
A n^n \left(x_j^2 + x_j \sum_{i \neq j} x_i \right) + D x_j = A D n^n x_j + \frac{D^{n+1}}{n^n \prod_{i \neq j} x_i}
$$
Divide by $A n^n$:
$$
x_j^2 + x_j \sum_{i \neq j} x_i + \frac{D}{A n^n} x_j = D x_j + \frac{D^{n+1}}{A n^{2n} \prod_{i \neq j} x_i}
$$
Bring terms to one side:
$$
x_j^2 + x_j \left(\sum_{i \neq j} x_i + \frac{D}{A n^n} - D\right) - \frac{D^{n+1}}{A n^{2n} \prod_{i \neq j} x_i} = 0
$$
Define:
$$
f(x_j) = x_j^2 + x_j \left(\sum_{i \neq j} x_i + \frac{D}{A n^n} - D\right) - \frac{D^{n+1}}{A n^{2n} \prod_{i \neq j} x_i}
$$
Taking the derivative:
$$
f'(x_j) = 2 x_j + \sum_{i \neq j} x_i + \frac{D}{A n^n} - D
$$
### Newton Iteration
$$
x_j^{\text{new}} = x_j - \frac{f(x_j)}{f'(x_j)} = \frac{x_j f'(x_j) - f(x_j)}{f'(x_j)}
$$
Substitute in the expressions:
$$
\begin{aligned}
x_j^{\text{new}} &= \frac{x_j \left(2 x_j + \sum_{i \neq j} x_i + \frac{D}{A n^n} - D\right) - \left(x_j^2 + x_j \left(\sum_{i \neq j} x_i + \frac{D}{A n^n} - D\right) - \frac{D^{n+1}}{A n^{2n} \prod_{i \neq j} x_i} \right)}{2 x_j + \sum_{i \neq j} x_i + \frac{D}{A n^n} - D} \\
&= \frac{x_j^2 + \frac{D^{n+1}}{A n^{2n} \prod_{i \neq j} x_i}}{2 x_j + \sum_{i \neq j} x_i + \frac{D}{A n^n} - D}
\end{aligned}
$$
This corresponds to the `newton_x` function in `math.vy`.
---
## Cryptoswap Derivations
### Newton Step for `newton_D()` in Tricrypto and Twocrypto
This derivation explains the mathematical logic behind the `newton_D()` function used in Curve’s tricrypto and twocrypto pools.
---
## Invariant Function Definitions
We start with the core function:
$$
F = K D^{n-1} S + P - K D^n - \left(\frac{D}{n}\right)^n
$$
Where:
- $D$: the invariant
- $S = \sum x_i$
- $P = \prod x_i$
- $n$: number of tokens (typically 2 or 3)
- $\gamma$: price scale parameter
- $A$: amplification coefficient
---
### Intermediate Definitions
$$
K = \frac{A K_0 \gamma^2}{(\gamma + 1 - K_0)^2}, \quad K_0 = \frac{P n^n}{D^n}
$$
$$
g = \gamma + 1 - K_0, \quad \hat{A} = n^n A
$$
$$
m_1 = \frac{D g^2}{\hat{A} \gamma^2}, \quad m_2 = \frac{2n K_0}{g}
$$
$$
\text{neg\_fprime} = S + S m_2 + \frac{m_1 n}{K_0} - m_2 D
$$
---
## Derivative of $K$
$$
K' = \left( \frac{A K_0 \gamma^2}{(\gamma + 1 - K_0)^2} \right)'
$$
$$
= \left( \frac{A \gamma^2}{(\gamma + 1 - K_0)^2} + \frac{2 A K_0 \gamma^2}{(\gamma + 1 - K_0)^3} \right) \cdot \left( -n \frac{P n^n}{D^{n+1}} \right)
$$
$$
= -n \left( \frac{A \gamma^2}{g^2} + \frac{2 A K_0 \gamma^2}{g^3} \right) \cdot \frac{K_0}{D}
$$
---
## Derivative of $F$
$$
F = K D^{n-1} S + P - K D^n - \left( \frac{D}{n} \right)^n
$$
Taking the derivative:
$$
F' = (n-1) K D^{n-2} S - n K D^{n-1} - \frac{D^{n-1}}{n^{n-1}} + K' D^{n-1} (S - D)
$$
Substitute $K$ and $K'$:
$$
F' = - \frac{A K_0 \gamma^2}{g^2} D^{n-2} S - \frac{2 n A K_0^2 \gamma^2}{g^3} D^{n-2} (S - D) - \frac{D^{n-1}}{n^{n-1}}
$$
---
## Derivation of $\frac{F}{F'}$
$$
\frac{F}{F'} =
\frac
{
\frac{A K_0 \gamma^2}{g^2} D^{n-1} S + P - \frac{A K_0 \gamma^2}{g^2} D^n - \left( \frac{D}{n} \right)^n
}
{
- \frac{A K_0 \gamma^2}{g^2} D^{n-2} S
+ \frac{2 n A K_0^2 \gamma^2}{g^3} D^{n-2}(D - S)
- \left( \frac{D}{n} \right)^{n-1}
}
$$
Divide numerator and denominator by $D^n / n^n$:
$$
= \frac
{
\frac{\hat{A} K_0 \gamma^2}{g^2} D^{-1} S + K_0 - \frac{\hat{A} K_0 \gamma^2}{g^2} - 1
}
{
- \frac{\hat{A} K_0 \gamma^2}{g^2} D^{-2} S + \frac{2n \hat{A} K_0^2 \gamma^2}{g^3} D^{-2} (D - S) - \frac{n}{D}
}
$$
Divide numerator and denominator by $\frac{\hat{A} \gamma^2}{g^2 D}$:
$$
= \frac
{
K_0 S + m_1 (K_0 - 1) - K_0 D
}
{
- \frac{K_0 S}{D} + \frac{m_2 K_0}{D} (D - S) - \frac{n m_1}{D}
}
$$
Multiply numerator and denominator by $D$:
$$
= \frac
{
K_0 S D + m_1 (K_0 - 1) D - K_0 D^2
}
{
- K_0 S + m_2 K_0 (D - S) - n m_1
}
$$
Divide numerator and denominator by $K_0$:
$$
= \frac
{
S D + m_1 \left(1 - \frac{1}{K_0}\right) D - D^2
}
{
- S + m_2 (D - S) - \frac{n m_1}{K_0}
}
$$
Distribute:
$$
= \frac
{
S D + m_1 \left(1 - \frac{1}{K_0} \right) D - D^2
}
{
- S - m_2 S + m_2 D - \frac{n m_1}{K_0}
}
$$
Substitute the denominator with $-\text{neg\_fprime}$:
$$
= \frac{S D + m_1 \left(1 - \frac{1}{K_0} \right) D - D^2}{-\text{neg\_fprime}}
$$
---
## Newton Iteration Step
$$
D_{k+1} = D_k - \frac{F}{F'} = D_k + \frac{S D_k + m_1 \left(1 - \frac{1}{K_0} \right) D_k - D_k^2}{\text{neg\_fprime}}
$$
$$
= \frac{\text{neg\_fprime} D_k + S D_k + m_1 \left(1 - \frac{1}{K_0} \right) D_k - D_k^2}{\text{neg\_fprime}}
$$
---
## Final Form: Positive and Negative Contributions
Separate into two parts:
**Positive Term $D_+$:**
$$
D_+ = \frac{(\text{neg\_fprime} + S) D_k}{\text{neg\_fprime}}
$$
**Negative Term $D_-$:**
$$
D_- = \frac{D_k^2 - m_1 \left( \frac{K_0 - 1}{K_0} \right) D_k}{\text{neg\_fprime}}
$$
**Final Newton Step:**
$$
D_{k+1} = D_+ - D_-
$$
---
## Notebooks
Notebooks are easy-to-use notebooks written in Python aiming to showcase the usage of smart contracts and their functionalities. They can be interactively designed and directly run in the browser to make the user experience as easy and smooth as possible.
:::github[GitHub]
All hosted Jupyter notebooks can also be found in the [ `curve-notebooks`](https://github.com/CurveDocs/curve-notebook) repository. A full list of all hosted notebooks can be found [here](#notebook-list).
:::
---
## Google Colab and JupyterHub
The first notebooks were hosted on a [JupyterHub server from Vyper](https://try.vyperlang.org/hub/). Due to performance issues, hosting was switched to [Google Colab](https://colab.google/). Old notebooks remain hosted on the JupyterHub server, but all new ones will be hosted on Google Colab.
---
## Vyper and Titanoboa
All Curve Smart Contracts are written in [Vyper](https://github.com/vyperlang).
For notebooks, mostly [Titanoboa](https://github.com/vyperlang/titanoboa) is used. Titanoboa is a Vyper interpreter with pretty tracebacks, forking, debugging features, and more! Titanoboa's goal is to provide a modern, advanced, and integrated development experience for Vyper users.
:::colab[Notebook: Titanoboa Guide]
A very simple notebook on the basic usage of Titanoboa and how it's used throughout all the notebooks can be found here: [https://colab.research.google.com/drive/1zHMuvNVZP8oB-Q1dA8NqgGLFpLI2JGni?usp=sharing](https://colab.research.google.com/drive/1zHMuvNVZP8oB-Q1dA8NqgGLFpLI2JGni).
:::
---
## How to run Notebooks
For notebooks hosted on Google Colab, a user only needs to set up two "Secrets." For consistency, all notebooks use a secret named `RPC_ETHEREUM` for HTTP API keys (e.g., from [Alchemy](https://www.alchemy.com/)) and a `ETHERSCAN_API_KEY` secret holding a valid [Etherscan API key](https://docs.etherscan.io/getting-started/viewing-api-usage-statistics).
After setting up these two secrets, the notebook can successfully be run directly in the browser.
---
## Notebook List
### Curve Lending
| Contract | Description | Link |
| :-------: | ----------- | :-----: |
| [`Vault`](../lending/contracts/vault.md) | Obtaining Vault Shares: `deposit` and `mint` | [:logos-googlecolab: here](https://colab.research.google.com/drive/1Qj9nOk5TYXp6j6go3VIh6--r5VILnoo9?usp=sharing) |
| [`Vault`](../lending/contracts/vault.md) | Withdrawing Assets: `withdraw` and `redeem` | [:logos-googlecolab: here](https://colab.research.google.com/drive/1Ta69fsIc7zmtjFlQ94a8MDYYLeo4GJJI?usp=sharing) |
| [`OneWayLendingFactory`](../lending/contracts/oneway-factory.md) | Changing default borrow rates: `set_default_rates` | [:logos-googlecolab: here](https://colab.research.google.com/drive/1mQV5yDyBqZrVSIOweP2g1Qu3WWjsgZtv?usp=sharing) |
| [`OneWayLendingFactory`](../lending/contracts/oneway-factory.md) | Changing implementations: `set_implementations` | [:logos-googlecolab: here](https://colab.research.google.com/drive/1r3Vhb28Wy8iX_YRBNpfnwjzS4dKuMADf?usp=sharing) |
| [`Controller`](../crvusd/controller.md) | Creating a simple loan: `create_loan` | [:logos-googlecolab: here](https://colab.research.google.com/drive/1MTtpbdeTDVB3LxzGhFc4vwLsDM_xJWKz?usp=sharing) |
---
## Useful Resources
This section hosts useful resources for the various components of the Curve ecosystem.
## Stableswap
- https://atulagarwal.dev/posts/curveamm/stableswap/
- https://xord.com/research/curve-stableswap-a-comprehensive-mathematical-guide/
- https://miguelmota.com/blog/understanding-stableswap-curve/
- https://hackmd.io/@alltold/curve-magic
- https://medium.com/defireturns/impermanent-loss-and-apy-for-curves-lps-f75aa2e8c9d6
## Cryptoswap
- https://nagaking.substack.com/p/deep-dive-curve-v2-parameters
- https://0xreviews.xyz/posts/2022-02-28-curve-newton-method
- https://twitter.com/0xstan_/status/1644931391111725057?s=46&t=HudpwDodTBLJargV6p63IA
- https://medium.com/defireturns/impermanent-loss-and-apy-for-curves-lps-f75aa2e8c9d6
## Curve Stablecoin (crvUSD)
- https://crvusd-rate.0xreviews.xyz/
- https://twitter.com/definikola/status/1674430800107044871
- https://mirror.xyz/0x290101596c9f85eB7194f6090a8c94fF5AAa32ca/esqF1zwoaZ4ZSIjt-faZZiuKwLLw34nD0SGlqD2fZ6Q
- https://mirror.xyz/albertlin.eth/H0m3nyq65anotTWhTdWDIWEfMPOofNPy-0qyARYXNF4
- https://www.curve.wiki/post/from-uniswap-v3-to-crvusd-llamma-%E8%8B%B1%E6%96%87%E7%89%88
- https://www.youtube.com/watch?v=p5G9injrXk8&t=2602s
- https://x.com/0xnocta/status/1659111335542571009
- https://curve.substack.com/p/august-15-2023-all-or-nothing?utm_campaign=post&utm_medium=web&triedRedirect=true
- https://curve.substack.com/p/crvusd-faq
- https://community.chaoslabs.xyz/crv-usd/risk/overview
## Lending
- https://mixbytes.io/blog/modern-defi-lending-protocols-how-its-made-curve-llamalend
## Curve Integration
- https://blog.pessimistic.io/curvev1-integration-tips-a49af7b4b46a
- https://curve.substack.com/p/october-18-2022-how-meta?utm_campaign=10-18-22
- https://blog.curvemonitor.com/posts/exchange-received/
---
## Whitepapers
Curve Finance whitepapers covering core protocol, governance, and stablecoin mechanisms.
---
## Crosschain scrvUSD
`scrvUSD` on Ethereum is an ERC-4626 compatible token. While the contract provides a price through various methods, such as `pricePerShare` or `pricePerAsset`, it is not treated as an ERC-4626 token when bridged to other chains. Consequently, it will lack methods to return its continuously updating price. To address this, Curve uses a system to commit to and verify the price of `scrvUSD` on other chains.
The source code for the contracts is available on [GitHub](https://github.com/curvefi/curve-xdao):
- [:logos-vyper: `scrvUSDOracle.vy`](https://github.com/curvefi/curve-xdao/blob/feat/scrvusd-oracle/contracts/oracles/scrvUSDOracle.vy) written in [Vyper](https://vyperlang.org/) version `0.4.0`
- [:logos-vyper: `BlockHashOracle.vy`](https://github.com/curvefi/curve-xdao/blob/feat/scrvusd-oracle/contracts/oracles/BlockHashOracle.vy) written in [Vyper](https://vyperlang.org/) version `0.3.10`
- [:logos-solidity: `ScrvusdProver.sol`](https://github.com/curvefi/curve-xdao/blob/feat/scrvusd-oracle/contracts/provers/ScrvusdProver.sol) written in [Solidity](https://soliditylang.org/) version `0.8.18`
**NOTE: Source code and versions may vary between different chains.**
**:logos-optimism: Optimism**
|Contract | Address |
| ------------- | ---------------- |
| `scrvUSDOracle` | [`0xC772063cE3e622B458B706Dd2e36309418A1aE42`](https://optimistic.etherscan.io/address/0xC772063cE3e622B458B706Dd2e36309418A1aE42) |
| `Prover` | [`0x47ca04Ee05f167583122833abfb0f14aC5677Ee4`](https://optimistic.etherscan.io/address/0x47ca04Ee05f167583122833abfb0f14aC5677Ee4) |
| `BlockHashOracle` | [`0x988d1037e9608B21050A8EFba0c6C45e01A3Bce7`](https://optimistic.etherscan.io/address/0x988d1037e9608B21050A8EFba0c6C45e01A3Bce7) |
**:logos-fraxtal: Fraxtal**
| Contract | Address |
| ------------- | ---------------- |
| `scrvUSDOracle` | [`0x09F8D940EAD55853c51045bcbfE67341B686C071`](https://fraxscan.com/address/0x09F8D940EAD55853c51045bcbfE67341B686C071) |
| `Prover` | [`0x0094Ad026643994c8fB2136ec912D508B15fe0E5`](https://fraxscan.com/address/0x0094Ad026643994c8fB2136ec912D508B15fe0E5) |
| `BlockHashOracle` | [`0xbD2775B8eADaE81501898eB208715f0040E51882`](https://fraxscan.com/address/0xbD2775B8eADaE81501898eB208715f0040E51882) |
**:logos-base: Base**
| Contract | Address |
| ------------- | ---------------- |
| `scrvUSDOracle` | [`0x3d8EADb739D1Ef95dd53D718e4810721837c69c1`](https://basescan.org/address/0x3d8EADb739D1Ef95dd53D718e4810721837c69c1) |
| `Prover` | [`0x6a2691068C7CbdA03292Ba0f9c77A25F658bAeF5`](https://basescan.org/address/0x6a2691068C7CbdA03292Ba0f9c77A25F658bAeF5) |
| `BlockHashOracle` | [`0x3c0a405E914337139992625D5100Ea141a9C4d11`](https://basescan.org/address/0x3c0a405E914337139992625D5100Ea141a9C4d11) |
**:logos-mantle: Mantle**
| Contract | Address |
| ------------- | ---------------- |
| `scrvUSDOracle` | [`0xbD2775B8eADaE81501898eB208715f0040E51882`](https://mantlescan.xyz/address/0xbD2775B8eADaE81501898eB208715f0040E51882) |
| `Prover` | [`0x09F8D940EAD55853c51045bcbfE67341B686C071`](https://mantlescan.xyz/address/0x09F8D940EAD55853c51045bcbfE67341B686C071) |
| `BlockHashOracle` | [`0x004A476B5B76738E34c86C7144554B9d34402F13`](https://mantlescan.xyz/address/0x004A476B5B76738E34c86C7144554B9d34402F13) |
The cross-chain scrvUSD system operates through three main components working together:
1. **Block Hash Oracle**:
- Provides Ethereum block hash values across different chains.
- Maintains a record of the latest known Ethereum block hashes on L2s.
- Implemented as a separate contract to handle uncertain block timing and enable reuse.
2. **Prover**:
- Uses verified block hashes to validate storage proofs.
- Each block hash represents a Merkle tree containing various data, including storage slots.
- Verifies all storage slots needed for rate replication.
3. **scrvUSD Oracle**:
- Receives verified storage values from the Prover.
- Calculates and stores the scrvUSD rate.
- Implements time-weighted updates to prevent sudden changes that could enable sandwich attacks.
- Controls the rate of change using the `max_acceleration` parameter.
---
## scrvUSD Oracle
Contract that contains information about the price of scrvUSD. It uses a `max_acceleration` parameter to limit the rate of price updates. The oracle includes a `price_oracle` method to ensure compatibility with other smart contracts, such as Stableswap implementations.
## Price Methods
### `update_price`
::::description[`scrvUSDOracle.update_price(_parameters: uint256[ASSETS_PARAM_CNT + SUPPLY_PARAM_CNT]) -> uint256`]
Function to update the price of the scrvUSD token.
| Input | Type | Description |
| ------------- | --------- | ---------------------------- |
| `_parameters` | `uint256[ASSETS_PARAM_CNT + SUPPLY_PARAM_CNT]` | Parameters |
Returns: relative price change of final price with 10**18 precision (`uint256`).
Emits: `PriceUpdate` event.
```vyper
event PriceUpdate:
new_price: uint256 # price to achieve
at: uint256 # timestamp at which price will be achieved
struct Interval:
previous: uint256
future: uint256
# scrvUSD Vault rate replication
# 0 total_debt
# 1 total_idle
ASSETS_PARAM_CNT: constant(uint256) = 2
# 0 totalSupply
# 1 full_profit_unlock_date
# 2 profit_unlocking_rate
# 3 last_profit_update
# 4 balance_of_self
# 5 block.timestamp
SUPPLY_PARAM_CNT: constant(uint256) = 6
MAX_BPS_EXTENDED: constant(uint256) = 1_000_000_000_000
@external
def update_price(
_parameters: uint256[ASSETS_PARAM_CNT + SUPPLY_PARAM_CNT],
) -> uint256:
"""
@notice Update price using `_parameters`
@param _parameters Parameters of
@return Relative price change of final price with 10^18 precision
"""
assert msg.sender == self.prover
current_price: uint256 = self._price_per_share(block.timestamp)
new_price: uint256 = self._total_assets(_parameters) * 10 **18 //\
self._total_supply(_parameters)
# Price is always growing and updates are never from future,
# hence allow only increasing updates
future_price: uint256 = self.price.future
if new_price > future_price:
self.price = Interval(previous=current_price, future=new_price)
rel_price_change: uint256 = (new_price - current_price) * 10 **18 // current_price + 1 # 1 for rounding up
future_ts: uint256 = block.timestamp + rel_price_change // self.max_acceleration
self.time = Interval(previous=block.timestamp, future=future_ts)
log PriceUpdate(new_price, future_ts)
return new_price * 10 **18 // future_price
return 10 **18
@view
@internal
def _price_per_share(ts: uint256) -> uint256:
"""
@notice Using linear interpolation assuming updates are often enough
for absolute difference \approx relative difference
"""
price: Interval = self.price
time: Interval = self.time
if ts >= time.future:
return price.future
if ts <= time.previous:
return price.previous
return (price.previous * (time.future - ts) + price.future * (ts - time.previous)) // (time.future - time.previous)
@view
@internal
def _total_assets(parameters: uint256[ASSETS_PARAM_CNT + SUPPLY_PARAM_CNT]) -> uint256:
"""
@notice Total amount of assets that are in the vault and in the strategies.
"""
return parameters[0] + parameters[1]
@view
@internal
def _total_supply(parameters: uint256[ASSETS_PARAM_CNT + SUPPLY_PARAM_CNT]) -> uint256:
# Need to account for the shares issued to the vault that have unlocked.
return parameters[ASSETS_PARAM_CNT + 0] -\
self._unlocked_shares(
parameters[ASSETS_PARAM_CNT + 1], # full_profit_unlock_date
parameters[ASSETS_PARAM_CNT + 2], # profit_unlocking_rate
parameters[ASSETS_PARAM_CNT + 3], # last_profit_update
parameters[ASSETS_PARAM_CNT + 4], # balance_of_self
parameters[ASSETS_PARAM_CNT + 5], # block.timestamp
)
```
This example updates the price of the scrvUSD token.
```shell
>>> scrvUSDOracle.update_price([21000000000000000000, 5000000000000000000, 20000000000000000000, 1700000000, 100000000000000, 1699000000, 1000000000000000000, 1700000001])
1009393556372147140
```
::::
### `price`
::::description[`scrvUSDOracle.price() -> Interval: view`]
Getter for the previous and future price of crvUSD.
Returns: `Interval` struct containing `previous` and `future` prices (`Interval`).
```vyper
struct Interval:
previous: uint256
future: uint256
price: public(Interval) # price of asset per share
```
```shell
>>> scrvUSDOracle.price()
(1008353536323212312, 1009393556372147140)
```
::::
### `time`
::::description[`scrvUSDOracle.time() -> Interval: view`]
Getter for the previous and future time of when the price will be updated.
Returns: `Interval` struct containing `previous` and `future` timestamps (`Interval`).
```vyper
struct Interval:
previous: uint256
future: uint256
time: public(Interval)
```
```shell
>>> scrvUSDOracle.time()
(1700000000, 1700001000)
```
::::
### `pricePerShare`
::::description[`scrvUSDOracle.pricePerShare(_ts: uint256) -> uint256: view`]
:::warning
This function is not precise. The price is smoothed over time to eliminate sharp changes. Only timestamps near the future are supported.
:::
Getter for the price per share of the scrvUSD token. The function uses linear interpolation to calculate the price and assumes that updates are often enough for the absolute difference to be approximately equal to the relative difference.
| Input | Type | Description |
| ------------- | --------- | ---------------------------- |
| `_ts` | `uint256` | Timestamp to get the price at |
Returns: price per share of the scrvUSD token (`uint256`).
```vyper
struct Interval:
previous: uint256
future: uint256
@view
@external
def pricePerShare(ts: uint256=block.timestamp) -> uint256:
"""
@notice Get the price per share (pps) of the vault.
@dev NOT precise. Price is smoothed over time to eliminate sharp changes.
@param ts Timestamp to look price at. Only near future is supported.
@return The price per share.
"""
return self._price_per_share(ts)
@view
@internal
def _price_per_share(ts: uint256) -> uint256:
"""
@notice Using linear interpolation assuming updates are often enough
for absolute difference \approx relative difference
"""
price: Interval = self.price
time: Interval = self.time
if ts >= time.future:
return price.future
if ts <= time.previous:
return price.previous
return (price.previous * (time.future - ts) + price.future * (ts - time.previous)) // (time.future - time.previous)
```
```shell
>>> scrvUSDOracle.pricePerShare(1700000500)
1008873546347679726
```
::::
### `pricePerAsset`
::::description[`scrvUSDOracle.pricePerAsset(_ts: uint256) -> uint256: view`]
:::warning
This function is not precise. The price is smoothed over time to eliminate sharp changes. Only timestamps near the future are supported.
:::
Getter for the price per asset of the scrvUSD token. The function uses linear interpolation to calculate the price and assumes that updates are often enough for the absolute difference to be approximately equal to the relative difference.
| Input | Type | Description |
| ------------- | --------- | ---------------------------- |
| `_ts` | `uint256` | Timestamp to get the price at |
Returns: price per asset of the scrvUSD token (`uint256`).
```vyper
struct Interval:
previous: uint256
future: uint256
@view
@external
def pricePerAsset(ts: uint256=block.timestamp) -> uint256:
"""
@notice Get the price per asset of the vault.
@dev NOT precise. Price is smoothed over time to eliminate sharp changes.
@param ts Timestamp to look price at. Only near future is supported.
@return The price per share.
"""
return 10 **36 // self._price_per_share(ts)
@view
@internal
def _price_per_share(ts: uint256) -> uint256:
"""
@notice Using linear interpolation assuming updates are often enough
for absolute difference \approx relative difference
"""
price: Interval = self.price
time: Interval = self.time
if ts >= time.future:
return price.future
if ts <= time.previous:
return price.previous
return (price.previous * (time.future - ts) + price.future * (ts - time.previous)) // (time.future - time.previous)
```
```shell
>>> scrvUSDOracle.pricePerAsset(1700000500)
991206327886504218
```
::::
### `price_oracle`
::::description[`scrvUSDOracle.price_oracle() -> uint256: view`]
Getter for the price of the scrvUSD token. This function is an alias for `pricePerShare` and `pricePerAsset` and is made for compatibility reasons.
Returns: price of scrvUSD (`uint256`).
```vyper
struct Interval:
previous: uint256
future: uint256
@view
@external
def price_oracle(i: uint256=0) -> uint256:
"""
@notice Alias of `pricePerShare` and `pricePerAsset` made for compatability
@param i 0 for scrvusd per crvusd, 1 for crvusd per scrvusd
@return Price with 10^18 precision
"""
return self._price_per_share(block.timestamp) if i == 0 else 10 **36 // self._price_per_share(block.timestamp)
@view
@internal
def _price_per_share(ts: uint256) -> uint256:
"""
@notice Using linear interpolation assuming updates are often enough
for absolute difference \approx relative difference
"""
price: Interval = self.price
time: Interval = self.time
if ts >= time.future:
return price.future
if ts <= time.previous:
return price.previous
return (price.previous * (time.future - ts) + price.future * (ts - time.previous)) // (time.future - time.previous)
```
```shell
>>> scrvUSDOracle.price_oracle()
1008873546347679726
>>> scrvUSDOracle.price_oracle(1)
991206327886504218
```
::::
---
## Oracle Acceleration
Because the rates are stored over time, the price can change suddenly and can lead to sandwich attacks. To prevent this, the `max_acceleration` parameter is used to limit the rate of price updates.
### `max_acceleration`
::::description[`scrvUSDOracle.max_acceleration() -> uint256: view`]
Getter for the maximum acceleration. The value is set at initialization and can be changed by the `owner` using the [`set_max_acceleration`](#set_max_acceleration) function.
Returns: maximum acceleration (`uint256`).
```vyper
max_acceleration: public(uint256) # precision 10**18
@deploy
def __init__(_initial_price: uint256, _max_acceleration: uint256):
"""
@param _initial_price Initial price of asset per share (10**18)
@param _max_acceleration Maximum acceleration (10**12)
"""
self.price = Interval(previous=_initial_price, future=_initial_price)
self.time = Interval(previous=block.timestamp, future=block.timestamp)
self.max_acceleration = _max_acceleration
ownable.__init__()
```
```shell
>>> scrvUSDOracle.max_acceleration()
1000000000000
```
::::
### `set_max_acceleration`
::::description[`scrvUSDOracle.set_max_acceleration(_max_acceleration: uint256)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function is only callable by the `owner`.
:::
Function to set the maximum acceleration.
| Input | Type | Description |
| ------------- | --------- | ---------------------------- |
| `_max_acceleration` | `uint256` | Maximum acceleration |
```vyper
max_acceleration: public(uint256) # precision 10**18
@external
def set_max_acceleration(_max_acceleration: uint256):
"""
@notice Set maximum acceleration of scrvUSD.
Must be less than StableSwap's minimum fee.
fee / (2 * block_time) is considered to be safe.
@param _max_acceleration Maximum acceleration (per sec)
"""
ownable._check_owner()
assert 10 **8 <= _max_acceleration and _max_acceleration <= 10 **18
self.max_acceleration = _max_acceleration
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
```shell
>>> scrvUSDOracle.max_acceleration()
1000000000000000000
>>> scrvUSDOracle.set_max_acceleration(10**11)
>>> scrvUSDOracle.max_acceleration()
100000000000000000
```
::::
---
## Prover
### `prover`
::::description[`scrvUSDOracle.prover() -> address: view`]
Getter for the prover address. The address can be changed using the [`set_prover`](#set_prover) function.
Returns: prover contract (`address`).
```vyper
prover: public(address)
```
```shell
>>> scrvUSDOracle.prover()
'0x47ca04Ee05f167583122833abfb0f14aC5677Ee4'
```
::::
### `set_prover`
::::description[`scrvUSDOracle.set_prover(_prover: address)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function is only callable by the `owner`.
:::
Function to set the prover contract.
| Input | Type | Description |
| ------------- | --------- | ---------------------------- |
| `_prover` | `address` | Prover contract |
Emits: `SetProver` event.
```vyper
prover: public(address)
@external
def set_prover(_prover: address):
"""
@notice Set the account with prover permissions.
"""
ownable._check_owner()
self.prover = _prover
log SetProver(_prover)
```
```vyper
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
```
This example sets the prover to the `0x47ca04Ee05f167583122833abfb0f14aC5677Ee4` contract.
```shell
>>> scrvUSDOracle.prover()
'0x0000000000000000000000000000000000000000'
>>> scrvUSDOracle.set_prover('0x47ca04Ee05f167583122833abfb0f14aC5677Ee4')
>>> scrvUSDOracle.prover()
'0x47ca04Ee05f167583122833abfb0f14aC5677Ee4'
```
::::
---
## Block Hash Oracle
The `BlockHashOracle` contract is providing Ethereum's `blockhash(block number)` values. Optimism stores some latest known blockhash, so the OP stack oracle works like simply saving latest known.
### `commit`
::::description[`BlockHashOracle.commit() -> uint256`]
Function to commit (and apply) a block hash. Same as `apply()` but also saves the committer.
Returns: block number (`uint256`).
Emits: `CommitBlockHash` and `ApplyBlockHash` events.
```vyper
event CommitBlockHash:
committer: indexed(address)
number: indexed(uint256)
hash: bytes32
event ApplyBlockHash:
number: indexed(uint256)
hash: bytes32
L1_BLOCK: constant(IL1Block) = IL1Block(0x4200000000000000000000000000000000000015)
block_hash: public(HashMap[uint256, bytes32])
commitments: public(HashMap[address, HashMap[uint256, bytes32]])
@external
def commit() -> uint256:
"""
@notice Commit (and apply) a block hash.
@dev Same as `apply()` but saves committer
"""
number: uint256 = 0
hash: bytes32 = empty(bytes32)
number, hash = self._update_block_hash()
self.commitments[msg.sender][number] = hash
log CommitBlockHash(msg.sender, number, hash)
log ApplyBlockHash(number, hash)
return number
@internal
def _update_block_hash() -> (uint256, bytes32):
number: uint256 = convert(staticcall L1_BLOCK.number(), uint256)
hash: bytes32 = staticcall L1_BLOCK.hash()
self.block_hash[number] = hash
return number, hash
```
```shell
>>> BlockHashOracle.commit()
```
::::
### `apply`
::::description[`BlockHashOracle.apply() -> uint256`]
Function to apply a block hash.
Returns: block number (`uint256`).
Emits: `ApplyBlockHash` event.
```vyper
event CommitBlockHash:
committer: indexed(address)
number: indexed(uint256)
hash: bytes32
event ApplyBlockHash:
number: indexed(uint256)
hash: bytes32
L1_BLOCK: constant(IL1Block) = IL1Block(0x4200000000000000000000000000000000000015)
block_hash: public(HashMap[uint256, bytes32])
commitments: public(HashMap[address, HashMap[uint256, bytes32]])
@external
def apply() -> uint256:
"""
@notice Apply a block hash.
"""
number: uint256 = 0
hash: bytes32 = empty(bytes32)
number, hash = self._update_block_hash()
log ApplyBlockHash(number, hash)
return number
@internal
def _update_block_hash() -> (uint256, bytes32):
number: uint256 = convert(staticcall L1_BLOCK.number(), uint256)
hash: bytes32 = staticcall L1_BLOCK.hash()
self.block_hash[number] = hash
return number, hash
```
```shell
>>> BlockHashOracle.apply()
```
::::
### `get_block_hash`
::::description[`BlockHashOracle.get_block_hash(_number: uint256) -> bytes32: view`]
Getter for the block hash of a given block number. This function will revert if the block hash has not been set.
| Input | Type | Description |
| ----- | ---- | ----------- |
| `_number` | `uint256` | Block number |
Returns: block hash (`bytes32`).
```vyper
block_hash: public(HashMap[uint256, bytes32])
@view
@external
def get_block_hash(_number: uint256) -> bytes32:
"""
@notice Query the block hash of a block.
@dev Reverts for block numbers which have yet to be set.
"""
block_hash: bytes32 = self.block_hash[_number]
assert block_hash != empty(bytes32)
return block_hash
```
This example returns the block hash for block number 21192041 (on Ethereum).
```shell
>>> BlockHashOracle.get_block_hash(21192041)
'0x9db78f319e1bfde9cb0723b6e96de3dce6d378b01b341a5e45546ac4b7f7269a'
>>> BlockHashOracle.get_block_hash(21192042)
Error: Returned error: execution reverted
```
::::
### `block_hash`
::::description[`BlockHashOracle.block_hash(_number: uint256) -> bytes32: view`]
Getter for the block hash of a given block number.
| Input | Type | Description |
| ----- | ---- | ----------- |
| `_number` | `uint256` | Block number |
Returns: block hash (`bytes32`).
```vyper
block_hash: public(HashMap[uint256, bytes32])
```
This example returns the block hash for block number 21192041 (on Ethereum).
```shell
>>> BlockHashOracle.block_hash(21192041)
'0x9db78f319e1bfde9cb0723b6e96de3dce6d378b01b341a5e45546ac4b7f7269a'
>>> BlockHashOracle.block_hash(21192042)
'0x0000000000000000000000000000000000000000000000000000000000000000'
```
::::
### `commitments`
::::description[`BlockHashOracle.commitments(_committer: address, _number: uint256) -> bytes32: view`]
Getter for the block hash of a given block number.
| Input | Type | Description |
| ----- | ---- | ----------- |
| `_committer` | `address` | The committer's address |
| `_number` | `uint256` | The block number |
Returns: block hash (`bytes32`).
```vyper
commitments: public(HashMap[address, HashMap[uint256, bytes32]])
```
```shell
>>> BlockHashOracle.commitments('0x47ca04Ee05f167583122833abfb0f14aC5677Ee4', 21192041)
'0x9db78f319e1bfde9cb0723b6e96de3dce6d378b01b341a5e45546ac4b7f7269a'
```
::::
---
## scrvUSD Prover
### `prove`
::::description[`ScrvusdProver.prove(bytes, bytes) -> uint256`]
Function to prove parameters of scrvUSD rate.
| Input | Type | Description |
| ----- | ---- | ----------- |
| `_block_header_rlp` | `bytes` | The block header of any block |
| `_proof_rlp` | `bytes` | The state proof of the parameters |
Returns: relative price change (`uint256`).
```solidity
interface IBlockHashOracle {
function get_block_hash(uint256 _number) external view returns (bytes32);
}
interface IscrvUSDOracle {
function update_price(
uint256[2 + 6] memory _parameters
) external returns (uint256);
}
/// @title Scrvusd Prover
/// @author Curve Finance
contract ScrvusdProver {
using RLPReader for bytes;
using RLPReader for RLPReader.RLPItem;
address constant SCRVUSD =
0x0655977FEb2f289A4aB78af67BAB0d17aAb84367;
bytes32 constant SCRVUSD_HASH =
keccak256(abi.encodePacked(SCRVUSD));
address public immutable BLOCK_HASH_ORACLE;
address public immutable SCRVUSD_ORACLE;
uint256 constant PARAM_CNT = 2 + 6;
uint256 constant PROOF_CNT = PARAM_CNT - 1; // -1 for timestamp obtained from block header
constructor(address _block_hash_oracle, address _scrvusd_oracle) {
BLOCK_HASH_ORACLE = _block_hash_oracle;
SCRVUSD_ORACLE = _scrvusd_oracle;
}
/// Prove parameters of scrvUSD rate.
/// @param _block_header_rlp The block header of any block.
/// @param _proof_rlp The state proof of the parameters.
function prove(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (uint256) {
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(
_block_header_rlp
);
require(block_header.hash != bytes32(0)); // dev: invalid blockhash
require(
block_header.hash ==
IBlockHashOracle(BLOCK_HASH_ORACLE).get_block_hash(
block_header.number
)
); // dev: blockhash mismatch
// convert _proof_rlp into a list of `RLPItem`s
RLPReader.RLPItem[] memory proofs = _proof_rlp.toRlpItem().toList();
require(proofs.length == 1 + PROOF_CNT); // dev: invalid number of proofs
// 0th proof is the account proof for the scrvUSD contract
Verifier.Account memory account = Verifier.extractAccountFromProof(
SCRVUSD_HASH, // position of the account is the hash of its address
block_header.stateRootHash,
proofs[0].toList()
);
require(account.exists); // dev: scrvUSD account does not exist
// iterate over proofs
uint256[PROOF_CNT] memory PARAM_SLOTS = [
// Assets parameters
uint256(21), // total_debt
22, // total_idle
// Supply parameters
20, // totalSupply
38, // full_profit_unlock_date
39, // profit_unlocking_rate
40, // last_profit_update
uint256(keccak256(abi.encode(18, SCRVUSD))) // balance_of_self
// ts from block header
];
uint256[PARAM_CNT] memory params;
Verifier.SlotValue memory slot;
uint256 i = 0;
for (uint256 idx = 1; idx < 1 + PROOF_CNT; idx++) {
slot = Verifier.extractSlotValueFromProof(
keccak256(abi.encode(PARAM_SLOTS[i])),
account.storageRoot,
proofs[idx].toList()
);
// Some slots may not be used => not exist, e.g. total_idle
// require(slot.exists);
params[i] = slot.value;
i++;
}
params[i] = block_header.timestamp;
return IscrvUSDOracle(SCRVUSD_ORACLE).update_price(params);
}
}
```
```shell
>>> ScrvusdProver.prove(block_header_rlp, proof_rlp)
```
::::
### `BLOCK_HASH_ORACLE`
::::description[`ScrvusdProver.BLOCK_HASH_ORACLE() -> address: view`]
Getter for the `BlockHashOracle` contract.
Returns: `BlockHashOracle` contract (`address`).
```solidity
address public immutable BLOCK_HASH_ORACLE;
constructor(address _block_hash_oracle, address _scrvusd_oracle) {
BLOCK_HASH_ORACLE = _block_hash_oracle;
SCRVUSD_ORACLE = _scrvusd_oracle;
}
```
```shell
>>> ScrvusdProver.BLOCK_HASH_ORACLE()
'0x988d1037e9608B21050A8EFba0c6C45e01A3Bce7'
```
::::
### `SCRVUSD_ORACLE`
::::description[`ScrvusdProver.SCRVUSD_ORACLE() -> address: view`]
Getter for the `scrvUSDOracle` contract.
Returns: `scrvUSDOracle` contract (`address`).
```solidity
address public immutable SCRVUSD_ORACLE;
constructor(address _block_hash_oracle, address _scrvusd_oracle) {
BLOCK_HASH_ORACLE = _block_hash_oracle;
SCRVUSD_ORACLE = _scrvusd_oracle;
}
```
```shell
>>> ScrvusdProver.SCRVUSD_ORACLE()
'0xC772063cE3e622B458B706Dd2e36309418A1aE42'
```
::::
---
## Blockhash
*soon*
---
## scrvUSD Crosschain Oracle
:::vyper[`ScrvusdOracleV2.vy`]
The source code for the `ScrvusdOracleV2` contract is available on [GitHub](https://github.com/curvefi/storage-proofs/blob/main/contracts/scrvusd/oracles/ScrvusdOracleV2.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.4.0`.
The oracle contracts are deployed on various chains at: *soon*
:::
---
## Price Methods
The contract has three different functions for the scrvUSD share price (or its inverse when setting `_i = 1`) using different approximations:
- [`price_v0`](#price_v0) provides a lower-bound estimate of the scrvUSD share price (or its inverse when _i is 1) by combining a historically smoothed price with a raw price derived from previous vault parameters.
- [`price_v1`](#price_v1) returns an approximate share price (or its inverse when _i is 1) by calculating a raw price based on current timestamp data and stored parameters, assuming no external interactions have altered it.
- [`price_v2`](#price_v2) offers an alternative approximation (or its inverse when _i is 1) that factors in expected rewards accrual by using current block timestamps for both the price and parameter calculations.
### `update_price`
::::description[`ScrvusdOracleV2.update_price(_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256) -> uint256`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the `PRICE_PARAMETERS_VERIFIER` role.
:::
Function to update the price using scrvUSD vault parameters.
| Input | Type | Description |
| --------------- | --------- | ---------------------------- |
| `_parameters` | `uint256[ALL_PARAM_CNT]` | Parameters of the Yearn Vault |
| `_ts` | `uint256` | Timestamp at which the parameters are true |
| `_block_number` | `uint256` | Block number of parameters to linearize updates |
Returns: absolute relative price change of the final price with 10^18 precision (`uint256`).
Emits: `PriceUpdate` event.
```vyper
event PriceUpdate:
new_price: uint256 # price to achieve
price_params_ts: uint256 # timestamp at which price is recorded
block_number: uint256
# scrvUSD Vault rate replication
ALL_PARAM_CNT: constant(uint256) = 2 + 5
MAX_BPS_EXTENDED: constant(uint256) = 1_000_000_000_000
last_block_number: public(uint256) # Warning: used both for price parameters and unlock_time
# smoothening
last_prices: uint256[3]
last_update: uint256
# scrvUSD replication parameters
profit_max_unlock_time: public(uint256)
price_params: PriceParams
price_params_ts: uint256
@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256
) -> uint256:
"""
@notice Update price using `_parameters`
@param _parameters Parameters of Yearn Vault to calculate scrvUSD price
@param _ts Timestamp at which these parameters are true
@param _block_number Block number of parameters to linearize updates
@return Absolute relative price change of final price with 10^18 precision
"""
access_control._check_role(PRICE_PARAMETERS_VERIFIER, msg.sender)
# Allowing same block updates for fixing bad blockhash provided (if possible)
assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number = _block_number
self.last_prices = [self._price_v0(), self._price_v1(), self._price_v2()]
self.last_update = block.timestamp
ts: uint256 = self.price_params_ts
current_price: uint256 = self._raw_price(ts, ts)
self.price_params = PriceParams(
total_debt=_parameters[0],
total_idle=_parameters[1],
total_supply=_parameters[2],
full_profit_unlock_date=_parameters[3],
profit_unlocking_rate=_parameters[4],
last_profit_update=_parameters[5],
balance_of_self=_parameters[6],
)
self.price_params_ts = _ts
new_price: uint256 = self._raw_price(_ts, _ts)
log PriceUpdate(new_price, _ts, _block_number)
if new_price > current_price:
return (new_price - current_price) * 10**18 // current_price
return (current_price - new_price) * 10**18 // current_price
@view
def _price_v0() -> uint256:
return self._smoothed_price(
self.last_prices[0],
self._raw_price(self.price_params_ts, self.price_params.last_profit_update),
)
@view
def _price_v1() -> uint256:
return self._smoothed_price(
self.last_prices[1], self._raw_price(block.timestamp, self.price_params_ts)
)
@view
def _price_v2() -> uint256:
return self._smoothed_price(
self.last_prices[2], self._raw_price(block.timestamp, block.timestamp)
)
@view
def _smoothed_price(last_price: uint256, raw_price: uint256) -> uint256:
# Ideally should be (max_price_increment / 10**18) **(block.timestamp - self.last_update)
# Using linear approximation to simplify calculations
max_change: uint256 = (
self.max_price_increment * (block.timestamp - self.last_update) * last_price // 10**18
)
# -max_change <= (raw_price - last_price) <= max_change
if unsafe_sub(raw_price + max_change, last_price) > 2 * max_change:
return last_price + max_change if raw_price > last_price else last_price - max_change
return raw_price
@view
def _raw_price(ts: uint256, parameters_ts: uint256) -> uint256:
"""
@notice Price replication from scrvUSD vault
"""
parameters: PriceParams = self._obtain_price_params(parameters_ts)
return self._total_assets(parameters) * 10**18 // self._total_supply(parameters, ts)
```
```vyper
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
This example updates the price of scrvUSD.
```shell
>>> ScrvusdOracleV2.update_price()
```
::::
### `raw_price`
::::description[`ScrvusdOracleV2.raw_price(_i: uint256 = 0, _ts: uint256 = block.timestamp, _parameters_ts: uint256 = block.timestamp) -> uint256: view`]
Function to compute the raw approximated share or asset price without smoothening out.
| Input | Type | Description |
| ------------- | --------- | ---------------------------- |
| `_i` | `uint256` | 0 for `pricePerShare()` and 1 for `pricePerAsset()`; defaults to 0 |
| `_ts` | `uint256` | Timestamp at which to see the price (only near period is supported) |
| `_parameters_ts` | `uint256` | Timestamp for the price parameters |
Returns: raw `pricePerShare()` or `pricePerAsset()` (`uint256`).
```vyper
@view
@external
def raw_price(
_i: uint256 = 0, _ts: uint256 = block.timestamp, _parameters_ts: uint256 = block.timestamp
) -> uint256:
"""
@notice Get approximate `scrvUSD.pricePerShare()` without smoothening
@param _i 0 (default) for `pricePerShare()` and 1 for `pricePerAsset()`
@param _ts Timestamp at which to see price (only near period is supported)
"""
p: uint256 = self._raw_price(_ts, _parameters_ts)
return p if _i == 0 else 10**36 // p
@view
def _raw_price(ts: uint256, parameters_ts: uint256) -> uint256:
"""
@notice Price replication from scrvUSD vault
"""
parameters: PriceParams = self._obtain_price_params(parameters_ts)
return self._total_assets(parameters) * 10**18 // self._total_supply(parameters, ts)
@view
def _obtain_price_params(parameters_ts: uint256) -> PriceParams:
"""
@notice Obtain Price parameters true or assumed to be true at `parameters_ts`.
Assumes constant gain(in crvUSD rewards) through distribution periods.
@param parameters_ts Timestamp to obtain parameters for
@return Assumed `PriceParams`
"""
params: PriceParams = self.price_params
period: uint256 = self.profit_max_unlock_time
if params.last_profit_update + period >= parameters_ts:
return params
number_of_periods: uint256 = min(
(parameters_ts - params.last_profit_update) // period,
self.max_v2_duration,
)
# locked shares at moment params.last_profit_update
gain: uint256 = (
params.balance_of_self * (params.total_idle + params.total_debt) // params.total_supply
)
params.total_idle += gain * number_of_periods
# functions are reduced from `VaultV3._process_report()` given assumptions with constant gain
for _: uint256 in range(number_of_periods, bound=MAX_V2_DURATION):
new_balance_of_self: uint256 = (
params.balance_of_self
* (params.total_supply - params.balance_of_self) // params.total_supply
)
params.total_supply -= (
params.balance_of_self * params.balance_of_self // params.total_supply
)
params.balance_of_self = new_balance_of_self
if params.full_profit_unlock_date > params.last_profit_update:
# copy from `VaultV3._process_report()`
params.profit_unlocking_rate = params.balance_of_self * MAX_BPS_EXTENDED // (
params.full_profit_unlock_date - params.last_profit_update
)
else:
params.profit_unlocking_rate = 0
params.full_profit_unlock_date += number_of_periods * period
params.last_profit_update += number_of_periods * period
return params
@view
def _total_assets(p: PriceParams) -> uint256:
"""
@notice Total amount of assets that are in the vault and in the strategies.
"""
return p.total_idle + p.total_debt
@view
def _total_supply(p: PriceParams, ts: uint256) -> uint256:
# Need to account for the shares issued to the vault that have unlocked.
return p.total_supply - self._unlocked_shares(
p.full_profit_unlock_date,
p.profit_unlocking_rate,
p.last_profit_update,
p.balance_of_self,
ts, # block.timestamp
)
```
This example returns the raw share or asset price of scrvUSD.
```shell
>>> ScrvusdOracleV2.raw_price(0)
# returns pricePerShare()
>>> ScrvusdOracleV2.raw_price(1)
# returns pricePerAsset()
```
::::
### `price_v0`
::::description[`ScrvusdOracleV2.price_v0(_i: uint256 = 0) -> uint256: view`]
Getter for the lower bound of the share or asset price.
| Input | Type | Description |
| ------------- | --------- | ---------------------------- |
| `_i` | `uint256` | 0 for `pricePerShare()` and 1 for `pricePerAsset()` |
Returns: lower bound of `pricePerShare()` (`uint256`).
```vyper
struct PriceParams:
# assets
total_debt: uint256
total_idle: uint256
# supply
total_supply: uint256
full_profit_unlock_date: uint256
profit_unlocking_rate: uint256
last_profit_update: uint256
balance_of_self: uint256
last_block_number: public(uint256) # Warning: used both for price parameters and unlock_time
# smoothening
last_prices: uint256[3]
last_update: uint256
# scrvUSD replication parameters
profit_max_unlock_time: public(uint256)
price_params: PriceParams
price_params_ts: uint256
max_price_increment: public(uint256) # precision 10**18
max_v2_duration: public(uint256) # number of periods(weeks)
@view
@external
def price_v0(_i: uint256 = 0) -> uint256:
"""
@notice Get lower bound of `scrvUSD.pricePerShare()`
@dev Price is updated in steps, need to verify every % changed
@param _i 0 (default) for `pricePerShare()` and 1 for `pricePerAsset()`
"""
return self._price_v0() if _i == 0 else 10**36 // self._price_v0()
@view
def _price_v0() -> uint256:
return self._smoothed_price(
self.last_prices[0],
self._raw_price(self.price_params_ts, self.price_params.last_profit_update),
)
@view
def _smoothed_price(last_price: uint256, raw_price: uint256) -> uint256:
# Ideally should be (max_price_increment / 10**18) **(block.timestamp - self.last_update)
# Using linear approximation to simplify calculations
max_change: uint256 = (
self.max_price_increment * (block.timestamp - self.last_update) * last_price // 10**18
)
# -max_change <= (raw_price - last_price) <= max_change
if unsafe_sub(raw_price + max_change, last_price) > 2 * max_change:
return last_price + max_change if raw_price > last_price else last_price - max_change
return raw_price
@view
@external
def raw_price(
_i: uint256 = 0, _ts: uint256 = block.timestamp, _parameters_ts: uint256 = block.timestamp
) -> uint256:
"""
@notice Get approximate `scrvUSD.pricePerShare()` without smoothening
@param _i 0 (default) for `pricePerShare()` and 1 for `pricePerAsset()`
@param _ts Timestamp at which to see price (only near period is supported)
"""
p: uint256 = self._raw_price(_ts, _parameters_ts)
return p if _i == 0 else 10**36 // p
```
This example returns the lower bound of `pricePerShare()` or `pricePerAsset()`.
```shell
>>> ScrvusdOracleV2.price_v0(0)
# returns pricePerShare()
>>> ScrvusdOracleV2.price_v0(1)
# returns pricePerAsset()
```
::::
### `price_v1`
::::description[`ScrvusdOracleV2.price_v1(_i: uint256 = 0) -> uint256: view`]
Getter for the approximate share or asset price assuming no new interactions.
| Input | Type | Description |
| ------------- | --------- | ---------------------------- |
| `_i` | `uint256` | 0 for `pricePerShare()` and 1 for `pricePerAsset()` |
Returns: approximate `pricePerShare()` (`uint256`).
```vyper
struct PriceParams:
# assets
total_debt: uint256
total_idle: uint256
# supply
total_supply: uint256
full_profit_unlock_date: uint256
profit_unlocking_rate: uint256
last_profit_update: uint256
balance_of_self: uint256
last_block_number: public(uint256) # Warning: used both for price parameters and unlock_time
# smoothening
last_prices: uint256[3]
last_update: uint256
# scrvUSD replication parameters
profit_max_unlock_time: public(uint256)
price_params: PriceParams
price_params_ts: uint256
max_price_increment: public(uint256) # precision 10**18
max_v2_duration: public(uint256) # number of periods(weeks)
@view
@external
def price_v1(_i: uint256 = 0) -> uint256:
"""
@notice Get approximate `scrvUSD.pricePerShare()`
@dev Price is simulated as if noone interacted to change `scrvUSD.pricePerShare()`,
need to adjust rate when too off.
@param _i 0 (default) for `pricePerShare()` and 1 for `pricePerAsset()`
"""
return self._price_v1() if _i == 0 else 10**36 // self._price_v1()
@view
def _price_v1() -> uint256:
return self._smoothed_price(
self.last_prices[1], self._raw_price(block.timestamp, self.price_params_ts)
)
@view
def _smoothed_price(last_price: uint256, raw_price: uint256) -> uint256:
# Ideally should be (max_price_increment / 10**18) **(block.timestamp - self.last_update)
# Using linear approximation to simplify calculations
max_change: uint256 = (
self.max_price_increment * (block.timestamp - self.last_update) * last_price // 10**18
)
# -max_change <= (raw_price - last_price) <= max_change
if unsafe_sub(raw_price + max_change, last_price) > 2 * max_change:
return last_price + max_change if raw_price > last_price else last_price - max_change
return raw_price
@view
@external
def raw_price(
_i: uint256 = 0, _ts: uint256 = block.timestamp, _parameters_ts: uint256 = block.timestamp
) -> uint256:
"""
@notice Get approximate `scrvUSD.pricePerShare()` without smoothening
@param _i 0 (default) for `pricePerShare()` and 1 for `pricePerAsset()`
@param _ts Timestamp at which to see price (only near period is supported)
"""
p: uint256 = self._raw_price(_ts, _parameters_ts)
return p if _i == 0 else 10**36 // p
```
This example returns the approximate `pricePerShare()` or `pricePerAsset()`.
```shell
>>> ScrvusdOracleV2.price_v1(0)
# returns pricePerShare()
>>> ScrvusdOracleV2.price_v1(1)
# returns pricePerAsset()
```
::::
### `price_v2`
::::description[`ScrvusdOracleV2.price_v2(_i: uint256 = 0) -> uint256: view`]
Getter for the approximate share or asset price assuming constant rewards over time.
| Input | Type | Description |
| ------------- | --------- | ---------------------------- |
| `_i` | `uint256` | 0 for `pricePerShare()` and 1 for `pricePerAsset()` |
Returns: approximate `pricePerShare()` (`uint256`).
```vyper
struct PriceParams:
# assets
total_debt: uint256
total_idle: uint256
# supply
total_supply: uint256
full_profit_unlock_date: uint256
profit_unlocking_rate: uint256
last_profit_update: uint256
balance_of_self: uint256
last_block_number: public(uint256) # Warning: used both for price parameters and unlock_time
# smoothening
last_prices: uint256[3]
last_update: uint256
# scrvUSD replication parameters
profit_max_unlock_time: public(uint256)
price_params: PriceParams
price_params_ts: uint256
max_price_increment: public(uint256) # precision 10**18
max_v2_duration: public(uint256) # number of periods(weeks)
@view
@external
def price_v2(_i: uint256 = 0) -> uint256:
"""
@notice Get approximate `scrvUSD.pricePerShare()`
@dev Uses assumption that crvUSD gains same rewards.
@param _i 0 (default) for `pricePerShare()` and 1 for `pricePerAsset()`
"""
return self._price_v2() if _i == 0 else 10**36 // self._price_v2()
@view
def _price_v2() -> uint256:
return self._smoothed_price(
self.last_prices[2], self._raw_price(block.timestamp, block.timestamp)
)
@view
def _smoothed_price(last_price: uint256, raw_price: uint256) -> uint256:
# Ideally should be (max_price_increment / 10**18) **(block.timestamp - self.last_update)
# Using linear approximation to simplify calculations
max_change: uint256 = (
self.max_price_increment * (block.timestamp - self.last_update) * last_price // 10**18
)
# -max_change <= (raw_price - last_price) <= max_change
if unsafe_sub(raw_price + max_change, last_price) > 2 * max_change:
return last_price + max_change if raw_price > last_price else last_price - max_change
return raw_price
@view
@external
def raw_price(
_i: uint256 = 0, _ts: uint256 = block.timestamp, _parameters_ts: uint256 = block.timestamp
) -> uint256:
"""
@notice Get approximate `scrvUSD.pricePerShare()` without smoothening
@param _i 0 (default) for `pricePerShare()` and 1 for `pricePerAsset()`
@param _ts Timestamp at which to see price (only near period is supported)
"""
p: uint256 = self._raw_price(_ts, _parameters_ts)
return p if _i == 0 else 10**36 // p
```
This example returns the approximate `pricePerShare()` or `pricePerAsset()` using the assumption that crvUSD gains same rewards.
```shell
>>> ScrvusdOracleV2.price_v2(0)
# returns pricePerShare()
>>> ScrvusdOracleV2.price_v2(1)
# returns pricePerAsset()
```
::::
### `last_block_number`
::::description[`ScrvusdOracleV2.last_block_number() -> uint256: view`]
Getter for the block number corresponding to the most recent update applied to the oracle (either for the price or `profit_max_unlock_time`). This value is updated during calls to the `update_price()` function to ensure that only updates from the same or a later block are accepted to prevent outdated information from being used.
Returns: the last block number the oracle was updated (`uint256`).
```vyper
last_block_number: public(uint256) # Warning: used both for price parameters and unlock_time
```
This example returns the block number of the most recent update.
```shell
>>> ScrvusdOracleV2.last_block_number()
17153668
```
::::
---
## Adjustable Parameters
The oracle has the following adjustable parameters:
- [`profit_max_unlock_time`](#profit_max_unlock_time): period over which accrued profits are gradually unlocked to smooth the share price transition.
- [`max_price_increment`](#max_price_increment): caps the maximum rate at which the share price can change per second to prevent abrupt price fluctuations.
- [`max_v2_duration`](#max_v2_duration): limits the number of periods used in the v2 price approximation, restricting how far future reward accrual is projected.
To guard the respective functions which can change the parameters, the contract uses a snekmate module with different roles.
### `profit_max_unlock_time`
::::description[`ScrvusdOracleV2.profit_max_unlock_time() -> uint256: view`]
Getter for the duration in seconds over which rewards are gradually unlocked, thereby smoothing out share price adjustments. It is initially set to one week (7 * 86400 seconds) to align with the current Yearn Vault setting and can only be updated by the `VERIFIER` role using the [`update_profit_max_unlock_time`](#update_profit_max_unlock_time) function.
Returns: profit max unlock time (`uint256`).
```vyper
profit_max_unlock_time: public(uint256)
@deploy
def __init__(_initial_price: uint256):
"""
@param _initial_price Initial price of asset per share (10**18)
"""
...
self.profit_max_unlock_time = 7 * 86400 # Week by default
...
```
This example returns the current `profit_max_unlock_time`.
```shell
>>> ScrvusdOracleV2.profit_max_unlock_time()
604800
```
::::
### `update_profit_max_unlock_time`
::::description[`ScrvusdOracleV2.update_profit_max_unlock_time(_profit_max_unlock_time: uint256, _block_number: uint256) -> bool`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the `UNLOCK_TIME_VERIFIER` role.
:::
Function to set a new value for `profit_max_unlock_time`. This happens within the [`ScrvUSDVeriferV2`](./verifier.md#scrvusd-verifier-v2) contract when a period is verified using a block hash ([`verifyPeriodByBlockHash()`](./verifier.md#verifyperiodbyblockhash)).
| Input | Type | Description |
| ------------- | --------- | ---------------------------- |
| `_profit_max_unlock_time` | `uint256` | New `profit_max_unlock_time` value |
| `_block_number` | `uint256` | Block number of parameters to linearize updates |
Returns: boolean whether the value changed (`bool`).
```vyper
from snekmate.auth import access_control
initializes: access_control
exports: (
access_control.supportsInterface,
access_control.hasRole,
access_control.DEFAULT_ADMIN_ROLE,
access_control.grantRole,
access_control.revokeRole,
)
UNLOCK_TIME_VERIFIER: public(constant(bytes32)) = keccak256("UNLOCK_TIME_VERIFIER")
last_block_number: public(uint256) # Warning: used both for price parameters and unlock_time
profit_max_unlock_time: public(uint256)
@external
def update_profit_max_unlock_time(_profit_max_unlock_time: uint256, _block_number: uint256) -> bool:
"""
@notice Update price using `_parameters`
@param _profit_max_unlock_time New `profit_max_unlock_time` value
@param _block_number Block number of parameters to linearize updates
@return Boolean whether value changed
"""
access_control._check_role(UNLOCK_TIME_VERIFIER, msg.sender)
# Allowing same block updates for fixing bad blockhash provided (if possible)
assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number = _block_number
prev_value: uint256 = self.profit_max_unlock_time
self.profit_max_unlock_time = _profit_max_unlock_time
return prev_value != _profit_max_unlock_time
```
```vyper
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
This example updates the `profit_max_unlock_time` value.
```shell
>>> ScrvusdOracleV2.profit_max_unlock_time()
604800
>>> ScrvusdOracleV2.update_profit_max_unlock_time(302400, todo)
>>> ScrvusdOracleV2.profit_max_unlock_time()
302400
```
::::
### `max_price_increment`
::::description[`ScrvusdOracleV2.max_price_increment() -> uint256: view`]
Getter for the maximum allowed price increment per second for scrvusd, measured with a precision of $10^{18}$. It is initially set to `2 * 1012` — corresponding to 0.02 bps per second (or approximately 0.24 bps per block on Ethereum) and linearly approximated to a maximum of 63% APY — and can be updated via the [`set_max_price_increment`](#set_max_price_increment) function.
Returns: max price increment per second (`uint256`).
```vyper
max_price_increment: public(uint256) # precision 10**18
@deploy
def __init__(_initial_price: uint256):
"""
@param _initial_price Initial price of asset per share (10**18)
"""
...
# 2 * 10 **12 is equivalent to
# 1) 0.02 bps per second or 0.24 bps per block on Ethereum
# 2) linearly approximated to max 63% APY
self.max_price_increment = 2 * 10**12
....
```
This example returns the maximum price increment per second of scrvusd.
```shell
>>> ScrvusdOracleV2.max_price_increment()
2000000000000
```
::::
### `set_max_price_increment`
::::description[`ScrvusdOracleV2.set_max_price_increment(_max_price_increment: uint256)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the `DEFAULT_ADMIN_ROLE` role.
:::
Function to set a new value for `max_price_increment`. The new value must be less than the stableswaps minimum fee.
$\frac{\text{fee}}{2 \cdot \text{block\_time}}$ is considered to be safe.
| Input | Type | Description |
| ------------- | --------- | ---------------------------- |
| `_max_price_increment` | `uint256` | New `max_price_increment` value |
Emits: `SetMaxPriceIncrement` event.
```vyper
from snekmate.auth import access_control
initializes: access_control
exports: (
access_control.supportsInterface,
access_control.hasRole,
access_control.DEFAULT_ADMIN_ROLE,
access_control.grantRole,
access_control.revokeRole,
)
event SetMaxPriceIncrement:
max_acceleration: uint256
max_price_increment: public(uint256) # precision 10**18
@external
def set_max_price_increment(_max_price_increment: uint256):
"""
@notice Set maximum price increment of scrvUSD.
Must be less than StableSwap's minimum fee.
fee / (2 * block_time) is considered to be safe.
@param _max_price_increment Maximum acceleration (per sec)
"""
access_control._check_role(access_control.DEFAULT_ADMIN_ROLE, msg.sender)
assert 10**8 <= _max_price_increment and _max_price_increment <= 10**18
self.max_price_increment = _max_price_increment
log SetMaxPriceIncrement(_max_price_increment)
```
```vyper
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
This example updates the `max_price_increment` value.
```shell
>>> ScrvusdOracleV2.max_price_increment()
2000000000000
>>> ScrvusdOracleV2.set_max_price_increment(3000000000000)
>>> ScrvusdOracleV2.max_price_increment()
3000000000000
```
::::
### `max_v2_duration`
::::description[`ScrvusdOracleV2.max_v2_duration() -> uint256: view`]
Getter for the maximum duration for which the price_v2 approximation can be applied before capping further growth. It is initially set to 24 weeks and can be updated via the [`set_max_v2_duration`](#set_max_v2_duration) function.
Returns: max v2 duration (`uint256`).
```vyper
max_v2_duration: public(uint256) # number of periods(weeks)
@deploy
def __init__(_initial_price: uint256):
"""
@param _initial_price Initial price of asset per share (10**18)
"""
...
self.max_v2_duration = 4 * 6 # half a year
...
```
This example returns the `max_v2_duration` value.
```shell
>>> ScrvusdOracleV2.max_v2_duration()
24
```
::::
### `set_max_v2_duration`
::::description[`ScrvusdOracleV2.set_max_v2_duration(_max_v2_duration: uint256)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the `DEFAULT_ADMIN_ROLE` role.
:::
Function to set a new value for `max_v2_duration`. The new value must be less than `MAX_V2_DURATION` (4 years).
| Input | Type | Description |
| ------------- | --------- | ---------------------------- |
| `_max_v2_duration` | `uint256` | Maximum v2 approximation duration (in weeks) |
Emits: `SetMaxV2Duration` event.
```vyper
from snekmate.auth import access_control
initializes: access_control
exports: (
access_control.supportsInterface,
access_control.hasRole,
access_control.DEFAULT_ADMIN_ROLE,
access_control.grantRole,
access_control.revokeRole,
)
event SetMaxV2Duration:
max_v2_duration: uint256
MAX_V2_DURATION: constant(uint256) = 4 * 12 * 4 # 4 years
max_v2_duration: public(uint256) # number of periods(weeks)
@external
def set_max_v2_duration(_max_v2_duration: uint256):
"""
@notice Set maximum v2 approximation duration after which growth will be stopped.
@param _max_v2_duration Maximum v2 approximation duration (in number of periods)
"""
access_control._check_role(access_control.DEFAULT_ADMIN_ROLE, msg.sender)
assert _max_v2_duration <= MAX_V2_DURATION
self.max_v2_duration = _max_v2_duration
log SetMaxV2Duration(_max_v2_duration)
```
```vyper
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
This example updates the `max_v2_duration` value.
```shell
>>> ScrvusdOracleV2.max_v2_duration()
24
>>> ScrvusdOracleV2.set_max_v2_duration(26)
>>> ScrvusdOracleV2.max_v2_duration()
26
```
::::
---
## Snekmate Access Control
The contract makes use of the `access_control.vy` module for access control. More [here](https://github.com/pcaversaccio/snekmate/blob/main/src/snekmate/auth/access_control.vy).
---
## scrvUSD Crosschain Oracle V2
`scrvUSD` on Ethereum is an ERC-4626 compatible token. While the contract provides a price through various methods, such as `pricePerShare` or `pricePerAsset`, it is not treated as an ERC-4626 token when bridged to other chains. Consequently, it will lack methods to return its continuously updating price. To address this, Curve uses a system to commit to and verify the price of `scrvUSD` on other chains.
---
*The system relies on the following actors:*
- An offchain prover (from now on the prover), whose role is to fetch data from Ethereum that are useful to compute the growth rate of the vault, alongside with a proof that those data are valid.
- A smart contract that will be called by the prover (from now on the verifier) that will verify that the data provided alongside their proof.
- A smart contract that will provide the current price of scrvUSD, given the growth rate of the vault provided by the prover and verified by the verifier, to be used by the stableswap-ng pool on the target chain.
*Depending on the type of chain the proof (and hence its verification process) will be different:*
- On OP Stack-based chains the verifier will expect a blockhash (to be matched with the one available in a precompile) and a state proof of the memory slots relevant to the growth rate computation.
- On Taiko Stack-based chains the verifier will expect the blocknumber and a state proof of the memory slots relevant to the growth rate computation.
- On all other chains the prover will provide the same data as for the OP Stack, and relevant data to verify the proof will be bridged from Optimism using L0.
Here's the flowchart of the system for an OP Stack-based chain:
```mermaid
flowchart TD
A[Prover] --> |Generates from L1 state| E[State Proof]
E[State Proof] --> B[Verifier Contract]
B -->|Push update price if proof is correct| C[Price Oracle Contract]
C -->|Provides scrvUSD price| D[stableswap-ng Pool]
subgraph L2 Chain
E2[Precompile] --> |Used to obtain| E1
E1[L1 Blockhash] --> B
end
```
---
**Safety** The prover doesn't need to be trusted as the safety of the whole system relies on the fact that it is not feasible to push an update with a forged proof.
**Liveness** The prover needs to be online to provide the proof in a timely manner, if the prover is offline the system might not be able to provide a correct (or accurate) price for scrvUSD.
---
## scrvUSD Verifier
The two verifier contracts work together to securely update and maintain the scrvUSD oracle using **on-chain state proofs**.
`ScrvusdVerifierV1` extracts scrvUSD vault parameters (such as total debt, idle funds, supply, and profit unlocking metrics) from state proofs. It validates these parameters by verifying the block header or state root against the `BlockHashOracle` and then updates the scrvUSD oracle's price via its `update_price` (see oracle documentation) function.
`ScrvusdVerifierV2` focuses specifically on updating the profit unlocking duration (`profit_max_unlock_time`). It uses similar state proof techniques---verifying either an RLP-encoded block header or a state root---to extract the period value. This period is then sent to the scrvUSD oracle via the `update_profit_max_unlock_time` function.
*Together, these contracts ensure that the scrvUSD oracle remains accurate by securely integrating verified on-chain data.*
---
## scrvUSD Verifier V1
:::solidity[`ScrvusdVerifierV1.sol`]
The source code for the `ScrvusdVerifierV1` contract is available on [GitHub](https://github.com/curvefi/storage-proofs/blob/main/contracts/scrvusd/verifiers/ScrvusdVerifierV1.sol). The contract is written in [Solidity](https://soliditylang.org/) version `0.8.18`.
:::
### `verifyScrvusdByBlockHash`
::::description[`ScrvusdVerifierV1.verifyScrvusdByBlockHash(_block_header_rlp: bytes, _proof_rlp: bytes) -> uint256`]
This function verifies scrvUSD parameters using an RLP-encoded block header and a corresponding state proof. It parses the block header to ensure the `BlockHash` is valid and matches the expected value from the `BlockHashOracle`, then extracts the scrvUSD vault parameters from the state proof. It then updates the scrvUSD oracle with these parameters, returning the absolute relative price change scaled to $10^{18}$ precision.
| Input | Type | Description |
| ------------------- | ------- | ----------------------------------------------------------------- |
| `_block_header_rlp` | `bytes` | RLP-encoded block header containing block details |
| `_proof_rlp` | `bytes` | RLP-encoded state proof for the scrvUSD parameters |
Returns: absolute relative price change of the scrvUSD price (`uint256`).
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
uint256 constant PARAM_CNT = 2 + 5;
uint256 constant PROOF_CNT = 1 + PARAM_CNT;
interface IScrvusdOracle {
function update_price(
uint256[PARAM_CNT] memory _parameters,
uint256 _ts,
uint256 _block_number
) external returns (uint256);
}
interface IBlockHashOracle {
function get_block_hash(uint256 _number) external view returns (bytes32);
function get_state_root(uint256 _number) external view returns (bytes32);
}
contract ScrvusdVerifierV1 {
using RLPReader for bytes;
using RLPReader for RLPReader.RLPItem;
// Common constants
address constant SCRVUSD = 0x0655977FEb2f289A4aB78af67BAB0d17aAb84367;
bytes32 constant SCRVUSD_HASH = keccak256(abi.encodePacked(SCRVUSD));
// Storage slots of parameters
uint256[PROOF_CNT] internal PARAM_SLOTS = [
uint256(0), // filler for account proof
uint256(21), // total_debt
uint256(22), // total_idle
uint256(20), // totalSupply
uint256(38), // full_profit_unlock_date
uint256(39), // profit_unlocking_rate
uint256(40), // last_profit_update
uint256(keccak256(abi.encode(18, SCRVUSD))) // balanceOf(self)
];
address public immutable SCRVUSD_ORACLE;
address public immutable BLOCK_HASH_ORACLE;
constructor(address _block_hash_oracle, address _scrvusd_oracle)
{
BLOCK_HASH_ORACLE = _block_hash_oracle;
SCRVUSD_ORACLE = _scrvusd_oracle;
}
/// @param _block_header_rlp The RLP-encoded block header
/// @param _proof_rlp The state proof of the parameters
function verifyScrvusdByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (uint256) {
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
require(block_header.hash != bytes32(0), "Invalid blockhash");
require(
block_header.hash == IBlockHashOracle(BLOCK_HASH_ORACLE).get_block_hash(block_header.number),
"Blockhash mismatch"
);
uint256[PARAM_CNT] memory params = _extractParametersFromProof(block_header.stateRootHash, _proof_rlp);
return _updatePrice(params, block_header.timestamp, block_header.number);
}
/// @dev Extract parameters from the state proof using the given state root.
function _extractParametersFromProof(
bytes32 stateRoot,
bytes memory proofRlp
) internal view returns (uint256[PARAM_CNT] memory) {
RLPReader.RLPItem[] memory proofs = proofRlp.toRlpItem().toList();
require(proofs.length == PROOF_CNT, "Invalid number of proofs");
// Extract account proof
Verifier.Account memory account = Verifier.extractAccountFromProof(
SCRVUSD_HASH,
stateRoot,
proofs[0].toList()
);
require(account.exists, "scrvUSD account does not exist");
// Extract slot values
uint256[PARAM_CNT] memory params;
for (uint256 i = 1; i < PROOF_CNT; i++) {
Verifier.SlotValue memory slot = Verifier.extractSlotValueFromProof(
keccak256(abi.encode(PARAM_SLOTS[i])),
account.storageRoot,
proofs[i].toList()
);
// Slots might not exist, but typically we just read them.
params[i - 1] = slot.value;
}
return params;
}
/// @dev Calls the oracle to update the price parameters.
/// Both child contracts use the same oracle call, differing only in how they obtain the timestamp.
function _updatePrice(
uint256[PARAM_CNT] memory params,
uint256 ts,
uint256 number
) internal returns (uint256) {
return IScrvusdOracle(SCRVUSD_ORACLE).update_price(params, ts, number);
}
}
```
```shell
>>> ScrvusdVerifierV1.verifyScrvusdByBlockHash(block_header_rlp, proof_rlp)
```
::::
### `verifyScrvusdByStateRoot`
::::description[`ScrvusdVerifierV1.verifyScrvusdByStateRoot(_block_number: uint256, _proof_rlp: bytes) -> uint256`]
This function verifies scrvUSD parameters by retrieving the state root for a given block number from the block hash oracle and then extracting the scrvUSD vault parameters using a state proof. The extracted parameters are used to update the scrvUSD oracle, returning the absolute relative price change scaled to $10^{18}$ precision.
| Input | Type | Description |
| --------------- | ----------- | ----------------------------------------------------------------- |
| `_block_number` | `uint256` | Block number for which to retrieve the state root |
| `_proof_rlp` | `bytes` | RLP-encoded state proof for the scrvUSD parameters |
Returns: absolute relative price change of the scrvUSD price (`uint256`).
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
uint256 constant PARAM_CNT = 2 + 5;
uint256 constant PROOF_CNT = 1 + PARAM_CNT;
interface IScrvusdOracle {
function update_price(
uint256[PARAM_CNT] memory _parameters,
uint256 _ts,
uint256 _block_number
) external returns (uint256);
}
interface IBlockHashOracle {
function get_block_hash(uint256 _number) external view returns (bytes32);
function get_state_root(uint256 _number) external view returns (bytes32);
}
contract ScrvusdVerifierV1 {
using RLPReader for bytes;
using RLPReader for RLPReader.RLPItem;
// Common constants
address constant SCRVUSD = 0x0655977FEb2f289A4aB78af67BAB0d17aAb84367;
bytes32 constant SCRVUSD_HASH = keccak256(abi.encodePacked(SCRVUSD));
// Storage slots of parameters
uint256[PROOF_CNT] internal PARAM_SLOTS = [
uint256(0), // filler for account proof
uint256(21), // total_debt
uint256(22), // total_idle
uint256(20), // totalSupply
uint256(38), // full_profit_unlock_date
uint256(39), // profit_unlocking_rate
uint256(40), // last_profit_update
uint256(keccak256(abi.encode(18, SCRVUSD))) // balanceOf(self)
];
address public immutable SCRVUSD_ORACLE;
address public immutable BLOCK_HASH_ORACLE;
constructor(address _block_hash_oracle, address _scrvusd_oracle)
{
BLOCK_HASH_ORACLE = _block_hash_oracle;
SCRVUSD_ORACLE = _scrvusd_oracle;
}
/// @param _block_number Number of the block to use state root hash
/// @param _proof_rlp The state proof of the parameters
function verifyScrvusdByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (uint256) {
bytes32 state_root = IBlockHashOracle(BLOCK_HASH_ORACLE).get_state_root(_block_number);
uint256[PARAM_CNT] memory params = _extractParametersFromProof(state_root, _proof_rlp);
// Use last_profit_update as the timestamp surrogate
return _updatePrice(params, params[5], _block_number);
}
/// @dev Extract parameters from the state proof using the given state root.
function _extractParametersFromProof(
bytes32 stateRoot,
bytes memory proofRlp
) internal view returns (uint256[PARAM_CNT] memory) {
RLPReader.RLPItem[] memory proofs = proofRlp.toRlpItem().toList();
require(proofs.length == PROOF_CNT, "Invalid number of proofs");
// Extract account proof
Verifier.Account memory account = Verifier.extractAccountFromProof(
SCRVUSD_HASH,
stateRoot,
proofs[0].toList()
);
require(account.exists, "scrvUSD account does not exist");
// Extract slot values
uint256[PARAM_CNT] memory params;
for (uint256 i = 1; i < PROOF_CNT; i++) {
Verifier.SlotValue memory slot = Verifier.extractSlotValueFromProof(
keccak256(abi.encode(PARAM_SLOTS[i])),
account.storageRoot,
proofs[i].toList()
);
// Slots might not exist, but typically we just read them.
params[i - 1] = slot.value;
}
return params;
}
/// @dev Calls the oracle to update the price parameters.
/// Both child contracts use the same oracle call, differing only in how they obtain the timestamp.
function _updatePrice(
uint256[PARAM_CNT] memory params,
uint256 ts,
uint256 number
) internal returns (uint256) {
return IScrvusdOracle(SCRVUSD_ORACLE).update_price(params, ts, number);
}
}
```
```shell
>>> ScrvusdVerifierV1.verifyScrvusdByStateRoot(21192041, proof_rlp)
```
::::
---
## scrvUSD Verifier V2
:::solidity[`ScrvusdVerifierV2.sol`]
The source code for the `ScrvusdVerifierV2` contract is available on [GitHub](https://github.com/curvefi/storage-proofs/blob/main/contracts/scrvusd/verifiers/ScrvusdVerifierV2.sol). The contract is written in [Solidity](https://soliditylang.org/) version `0.8.18`.
:::
### `verifyPeriodByBlockHash`
::::description[`ScrvusdVerifierV2.verifyPeriodByBlockHash(_block_header_rlp: bytes, _proof_rlp: bytes) -> bool`]
This function verifies the period using an RLP-encoded block header and a corresponding state proof. It parses the block header to ensure the block hash is valid and matches the expected value from the block hash oracle, then extracts the period from the state proof. Finally, it uses the extracted period to update the scrvUSD oracle's `profit_max_unlock_time`.
| Input | Type | Description |
| ------------------- | ------- | ----------------------------------------------------------------- |
| `_block_header_rlp` | `bytes` | RLP-encoded block header containing block information |
| `_proof_rlp` | `bytes` | RLP-encoded state proof for the period |
Returns: boolean indicating whether the update was successful (`bool`).
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
interface IScrvusdOracleV2 {
function update_profit_max_unlock_time(
uint256 _profit_max_unlock_time,
uint256 _block_number
) external returns (bool);
}
contract ScrvusdVerifierV2 is ScrvusdVerifierV1 {
using RLPReader for bytes;
using RLPReader for RLPReader.RLPItem;
uint256 internal PERIOD_SLOT = 37; // profit_max_unlock_time
constructor(address _block_hash_oracle, address _scrvusd_oracle)
ScrvusdVerifierV1(_block_hash_oracle, _scrvusd_oracle) {}
/// @param _block_header_rlp The RLP-encoded block header
/// @param _proof_rlp The state proof of the period
function verifyPeriodByBlockHash(
bytes memory _block_header_rlp,
bytes memory _proof_rlp
) external returns (bool) {
Verifier.BlockHeader memory block_header = Verifier.parseBlockHeader(_block_header_rlp);
require(block_header.hash != bytes32(0), "Invalid blockhash");
require(
block_header.hash == IBlockHashOracle(ScrvusdVerifierV1.BLOCK_HASH_ORACLE).get_block_hash(block_header.number),
"Blockhash mismatch"
);
uint256 period = _extractPeriodFromProof(block_header.stateRootHash, _proof_rlp);
return IScrvusdOracleV2(SCRVUSD_ORACLE).update_profit_max_unlock_time(period, block_header.number);
}
/// @dev Extract period from the state proof using the given state root.
function _extractPeriodFromProof(
bytes32 stateRoot,
bytes memory proofRlp
) internal view returns (uint256) {
RLPReader.RLPItem[] memory proofs = proofRlp.toRlpItem().toList();
require(proofs.length == 2, "Invalid number of proofs");
// Extract account proof
Verifier.Account memory account = Verifier.extractAccountFromProof(
ScrvusdVerifierV1.SCRVUSD_HASH,
stateRoot,
proofs[0].toList()
);
require(account.exists, "scrvUSD account does not exist");
Verifier.SlotValue memory slot = Verifier.extractSlotValueFromProof(
keccak256(abi.encode(PERIOD_SLOT)),
account.storageRoot,
proofs[1].toList()
);
require(slot.exists);
return slot.value;
}
}
```
```shell
>>> ScrvusdVerifierV2.verifyPeriodByBlockHash(block_header_rlp, proof_rlp)
```
::::
### `verifyPeriodByStateRoot`
::::description[`ScrvusdVerifierV2.verifyPeriodByStateRoot(_block_number: uint256, _proof_rlp: bytes) -> bool`]
This function verifies the period by retrieving the state root for a given block number from the block hash oracle and then using a state proof to extract the period. The extracted period is used to update the scrvUSD oracle's `profit_max_unlock_time`.
| Input | Type | Description |
| --------------- | ----------- | -------------------------------------------------------- |
| `_block_number` | `uint256` | Block number for which to retrieve the state root |
| `_proof_rlp` | `bytes` | RLP-encoded state proof for the period |
Returns: boolean indicating whether the update was successful (`bool`).
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
interface IScrvusdOracleV2 {
function update_profit_max_unlock_time(
uint256 _profit_max_unlock_time,
uint256 _block_number
) external returns (bool);
}
contract ScrvusdVerifierV2 is ScrvusdVerifierV1 {
using RLPReader for bytes;
using RLPReader for RLPReader.RLPItem;
uint256 internal PERIOD_SLOT = 37; // profit_max_unlock_time
constructor(address _block_hash_oracle, address _scrvusd_oracle)
ScrvusdVerifierV1(_block_hash_oracle, _scrvusd_oracle) {}
/// @param _block_number Number of the block to use state root hash
/// @param _proof_rlp The state proof of the period
function verifyPeriodByStateRoot(
uint256 _block_number,
bytes memory _proof_rlp
) external returns (bool) {
bytes32 state_root = IBlockHashOracle(ScrvusdVerifierV1.BLOCK_HASH_ORACLE).get_state_root(_block_number);
uint256 period = _extractPeriodFromProof(state_root, _proof_rlp);
return IScrvusdOracleV2(SCRVUSD_ORACLE).update_profit_max_unlock_time(period, _block_number);
}
/// @dev Extract period from the state proof using the given state root.
function _extractPeriodFromProof(
bytes32 stateRoot,
bytes memory proofRlp
) internal view returns (uint256) {
RLPReader.RLPItem[] memory proofs = proofRlp.toRlpItem().toList();
require(proofs.length == 2, "Invalid number of proofs");
// Extract account proof
Verifier.Account memory account = Verifier.extractAccountFromProof(
ScrvusdVerifierV1.SCRVUSD_HASH,
stateRoot,
proofs[0].toList()
);
require(account.exists, "scrvUSD account does not exist");
Verifier.SlotValue memory slot = Verifier.extractSlotValueFromProof(
keccak256(abi.encode(PERIOD_SLOT)),
account.storageRoot,
proofs[1].toList()
);
require(slot.exists);
return slot.value;
}
}
```
```shell
>>> ScrvusdVerifierV2.verifyPeriodByStateRoot(21192041, proof_rlp)
```
::::
---
## Savings crvUSD
Savings crvUSD, in short scrvUSD, is a savings version of crvUSD.
---
## Smart Contracts
The documentation of the `FeeSplitter` contract can be found [here](https://docs.curve.fi/fees/FeeSplitter/).
The Vault contract is based on Yearn's `VaultV3.vy` contract, more precisely [version `3.0.4`](https://github.com/yearn/yearn-Vaults-v3/blob/104a2b233bc6d43ba40720d68355b04d2dc31795/contracts/VaultV3.vy). It is an `ERC4626` compliant Vault that handles all logic associated with deposits, withdrawals, strategy management, profit reporting, etc.
The `RewardsHandler` is a peripheral contract that implements the `weight` function needed by the [`FeeSplitter`](https://docs.curve.fi/fees/FeeSplitter/). This function enables the Vault to receive rewards to depositors based on the proportion of crvUSD deposited in the Vault.
Helper contract to calculate the circulating supply of crvUSD by excluding e.g. pre-minted crvUSD for flashloans.
System of contracts to provide the price of `scrvUSD` on other chains.
---
## Deployments
The main `scrvUSD` contracts are deployed on :logos-ethereum: Ethereum at the following addresses:
- `scrvUSD / Vault` at [0x0655977FEb2f289A4aB78af67BAB0d17aAb84367](https://etherscan.io/address/0x0655977FEb2f289A4aB78af67BAB0d17aAb84367)
- `RewardsHandler` at [0xe8d1e2531761406af1615a6764b0d5ff52736f56](https://etherscan.io/address/0xe8d1e2531761406af1615a6764b0d5ff52736f56)
- `StablecoinLens` at [0xe24e2db9f6bb40bbe7c1c025bc87104f5401ecd7](https://etherscan.io/address/0xe24e2db9f6bb40bbe7c1c025bc87104f5401ecd7)
Additionally, the following cross-chain versions (using [Oracle V0](./crosschain/oracle-v0.md)) of the `scrvUSD` token are deployed:
- :logos-optimism: `Optimism` at [0x289f635106d5b822a505b39ac237a0ae9189335b](https://optimistic.etherscan.io/address/0x289f635106d5b822a505b39ac237a0ae9189335b)
- :logos-base: `Base` at [0x646a737b9b6024e49f5908762b3ff73e65b5160c](https://basescan.org/address/0x646a737b9b6024e49f5908762b3ff73e65b5160c)
- :logos-fraxtal: `Fraxtal` at [0xaB94C721040b33aA8b0b4D159Da9878e2a836Ed0](https://fraxscan.com/address/0xaB94C721040b33aA8b0b4D159Da9878e2a836Ed0)
- :logos-fantom: `Fantom` at [`0x5191946500e75f0A74476F146dF7d386e52961d9`](https://ftmscout.com/address/0x5191946500e75f0A74476F146dF7d386e52961d9)
- :logos-bsc: `BinanceSmartChain` at [`0x0094Ad026643994c8fB2136ec912D508B15fe0E5`](https://bscscan.com/address/0x0094Ad026643994c8fB2136ec912D508B15fe0E5)
- :logos-avalanche: `Avalanche` at [`0xA3ea433509F7941df3e33857D9c9f212Ad4A4e64`](https://snowscan.xyz/address/0xA3ea433509F7941df3e33857D9c9f212Ad4A4e64)
---
## Vault Implementation Details
The Vault is an unmodified instance of the [Yearn V3 multi-strategy Vault](https://github.com/yearn/yearn-Vaults-v3) that accepts crvUSD deposits. The crvUSD deposited into the Vault are not rehypothecated, they sit idle in the Vault to earn yield.
This Vault aims to be as cheap as possible for users to deposit and withdraw funds. For this reason funds deposited in the Vault are not moved anywhere and are always available to be redeemed.
Although the Vault is called "multi-strategy" it actually doesn't contain any strategies. This is possible thanks to yearn Vaults' v3.0.3 ability to [report on self](https://github.com/yearn/yearn-Vaults-v3/pull/205).
---
## Rewards
The Vault receives a dynamic percentage of the interest fees generated by the crvUSD from the `FeeSplitter` contract which is then distributed linearly across all depositors with respect to their share of the total deposits. Inbetween the `FeeSplitter` and the `Vault`, there is a `RewardsHandler` contract that handles the calculation of the dynamic percentage, which is determined by the ratio of crvUSD deposited into the Vault over the total circulating supply of crvUSD.
---
## FeeSplitter Interaction and Weight Calculation
For the `FeeSplitter` to send funds to the `RewardsHandler`, the `RewardsHandler` must be added as a receiver in the `FeeSplitter` by the DAO. Once this condition is met, the `FeeSplitter` will send funds to the `RewardsHandler` according to what the `weight` function in the `RewardsHandler` returns (this value is dynamic).
The `take_snapshot` function allows anyone to take snapshots of the ratio of crvUSD in the Vault compared to the circulating supply of crvUSD. This ratio is used to determine the percentage of the fees that can be requested by the `FeeSplitter`.
For instance if the time-weighed average of the ratio is 0.1 (10% of the circulating supply is deposited into the Vault), the `FeeSplitter` will request 10% of the fees generated by the crvUSD controllers.
---
## Rewards Allocation
Rewards allocated to scrvUSD come from crvUSD interest fees or any external donations sent to the `RewardsHandler` contract.
The ultimate amount of rewards is dynamic and is determined by the ratio of the staked supply to the total supply of crvUSD. Although it is dynamic, the weight has an upper and lower bound.
Rewards are distributed to scrvUSD holders thought the `RewardsHandler` contract using a simple `process_rewards` function. This function permnissionlessly lets anyone distribute rewards to the crvUSD Vault.
Although the weight is dynamic, it has a upper and lower bound:
1. The lower bound is defined in the `RewardsHandler` contract as `minimum_weight`. This is the minimum percentage of rewards that scrvUSD will receive.
2. The upper bound is defined in the `FeeSplitter` and represents the maximum percentage of rewards that scrvUSD will receive from the FeeSplitter. The FeeSplitter allows for dynamic weights, which is the case for scrvUSD. This upper value can be checked in the `FeeSplitter` contract by calling `FeeSplitter.receivers(i)`, where `i` is the index of the receiver. This method returns the address and the maximum weight of the receiver.
---
## RewardsHandler
The `RewardsHandler` contract manages the distribution of crvUSD rewards to `Savings crvUSD (scrvUSD)`. The contract takes snapshots of the ratio of crvUSD deposited into the Vault relative to the total circulating supply of crvUSD to calculate a time-weighted average of this ratio to determine the amount of rewards to request from the `FeeSplitter`.
:::vyper[`RewardsHandler.vy`]
The source code for the `RewardsHandler.vy` contract is available on [GitHub](https://github.com/curvefi/scrvusd/blob/main/contracts/RewardsHandler.vy). The contract is written in [Vyper](https://vyperlang.org/) version `~=0.4`.
The contract is deployed on :logos-ethereum: Ethereum at [`0xe8d1e2531761406af1615a6764b0d5ff52736f56`](https://etherscan.io/address/0xe8d1e2531761406af1615a6764b0d5ff52736f56).
The source code was audited by [:logos-chainsecurity: ChainSecurity](https://www.chainsecurity.com/). The audit report is available on [GitHub](https://github.com/curvefi/scrvusd/blob/main/audits/ChainSecurity_Curve_scrvUSD_audit.pdf).
:::
---
## General Explainer
The weight allocated to the `RewardsHandler` in the `FeeSplitter` is determined by the time-weighted average of the ratio of crvUSD deposited into the Vault compared to the total circulating supply of crvUSD. The weight allocated to the `RewardsHandler` can be permissionlessly distributed as rewards to the `Savings Vault (scrvUSD)` by anyone calling the [`process_rewards`](#process_rewards) function.
To calculate this time-weighted average, the `RewardsHandler` uses a `TWA module` that takes snapshots of the deposited supply ratio and stores them in a `Snapshot` struct. All structs are stored in a dynamic array called `snapshots`. Each snapshot includes a ratio value and the timestamp at which it was taken.
```vyper
from contracts.interfaces import IStablecoinLens
@external
def take_snapshot():
"""
@notice Function that anyone can call to take a snapshot of the current
deposited supply ratio in the vault. This is used to compute the time-weighted
average of the TVL to decide on the amount of rewards to ask for (weight).
@dev There's no point in MEVing this snapshot as the rewards distribution rate
can always be reduced (if a malicious actor inflates the value of the snapshot)
or the minimum amount of rewards can always be increased (if a malicious actor
deflates the value of the snapshot).
"""
self._take_snapshot()
@internal
def _take_snapshot():
"""
@notice Internal function to take a snapshot of the current deposited supply
ratio in the vault.
"""
# get the circulating supply from a helper contract.
# supply in circulation = controllers' debt + peg keppers' debt
circulating_supply: uint256 = staticcall self.stablecoin_lens.circulating_supply()
# obtain the supply of crvUSD contained in the vault by checking its totalAssets.
# This will not take into account rewards that are not yet distributed.
supply_in_vault: uint256 = staticcall vault.totalAssets()
# here we intentionally reduce the precision of the ratio because the
# dynamic weight interface expects a percentage in BPS.
supply_ratio: uint256 = supply_in_vault * MAX_BPS // circulating_supply
twa._take_snapshot(supply_ratio)
```
```vyper
event SnapshotTaken:
value: uint256
timestamp: uint256
snapshots: public(DynArray[Snapshot, MAX_SNAPSHOTS])
min_snapshot_dt_seconds: public(uint256) # Minimum time between snapshots in seconds
twa_window: public(uint256) # Time window in seconds for TWA calculation
last_snapshot_timestamp: public(uint256) # Timestamp of the last snapshot
@internal
def _take_snapshot(_value: uint256):
"""
@notice Stores a snapshot of the tracked value.
@param _value The value to store.
"""
if (len(self.snapshots) == 0) or ( # First snapshot
self.last_snapshot_timestamp + self.min_snapshot_dt_seconds <= block.timestamp # after dt
):
self.last_snapshot_timestamp = block.timestamp
self.snapshots.append(
Snapshot(tracked_value=_value, timestamp=block.timestamp)
) # store the snapshot into the DynArray
log SnapshotTaken(_value, block.timestamp)
```
---
## Snapshots
Snapshots are used to calculate the time-weighted average (TWA) of the ratio between crvUSD deposited into the Vault and the total circulating supply of crvUSD. Each snapshot stores the ratio of crvUSD deposited in the Vault to the circulating supply of crvUSD, along with the timestamp when the snapshot was taken. Taking a snapshot is fully permissionless—anyone can take one by calling the [`take_snapshot`](#take_snapshot) function. The snapshot values are stored in a `Snapshot` struct, and each struct is saved in a dynamic array called `snapshots`.
```vyper
MAX_SNAPSHOTS: constant(uint256) = 10**18 # 31.7 billion years if snapshot every second
snapshots: public(DynArray[Snapshot, MAX_SNAPSHOTS])
struct Snapshot:
tracked_value: uint256
timestamp: uint256
```
Snapshots can only be taken once a minimum time interval ([`min_snapshot_dt_seconds`](#min_snapshot_dt_seconds)) has passed since the last one. The TWA is then computed using the trapezoidal rule, iterating over the stored snapshots in reverse chronological order to calculate the weighted average of the tracked value over the specified time window ([`twa_window`](#twa_window)).
*Snapshots are taken by calling the [`take_snapshot`](#take_snapshot) function. When this function is called, the snapshot value is computed and stored as follows:*
1. **Determine the circulating supply of crvUSD.** Directly calling `crvUSD.totalSupply()` is not feasible because some crvUSD is minted to specific contracts and is not part of the circulating supply (e.g., unborrowed crvUSD held by Controllers, crvUSD allocated to PegKeepers, or crvUSD assigned to the [`FlashLender`](../crvusd/flash-lender.md)). Therefore, the [`StablecoinLens`](./stablecoin-lens.md) contract is used to obtain the actual circulating supply of crvUSD.
2. **Obtain the amount of crvUSD held in the Vault** by calling `Vault.totalAssets()`, which excludes rewards that have not yet been distributed.
3. **Calculate the supply ratio** as follows:
$$\text{SupplyRatio} = \frac{\text{SupplyInVault} \times10^{18}}{\text{CirculatingSupply}}$$
4. **Store the calculated supply ratio**and the timestamp at which the snapshot was taken in the dynamic array.
---
### `take_snapshot`
::::description[`RewardsHandler.take_snapshot()`]
:::warning[MEVing Snapshot Taking]
There's no point in MEVing this snapshot as the rewards distribution rate can always be reduced (if a malicious actor inflates the value of the snapshot) or the minimum amount of rewards can always be increased (if a malicious actor deflates the value of the snapshot).
:::
Function to take a snapshot of the current deposited supply ratio in the Vault. This function is fully permissionless and can be called by anyone. Snapshots are used to compute the time-weighted average of the TVL to decide on the amount of rewards to ask for (weight).
Minimum time inbetween snapshots is defined by `min_snapshot_dt_seconds`. The maximum number of snapshots is set to `10^18`, which is equivalent to 31.7 billion years if a snapshot were to be taken every second.
Emits: `SnapshotTaken`
```vyper
@external
def take_snapshot():
"""
@notice Function that anyone can call to take a snapshot of the current
deposited supply ratio in the vault. This is used to compute the time-weighted
average of the TVL to decide on the amount of rewards to ask for (weight).
@dev There's no point in MEVing this snapshot as the rewards distribution rate
can always be reduced (if a malicious actor inflates the value of the snapshot)
or the minimum amount of rewards can always be increased (if a malicious actor
deflates the value of the snapshot).
"""
self._take_snapshot()
@internal
def _take_snapshot():
"""
@notice Internal function to take a snapshot of the current deposited supply
ratio in the vault.
"""
# get the circulating supply from a helper contract.
# supply in circulation = controllers' debt + peg keppers' debt
circulating_supply: uint256 = staticcall self.stablecoin_lens.circulating_supply()
# obtain the supply of crvUSD contained in the vault by checking its totalAssets.
# This will not take into account rewards that are not yet distributed.
supply_in_vault: uint256 = staticcall vault.totalAssets()
# here we intentionally reduce the precision of the ratio because the
# dynamic weight interface expects a percentage in BPS.
supply_ratio: uint256 = supply_in_vault * MAX_BPS // circulating_supply
twa._take_snapshot(supply_ratio)
```
```shell
>>> RewardsHandler.take_snapshot()
```
::::
### `snapshots`
::::description[`TWA.snapshots(arg: uint256) -> DynArray[Snapshot, MAX_SNAPSHOTS]`]
Getter for a `Snapshot` struct at a specific index. First snapshot is at index `0`, second at index `1`, etc.
Returns: `Snapshot` struct containing the ratio of deposited crvUSD into the Vault to the total circulating supply of crvUSD (`uint256`) and the timestamp (`uint256`).
| Input | Type | Description |
| ----- | --------- | ------------------------------------ |
| `arg` | `uint256` | Index of the snapshot to return |
```vyper
event SnapshotTaken:
value: uint256
timestamp: uint256
MAX_SNAPSHOTS: constant(uint256) = 10**18 # 31.7 billion years if snapshot every second
snapshots: public(DynArray[Snapshot, MAX_SNAPSHOTS])
struct Snapshot:
tracked_value: uint256
timestamp: uint256
@internal
def _take_snapshot(_value: uint256):
"""
@notice Stores a snapshot of the tracked value.
@param _value The value to store.
"""
if self.last_snapshot_timestamp + self.min_snapshot_dt_seconds <= block.timestamp:
self.last_snapshot_timestamp = block.timestamp
self.snapshots.append(
Snapshot(tracked_value=_value, timestamp=block.timestamp)
) # store the snapshot into the DynArray
log SnapshotTaken(_value, block.timestamp)
```
```shell
>>> RewardsHandler.snapshots(0)
(153, 1729000000)
```
::::
### `min_snapshot_dt_seconds`
::::description[`TWA.min_snapshot_dt_seconds() -> uint256: view`]
Getter for the minimum time between snapshots in seconds. This value can be changed using the [`set_twa_snapshot_dt`](#set_twa_snapshot_dt) function.
Returns: minimum time between snapshots in seconds (`uint256`).
```vyper
min_snapshot_dt_seconds: public(uint256) # Minimum time between snapshots in seconds
@deploy
def __init__(_twa_window: uint256, _min_snapshot_dt_seconds: uint256):
self._set_twa_window(_twa_window)
self._set_snapshot_dt(max(1, _min_snapshot_dt_seconds))
```
```shell
>>> RewardsHandler.min_snapshot_dt_seconds()
3600
```
::::
### `last_snapshot_timestamp`
::::description[`TWA.last_snapshot_timestamp() -> uint256: view`]
Getter for the timestamp of the last snapshot taken. This variable is adjusted each time a snapshot is taken.
Returns: timestamp of the last snapshot taken (`uint256`).
```vyper
last_snapshot_timestamp: public(uint256) # Timestamp of the last snapshot
@internal
def _take_snapshot(_value: uint256):
"""
@notice Stores a snapshot of the tracked value.
@param _value The value to store.
"""
if self.last_snapshot_timestamp + self.min_snapshot_dt_seconds <= block.timestamp:
self.last_snapshot_timestamp = block.timestamp
self.snapshots.append(
Snapshot(tracked_value=_value, timestamp=block.timestamp)
) # store the snapshot into the DynArray
log SnapshotTaken(_value, block.timestamp)
```
```shell
>>> RewardsHandler.last_snapshot_timestamp()
1729000000
```
::::
### `get_len_snapshots`
::::description[`TWA.get_len_snapshots() -> uint256: view`]
Getter for the total number of snapshots taken and stored. Increments by one each time a snapshot is taken.
Returns: total number of snapshots stored (`uint256`).
```vyper
snapshots: public(DynArray[Snapshot, MAX_SNAPSHOTS])
@external
@view
def get_len_snapshots() -> uint256:
"""
@notice Returns the number of snapshots stored.
"""
return len(self.snapshots)
```
```shell
>>> RewardsHandler.get_len_snapshots()
42
```
::::
---
## Weights and TWA
The `weight` represents the percentage of the total rewards requested from the `FeeSplitter`. This value is denominated in 10000 BPS (100%). E.g. if the weight is 500, then RewardsHandler will request 5% of the total rewards from the `FeeSplitter`.
The `weight` is computed as a time-weighted average (TWA) of the ratio between deposited crvUSD in the Vault and total circulating supply of crvUSD.
Weight calculation is handled using a time-weighted average (TWA) module. While this module can be used to calculate any kind of time-weighted value, the scrvUSD system uses it to compute the time-weighted average of the deposited crvUSD in the Vault compared to the total circulating crvUSD supply.
The value is calculated over a specified time window defined by `twa_window` by iterating backwards over the snapshots stored in the `snapshots` dynamic array.
### `compute_twa`
::::description[`TWA.compute_twa() -> uint256: view`]
Function to compute the time-weighted average of the ratio between deposited crvUSD in the Vault and total circulating supply of crvUSD by iterating over the stored snapshots in reverse chronological order.
Returns: time-weighted average of the ratio between deposited crvUSD and total circulating supply of crvUSD (`uint256`).
```vyper
snapshots: public(DynArray[Snapshot, MAX_SNAPSHOTS])
min_snapshot_dt_seconds: public(uint256) # Minimum time between snapshots in seconds
twa_window: public(uint256) # Time window in seconds for TWA calculation
last_snapshot_timestamp: public(uint256) # Timestamp of the last snapshot
struct Snapshot:
tracked_value: uint256
timestamp: uint256
@external
@view
def compute_twa() -> uint256:
"""
@notice External endpoint for _compute() function.
"""
return self._compute()
@internal
@view
def _compute() -> uint256:
"""
@notice Computes the TWA over the specified time window by iterating backwards over the snapshots.
@return The TWA for tracked value over the self.twa_window.
"""
num_snapshots: uint256 = len(self.snapshots)
if num_snapshots == 0:
return 0
time_window_start: uint256 = block.timestamp - self.twa_window
total_weighted_tracked_value: uint256 = 0
total_time: uint256 = 0
# Iterate backwards over all snapshots
index_array_end: uint256 = num_snapshots - 1
for i: uint256 in range(0, num_snapshots, bound=MAX_SNAPSHOTS): # i from 0 to (num_snapshots-1)
i_backwards: uint256 = index_array_end - i
current_snapshot: Snapshot = self.snapshots[i_backwards]
next_snapshot: Snapshot = current_snapshot
if i != 0: # If not the first iteration (last snapshot), get the next snapshot
next_snapshot = self.snapshots[i_backwards + 1]
# Time Axis (Increasing to the Right) --->
# SNAPSHOT
# |---------|---------|---------|------------------------|---------|---------|
# t0 time_window_start interval_start interval_end block.timestamp (Now)
interval_start: uint256 = current_snapshot.timestamp
# Adjust interval start if it is before the time window start
if interval_start < time_window_start:
interval_start = time_window_start
interval_end: uint256 = interval_start
if i == 0: # First iteration - we are on the last snapshot (i_backwards = num_snapshots - 1)
# For the last snapshot, interval end is block.timestamp
interval_end = block.timestamp
else:
# For other snapshots, interval end is the timestamp of the next snapshot
interval_end = next_snapshot.timestamp
if interval_end <= time_window_start:
break
time_delta: uint256 = interval_end - interval_start
# Interpolation using the trapezoidal rule
averaged_tracked_value: uint256 = (current_snapshot.tracked_value + next_snapshot.tracked_value) // 2
# Accumulate weighted rate and time
total_weighted_tracked_value += averaged_tracked_value * time_delta
total_time += time_delta
if total_time == 0 and len(self.snapshots) == 1:
# case when only snapshot is taken in the block where computation is called
return self.snapshots[0].tracked_value
assert total_time > 0, "Zero total time!"
twa: uint256 = total_weighted_tracked_value // total_time
return twa
```
```shell
>>> RewardsHandler.compute_twa()
153
```
::::
### `twa_window`
::::description[`TWA.twa_window() -> uint256: view`]
Getter for the time window in seconds which is applied to the TWA calculation, essentially the length of the time window over which the TWA is computed. This value can be changed using the [`set_twa_window`](#set_twa_window) function.
Returns: time window in seconds for TWA calculation (`uint256`).
```vyper
twa_window: public(uint256) # Time window in seconds for TWA calculation
@deploy
def __init__(_twa_window: uint256, _min_snapshot_dt_seconds: uint256):
self._set_twa_window(_twa_window)
self._set_snapshot_dt(max(1, _min_snapshot_dt_seconds))
```
```shell
>>> RewardsHandler.twa_window()
604800
```
::::
### `weight`
::::description[`RewardsHandler.weight() -> uint256: view`]
Getter for the weight of the rewards. This is the time-weighted average of the ratio between deposited crvUSD in the Vault and total circulating supply of crvUSD. This function is part of the dynamic weight interface expected by the `FeeSplitter` to know what percentage of funds should be sent for rewards distribution. Weight value is denominated in 10000 BPS (100%). E.g. if the weight is 2000, then `RewardsHandler` will request 20% of the total rewards from the `FeeSplitter`.
Returns: requested weight (`uint256`).
```vyper
MAX_BPS: constant(uint256) = 10**4 # 100%
# scaling factor for the deposited token / circulating supply ratio.
scaling_factor: public(uint256)
# the minimum amount of rewards requested to the FeeSplitter.
minimum_weight: public(uint256)
@external
@view
def weight() -> uint256:
"""
@notice this function is part of the dynamic weight interface expected by the
FeeSplitter to know what percentage of funds should be sent for rewards
distribution to scrvUSD vault depositors.
@dev `minimum_weight` acts as a lower bound for the percentage of rewards that
should be distributed to depositors. This is useful to bootstrapping TVL by asking
for more at the beginning and can also be increased in the future if someone
tries to manipulate the time-weighted average of the tvl ratio.
"""
raw_weight: uint256 = twa._compute() * self.scaling_factor // MAX_BPS
return max(raw_weight, self.minimum_weight)
```
```shell
>>> RewardsHandler.weight()
500
```
::::
### `minimum_weight`
::::description[`RewardsHandler.minimum_weight() -> uint256: view`]
Getter for the minimum weight. This is the minimum weight requested from the `FeeSplitter`. Value is set at initialization and can be changed by the [`set_minimum_weight`](#set_minimum_weight) function.
Returns: minimum weight (`uint256`).
```vyper
# the minimum amount of rewards requested to the FeeSplitter.
minimum_weight: public(uint256)
@deploy
def __init__(
_stablecoin: IERC20,
_vault: IVault,
minimum_weight: uint256,
scaling_factor: uint256,
controller_factory: lens.IControllerFactory,
admin: address,
):
...
self._set_minimum_weight(minimum_weight)
...
```
```shell
>>> RewardsHandler.minimum_weight()
500
```
::::
### `scaling_factor`
::::description[`RewardsHandler.scaling_factor() -> uint256: view`]
Getter for the scaling factor for the ratio between deposited crvUSD in the Vault and total circulating supply of crvUSD.
Returns: scaling factor (`uint256`).
```vyper
# scaling factor for the deposited token / circulating supply ratio.
scaling_factor: public(uint256)
```
```shell
>>> RewardsHandler.scaling_factor()
10000
```
::::
---
## Reward Distribution
Rewards are distributed to the Vault thought the `RewardsHandler` contract using a simple `process_rewards` function. This function permnissionlessly lets anyone distribute rewards to the Savings Vault.
### `process_rewards`
::::description[`RewardsHandler.process_rewards()`]
Function to process the crvUSD rewards by transferring the available balance to the Vault and then calling the `process_report` function to start streaming the rewards to scrvUSD. This function is permissionless and can be called by anyone. When calling this function, the contracts entire crvUSD balance will be transferred and used as rewards for the stakers.
```vyper
# the time over which rewards will be distributed mirror of the private
# `profit_max_unlock_time` variable from yearn vaults.
distribution_time: public(uint256)
@external
def process_rewards(take_snapshot: bool = True):
"""
@notice Permissionless function that let anyone distribute rewards (if any) to
the crvUSD vault.
"""
# optional (advised) snapshot before distributing the rewards
if take_snapshot:
self._take_snapshot()
# prevent the rewards from being distributed untill the distribution rate
# has been set
assert (staticcall vault.profitMaxUnlockTime() != 0), "rewards should be distributed over time"
# any crvUSD sent to this contract (usually through the fee splitter, but
# could also come from other sources) will be used as a reward for scrvUSD
# vault depositors.
available_balance: uint256 = staticcall stablecoin.balanceOf(self)
assert available_balance > 0, "no rewards to distribute"
# we distribute funds in 2 steps:
# 1. transfer the actual funds
extcall stablecoin.transfer(vault.address, available_balance)
# 2. start streaming the rewards to users
extcall vault.process_report(vault.address)
```
```shell
>>> RewardsHandler.process_rewards()
''
```
::::
### `distribution_time`
::::description[`RewardsHandler.distribution_time() -> uint256: view`]
Getter for the distribution time. This is the time it takes to stream the rewards.
Returns: distribution time (`uint256`).
```vyper
@view
@external
def distribution_time() -> uint256:
"""
@notice Getter for the distribution time of the rewards.
@return uint256 The time over which vault rewards will be distributed.
"""
return staticcall vault.profitMaxUnlockTime()
```
```shell
>>> RewardsHandler.distribution_time()
604800
```
::::
---
## Admin Controls
The contract uses the [Multi-Role-Based Access Control Module](https://github.com/pcaversaccio/snekmate/blob/main/src/snekmate/auth/access_control.vy) from [Snekmate](https://github.com/pcaversaccio/snekmate) to manage roles and permissions. This module ensures that only specific addresses assigned the `RATE_MANAGER` role can modify key parameters such as the Time-Weighted Average (TWA) window, the minimum time between snapshots, and the distribution time. Roles can only be granted or revoked by the `DEFAULT_ADMIN_ROLE` defined in the access module.
For a detailed explanation of how to use the access control module, please refer to the source code where its mechanics are explained in detail: [Snekmate access_control.vy](https://github.com/pcaversaccio/snekmate/blob/main/src/snekmate/auth/access_control.vy).
### `set_twa_window`
::::description[`RewardsHandler.set_twa_window(_twa_window: uint256)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function is restricted to the `RATE_MANAGER` role.
:::
Function to set a new value for the `twa_window` variable in the `TWA` module. This value represents the time window over which the time-weighted average (TWA) is calculated.
Emits: `TWAWindowUpdated` event.
| Input | Type | Description |
| ------------- | --------- | ---------------------------- |
| `_twa_window` | `uint256` | New value for the TWA window |
```vyper
from snekmate.auth import access_control
initializes: access_control
exports: (
# we don't expose `supportsInterface` from access control
access_control.grantRole,
access_control.revokeRole,
access_control.renounceRole,
access_control.set_role_admin,
access_control.DEFAULT_ADMIN_ROLE,
access_control.hasRole,
access_control.getRoleAdmin,
)
RATE_MANAGER: public(constant(bytes32)) = keccak256("RATE_MANAGER")
@external
def set_twa_window(_twa_window: uint256):
"""
@notice Setter for the time-weighted average window
@param _twa_window The time window used to compute the TWA value of the
balance/supply ratio.
"""
access_control._check_role(RATE_MANAGER, msg.sender)
twa._set_twa_window(_twa_window)
```
```vyper
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
This example sets the TWA window from 604800 seconds (1 week) to 302400 seconds (1/2 week).
```shell
>>> RewardsHandler.twa_window()
604800
>>> RewardsHandler.set_twa_window(302400)
>>> RewardsHandler.twa_window()
302400
```
::::
### `set_twa_snapshot_dt`
::::description[`RewardsHandler.set_twa_snapshot_dt(_min_snapshot_dt_seconds: uint256)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function is restricted to the `RATE_MANAGER` role.
:::
Function to set a new value for the `min_snapshot_dt_seconds` variable in the `TWA` module. This value represents the minimum time between snapshots.
Emits: `SnapshotIntervalUpdated` event.
| Input | Type | Description |
| -------------------------- | --------- | ------------------------------------ |
| `_min_snapshot_dt_seconds` | `uint256` | New value for the minimum time between snapshots |
```vyper
from snekmate.auth import access_control
initializes: access_control
exports: (
# we don't expose `supportsInterface` from access control
access_control.grantRole,
access_control.revokeRole,
access_control.renounceRole,
access_control.set_role_admin,
access_control.DEFAULT_ADMIN_ROLE,
access_control.hasRole,
access_control.getRoleAdmin,
)
RATE_MANAGER: public(constant(bytes32)) = keccak256("RATE_MANAGER")
@external
def set_twa_snapshot_dt(_min_snapshot_dt_seconds: uint256):
"""
@notice Setter for the time-weighted average minimal frequency.
@param _min_snapshot_dt_seconds The minimum amount of time that should pass
between two snapshots.
"""
access_control._check_role(RATE_MANAGER, msg.sender)
twa._set_snapshot_dt(_min_snapshot_dt_seconds)
```
```vyper
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
This example sets the minimum time between snapshots from 3600 seconds (1 hour) to 7200 seconds (2 hours).
```shell
>>> RewardsHandler.min_snapshot_dt_seconds()
3600
>>> RewardsHandler.set_twa_snapshot_dt(7200)
>>> RewardsHandler.min_snapshot_dt_seconds()
7200
```
::::
### `set_distribution_time`
::::description[`RewardsHandler.set_distribution_time(new_distribution_time: uint256)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function is restricted to the `RATE_MANAGER` role.
:::
Function to set the distribution time for the rewards. This is the time it takes to stream the rewards. Setting this value to 0 will immediately distribute all the rewards. If the value is set to a number greater than 0, the rewards will be distributed over the specified number of seconds.
Emits: `UpdateProfitMaxUnlockTime` and `StrategyReported` events from the `Vault` contract.
| Input | Type | Description |
| ---------------------- | --------- | ------------------------ |
| `new_distribution_time`| `uint256` | New distribution time |
```vyper
from snekmate.auth import access_control
initializes: access_control
exports: (
# we don't expose `supportsInterface` from access control
access_control.grantRole,
access_control.revokeRole,
access_control.renounceRole,
access_control.set_role_admin,
access_control.DEFAULT_ADMIN_ROLE,
access_control.hasRole,
access_control.getRoleAdmin,
)
RATE_MANAGER: public(constant(bytes32)) = keccak256("RATE_MANAGER")
@external
def set_distribution_time(new_distribution_time: uint256):
"""
@notice Admin function to correct the distribution rate of the rewards. Making
this value lower will reduce the time it takes to stream the rewards, making it
longer will do the opposite. Setting it to 0 will immediately distribute all the
rewards.
@dev This function can be used to prevent the rewards distribution from being
manipulated (i.e. MEV twa snapshots to obtain higher APR for the vault). Setting
this value to zero can be used to pause `process_rewards`.
"""
access_control._check_role(RATE_MANAGER, msg.sender)
# change the distribution time of the rewards in the vault
extcall vault.setProfitMaxUnlockTime(new_distribution_time)
# enact the changes
extcall vault.process_report(vault.address)
```
```vyper
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
This example sets the distribution time from 1 week to 1/2 week.
```shell
>>> RewardsHandler.distribution_time()
604800
>>> RewardsHandler.set_distribution_time(302400)
>>> RewardsHandler.distribution_time()
302400
```
::::
### `set_stablecoin_lens`
::::description[`RewardsHandler.set_stablecoin_lens(_lens: address)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function is restricted to the `LENS_MANAGER` role.
:::
Function to set a new `stablecoin_lens` address.
Emits: `StablecoinLensUpdated` event.
| Input | Type | Description |
| -------------------------- | --------- | ------------------------------------ |
| `_lens` | `address` | New `stablecoin_lens` address |
```vyper
from snekmate.auth import access_control
initializes: access_control
exports: (
# we don't expose `supportsInterface` from access control
access_control.grantRole,
access_control.revokeRole,
access_control.renounceRole,
access_control.set_role_admin,
access_control.DEFAULT_ADMIN_ROLE,
access_control.hasRole,
access_control.getRoleAdmin,
)
LENS_MANAGER: public(constant(bytes32)) = keccak256("LENS_MANAGER")
event StablecoinLensUpdated:
new_stablecoin_lens: IStablecoinLens
stablecoin_lens: public(IStablecoinLens)
@internal
def _set_stablecoin_lens(_lens: IStablecoinLens):
assert _lens.address != empty(address), "no lens"
self.stablecoin_lens = _lens
log StablecoinLensUpdated(_lens)
```
This example sets the `stablecoin_lens` address to `ZERO_ADDRESS`. This is just an example but would not make sense in practice.
```shell
>>> RewardsHandler.stablecoin_lens()
'0xe24e2dB9f6Bb40bBe7c1C025bc87104F5401eCd7'
>>> RewardsHandler.set_stablecoin_lens('0x0000000000000000000000000000000000000000')
>>> RewardsHandler.stablecoin_lens()
'0x0000000000000000000000000000000000000000'
```
::::
### `set_minimum_weight`
::::description[`RewardsHandler.set_minimum_weight(new_minimum_weight: uint256)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function is restricted to the `RATE_MANAGER` role.
:::
Function to set the minimum weight that the vault will ask for.
| Input | Type | Description |
| ---------------------- | --------- | ------------------------ |
| `new_minimum_weight` | `uint256` | New minimum weight |
```vyper
from snekmate.auth import access_control
initializes: access_control
exports: (
# we don't expose `supportsInterface` from access control
access_control.grantRole,
access_control.revokeRole,
access_control.renounceRole,
access_control.set_role_admin,
access_control.DEFAULT_ADMIN_ROLE,
access_control.hasRole,
access_control.getRoleAdmin,
)
event MinimumWeightUpdated:
new_minimum_weight: uint256
MAX_BPS: constant(uint256) = 10**4 # 100%
@external
def set_minimum_weight(new_minimum_weight: uint256):
"""
@notice Update the minimum weight that the the vault will ask for.
@dev This function can be used to prevent the rewards requested from being
manipulated (i.e. MEV twa snapshots to obtain lower APR for the vault). Setting
this value to zero makes the amount of rewards requested fully determined by the
twa of the deposited supply ratio.
"""
access_control._check_role(RATE_MANAGER, msg.sender)
self._set_minimum_weight(new_minimum_weight)
@internal
def _set_minimum_weight(new_minimum_weight: uint256):
assert new_minimum_weight <= MAX_BPS, "minimum weight should be <= 100%"
self.minimum_weight = new_minimum_weight
log MinimumWeightUpdated(new_minimum_weight)
```
```vyper
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
This example sets the minimum weight the `RewardsHandler` will ask for from 5% to 10%.
```shell
>>> RewardsHandler.minimum_weight()
500 # 5%
>>> RewardsHandler.set_minimum_weight(1000)
>>> RewardsHandler.minimum_weight()
1000 # 10%
```
::::
### `set_scaling_factor`
::::description[`RewardsHandler.set_scaling_factor(new_scaling_factor: uint256)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function is restricted to the `RATE_MANAGER` role.
:::
Function to change the scaling factor value.
Emits: `ScalingFactorUpdated` event.
```vyper
event ScalingFactorUpdated:
new_scaling_factor: uint256
# scaling factor for the deposited token / circulating supply ratio.
scaling_factor: public(uint256)
@external
def set_scaling_factor(new_scaling_factor: uint256):
"""
@notice Update the scaling factor that is used in the weight calculation.
This factor can be used to adjust the rewards distribution rate.
"""
access_control._check_role(RATE_MANAGER, msg.sender)
self._set_scaling_factor(new_scaling_factor)
@internal
def _set_scaling_factor(new_scaling_factor: uint256):
self.scaling_factor = new_scaling_factor
log ScalingFactorUpdated(new_scaling_factor)
```
```vyper
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
This example sets the scaling factor from 10000 to 15000.
```shell
>>> RewardsHandler.scaling_factor()
10000
>>> RewardsHandler.set_scaling_factor(15000)
>>> RewardsHandler.scaling_factor()
15000
```
::::
---
## Other Methods
### `vault`
::::description[`RewardsHandler.vault() -> address: view`]
Getter for the [YearnV3 Vault contract](https://github.com/yearn/yearn-vaults-v3). This contract address is at the same time also the address of the `scrvUSD` token.
Returns: YearnV3 Vault (`address`).
```vyper
from interfaces import IVault
vault: public(immutable(IVault))
@deploy
def __init__(
_stablecoin: IERC20,
_vault: IVault,
minimum_weight: uint256,
scaling_factor: uint256,
controller_factory: lens.IControllerFactory,
admin: address,
):
...
vault = _vault
```
```shell
>>> RewardsHandler.vault()
'0x0655977FEb2f289A4aB78af67BAB0d17aAb84367'
```
::::
### `stablecoin`
::::description[`RewardsHandler.stablecoin() -> address: view`]
Getter for the crvUSD stablecoin address.
Returns: crvUSD stablecoin (`address`).
```vyper
stablecoin: immutable(IERC20)
@deploy
def __init__(
_stablecoin: IERC20,
_vault: IVault,
minimum_weight: uint256,
scaling_factor: uint256,
controller_factory: lens.IControllerFactory,
admin: address,
):
...
stablecoin = _stablecoin
...
```
```shell
>>> RewardsHandler.stablecoin()
'0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'
```
::::
### `stablecoin_lens`
::::description[`RewardsHandler.stablecoin_lens() -> IStablecoinLens: view`]
Getter for the `stablecoin_lens` address. This value can be changed via the `set_stablecoin_lens` function.
Returns: `stablecoin_lens` contract (`address`).
```vyper
from contracts.interfaces import IStablecoinLens
stablecoin_lens: public(IStablecoinLens)
```
```shell
>>> RewardsHandler.stablecoin_lens()
'0xe24e2dB9f6Bb40bBe7c1C025bc87104F5401eCd7'
```
::::
### `supportsInterface`
::::description[`RewardsHandler.supportsInterface(interface_id: bytes4) -> bool: view`]
Function to check if the contract implements a specific interface.
Returns: `True` if the contract implements the interface, `False` otherwise.
| Input | Type | Description |
| -------------- | -------- | --------------------- |
| `interface_id` | `bytes4` | Interface ID to check |
```vyper
_SUPPORTED_INTERFACES: constant(bytes4[1]) = [
0xA1AAB33F, # The ERC-165 identifier for the dynamic weight interface.
]
from snekmate.auth import access_control
initializes: access_control
exports: (
# we don't expose `supportsInterface` from access control
access_control.grantRole,
access_control.revokeRole,
access_control.renounceRole,
access_control.set_role_admin,
access_control.DEFAULT_ADMIN_ROLE,
access_control.hasRole,
access_control.getRoleAdmin,
)
@external
@view
def supportsInterface(interface_id: bytes4) -> bool:
"""
@dev Returns `True` if this contract implements the interface defined by
`interface_id`.
@param interface_id The 4-byte interface identifier.
@return bool The verification whether the contract implements the interface or
not.
"""
return (
interface_id in access_control._SUPPORTED_INTERFACES
or interface_id in _SUPPORTED_INTERFACES
)
```
```shell
>>> RewardsHandler.supportsInterface('0xA1AAB33F')
True
>>> RewardsHandler.supportsInterface('0x00000000')
False
```
::::
### `recover_erc20`
::::description[`RewardsHandler.recover_erc20(token: IERC20, receiver: address)`]
:::guard[Guarded Method by [Snekmate](https://github.com/pcaversaccio/snekmate)]
This contract makes use of a Snekmate module to manage roles and permissions. This specific function is restricted to the `RECOVERY_MANAGER` role.
:::
Function to recover funds accidently sent to the contract. This function can not recover `crvUSD` tokens as any `crvUSD` tokens sent to the contract are considered as donations and will be distributed to stakers.
| Input | Type | Description |
| ---------- | --------- | -------------------------------------- |
| `token` | `IERC20` | Address of the token to recover |
| `receiver` | `address` | Receier address of the recovered funds |
```vyper
from ethereum.ercs import IERC20
from snekmate.auth import access_control
initializes: access_control
exports: (
# we don't expose `supportsInterface` from access control
access_control.grantRole,
access_control.revokeRole,
access_control.renounceRole,
access_control.set_role_admin,
access_control.DEFAULT_ADMIN_ROLE,
access_control.hasRole,
access_control.getRoleAdmin,
)
RECOVERY_MANAGER: public(constant(bytes32)) = keccak256("RECOVERY_MANAGER")
@external
def recover_erc20(token: IERC20, receiver: address):
"""
@notice This is a helper function to let an admin rescue funds sent by mistake
to this contract. crvUSD cannot be recovered as it's part of the core logic of
this contract.
"""
access_control._check_role(RECOVERY_MANAGER, msg.sender)
# if crvUSD was sent by accident to the contract the funds are lost and will
# be distributed as rewards on the next `process_rewards` call.
assert token != stablecoin, "can't recover crvusd"
# when funds are recovered the whole balanced is sent to a trusted address.
balance_to_recover: uint256 = staticcall token.balanceOf(self)
assert extcall token.transfer(receiver, balance_to_recover, default_return_value=True)
```
```vyper
@internal
@view
def _check_role(role: bytes32, account: address):
"""
@dev Reverts with a standard message if `account`
is missing `role`.
"""
assert self.hasRole[role][account], "access_control: account is missing role"
```
In this example, all [`wETH`](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) tokens sent to the contract are recovered and sent to [Curve Fee Collector](https://etherscan.io/address/0xa2Bcd1a4Efbd04B63cd03f5aFf2561106ebCCE00).
```shell
>>> RewardsHandler.recover_erc20('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', '0xa2Bcd1a4Efbd04B63cd03f5aFf2561106ebCCE00')
```
::::
---
## Access Control Module
Ownership in this contract is handled by the [Access Control Module](https://github.com/pcaversaccio/snekmate/blob/main/src/snekmate/auth/access_control.vy) provided by [Snekmate](https://github.com/pcaversaccio/snekmate).
### `DEFAULT_ADMIN_ROLE`
::::description[`RewardsHandler.DEFAULT_ADMIN_ROLE() -> bytes32: view`]
Getter for the `DEFAULT_ADMIN_ROLE` role which is the keccak256 hash of the string "DEFAULT_ADMIN_ROLE". This variable is needed for compatibility with the access control module.
Returns: `DEFAULT_ADMIN_ROLE` (`bytes32`).
```vyper
# we use access control because we want to have multiple addresses being able
# to adjust the rate while only the dao (which has the `DEFAULT_ADMIN_ROLE`)
# can appoint `RATE_MANAGER`s
from snekmate.auth import access_control
initializes: access_control
exports: (
# we don't expose `supportsInterface` from access control
access_control.grantRole,
access_control.revokeRole,
access_control.renounceRole,
access_control.set_role_admin,
access_control.DEFAULT_ADMIN_ROLE,
access_control.hasRole,
access_control.getRoleAdmin,
)
```
```shell
>>> RewardsHandler.DEFAULT_ADMIN_ROLE()
'0x0000000000000000000000000000000000000000000000000000000000000000'
```
::::
### `RATE_MANAGER`
::::description[`RewardsHandler.RATE_MANAGER() -> bytes32: view`]
Getter for the `RATE_MANAGER` role which is the keccak256 hash of the string "RATE_MANAGER". This variable is needed for compatibility with the access control module.
Returns: `RATE_MANAGER` (`bytes32`).
```vyper
RATE_MANAGER: constant(bytes32) = keccak256("RATE_MANAGER")
```
```shell
>>> RewardsHandler.RATE_MANAGER()
'0x2eb8ae3bf4f7ccce3124b351006550c82803b59ffcc079d490ebdc6c9946d68c'
```
::::
### `RECOVERY_MANAGER`
::::description[`RewardsHandler.RECOVERY_MANAGER() -> bytes32: view`]
Getter for the `RECOVERY_MANAGER` role which is the keccak256 hash of the string "RECOVERY_MANAGER". This variable is needed for compatibility with the access control module.
Returns: `RECOVERY_MANAGER` (`bytes32`).
```vyper
RECOVERY_MANAGER: constant(bytes32) = keccak256("RECOVERY_MANAGER")
```
```shell
>>> RewardsHandler.RECOVERY_MANAGER()
'0xb32d0a30ffa04d208c058eb0743834d445076a8f1b0a9e5e8e6eb9d3d1f5b97b'
```
::::
### `LENS_MANAGER`
::::description[`RewardsHandler.LENS_MANAGER() -> bytes32: view`]
Getter for the `LENS_MANAGER` role which is the keccak256 hash of the string "LENS_MANAGER". This variable is needed for compatibility with the access control module.
Returns: `LENS_MANAGER` (`bytes32`).
```vyper
LENS_MANAGER: constant(bytes32) = keccak256("LENS_MANAGER")
```
```shell
>>> RewardsHandler.LENS_MANAGER()
'0xd36e60f1df655e9ed2a62ee9a03cfba12c2a2e8dd9309dbab3290ef55d30cf20'
```
::::
### `hasRole`
::::description[`RewardsHandler.hasRole(role: bytes32, account: address) -> bool: view`]
Getter to check if an account has a specific role.
| Input | Type | Description |
| ---------- | --------- | ------------------------------------ |
| `role` | `bytes32` | Role to check |
| `account` | `address` | Account to check the role for |
```vyper
# we use access control because we want to have multiple addresses being able
# to adjust the rate while only the dao (which has the `DEFAULT_ADMIN_ROLE`)
# can appoint `RATE_MANAGER`s
from snekmate.auth import access_control
initializes: access_control
exports: (
# we don't expose `supportsInterface` from access control
access_control.grantRole,
access_control.revokeRole,
access_control.renounceRole,
access_control.set_role_admin,
access_control.DEFAULT_ADMIN_ROLE,
access_control.hasRole,
access_control.getRoleAdmin,
)
```
This example checks if `0x40907540d8a6C65c637785e8f8B742ae6b0b9968` has the `DEFAULT_ADMIN_ROLE` role.
```shell
>>> RewardsHandler.hasRole('0x0000000000000000000000000000000000000000000000000000000000000000', '0x40907540d8a6C65c637785e8f8B742ae6b0b9968')
true
```
::::
### `getRoleAdmin`
::::description[`RewardsHandler.getRoleAdmin(role: bytes32) -> bytes32: view`]
Getter to get the admin role for a specific role.
| Input | Type | Description |
| ---------- | --------- | ------------------------------------ |
| `role` | `bytes32` | Role to get the admin role for |
```vyper
# we use access control because we want to have multiple addresses being able
# to adjust the rate while only the dao (which has the `DEFAULT_ADMIN_ROLE`)
# can appoint `RATE_MANAGER`s
from snekmate.auth import access_control
initializes: access_control
exports: (
# we don't expose `supportsInterface` from access control
access_control.grantRole,
access_control.revokeRole,
access_control.renounceRole,
access_control.set_role_admin,
access_control.DEFAULT_ADMIN_ROLE,
access_control.hasRole,
access_control.getRoleAdmin,
)
```
This example returns the admin role for the `RATE_MANAGER` role.
```shell
>>> RewardsHandler.getRoleAdmin('0x2eb8ae3bf4f7ccce3124b351006550c82803b59ffcc079d490ebdc6c9946d68c')
0x0000000000000000000000000000000000000000000000000000000000000000
```
::::
### `grantRole`
::::description[`RewardsHandler.grantRole(role: bytes32, account: address)`]
Grants a role to an account.
| Input | Type | Description |
| ---------- | --------- | ------------------------------------ |
| `role` | `bytes32` | Role to grant |
| `account` | `address` | Account to grant the role to |
```vyper
# we use access control because we want to have multiple addresses being able
# to adjust the rate while only the dao (which has the `DEFAULT_ADMIN_ROLE`)
# can appoint `RATE_MANAGER`s
from snekmate.auth import access_control
initializes: access_control
exports: (
# we don't expose `supportsInterface` from access control
access_control.grantRole,
access_control.revokeRole,
access_control.renounceRole,
access_control.set_role_admin,
access_control.DEFAULT_ADMIN_ROLE,
access_control.hasRole,
access_control.getRoleAdmin,
)
```
This example grants the `RATE_MANAGER` role to `0x40907540d8a6C65c637785e8f8B742ae6b0b9968`.
```shell
>>> RewardsHandler.grantRole('0x2eb8ae3bf4f7ccce3124b351006550c82803b59ffcc079d490ebdc6c9946d68c', '0x40907540d8a6C65c637785e8f8B742ae6b0b9968')
```
::::
### `revokeRole`
::::description[`RewardsHandler.revokeRole(role: bytes32, account: address)`]
Revokes a role from an account.
| Input | Type | Description |
| ---------- | --------- | ------------------------------------ |
| `role` | `bytes32` | Role to revoke |
| `account` | `address` | Account to revoke the role from |
```vyper
# we use access control because we want to have multiple addresses being able
# to adjust the rate while only the dao (which has the `DEFAULT_ADMIN_ROLE`)
# can appoint `RATE_MANAGER`s
from snekmate.auth import access_control
initializes: access_control
exports: (
# we don't expose `supportsInterface` from access control
access_control.grantRole,
access_control.revokeRole,
access_control.renounceRole,
access_control.set_role_admin,
access_control.DEFAULT_ADMIN_ROLE,
access_control.hasRole,
access_control.getRoleAdmin,
)
```
This example revokes the `RATE_MANAGER` role from `0x40907540d8a6C65c637785e8f8B742ae6b0b9968`.
```shell
>>> RewardsHandler.revokeRole('0x2eb8ae3bf4f7ccce3124b351006550c82803b59ffcc079d490ebdc6c9946d68c', '0x40907540d8a6C65c637785e8f8B742ae6b0b9968')
```
::::
### `renounceRole`
::::description[`RewardsHandler.renounceRole(role: bytes32, account: address)`]
Renounces a role.
| Input | Type | Description |
| ---------- | --------- | ------------------------------------ |
| `role` | `bytes32` | Role to renounce |
| `account` | `address` | Account to renounce the role from |
```vyper
# we use access control because we want to have multiple addresses being able
# to adjust the rate while only the dao (which has the `DEFAULT_ADMIN_ROLE`)
# can appoint `RATE_MANAGER`s
from snekmate.auth import access_control
initializes: access_control
exports: (
# we don't expose `supportsInterface` from access control
access_control.grantRole,
access_control.revokeRole,
access_control.renounceRole,
access_control.set_role_admin,
access_control.DEFAULT_ADMIN_ROLE,
access_control.hasRole,
access_control.getRoleAdmin,
)
```
This example renounces the `RATE_MANAGER` role from `0x40907540d8a6C65c637785e8f8B742ae6b0b9968`.
```shell
>>> RewardsHandler.renounceRole('0x2eb8ae3bf4f7ccce3124b351006550c82803b59ffcc079d490ebdc6c9946d68c', '0x40907540d8a6C65c637785e8f8B742ae6b0b9968')
```
::::
### `set_role_admin`
::::description[`RewardsHandler.set_role_admin(role: bytes32, admin_role: bytes32)`]
Sets the admin role for a role.
| Input | Type | Description |
| ---------- | --------- | ------------------------------------ |
| `role` | `bytes32` | Role to set the admin role for |
| `admin_role` | `bytes32` | New admin role |
```vyper
# we use access control because we want to have multiple addresses being able
# to adjust the rate while only the dao (which has the `DEFAULT_ADMIN_ROLE`)
# can appoint `RATE_MANAGER`s
from snekmate.auth import access_control
initializes: access_control
exports: (
# we don't expose `supportsInterface` from access control
access_control.grantRole,
access_control.revokeRole,
access_control.renounceRole,
access_control.set_role_admin,
access_control.DEFAULT_ADMIN_ROLE,
access_control.hasRole,
access_control.getRoleAdmin,
)
```
This example sets the admin role for the `RATE_MANAGER` role to the `DEFAULT_ADMIN_ROLE`.
```shell
>>> RewardsHandler.set_role_admin('0x2eb8ae3bf4f7ccce3124b351006550c82803b59ffcc079d490ebdc6c9946d68c', '0x0000000000000000000000000000000000000000000000000000000000000000')
```
::::
---
## StablecoinLens
The `StablecoinLens` contract calculates the accurate circulating supply of crvUSD by summing the debt of all `PegKeepers` and the total debt of all `Controllers`. This approach is necessary because simply calling `crvUSD.totalSupply()` returns an inflated number, as it includes idle crvUSD in `PegKeepers`, unborrowed crvUSD in `Controllers`, and crvUSD allocated to the `FlashLender` or other venues.
:::vyper[`StablecoinLens.vy`]
The source code for the `StablecoinLens.vy` contract is available on [GitHub](https://github.com/curvefi/scrvusd/blob/main/contracts/StablecoinLens.vy). The contract is written using [Vyper](https://vyperlang.org/) version `~=0.4`.
The contract is deployed on :logos-ethereum: Ethereum at [`0xe24e2db9f6bb40bbe7c1c025bc87104f5401ecd7`](https://etherscan.io/address/0xe24e2db9f6bb40bbe7c1c025bc87104f5401ecd7).
The source code was audited by [:logos-chainsecurity: ChainSecurity](https://www.chainsecurity.com/). The audit report is available on [GitHub](https://github.com/curvefi/scrvusd/blob/main/audits/ChainSecurity_Curve_scrvUSD_audit.pdf).
:::
:::danger[Warning: Usage of `StablecoinLens.vy` contract]
In theory, the calculation of the true circulating supply of crvUSD could be manipulated using MEV techniques. For example, one could take a flash loan of up to 1 million crvUSD or borrow a significant amount of crvUSD from a Controller, then take a snapshot via `RewardsHandler.take_snapshot()`, and subsequently repay the debt. However, there is a lower bound defined by `minimum_weight` and an upper bound defined by the `FeeSplitter` cap.
Ultimately, as this calculation is a moving average, successful manipulation would require repeated MEV actions over multiple snapshots to have a substantial impact.
**Nontheless, the contract should not be used by third parties before consulting with the Curve team.**
:::
---
### `circulating_supply`
::::description[`StablecoinLens.circulating_supply() -> uint256: view`]
Function to compute the true circulating supply of crvUSD. Calling `totalSupply` directly returns an inflated figure, as it includes idle crvUSD in `PegKeepers`, unborrowed crvUSD in `Controllers`, and crvUSD allocated to the `FlashLender` contract. The true circulating supply is calculated by summing the debt of all `PegKeepers` and the total debt of each `Controller` in the factory.
*Calculation logic:*
1. Fetches a predefined crvUSD `Controller` from the `ControllerFactory`, in this case the WETH `Controller`. This is hardcoded and can not be changed.
2. Fetches the `MonetaryPolicy` from the WETH `Controller` by calling the `monetary_policy()` function.
3. Iterates over the `PegKeepers` in the `MonetaryPolicy`, summing the debt of all `PegKeepers`. Idle sitting crvUSD in `PegKeepers` are not included in the calculation as they are not circulating.
4. Iterates over all crvUSD `Controllers` and sums the total debt of each `Controller`.
5. Returns the combined sum of the debt of all `PegKeepers` and the total debt of all `Controllers`.
Returns: true circulating supply of crvUSD (`uint256`).
```vyper
# pragma version ~=0.4
from interfaces import IPegKeeper
from interfaces import IController
from interfaces import IControllerFactory
from interfaces import IMonetaryPolicy
# bound from factory
MAX_CONTROLLERS: constant(uint256) = 50000
# bound from monetary policy
MAX_PEG_KEEPERS: constant(uint256) = 1001
# could have been any other controller
WETH_CONTROLLER_IDX: constant(uint256) = 3
# the crvusd controller factory
factory: immutable(IControllerFactory)
@deploy
def __init__(_factory: IControllerFactory):
factory = _factory
@view
@external
def circulating_supply() -> uint256:
return self._circulating_supply()
@view
@internal
def _circulating_supply() -> uint256:
"""
@notice Compute the circulating supply for crvUSD, `totalSupply` is incorrect
since it takes into account all minted crvUSD (i.e. flashloans)
@dev This function sacrifices some gas to fetch peg keepers from a unique source
of truth to avoid having to manually maintain multiple lists across several
contracts. For this reason we read the list of peg keepers contained in the
monetary policy returned by a controller in the factory. factory -> weth
controller -> monetary policy -> peg keepers This function is not exposed as
external as it can be easily manipulated and should not be used by third party
contracts.
"""
circulating_supply: uint256 = 0
# Fetch the weth controller (index 3) under the assumption that
# weth will always be a valid collateral for crvUSD, therefore its
# monetary policy should always be up to date.
controller: IController = staticcall factory.controllers(WETH_CONTROLLER_IDX)
# We obtain the address of the current monetary policy used by the
# weth controller because it contains a list of all the peg keepers.
monetary_policy: IMonetaryPolicy = staticcall controller.monetary_policy()
# Iterate over the peg keepers (since it's a fixed size array we
# wait for a zero address to stop iterating).
for i: uint256 in range(MAX_PEG_KEEPERS):
pk: IPegKeeper = staticcall monetary_policy.peg_keepers(i)
if pk.address == empty(address):
# end of array
break
circulating_supply += staticcall pk.debt()
n_controllers: uint256 = staticcall factory.n_collaterals()
for i: uint256 in range(n_controllers, bound=MAX_CONTROLLERS):
controller = staticcall factory.controllers(i)
# add crvUSD minted by controller
circulating_supply += staticcall controller.total_debt()
return circulating_supply
```
```shell
>>> StablecoinLens.circulating_supply()
856304219377646153994502194
```
::::
---
## Security
Curve Finance prioritizes the security of its protocols and user funds above all else. We maintain a bug bounty program to encourage responsible disclosure of potential vulnerabilities and actively collaborate with security researchers and whitehat hackers to ensure the safety of our ecosystem.
:::github[Security Contact & Disclosure Reports]
For security-related inquiries and vulnerability reports: security@curve.fi
Security audits and disclosure reports are available on [GitHub](https://github.com/curvefi/security-incident-reports)
:::
---
## Bug Bounty
**Scope** — Issues which can lead to substantial loss of money, critical bugs like a broken liveness condition or irreversible loss of funds.
**Disclosure Policy** — Let us know as soon as possible upon discovery of a potential security issue. Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a third-party.
**Exclusions** — Already known vulnerabilities. Vulnerabilities in front-end code not leading to smart contract vulnerabilities.
**Eligibility** — You must be the first reporter of the vulnerability. You must be able to verify a signature from same address. Provide enough information about the vulnerability.
| Likelihood / Severity | Low | Moderate | High |
| :-: | :-: | :-: | :-: |
| Almost Certain | $10,000 | $50,000 | $250,000 |
| Possible | $1,000 | $10,000 | $50,000 |
| Unlikely | $250 | $1,000 | $5,000 |
---
## Security Audits
---
## curve-api API(Api)
---
## curve-prices API(Api)
---
## Adding Permissionless Rewards
Permissionless rewards are third-party incentives that can simply be added to a Curve gauge. They don't require any DAO approval as they are not based on gauge weights and CRV emissions but rather use external rewards provided by someone else. While conceptually permissionless, only the **gauge manager** (typically the deployer) can authorize and deposit these rewards. This is done to prevent malicious actors from spamming the gauge with fake scam tokens.
Currently, there is no UI for this process. This guide explains how to add permissionless rewards using **Etherscan**.
## Before You Start
To add permissionless rewards:
- You must be the **manager** of the gauge (typically the address that deployed it)
- The reward token must be transferable and follow standard behavior
- You need a token balance and must approve the gauge contract to spend it
- You will interact directly with **Etherscan**
You can check the `manager` or `admin` address under the **Read Contract** tab of your gauge contract.
Only a maximum of **8 unique reward tokens** can be added to a single gauge. Once added, a reward token cannot be removed.
## Understanding the Gauge Manager Role
The gauge deployer is automatically assigned as the **manager**. This role allows:
- Transferring manager status to another address
- Calling `add_reward` to register a reward token and assign a distributor address
For example, if you want to add USDC as a permissionless reward, the `manager` would call `add_reward` with the USDC token address and the address that will act as the distributor. The distributor is the only address allowed to deposit that token to the gauge.
Only the **manager** can call `add_reward`, and only the assigned **distributor** can deposit rewards.
## Step 1: Set Distributor for a Token
Go to your **Gauge Contract** on [Etherscan](https://etherscan.io), connect your wallet, and open the **Write Contract** tab. Make sure you are connected with the `manager` wallet.
Call `add_reward` and input:
- `_reward_token`: the address of the reward token you want to add
- `_distributor`: the address that will fund the gauge with this token
This function can only be called once per reward token.
## Step 2: Approve the Reward Token
Go to the **reward token's contract** (e.g. USDC) on Etherscan. Under **Write Contract**, call `approve`:
- `_spender`: the gauge contract address
- `_value`: the number of tokens you want to approve for deposit
This grants the gauge permission to transfer the reward tokens from the distributor.
## Step 3: Deposit the Reward Tokens
Return to the gauge contract's **Write Contract** tab and call `deposit_reward_token`:
- `_reward_token`: the token address previously added
- `_amount`: the amount of tokens to deposit
- `_epoch`: the number of seconds over which the tokens will be distributed
*(Optional – if omitted, defaults to one week or 604800 seconds)*
Rewards are streamed evenly over the specified time period. If more tokens are added during an ongoing epoch, the remaining balance and the new deposit are combined, and a new epoch begins.
Once the reward tokens are deposited, Curve LPs will:
- See the reward token listed in the Curve UI
- Receive the reward automatically when claiming CRV or other incentives
There is no additional setup needed on the user side.
## Claiming and Viewing Rewards
From a user side, rewards can simply be claimed via the UI. More here: [Claiming Rewards](/user/dex/liquidity).
---
## Getting CRV Emissions
Once your gauge has been whitelisted by the Curve DAO, it becomes eligible to receive CRV emissions — but this does **not** happen automatically. Emissions are distributed based on **gauge weight voting**, a weekly process where veCRV holders decide how Curve’s inflation is allocated across pools.
## CRV Emissions Depend on Gauge Weight Voting
Every week, the Curve DAO distributes newly minted CRV tokens across all **whitelisted gauges**. The distribution is **proportional** to how much voting weight each gauge receives from veCRV holders.
- veCRV holders vote for the gauges they want to support
- The more voting weight your gauge receives, the larger the share of CRV emissions it gets
- If your gauge receives **0% weight**, it receives **0 CRV** — even if it's whitelisted
## How Gauge Weight Voting Works
All veCRV holders can vote on gauges using the Curve voting interface. They can split their weight across multiple gauges or concentrate it on a single one.
- Votes are **relative**: if your gauge receives 10% of all votes, it will receive 10% of that week's CRV inflation
- You can vote at any time, but votes are **locked in** at the weekly update
- Weights are reset weekly — your gauge must continuously attract votes to maintain emissions
## Weekly Update Cycle
Gauge weights — and therefore CRV emissions — are updated **every Thursday at 00:00 UTC**.
- Any votes submitted before that time are included in the next cycle
- The system automatically recalculates weights and distributes emissions accordingly
- Historical and live weights can be viewed at: curve.finance/dao/ethereum/gauges
## How to Attract Gauge Weight
Being whitelisted is only the beginning. You still need to get veCRV holders to vote for your gauge. This can be done by:
- **Engaging with the Curve community**
- **Offering vote incentives** using platforms like Votium
- **Aligning incentives** with veCRV holders (e.g. providing value to Curve’s ecosystem)
## Where to Track Emissions
You can track gauge weights here: Gauge Weights
---
## Vote Incentives
soon
---
## Incentives: CRV Emissions vs. Permissionless Rewards
There are two primary ways to incentivize liquidity on Curve: **CRV emissions** and **permissionless rewards**. Understanding how each works — and when to use them — is essential for protocols aiming to grow and sustain liquidity in their pools.
While both mechanisms distribute rewards to liquidity providers (LPs), they operate under different rules and serve different strategic purposes.
## What Are CRV Emissions and How Do They Work?
CRV emissions are distributed weekly by the Curve DAO and serve as the core long-term incentive mechanism for Curve liquidity pools and lending markets. This system is fundamental to how the protocol works: veCRV holders vote on gauges to decide how Curve’s CRV inflation is allocated, effectively directing where new CRV emissions go each week.
Only **whitelisted gauges** are eligible to receive CRV emissions. But whitelisting alone is not enough — emissions are not fixed or automatic. To actually receive CRV, a gauge must attract **gauge weight votes** from veCRV holders. The more votes a gauge receives, the higher its share of the total weekly emissions.
For example, if a gauge receives 10% of all veCRV votes in a given week, it will receive 10% of that week's CRV inflation. Gauge weights are updated every **Thursday at 00:00 UTC**, and the emissions adjust accordingly.
- CRV is allocated through weekly **gauge weight voting** by veCRV holders.
- The amount of CRV a pool receives fluctuates based on how much gauge weight it attracts.
- Protocols do not pay for CRV emissions directly — rewards come from the Curve DAO's inflation schedule.
- To receive CRV emissions, a gauge must first be approved by governance and then continue to attract votes each week.
## What Are Permissionless Rewards and How Do They Work?
Permissionless rewards are third-party incentives that are distributed through the same gauges as well — but without needing DAO approval. They allow protocols or asset issuers (or really anyone who wants to put up rewards) to immediately start rewarding liquidity providers.
Unlike CRV emissions, permissionless rewards are funded and managed directly by the gauge deployer.
- **Only the gauge deployer**, or an address authorized by the gauge deployer, can add rewards to a gauge.
- This design prevents the system from being spammed with malicious or irrelevant tokens.
- Rewards can be **any token**, not just ERC-20 — though ERC-20 is most common.
- Some tokens (e.g. rebasing or non-standard tokens) may require special handling. If using exotic tokens, it’s recommended to contact the Curve team before adding them.
Permissionless rewards are generally visible in the Curve UI and claimable by LPs alongside CRV (if applicable). They are commonly used to bootstrap liquidity or test incentive models.
## When to Use CRV Emissions vs. Permissionless Rewards
Each incentive type serves a different role in your liquidity strategy. CRV emissions are typically suited for long-term sustainability, while permissionless rewards offer flexibility and speed. The right choice depends on your project’s goals, token model, and timeline.
CRV emissions do not require funding from the protocol, but the gauge must be approved by the DAO through an on-chain vote. This takes at least 7 days, and gauge weights are updated weekly — meaning emissions may not begin until 1–2 weeks after deployment. Emission amounts vary weekly, depending on how much gauge weight is allocated to the pool by veCRV holders. These rewards are also subject to the market price of CRV: if CRV declines, incentives become less effective; if it increases, the same emissions yield higher APRs at no additional cost to the protocol.
Permissionless rewards, on the other hand, can be deployed instantly — no governance approval is needed. Rewards can be denominated in stable assets (e.g. USDC) or native tokens, making them easier to plan and budget. Because they are not subject to market volatility or weekly voting, they provide predictable and targeted incentives.
Many protocols combine both: starting with permissionless rewards to attract early liquidity and gauge market interest, then pursuing CRV emissions through a DAO vote to scale long-term liquidity more efficiently. This flexible approach allows projects to manage cost, responsiveness, and growth dynamically.
---
## Guides
---
## Gauges & Incentives Overview
:::info
Before diving in, make sure to fully understand how CRV inflation based on gauge weights works.
Learn more here: [Gauge Weights](/user/dao/gauge-weights).
:::
A **gauge** is a smart contract that distributes rewards to liquidity providers (LPs) in Curve pools or lending vaults. Pools and lending vaults work perfectly fine without having a gauge; it's simply an additional contract on top that measures the provided liquidity of users.
When rewards (such as CRV emissions or third-party incentives) are *active* on a gauge, **users must stake their LP or vault tokens into the gauge contract** to be eligible for those rewards.
Protocols and asset issuers can use gauges to grow liquidity — by offering incentives to LPs through **CRV emissions**, **third-party token rewards**, or both.
To get started quickly, check these out:
## Why Use a Gauge?
Gauges enable protocols to reward users for providing liquidity in Curve pools — either with **CRV emissions**, **third-party incentives**, or both. These incentives create a yield opportunity for LPs, which in turn attracts deposits. The more rewards a pool offers, the more attractive it becomes to liquidity providers. As more users deposit into the pool to earn yield, the total value locked (TVL) increases. Higher TVL leads to deeper liquidity, reduced slippage, and a better trading experience for the token — ultimately making it more usable and integrated across DeFi.
Gauges also align incentives between protocols, LPs, and Curve governance:
- **Protocols** benefit from deep liquidity
- **LPs** earn yield from CRV and/or third-party rewards
- **veCRV holders** can vote on gauge weights and are (partly) incentivized through vote incentives
## Types of Rewards Through a Gauge
For a more detailed explainer see: [Incentives: CRV Emissions vs. Permissionless Rewards](./incentives.md)
### CRV Emissions (Gauge Weight Voting)
- Distributed weekly from Curve DAO’s CRV inflation schedule
- Allocated based on veCRV **gauge weight voting**
- Requires DAO approval to whitelist the gauge
### Permissionless Rewards (Third-Party Incentives)
- Any token can be added to a gauge (not recommended: rebasing tokens will lose the rebases)
- Useful to bootstrap liquidity or reward LPs using the protocol's own token
- Requires no governance or approval
## Deploying Gauges and Receiving CRV Emissions
Deploying a gauge is fully permissionless — **anyone can create a gauge** for a Curve pool or lending vaults. Once deployed, the gauge can immediately begin distributing **third-party incentives**, such as your project’s native token or other ERC-20 rewards.
However, if a gauge should be eligible to receive **CRV emissions**, it must first be **approved and whitelisted by the Curve DAO** through a governance vote. Only DAO-approved gauges are eligible to receive a share of Curve's weekly CRV inflation — and emissions are not fixed: they are determined by [veCRV gauge weight voting](/user/dao/gauge-weights), which takes place on a weekly basis.
> - Gauges can always and instantly after deployment distribute external (permissionless) incentives.
> - CRV emissions (via gauge weights) require DAO approval **and** veCRV weight. More here: [Gauge Weights](/user/dao/gauge-weights).
---
## Whitelisting a Gauge
For a gauge to be eligible to receive CRV emissions, it must be **whitelisted** via a DAO vote. More technically, the gauge needs to be added to the `GaugeController` contract through a governance vote. If the vote successfully passes, veCRV holders can direct their voting power to the gauge. The gauge receives CRV emissions proportional to the votes it receives relative to total votes. For example, if a gauge receives 1% of all veCRV votes, it will receive 1% of CRV inflation for that week.
The whitelisting process is fully decentralized and governed by the DAO. This guide walks through how to request approval from veCRV holders and get gauges added to the emissions system.
:::warning
Gauges for pool or lending markets including tokens with transfer restrictions, tax mechanisms, blacklists, or other non-standard behavior are generally **not approved** by the DAO. Only freely transferable, openly accessible tokens are considered eligible for CRV emissions.
While final decisions are made through governance, the DAO historically prioritizes protecting LPs and the protocol from potential value extraction or misuse. Ensure your token meets standard ERC-20 requirements before submitting a gauge proposal.
:::
---
## Step 1: Post a Governance Forum Proposal (Recommended)
Before submitting an on-chain vote, it's good practice to post a proposal on the Curve Governance Forum. This allows the community and veCRV holders to review your request and ask questions.
The post should include:
- A short explanation of your protocol or pool
- Why you're requesting CRV emissions
- How the pool benefits Curve (e.g. volume, stickiness, new use cases)
- A link to the gauge contract (already deployed)
- Any supporting audits or docs
This step is not required, but it increases transparency and support — especially for lesser-known protocols or assets. The more people know about the project, the higher the chance it passes.
## Step 2: Submit an On-Chain Vote
Once your gauge is deployed and you've shared your proposal, go to the Gauge Factory UI to start the DAO approval process.
To submit a whitelisting vote:
- Connect your wallet
- Enter your deployed gauge address
- Sign and confirm the transaction
:::info
To actually create an on-chain vote for the Curve DAO to vote on, you will need 2,500 veCRV. If you don't have any, feel free to reach out in public channels. There are more than enough people willing to create a proposal for you.
:::
After a few minutes, your proposal will appear on the Curve DAO proposals page, where veCRV holders can vote **yes** or **no**.
This begins a 7-day on-chain vote to whitelist your gauge to the `GaugeController`.
## Step 3: Promote the Vote
Once the vote is live, share it in Curve's Discord and Telegram to notify veCRV holders. Engaging the community increases your chances of success.
## Step 4: After the Vote
If the vote passes:
- Your gauge will be whitelisted and added to the GaugeController
- It becomes eligible to receive CRV emissions starting from the next epoch onwards
- Emissions begin with the next epoch (**Thursdays at 00:00 UTC**)
> CRV emissions are not automatic — veCRV holders must still vote for your gauge to assign weight and receive emissions.
---
## Next Steps
todo
---
## Deploying a Lending Market
:::important
**Deployments Paused**: Currently the Curve team has paused new deployments for any further lending markets, as we await the release of Llamalend v2 in the coming months.
:::
Currently, there's no UI for deploying lending markets, but you can deploy via Etherscan or contact the Curve team, who are happy to help deploy lending markets with the correct oracles and parameters.
**Important**: You can only deploy lending markets if one of the tokens is crvUSD (either as the borrowable token or as the collateral token).
## Requirements
**Data needed when deploying a new lending market:**
- Token addresses of the collateral and borrowable tokens
- Simulated parameters (A, fee, loan_discount, liquidation_discount)
- Minimum and maximum borrow rates (defaults to 0.5% and 50% if not set)
- Price oracle contract
- Name for the lending market
For more information on how to acquire this information, see: [Oracles & Parameters](../oracles-and-parameters.md).
## Deploying via Etherscan
This guide assumes you have all the required data from the [Requirements](#requirements) section ready.
First, locate the appropriate Factory contract on Etherscan for your target chain. Currently, Llamalend is deployed on the following chains:
- **Ethereum**: [0xeA6876DDE9e3467564acBeE1Ed5bac88783205E0](https://etherscan.io/address/0xeA6876DDE9e3467564acBeE1Ed5bac88783205E0#writeContract#F1)
- **Arbitrum**: [0xcaEC110C784c9DF37240a8Ce096D352A75922DeA](https://arbiscan.io/address/0xcaEC110C784c9DF37240a8Ce096D352A75922DeA#writeContract)
- **Fraxtal**: [0xf3c9bdab17b7016fbe3b77d17b1602a7db93ac66](https://fraxscan.com/address/0xf3c9bdab17b7016fbe3b77d17b1602a7db93ac66#writeContract)
- **Optimism**: [0x5EA8f3D674C70b020586933A0a5b250734798BeF](https://optimistic.etherscan.io/address/0x5EA8f3D674C70b020586933A0a5b250734798BeF#writeContract)
- **Sonic**: [0x30D1859DaD5A52aE03B6e259d1b48c4b12933993](https://sonicscan.org/address/0x30D1859DaD5A52aE03B6e259d1b48c4b12933993#writeContract)
## Deployment Methods
There are two deployment methods available:
1. **`create_from_pool` function**: Use this when both tokens exist in the same Curve liquidity pool with a suitable oracle. The factory will automatically use the pool's EMA oracle, eliminating the need for an external oracle.
2. **`create` function**: Use this when you need a custom price oracle, such as when a token has a Curve pool but is priced against USDC instead of crvUSD.
**Recommendation**: The `create_from_pool` function is the easiest approach. For example, if a protocol wants to create a lending market for their governance token, they should first create a TOKEN/crvUSD liquidity pool, which will provide the necessary oracle.
### create_from_pool
This function can be used when both tokens are in the same Curve liquidity pool with a reliable oracle (stableswap-ng, twocrypto-ng, or tricrypto-ng).
**Required inputs:**
- **borrowed_token**: Token address of the borrowable token
- **collateral_token**: Token address of the collateral token
- **A**: Value obtained from [simulations](../oracles-and-parameters.md#a-fee-loan--and-liquidation-discount)
- **fee**: Value obtained from [simulations](../oracles-and-parameters.md#a-fee-loan--and-liquidation-discount)
- **loan_discount**: Value obtained from [simulations](../oracles-and-parameters.md#a-fee-loan--and-liquidation-discount)
- **liquidation_discount**: Value obtained from [simulations](../oracles-and-parameters.md#a-fee-loan--and-liquidation-discount)
- **pool**: Curve liquidity pool contract address containing both tokens
- **name**: Name of the market (see [naming conventions](../oracles-and-parameters.md#name))
- **min_borrow_rate**: Minimum borrow rate (see [rate parameters](../oracles-and-parameters.md#borrow-rate-parameters))
- **max_borrow_rate**: Maximum borrow rate (see [rate parameters](../oracles-and-parameters.md#borrow-rate-parameters))
### create
This function can be used when the collateral token's oracle cannot be obtained from an existing Curve pool.
**Required inputs:**
The inputs are the same as the `create_from_pool` method above, except that instead of providing a pool contract address, you provide a custom price oracle (which must have a `price()` function to comply with the ABI):
- **borrowed_token**: Token address of the borrowable token
- **collateral_token**: Token address of the collateral token
- **A**: Value obtained from [simulations](../oracles-and-parameters.md#a-fee-loan--and-liquidation-discount)
- **fee**: Value obtained from [simulations](../oracles-and-parameters.md#a-fee-loan--and-liquidation-discount)
- **loan_discount**: Value obtained from [simulations](../oracles-and-parameters.md#a-fee-loan--and-liquidation-discount)
- **liquidation_discount**: Value obtained from [simulations](../oracles-and-parameters.md#a-fee-loan--and-liquidation-discount)
- **price_oracle**: Custom price oracle contract address (must have `price()` function)
- **name**: Name of the market (see [naming conventions](../oracles-and-parameters.md#name))
- **min_borrow_rate**: Minimum borrow rate (see [rate parameters](../oracles-and-parameters.md#borrow-rate-parameters))
- **max_borrow_rate**: Maximum borrow rate (see [rate parameters](../oracles-and-parameters.md#borrow-rate-parameters))
---
## Oracles & Parameters
When deploying a Llamalend lending market, two critical components must be carefully configured: **price oracles** and **market parameters**. These form the foundation for a secure, efficient, and profitable lending market.
For Llamalend's liquidation engine to work optimally, the system requires a smooth price oracle for the collateral asset. **Spot oracles are not recommended** as they can cause significant losses due to price jumps during liquidations. Proper parameter selection is equally important for market security and efficiency.
:::info
**Parameter paralysis and oracle confusion?** Don't let uncertainty hold you back! The [LlamaRisk](https://www.llamarisk.com/) wizards are here to save the day!
Jump into their Telegram and let them work their risk management magic. From oracles to liquidation thresholds, they've got your back!
:::
## Oracles
Llamalend markets require smooth (exponential moving average) oracles to price the collateral asset. Spot oracles do not work well as the liquidation mechanism of Llamalend requires smooth price feeds to ensure maximum efficiency. The easiest way to obtain this is through deploying a **liquidity pool** on Curve, which already comes with built-in oracles that can be used for lending markets.
Every Curve pool includes a direct built-in oracle that can be used for lending markets, provided the pool has sufficient TVL. Curve offers pools for all sorts of assets. Learn more here: [Curve Liquidity Pool Oracles](../pool/compare-amm.md#built-in-price-oracles).
:::important
**Critical**: The collateral asset oracle must be priced against **crvUSD**, not other stablecoins. If the Curve pool contains crvUSD (e.g., ETH/crvUSD), the oracle can be used directly. If the pool doesn't contain crvUSD (e.g., ETH/USDC), the oracle must be chained with a USDC/crvUSD oracle to create an ETH/crvUSD price feed.
:::
For assets without suitable Curve pools, you can deploy custom oracles using the [custom oracle deployment guide](https://paragraph.com/@curvefi/llamalend_market_deploy#h-deploying-a-custom-priceoracle-contract). If this sounds complicated, the Curve team is happy to help with deployment - simply reach out on Telegram.
:::tip-green
Llamalend price oracles are particularly complex due to the requirements of the liquidation engine (smooth EMA feeds, crvUSD-denominated pricing, etc.). Swiss Stake, the service provider developing Curve's technology, offers consulting services to guide protocols in the design and integration of these oracles, and thorough audits of client-implemented oracle solutions to ensure correctness and security. Inquiries: [inquiries@curve.finance](mailto:inquiries@curve.finance).
:::
## Parameters
Parameter selection when creating a new lending market is the foundation for an optimally working liquidation engine and secure lending market. The process of finding optimal parameters requires a thorough analysis of historical price data for the collateral asset.
### A, fee, loan- and liquidation discount
The following parameters need to be simulated and set at market deployment:
- **Band width factor (A)**: Determines the range of prices where liquidations can occur
- **Fee**: Fee for exchanging tokens inside the AMM (LLAMMA)
- **Liquidation discount**: Price discount applied during liquidations
- **Loan discount**: Collateral discount for loan-to-value calculations
A dedicated GitHub repository was set up for running these simulations: [LLAMMA Simulator](https://github.com/curvefi/llamma-simulator). An in-depth explainer can be found here: [Parameter Analysis Guide](https://paragraph.com/@curvefi/llamma-simulator).
### Borrow Rate Parameters
Each lending market requires configurable minimum and maximum borrow rate values that adjust dynamically based on market utilization. The interest rate model ensures efficient capital allocation by charging:
- **Minimum rate** when utilization is 0% (no borrowing activity)
- **Maximum rate** when utilization reaches 100% (fully utilized market)
- **Semi-Logarithmic scaling** between these bounds based on current utilization
Rates are bounded by system constants: `MIN_RATE` at 1% and `MAX_RATE` at 1000%.
**Optimal Rate Configuration:**
Most markets target approximately 80% utilization as the optimal point where competitive market rates should be charged. This provides lenders with sufficient buffer to withdraw assets while maintaining healthy utilization levels.
For example, if the current market rate for borrowing crvUSD is 10%, configure minimum and maximum rates so that the 80% utilization point equals 10%. This ensures lenders can exit positions without facing fully utilized markets.
**Rate Calculation:**
Borrow rates are calculated per second and must be converted from annual percentage rates (APR) using this formula:
$$
APR = \frac{rate}{10^{18}} \times (60 \times 60 \times 24 \times 365)
$$
**Example**: To achieve 1% APR, set the rate parameter to `317097919`.
## Name
The market name can be chosen freely. While the UI currently doesn't use the name variable from the contract for display, it's still good practice to give it a clear, understandable name.
**Naming Convention**: Usually follows the pattern of the collateral token name appended by the market direction (long/short).
**Examples**:
- **ETH-long**: ETH as collateral token, crvUSD as borrowable token
- **BTC-short**: crvUSD as collateral token, BTC as borrowable token
---
## Lending Overview
Curve's lending infrastructure, **Llamalend**, is a decentralized, permissionless lending system that enables protocols and asset issuers to create lending markets for their tokens. It facilitates lending and borrowing between users while providing powerful tools for asset proliferation and ecosystem growth.
**Borrowers** can leverage Llamalend for yield farming, leverage trading, or obtaining working capital without selling collateral. **Lenders** earn interest while contributing to market liquidity across diverse asset pairs with different risk profiles.
To get started quickly, check these out:
## Core Architecture
Llamalend operates on an **isolated market** model where each lending market has a single collateral token and a single borrowable token. All lending markets are completely independent from each other, preventing cross-contamination and allowing for precise risk management per asset pair.
Every lending market must include **crvUSD** as either the collateral asset or the borrowable asset. This design ensures deep integration with Curve's stablecoin ecosystem while maintaining market isolation. For example, a market could have ETH as collateral and crvUSD as borrowable, or crvUSD as collateral and USDC as borrowable.
- **Gauges for Vaults** - Lending vaults are fully compatible with Curve's gauge system, meaning lending vaults can receive gauge weights and therefore future CRV emissions to attract more supply to the market
- **Fully Permissionless** - Deploy lending markets instantly without DAO approvals
- **Isolated Markets** - Each market operates independently with its own risk parameters, preventing cross-contamination
- **crvUSD Integration** - Deep integration with Curve's stablecoin ecosystem
- **Customizable Risk Management** - Tailored liquidation thresholds and interest rate models per asset pair
## How Markets are Deployed
Deploying Llamalend markets is fully permissionless through a dedicated Factory contract. Anyone can create lending markets for any token pair, with the only requirement being that one of the tokens must be crvUSD. This ensures deep integration with Curve's stablecoin ecosystem while maintaining market isolation.
## How It Works
Users deposit collateral tokens to borrow against them, maintaining health factors above liquidation thresholds. Interest accrues on borrowed amounts and is paid to lenders providing market liquidity. Automated liquidation mechanisms protect against bad debt accumulation.
## Incentives & Liquidity Growth
Lending vaults are fully compatible with Curve's gauge system, enabling multiple strategies to attract and retain liquidity:
- **CRV Emissions** - Put lending vaults up for DAO vote to receive CRV emissions, incentivizing long-term liquidity provision
- **Permissionless Rewards** - Add custom token incentives directly to lending markets to boost initial liquidity
- **Vote Incentives** - Provide rewards to veCRV holders who vote for your lending vault gauge
These mechanisms help bootstrap initial liquidity and create sustainable demand for lending markets, making them more attractive to both borrowers and lenders.
For detailed deployment instructions and parameter optimization, see the [Deployment Guide](./guides/deploy-lending-market.md) and [Oracles & Parameters](./oracles-and-parameters.md) documentation.
---
## Post Deployment
After deploying a lending market, there are a few important steps to ensure it’s visible in the UI, functional, and capable of attracting liquidity and rewards.
## Seed Initial Liquidity
To attract the first borrowers, the lending market (more precisely the lending vault) needs supplied assets to borrow from. It makes sense to implement a strategy to get the market going by either supplying some own liquidity or getting some gauge weight on the lending vault.
## Token Logo Submission
Curve maintains a comprehensive list of token icons. If your pool's assets are not included in this list, they will display default logos in the Curve UI. To submit custom logos for your tokens, open a pull request to the [curve-assets repository](https://github.com/curvefi/curve-assets?tab=readme-ov-file#adding-a-token-icon).
## Monitor Performance and Update Parameters
After deployment, it definitely makes sense to monitor the markets on a constant basis. Check parameters like market utilization and borrow rates and adjust them accordingly to make it an optimal experience for borrowers and lenders.
For markets with low liquid tokens, it's crucial to measure the overall liquidity of the asset to ensure liquidations can be facilitated without any problems when needed. This includes monitoring trading volumes, price impact, and available liquidity across different exchanges and AMMs.
## Grow Liquidity Using Incentives
After deploying your lending market, you can boost liquidity and attract more users through various incentive mechanisms:
- **Gauge Integration** - Put up a DAO vote for your lending vault to be eligible to receive CRV emissions in order to attract more suppliers
- **External Rewards** - Add your own token incentives to make borrowing more attractive
- **Vote Incentives** - Provide rewards to veCRV holders who vote for your gauge
These strategies help bootstrap initial liquidity and create sustainable demand for your lending market.
---
## How Curve Compares to Other AMMs
Curve's automated market makers (AMMs) solve a fundamental problem that other protocols struggle with: **providing deep, concentrated liquidity without requiring constant user intervention**. Here are Curve's benefits:
| Feature / Requirement | Curve Pools (Stableswap/Cryptoswap) | CLAMMs (e.g., Uniswap v3) |
| :--- | :--- | :--- |
| **Passive Liquidity Provision** | ✅ Anyone can earn, not just market makers | ❌ Requires constant monitoring/rebalancing |
| **Liquidity is Always "In-Range"** | ✅ Liquidity is governed by the algorithm automatically, there's always some liquidity in range | ❌ Capital cannot be used as soon as price moves out of range |
| **Consistent Liquidity** | ✅ Liquidity remains deep regardless of volatility | ❌ Liquidity gaps can form during rapid price movement |
| **High TVL Retention** | ✅ LPs don't abandon passive positions, passive liquidity is sticky | ❌ LPs frequently exit or move positions |
| **ERC20 LP Tokens** | ✅ A simple ERC20 token represents LP positions and has a defined $ value | ❌ LP positions are complex and based on ranges, requiring unique NFTs for representation |
While other AMMs force liquidity providers (LPs) to actively manage positions by constantly moving their liquidity to active price bands, Curve pools deliver concentrated liquidity benefits automatically. No need for setting ranges, constantly monitoring LP positions, or hustling for optimal liquidity concentration to earn fees.
For protocols, Curve offers lower maintenance requirements with no need to educate users on range management. It provides better user experience through consistent liquidity regardless of market conditions, higher TVL retention as LPs don't abandon positions during volatility, and proven reliability across multiple market cycles.
:::info
For a general overview of Stableswap and Cryptoswap algorithms, see: [Stableswap vs. Cryptoswap](/user/dex/stableswap-vs-cryptoswap) or deep dive into how they work: [Understanding Stableswap](./understanding-stableswap.md), [Understanding Cryptoswap](./understanding-cryptoswap.md), [Understanding FXSwap](./understanding-fxswap.md)
:::
Michael Egorov figuring out Stableswap algorithm5 months BDS (before DeFi summer)
## The Passive Liquidity Advantage
Most AMMs require LPs to choose price ranges for their liquidity positions, monitor market movements, and manually rebalance when prices exit their range. This creates significant overhead and often results in capital sitting idle when markets move, making the AMM unreliable.
Curve pools provide the same concentrated liquidity benefits but with zero maintenance. All liquidity stays active at all prices, pools adapt to market movements automatically, and 100% of capital earns fees at all times. LPs can deposit once and earn continuously without any intervention.
This approach delivers 100% capital utilization with all deposited capital earning fees continuously. Unlike range-based systems where capital can sit unused, Curve pools self-adjust for maximum efficiency. During market stress events including stablecoin de-pegs, bridge outages, rapid repricings, and extreme volatility, Curve pools have maintained consistent liquidity while other AMMs showed gaps or became illiquid.
The LP experience is truly passive with no dashboards to monitor, predictable returns through consistent fee generation, transparent pricing without impermanent loss surprises, and professional-grade reliability suitable for institutional capital.
## Stableswap: Concentrated Liquidity for Pegged Assets
For stablecoin pairs, liquid staking tokens, and other pegged assets, Stableswap automatically concentrates liquidity where it matters most – near the 1:1 peg.
The Stableswap invariant intelligently **combines two AMM invariants**: **constant-sum** (`x+y=k`) behavior near the peg for minimal slippage, and **constant-product** (`x*y=k`) behavior at wider spreads for continuous liquidity. A single amplification parameter (`A`) controls the concentration. Higher `A` values keep more liquidity near the peg, while lower `A` values spread it more evenly. The amplification factor can be changed at any time via a DAO vote to make the pool more or less concentrated depending on different conditions.
This approach delivers lower slippage for normal trading volumes, better capital efficiency than constant-product AMMs, and automatic adaptation to market conditions without any manual intervention required.
Learn more here: [Understanding Stableswap](understanding-stableswap.md)
## Cryptoswap: Intelligent Adaptive Liquidity
For volatile asset pairs like ETH/USDT or BTC/ETH, Cryptoswap represents a different approach to automated market making. Unlike other AMMs that require manual rebalancing or external oracles, **Cryptoswap automatically tracks market prices and rebalances accordingly**, protecting LP profitability in the process.
Cryptoswap uses an internal price oracle based on an Exponential Moving Average of recent trades to track market price. It only rebalances when the price moves beyond a minimum threshold and when trading fees exceed 50% of the rebalancing cost. This profit-aware approach ensures that rebalancing only occurs when it benefits LPs.
Compared to range-based AMMs like Uniswap v3, Cryptoswap eliminates liquidity gaps and provides automatic optimization without requiring manual position adjustments. Unlike oracle-based solutions, it has no external dependencies and prevents manipulation through internal price discovery. The controlled rebalancing minimizes impermanent loss while maintaining professional execution.
Cryptoswap uses two parameters to optimize liquidity distribution: `A` (amplification) controls liquidity concentration around the balanced price, while `gamma` controls the overall breadth of the liquidity curve. These can be tuned to balance capital efficiency with resilience to volatility.
Learn more here: [Understanding Cryptoswap](understanding-cryptoswap.md)
## Built-in Price Oracles
Every Curve pool comes with a built-in price oracle that provides reliable, **fully on-chain** price feeds without external or off-chain dependencies. Both Stableswap and Cryptoswap pools offer both **spot prices (from the most recent trade) and EMA prices (exponential moving average of recent trades)**. These oracles are manipulation-resistant through economic design and are trusted by major DeFi protocols including crvUSD and Llamalend.
Curve's Oracle Security Explained
## The Bottom Line
Curve delivers the benefits of concentrated liquidity without the complexity. While other AMMs require active management and constant attention, Curve pools work automatically to maximize capital efficiency and minimize slippage. The result is deeper liquidity, better trading experience, and truly passive yield generation.
For protocols looking to launch pools and LPs seeking professional-grade passive income, Curve offers the most efficient and reliable AMM experience in DeFi.
---
## Deploying a Cryptoswap Pool
Cryptoswap pools are appropriate for **two or three volatile assets** that are not pegged to each other — such as ETH/USDC or BTC/ETH. The creation wizard will guide you through the process, but if you have questions at any point, feel free to reach out to the Curve team in the [**Telegram**](https://t.me/curvefi) or [**Discord**](https://discord.gg/rgrfS7W).
For a basic explanation of Cryptoswap mechanics, see: [**Understanding Curve Pools**](../overview).
---
## Step 1: Choose Pool Type
Make sure your wallet is connected (top right corner) and your network is set to the chain where you want to deploy the pool. Curve supports pool deployment on all networks where Curve is live — just switch chains in your wallet, and the UI will follow automatically.
Select **Cryptoswap** as the pool type. This is used for volatile assets that do not share a common price peg — such as ETH, BTC, or volatile non-pegged pairs.
## Step 2: Select Tokens
In this step, you'll define which assets your pool will support. Cryptoswap pools support **two or three tokens**. To add a third token, click the blue **Add token** button.
:::warning
Due to safety reasons, **plain ETH cannot be used** in Cryptoswap pools. Please use WETH instead.
:::
## Step 3: Set Pool Parameters
You can choose a preset configuration or switch to advanced mode to fine-tune the pool's behavior.
> 💡 Initial price bounds are fetched automatically from [CoinGecko](https://www.coingecko.com/). If unavailable, you will need to input them manually.
### Preset Options
Presets vary depending on whether your pool contains two or three tokens.
#### Two-Asset Pools
- **Crypto:** For highly volatile pairs like ETH–LDO
- **Forex:** For relatively stable foreign exchange pairs like crvUSD–EURe
- **Liquid Staking Derivatives:** For LSDs such as cbETH–wETH
- **Liquid Restaking Tokens:** For restaking assets such as pufETH–wETH
#### Three-Asset Pools
- **Tricrypto:** For pools like USDT–wBTC–ETH
- **Three Coin Volatile:** For volatile tokens paired with ETH and a stablecoin
You can use these as-is or switch to **Advanced** mode to adjust parameters manually.
### Advanced Parameters
To fine-tune the pool, enable the **Advanced** toggle:
| Parameter | Description |
|----------|-------------|
| **Mid Fee / Out Fee** | The base and maximum fee range, charged depending on pool imbalance. More here: [Cryptoswap Dynamic Fees](../understanding-cryptoswap.md#dynamic-fees)|
| **Fee Gamma** | Controls how fee ramps up with imbalance. Suggested: `0.0023` (volatile) or `0.005` (less volatile). More here: [Cryptoswap Dynamic Fees](../understanding-cryptoswap.md#dynamic-fees)|
| **Amplification (A)** | Higher = flatter curve = tighter prices near balance. Use lower for volatile pairs. More here: [Cryptoswap Parameters](../understanding-cryptoswap.md#parameters)|
| **Gamma** | Shapes the bonding curve. Suggested: `0.000145` (volatile) or `0.0001` (less volatile). More here: [Cryptoswap Parameters](../understanding-cryptoswap.md#parameters)|
| **Allowed Extra Profit** | Sets profit-taking buffer. Suggested: `0.000002` (volatile) or `0.00000001` (less volatile). |
| **Adjustment Step** | Step size for rebalancing. Suggested: `0.000146` (volatile) or `0.0000055` (less volatile). |
| **Moving Average Time** | Smooths price changes over time using an EMA (in seconds). |
> 📖 **Further reading**: [Deep Dive: Curve V2 Parameters](https://nagaking.substack.com/p/deep-dive-curve-v2-parameters?s=curve), [Cryptoswap Explainer](../understanding-cryptoswap.md)
## Step 4: Enter Pool Info
After configuring your parameters, you’ll be prompted to set the **Pool Name** and **Pool Symbol**.
These values will be used as the ERC‑20 metadata for the pool’s LP token and will appear in block explorers and in the Curve UI.
- **Pool Name** – A human-readable label for the pool (e.g. `ETH/USDC Crypto`)
- **Pool Symbol** – The LP token symbol (e.g. `ETHUSDC`, `crvETHUSDC`)
> 💡 **Tip:** Use short, clear names that reflect the assets in the pool. These fields are immutable after deployment — double-check for typos.
## Step 5: Deploy the Pool
Before deploying, review all your settings in the **Summary panel** on the right. This includes:
- Selected tokens
- Pool parameters (A, Fees, Gamma, etc.)
- Pool name and symbol
Once everything looks correct, click the blue **Create Pool** button at the bottom of the page. Your wallet will prompt you to approve the transaction.
---
## What to do after deployment
✅ **Seed initial liquidity** — a pool with zero balance cannot process trades.
✅ (Optional) **[Create a gauge](/protocol/gauge/overview)** to distribute CRV or other incentives to LPs.
---
## Deploying an FXSwap Pool
FXSwap pools are appropriate for **two uncorrelated but low-volatility assets** — such as Forex pairs like `crvUSD/EURC` or other stable foreign exchange pairs. The creation wizard will guide you through the process, but if you have questions at any point, feel free to reach out to the Curve team in the [**Telegram**](https://t.me/curvefi) or [**Discord**](https://discord.gg/rgrfS7W).
For a basic explanation of FXSwap mechanics, see: [**Understanding FXSwap**](../understanding-fxswap.md).
---
## Step 1: Choose Pool Type
Make sure your wallet is connected (top right corner) and your network is set to the chain where you want to deploy the pool.
:::warning
The FXSwap pool algorithm is still fairly new. Although fully audited, it's still undergoing a "testing" phase to gather as much data as possible. Due to this, the chains where FXSwap pools can be deployed may be limited.
:::
Select **FXSwap** as the pool type.
## Step 2: Select Tokens
In this step, you'll define which assets your pool will support. FXSwap pools support **two tokens only**.
:::info
FXSwap pools are best suited for **highly liquid assets with lower relative volatility**. It is generally not recommended to use FXSwap for primary price discovery — use Cryptoswap pools for volatile assets instead.
:::
## Step 3: Set Pool Parameters
You can choose a preset configuration or switch to advanced mode to fine-tune the pool's behavior.
> 💡 Initial price bounds are fetched automatically from [CoinGecko](https://www.coingecko.com/). If unavailable, you will need to input them manually.
### Preset Options
The UI provides preset parameters. It's highly recommended to use these presets. If custom parameters are desired, please reach out via Telegram first to determine suitable parameters for your pool.
The preset also automatically sets suitable fee parameters:
You can use these as-is or switch to **Advanced** mode to adjust parameters manually.
### Advanced Parameters
To fine-tune the pool, enable the **Advanced** toggle. However, before experimenting with parameters here, it is best to consult with someone from the team to ensure everything works smoothly.
| Parameter | Description |
|----------|-------------|
| **Mid Fee / Out Fee** | The base and maximum fee range, charged depending on pool imbalance. More here: [FXSwap Dynamic Fees](../understanding-fxswap.md#dynamic-fees).|
| **Fee Gamma** | Controls how fee ramps up with imbalance. Lower gamma results in a sharper rise from Mid Fee to Out Fee as imbalance grows. More here: [FXSwap Dynamic Fees](../understanding-fxswap.md#dynamic-fees).|
| **Amplification (A)** | Higher = flatter curve = tighter prices near balance. **Note:** A values are scaled up in FXSwap — an `A` of **10,000** in FXSwap is equivalent to an `A` of **1** in Stableswap. More here: [FXSwap Parameters](../understanding-fxswap.md#amplification-factor-a).|
| **Gamma** | **Unused** in FXSwap logic. Retained solely for backward compatibility. |
| **Allowed Extra Profit** | Sets profit-taking buffer for rebalancing. |
| **Adjustment Step** | Step size for rebalancing operations. |
| **Moving Average Time** | Smooths price changes over time using an EMA (in seconds). |
| **Donation Duration** | Duration (in seconds) over which Refuels (donations) unlock linearly. Default is 7 days. More here: [FXSwap Refuels](../understanding-fxswap.md#refuels).|
> 📖 **Further reading**: [Understanding FXSwap](../understanding-fxswap.md)
## Step 4: Enter Pool Info
After configuring your parameters, you’ll be prompted to set the **Pool Name** and **Pool Symbol**.
These values will be used as the ERC‑20 metadata for the pool’s LP token and will appear in block explorers and in the Curve UI.
- **Pool Name** – A human-readable label for the pool (e.g. `crvUSD/EURC FX`)
- **Pool Symbol** – The LP token symbol (e.g. `crvUSDEURC`, `fxcrvUSDEURC`)
> 💡 **Tip:** Use short, clear names that reflect the assets in the pool. These fields are immutable after deployment — double-check for typos.
## Step 5: Deploy the Pool
Before deploying, review all your settings in the **Summary panel** on the right. This includes:
- Selected tokens
- Pool parameters (A, Fees, Donation Duration, etc.)
- Pool name and symbol
Once everything looks correct, click the blue **Create Pool** button at the bottom of the page. Your wallet will prompt you to approve the transaction.
---
## What to do after deployment
✅ **Seed initial liquidity** — a pool with zero balance cannot process trades.
✅ (Optional) **Add Refuels (Donations)** — Refuels help maintain pool efficiency by subsidizing rebalancing costs. You can add them manually via [crvhub.com/refuel](https://crvhub.com/refuel), or set up recurring automated refuels with the [**Donation Streamer**](./donation-streamer.md). More here: [FXSwap Refuels](../understanding-fxswap.md#refuels)
✅ (Optional) **[Create a gauge](/protocol/gauge/overview)** to distribute CRV or other incentives to LPs.
---
## Deploying a Stableswap Pool
The Stableswap pool creation is appropriate for assets expected to hold a price peg very close to each other, like a pair of dollarcoins. The creation wizard will guide you through the process of creating a pool, but if you have questions throughout you are encouraged to speak with a member of the Curve team in the [**Telegram**](https://t.me/curvefi) or [**Discord**](https://discord.gg/rgrfS7W).
Stableswap pools are liquidity pools containing **up to eight tokens** using the StableSwap algorithm. For a better understanding of StableSwap, please see here: [**Understanding Curve Pools**](../overview).
---
## Step 1: Choose Pool Type
Make sure your wallet is connected (top right corner) and your network is set to the chain where you want to deploy the pool. Curve supports pool deployment on all networks where Curve is live — just switch chains in your wallet, and the UI will follow automatically.
Select Stableswap as the pool type. This is used for assets that should maintain the same value — such as stablecoins (USDC, DAI, USDT), liquid staking tokens (ETH/stETH), or yield-bearing versions of pegged assets.
## Step 2: Select Tokens
In this step, you'll define which assets your pool will support. The interface allows you to choose **between two and eight tokens**, with support for standard **ERC-20s, tokens using oracles, rebasing assets, and ERC‑4626 vault tokens**.
To add a metapool, simply tick on "View Metapools" when selecting a the token and choose one of the metapools from that list.
*For the AMM to function correctly, the appropriate asset type needs to be chosen when selecting the assets. The following asset types are supported:*
### Standard (ERC‑20)
For most ERC‑20 tokens, no additional configuration is needed. Simply select the token and leave all type boxes unchecked.
:::warning ERC‑20 Token Safety
Curve contracts cannot detect malicious or non‑standard ERC‑20 behavior. Double-check that tokens do not charge transfer fees or use reentrancy tricks.
:::
### Oracle-Based Tokens
Check the **Oracle** box if a token requires an external rate feed to track its value relative to the other assets in the pool. You'll be asked to enter:
- The oracle contract address
- The function name used to return the rate (e.g. `stEthPerToken()`, `getExchangeRate()`)
This is appropriate for yield-bearing tokens like wstETH or rETH — tokens whose value accumulates over time relative to their underlying asset, and which are not ERC-4626 vaults.
#### What Kind of Oracle Is Required
The oracle must be a **redemption rate oracle** — sometimes called a NAV (Net Asset Value) oracle. It must return the **value of one token** expressed in terms of the pool's base asset, with **18-decimal precision**.
This is **not** a market price oracle (like a Chainlink ETH/USD price feed). It is an exchange rate that reflects what you would actually receive if you redeemed the token for its underlying asset.
For example:
- In a WETH/wstETH pool: the oracle returns how much ETH each wstETH is worth (e.g. `1.18e18`, meaning 1 wstETH = 1.18 ETH), using `stEthPerToken()` on the wstETH contract itself
- In a USDC/sUSDe pool: the oracle returns how many USDC each sUSDe is worth as it accrues yield
#### Up-Only Requirement
**The oracle value must only ever increase.** This is the most critical property for a safe Stableswap oracle.
When the pool is deployed, its liquidity is centered around the oracle rate. As the rate rises over time (reflecting accrued yield), the pool's center of liquidity shifts upward accordingly — this is the intended behavior.
If the oracle can return a lower value than a previous call:
- It tells the pool that the token has lost value, instantly shifting the center of liquidity downward
- LPs are forced to sell the appreciating asset at a discount — a guaranteed loss
- It creates a direct manipulation vector: anyone who can push the oracle down can sandwich the update and drain LP value
Redemption rates for legitimate yield-bearing assets (staking rewards, lending interest) are strictly non-decreasing by design. **If your oracle can ever decrease, do not use it in a Stableswap pool.**
#### Rate Limit Rule (2× Fee Sandwich Protection)
Even a correct redemption rate oracle is dangerous if it updates too fast. When the oracle shifts the pool's internal rate, that shift happens instantly on-chain. If the shift is larger than the pool fee, an attacker can sandwich the update and extract value from LPs:
1. **Front-run**: Buy the token before the oracle update
2. **Oracle update executes**: Pool's rate shifts up
3. **Back-run**: Sell the token at the new higher internal rate for a guaranteed profit
To prevent this, the oracle must never update by more than **2× the pool fee per block**:
$$
\Delta P_{oracle} \le 2 \times Fee_{pool}
$$
For example, with a 0.04% pool fee, the oracle must not shift by more than 0.08% in a single block. If the underlying asset accrues faster than this (unlikely for most LSTs but possible for other assets), the oracle contract must apply **rate smoothing** — capping the per-block change to stay within this bound. See [The Oracle Sandwich](../understanding-stableswap.md#1-the-oracle-sandwich-2-fee-rule) for the full explanation.
#### What to Avoid
:::danger Oracle Dangers
- **Never use a spot price** from any DeFi pool — these are trivially manipulable via flash loans
- **Never use withdrawal simulations** like `calc_withdraw_one_coin()` or any balance-based calculation — these are manipulable
- **Never use spot token balances** to derive value
- **Never use an oracle that can decrease** — the value must be strictly non-decreasing
- **Avoid oracles derived from low-liquidity markets** — never use a price source backed by less value than the Curve pool itself
:::
:::warning Oracle Requirements
- The oracle function must return a value with **1e18 precision** (18 decimals)
- Oracles may be externally controlled (e.g. by a multisig or EOA) — verify who controls it and whether they can update it arbitrarily
:::
:::tip-green
Swiss Stake, the service provider developing Curve's technology, offers consulting services to guide protocols and asset issuers in the design and integration of price feed oracles for StableSwap liquidity pools. Additionally, Swiss Stake provides thorough audits of client-implemented oracle solutions to ensure correctness and security. Inquiries: [inquiries@curve.finance](mailto:inquiries@curve.finance).
:::
### Rebasing Tokens
Enable the **Rebasing** option for tokens whose balances adjust automatically (e.g. stETH). These behave differently from standard ERC‑20s and require special handling in the AMM.
:::warning Rebasing Support
Make sure you understand how rebasing affects pool math and accounting. Unexpected results can occur if treated as a standard token.
:::
### ERC‑4626 Vault Tokens
Select **ERC‑4626** for tokens that follow the yield-bearing vault standard (e.g. Yearn, Beefy). These represent shares of an underlying asset and must be handled accordingly by the pool.
:::warning ERC‑4626 Caveats
Some ERC‑4626 implementations may be vulnerable to donation/inflation exploits. Only use well-audited and battle-tested vaults.
:::
Once you've selected and configured all your tokens, click **Next →** to continue to the pool parameters.
## Step 3: Set Pool Parameters
You can choose a preset configuration or switch to advanced mode to fine-tune the pool's behavior.
### Preset Options
The UI offers three default parameter sets optimized for typical use cases. These are great starting points if you're unsure what values to use.
You can use these as-is or switch to **Advanced** mode to adjust parameters manually.
### Advanced Parameters
To choose your own parameters, simply toggle on the "Advanced" toggle:
| Parameter | Range | Description |
|--------------------------|------------------|-----------------------------------------------------------------------------|
| **A (Amplification)** | `1` to `10,000` | Controls price stability near the peg. Higher = flatter curve = deeper liquidity. More here: [Stableswap A](../understanding-stableswap.md#amplification-factor-a)|
| **Swap Fee** | `0%` to `1%` | Charged on each swap. Higher fees discourage arbitrage and low-volume trades. |
| **Off-peg Fee Multiplier** | `0` to `12.5` | Dynamically increases the fee when the pool becomes imbalanced. More here: [Offpeg Fee Multipler](../understanding-stableswap.md#offpeg-fee-multiplier-and-dynamic-fees) |
| **Moving Average Time** | `60` to `3600s` | Smooths the oracle price over time to reduce short-term volatility. |
Once you're satisfied with the parameter settings, click **Next →** to continue.
## Step 4: Enter Pool Info
After configuring your parameters, you’ll be prompted to set the **Pool Name** and **Pool Symbol**.
These values will be used as the ERC‑20 metadata for the pool’s LP token and will appear in block explorers and in the Curve UI.
- **Pool Name** – A human-readable label for the pool (e.g. `USDC/DAI/USDT Stable`)
- **Pool Symbol** – The LP token symbol (e.g. `3CRV`, `sUSDeCRV`, `bUSD3CRV`)
> 💡 **Tip:** Use short, clear names that reflect the assets in the pool. For metapools, it’s common to include the base pool symbol (e.g. `sDAI3CRV` for a sDAI/3CRV pool). These fields are immutable after deployment — double-check for typos.
## Step 5: Deploy the Pool
Before deploying, review all your settings in the **Summary panel** on the right. This includes:
- Selected tokens and their types
- Pool parameters (Swap Fee, A, etc.)
- Pool name and symbol
Once everything looks correct, click the blue **Create Pool** button at the bottom of the page. Your wallet will prompt you to approve the transaction.
---
## What to do after deployment
✅ **Seed initial liquidity** — a pool with zero balance cannot process trades.
✅ (Optional) **[Create a gauge](/protocol/gauge/overview)** to distribute CRV or other incentives to LPs.
---
## Automating Refuels
:::vyper[`DonationStreamer.vy`]
The technical documentation for the `DonationStreamer.vy` contract can be found [here](/developer/amm/twocrypto-ng/implementations/donation-streamer). The contract is deployed at the same address on all supported chains:
- :logos-ethereum: Ethereum: [`0x2b786BB995978CC2242C567Ae62fd617b0eBC828`](https://etherscan.io/address/0x2b786BB995978CC2242C567Ae62fd617b0eBC828)
- :logos-gnosis: Gnosis: [`0x2b786BB995978CC2242C567Ae62fd617b0eBC828`](https://gnosisscan.io/address/0x2b786BB995978CC2242C567Ae62fd617b0eBC828)
- :logos-base: Base: [`0x2b786BB995978CC2242C567Ae62fd617b0eBC828`](https://basescan.org/address/0x2b786BB995978CC2242C567Ae62fd617b0eBC828)
:::
[FXSwap pools](../understanding-fxswap.md) use **refuels** to subsidize rebalancing and keep liquidity tight around the current price. While refuels can be added manually via [crvhub.com/refuel](https://crvhub.com/refuel), the **Donation Streamer** lets you automate this process — depositing tokens once and having them streamed into the pool over a set schedule.
This is useful for projects that want to keep their FXSwap pools consistently refueled without manual intervention.
:::info
The Donation Streamer UI is available at [**curvefi.github.io/refuel-automation**](https://curvefi.github.io/refuel-automation/).
:::
## How It Works
1. You choose which pool to refuel and how many tokens to deposit in total.
2. You set a schedule: how often refuels should happen (e.g. once per day) and for how many periods.
3. You set a small ETH bounty per period to incentivize someone to trigger each refuel.
4. Your tokens and ETH bounties are locked in the contract.
5. Each period, anyone can trigger the refuel and collect the bounty. An automated bot also runs every 8 hours, so refuels generally happen without any manual effort.
6. Once all periods are complete, the stream is finished. You can also cancel at any time to recover remaining tokens and unspent bounties.
## Supported Chains
The Donation Streamer is deployed at the same address on :logos-ethereum: Ethereum, :logos-gnosis: Gnosis, and :logos-base: Base.
## Creating a Refuel Stream
The UI has three sections: pool loading at the top, stream configuration in the middle, and the stream executor at the bottom.
### Step 1: Connect Your Wallet
Click **Connect Wallet** in the top-right corner. Make sure your wallet is set to the correct chain. The UI will show your connected address and chain ID.
### Step 2: Load the Pool
The **DonationStreamer Address** is pre-filled. Enter the **Pool Address** of the FXSwap pool you want to refuel and click **Load Pool**. The two info boxes below will populate with the pool's tokens (**Coin 0** and **Coin 1**), showing each token's name, decimals, your balance, and your current allowance.
### Step 3: Configure the Stream
Once the pool is loaded, fill in the fields in the middle section:
| Field | What it means |
| --- | --- |
| **Amount Coin 0** | Total amount of the first token (shown under Coin 0 above) to refuel across all periods |
| **Amount Coin 1** | Total amount of the second token (shown under Coin 1 above) to refuel across all periods |
| **Period Length (sec)** | How often a refuel should happen, in seconds (default `86400` = once per day) |
| **Number of Periods** | How many refuels to execute in total (default `7`) |
| **Reward per Period (ETH)** | A small ETH bounty paid to whoever triggers each refuel (default `0.01`) |
The **Total Reward** displayed next to these fields is calculated automatically as `Reward per Period × Number of Periods`. This ETH is sent with the transaction and held by the contract until claimed by executors.
:::tip
You can refuel with just one of the two tokens — set the other amount to `0`.
:::
**Example:** The screenshot below shows a stream being configured to refuel a crvUSD/ZCHF pool with 30 crvUSD over 3 periods (once per day), with a bounty of 0.001 ETH per execution (0.003 ETH total).
### Step 4: Approve and Create
1. Click **Approve Coin 0** and/or **Approve Coin 1** to allow the contract to transfer your tokens. Only approve tokens where the amount is greater than zero. The status bar below will confirm the approval (e.g. "Approved crvUSD.").
2. Click **Create Stream**. Your tokens and ETH bounties are locked in the contract, and the refuel schedule begins.
## Monitoring Streams
Once a pool is loaded, you can view all active streams for that pool in the **Pool Streams** section. Enter a **Lookback (count)** to control how many recent streams to scan and click **Load Pool Streams**.
Each stream card shows:
- **Stream ID and remaining periods** (e.g. "Stream #2 — Periods 2")
- **Pool and donor addresses** (your own streams are marked with **(you)**)
- **Remaining token amounts and ETH reward** still locked in the stream
- **Time until next execution** (e.g. "Execute (in 23h 59m 0s)")
## Execution
An [automated bot](https://github.com/curvefi/refuel-automation) runs every 8 hours and executes all due refuel streams across all supported chains. In most cases, you don't need to do anything after creating a stream.
To manually trigger due refuels (or to earn the ETH bounties yourself), use the **Stream Executor** section at the bottom of the UI. The **StreamExecutor Address** is pre-filled — just click **Execute via Executor** to batch-execute all currently due streams and collect the bounties.
## Tracking Refuel Consumption
:::info
Track refuel activity for any pool on the [**Refuel Monitor**](https://refuel.curvemonitor.com/ethereum).
:::
Select your pool to view detailed stats. For example: [refuel.curvemonitor.com/ethereum/0x027B...](https://refuel.curvemonitor.com/ethereum/0x027B40F5917FCd0eac57d7015e120096A5F92ca9).
The dashboard shows **Donation Shares** (total refuel buffer) vs **Unlocked Shares** (how much has been consumed by rebalancing), as well as **Daily Donations** in USD over the last 7 days.
It also includes a **Top Donors** leaderboard showing each donor's total contribution in USD, number of donations, and token breakdown.
## Cancelling a Stream
You can cancel your stream at any time to recover remaining tokens and unspent ETH bounties. In the **Pool Streams** section, find your stream (marked with **(you)**) and click the **Cancel** button.
## Further Reading
- [Understanding FXSwap — Refuels](../understanding-fxswap.md#refuels) — how refuels work and what they cost
- [DonationStreamer Contract Reference](/developer/amm/twocrypto-ng/implementations/donation-streamer) — technical documentation for the smart contract
- [FXSwap Implementation](/developer/amm/twocrypto-ng/implementations/fxswap) — how donations are handled at the pool level
---
## Overview & Pool Types
Curve offers a **permissionless** system for deploying liquidity pools — no DAO vote, no approval process, and no technical barrier beyond the gas cost to deploy. A full-featured interface is available in the Curve app, so you can launch a custom pool without writing any code.
To get started quickly, follow one of the deployment guides:
Factories make launching pools on Curve fast, flexible, and accessible to any project—whether you're a stablecoin issuer, LST protocol, synthetic-asset platform, or any other DeFi team looking to bootstrap deep, reliable liquidity.
## How are Pools Deployed?
In the background, new liquidity pools are deployed by making use of a **Pool Factory**, which essentially is a smart contract for deploying new pools. Each factory contains the logic and configuration for a specific pool type. Factories support a few different pool types:
- **Stableswap** — for pegged or correlated assets (e.g., USDC/USDT, stETH/ETH)
- **Cryptoswap** — for more volatile or uncorrelated assets (e.g., ETH/USDC)
- **FXSwap** - for lower-volatility assets like Forex pairs, or lower volatility Crypto pairs like BTC/ETH.
Once a factory is deployed, anyone can create a new pool of that type — either through the Curve UI or directly on-chain.
:::info
**Pool deployment is completely free of charge** beyond standard gas costs. There are no additional fees, no protocol charges, and no hidden costs associated with deploying a new pool on Curve.
:::
Pools deployed via a factory appear automatically on the Curve frontend after a short propagation period and are picked up by aggregators such as 1inch and CowSwap. You don't need to chase integrations.
## Choosing the Right Pool Type
Curve supports multiple pool designs to fit different kinds of assets. Selecting the correct type is essential to ensure low slippage, efficient trading, and capital-efficient liquidity.
Not sure which to use? Reach out in the official Curve channels.
### Stableswap Pool
Choose a **Stableswap pool** when your assets are expected to stay close or correlated in price — e.g., stablecoins (USDC/USDT), LSTs (wstETH/stETH), or yield-bearing stable assets like sDAI. Learn more here: [Understanding Stableswap](understanding-stableswap.md).
Stableswap-NG pools support a wide variety of token types beyond standard ERC-20 tokens. This flexibility allows you to create pools with yield-bearing tokens, rebasing tokens, and oracle-enabled tokens.
**Supported Asset Types:**
| Type | Description | Use Cases | Examples |
|------|-------------|-----------|----------|
| **0** | Standard ERC-20 | Basic tokens with no special features | USDC, USDT, DAI |
| **1** | Oracle-enabled | Tokens with rate oracles for accurate pricing | wstETH, cbETH |
| **2** | Rebasing | Tokens that change supply over time | stETH |
| **3** | ERC4626 Vault | Yield-bearing vault tokens | sDAI |
**Technical Requirements:**
- All tokens must be ERC-20 compatible (return True/revert, True/False, or None)
- Maximum 18 decimals supported
- Oracle tokens must have precision $\le 18$
- ERC4626 vaults support arbitrary precision for both vault and underlying tokens
### Cryptoswap
Choose a **Cryptoswap pool** when your assets are more volatile or uncorrelated—e.g., ETH/USDC or BTC/USDC. Cryptoswap pools use dynamic pricing that handles larger price swings while still retaining Curve’s efficiency advantages. Learn more about [Understanding Cryptoswap](understanding-cryptoswap.md).
### FXSwap
**FXSwap** is designed for uncorrelated low-volatility asset pairs like Forex (e.g., crvUSD/EURC) or lower volatility crypto pairs (e.g., BTC/ETH). It combines Stableswap's mathematical efficiency with Cryptoswap's dynamic rebalancing framework, plus a "refueling" mechanism that allows projects to fast-track rebalancing with external incentives. Learn more about [Understanding FXSwap](understanding-fxswap.md).
---
## Base Pools and Metapools
Stableswap pools on Curve support a powerful structure of **base pools** and **metapools**.
- A **base pool** is a regular Curve pool that the DAO has specifically approved to be used in metapools.
- A **metapool** pairs a token against an existing base pool rather than against a single token.
For example, the USDC/USDT pool might begin as a normal Stableswap pool. If the DAO adds it as a base pool, it can then be reused in other pools. A protocol such as Inverse can create a metapool that pairs their stablecoin DOLA against the base pool, resulting in a DOLA–USDC/USDT market. Users can directly swap DOLA/USDT or DOLA/USDC through this pool.
This approach gives new tokens a major advantage: they can **tap into the deep, established liquidity of the base pool** instead of needing to attract all liquidity themselves **by** pairing their token against an already existing and established pool with TVL.
---
## Pool Parameters
## Setting Parameters at Deployment
Pool parameters are set at the time of pool deployment, and most parameters can be changed by the DAO later through governance votes. This flexibility allows for parameter optimization as market conditions evolve and new data becomes available.
When deploying pools via the Curve UI, recommended parameters are automatically suggested based on the algorithm type and tokens used.
## Monitoring Parameters
Monitoring parameters is crucial for optimal pool performance. Depending on factors like market conditions, asset liquidity, trading volume, parameters can be optimized to help LPs earn more fees and provide better exchange rates for traders. Regular parameter adjustments ensure pools remain competitive and efficient.
:::info
**Struggling with parameters?** Don't worry, you're not alone! Jump into our Telegram and let the Curve wizards work their magic. From A to γ, we've got your back! 🧙♂️
:::
## Stableswap Parameters
| Name | Variable Name | Description |
| :--: | :-----------: | ----------- |
| Amplification coefficient | `A` | Controls liquidity concentration around the 1:1 price. Higher values reduce slippage near the peg, lower values spread liquidity more evenly. More here: [Stableswap `A`](understanding-stableswap.md#amplification-factor-a) |
| Regular fee | `fee` | Regular trading fee charged on swaps when pool is perfectly balanced |
| Off-peg fee multiplier | `offpeg_fee_multiplier` | Multiplier applied to fees when pool is imbalanced. The greater the imbalance, the higher the multiplier. More here: [Dynamic Fees](understanding-stableswap.md#offpeg-fee-multiplier-and-dynamic-fees) |
| EMA time | `ma_exp_time` | Time constant for exponential moving average price oracle |
## Cryptoswap Parameters
CryptoSwap extends StableSwap by keeping **A** and adding parameters that adapt the curve and the fees to volatile assets.
| Name | Variable Name | Role |
|------|:------:|-------------------|
| Amplification coefficient | `A` | Same meaning as in Stableswap, sets liquidity depth at balanced price. However, `A`=10,000 in Cryptoswap is `A`=1 in Stableswap, giving extra precicion for Cryptoswap pools. More here: [Cryptoswap Parameters](./understanding-cryptoswap.md#parameters)|
| Curve-width modifier | `γ` (gamma) | Controls how fast prices degrade when pools become unbalanced, lower values mean price declines are more gradual. More here: [Cryptoswap Parameters](./understanding-cryptoswap.md#parameters) |
| EMA half-life | `ma_time` | Time constant (in seconds) of the exponential moving average that tracks the market price |
| Allowed extra profit | `allowed_extra_profit` | Minimum theoretical arbitrage gain (in bp) before the contract updates its internal price scale, preventing micro-adjustments |
| Adjustment step | `adjustment_step` | The minimum rebalancing step (min movement of `price_scale`) |
| Mid fee | `fee_mid` | Swap fee when the pool is perfectly balanced. More here: [Cryptoswap Dynamic Fees](./understanding-cryptoswap.md#dynamic-fees)|
| Out fee | `out_fee` | Maximum fee charged at total imbalance. More here: [Cryptoswap Dynamic Fees](./understanding-cryptoswap.md#dynamic-fees) |
| Fee gamma | `fee_gamma` | Governs how quickly the fee rises between `fee_mid` and `out_fee` as the pool leaves equilibrium. More here: [Cryptoswap Dynamic Fees](./understanding-cryptoswap.md#dynamic-fees)|
## FXSwap Parameters
FXSwap parameters inherit the same ones as the Cryptoswap parameters above. Additionally, there are some governable parameters for the refuel (donation) mechanism:
| Name | Variable Name | Role |
|------|:------:|-------------------|
| Amplification coefficient | `A` | Controls liquidity concentration around the balanced price. Higher values reduce slippage near the peg, lower values spread liquidity more evenly. Same as Cryptoswap `A` (Stableswap `A` factored by 10,000). More here: [FXSwap `A`](understanding-stableswap.md#amplification-factor-a) |
| EMA half-life | `ma_time` | Time constant (in seconds) of the exponential moving average that tracks the market price |
| Allowed extra profit | `allowed_extra_profit` | Minimum theoretical arbitrage gain (in bp) before the contract updates its internal price scale, preventing micro-adjustments |
| Adjustment step | `adjustment_step` | The minimum rebalancing step (min movement of `price_scale`) |
| Mid fee | `fee_mid` | Swap fee when the pool is perfectly balanced. More here: [Cryptoswap Dynamic Fees](./understanding-cryptoswap.md#dynamic-fees)|
| Out fee | `out_fee` | Maximum fee charged at total imbalance. More here: [Cryptoswap Dynamic Fees](./understanding-cryptoswap.md#dynamic-fees) |
| Fee gamma | `fee_gamma` | Governs how quickly the fee rises between `fee_mid` and `out_fee` as the pool leaves equilibrium. More here: [Cryptoswap Dynamic Fees](./understanding-cryptoswap.md#dynamic-fees)|
| Donation Duration | `donation_duration` | Time required for refuels (donations) to fully unlock (default: 7 days) |
| Protection Period | `donation_protection_period` | Maximum duration donation protection can be extended (default: 10 minutes) |
| Protection LP Threshold | `donation_protection_lp_threshold` | LP addition threshold that triggers protection extension (default: 20%) |
| Maximum Donation Share Ratio | `donation_shares_max_ratio` | Cap on donation shares as percentage of total supply (default: 10%) |
---
## Post Deployment(Pool)
After deploying a pool, there are a few important steps to ensure it’s visible, functional, and capable of attracting liquidity and rewards.
## Seed Initial Liquidity
Before a pool can process any trades, it must be seeded with initial liquidity. To get the pool going, it's highly recommended to add liquidity in proportional balances. For two-coin pools 50/50, three-coin pools 33/33/33, etc.
The frontend filters out liquidity pools with less than $10k TVL. The pool will be hidden by default from the pool list. Users can still search for the pool using its name or contract address, or simply disable the "Hide very small pools" filter.
## Integrations
Liquidity pools deployed via the Curve UI are automatically picked up by major aggregators like 1inch, Odos, CowSwap, etc. No need to hustle for integrations!
## Token Logo Submission
Curve maintains a comprehensive list of token icons. If your pool's assets are not included in this list, they will display default logos in the Curve UI. To submit custom logos for your tokens, open a pull request to the [curve-assets repository](https://github.com/curvefi/curve-assets?tab=readme-ov-file#adding-a-token-icon).
## Monitor Performance and Update Parameters
After deployment, it's important to monitor your pool's performance regularly. Check trading volumes, TVL, and token balances to ensure healthy operation. Heavily skewed token balances could indicate incorrect parameterization, such as an amplification coefficient that's too high.
Some pools are later optimized by external contributors or protocols. However, **it's still a good idea to monitor your own pool** and explore whether tuning the parameters could improve performance.
If you'd like help evaluating or optimizing your pool, feel free to reach out in the official Curve channels.
## Add Refuels (FXSwap Pools)
If you deployed an **FXSwap pool**, consider adding refuels to subsidize rebalancing and keep liquidity tight around the current price. Refuels can be added manually via [crvhub.com/refuel](https://crvhub.com/refuel), or automated with the [**Donation Streamer**](./guides/donation-streamer.md). For more on how refuels work, see [Understanding FXSwap — Refuels](./understanding-fxswap.md#refuels).
## Grow Liquidity Using Incentives
Curve offers various ways for protocols or asset issuers to sustainably grow liquidity and asset proliferation. Examples include attracting gauge weight to receive CRV emissions or providing external incentives and points.
---
## Understanding Cryptoswap
## Historical Background
The CryptoSwap algorithm was also invented by Michael Egorov as an evolution of the original StableSwap model.
It was introduced in late 2021, alongside the launch of Curve’s “TriCrypto” pools (e.g., USDT–wBTC–ETH), which allowed swaps between uncorrelated assets — not just stablecoins.
CryptoSwap extended the StableSwap formula by introducing a dynamic amplification coefficient (A) that automatically adjusts based on market volatility. This made it possible to offer tight spreads and deep liquidity when prices are stable, but also greater resilience and arbitrage opportunities during large price moves.
The underlying math was formalized in the paper [“CryptoSwap: Constant Product and Constant Sum Market Maker with Dynamic Parameters”](../../../static/pdf/whitepapers/whitepaper_cryptoswap.pdf), published by Curve in 2021, and it marked Curve’s transition from being a purely stablecoin-focused AMM to a generalized DEX capable of efficiently trading volatile crypto pairs.
## How it Works
Cryptoswap is an automated market maker (AMM) pool developed by Curve for swapping between uncorrelated assets, such as `ETH` and `USDT`, where the exchange rate between these assets changes.
Cryptoswap pools build upon the core Stableswap algorithm, but with a key innovation: *where* liquidity is concentrated. Instead of targeting a fixed peg, Cryptoswap automatically concentrates and rebalances liquidity around the pool's **recent average price**. This allows it to efficiently support **volatile asset pairs** (e.g., `crvUSD/ETH`) while making the entire process **fully passive for liquidity providers**.
## Parameters
This article from Nagaking goes into detail about each of Cryptoswap's parameters: [Deep Dive: Curve v2 Parameters](https://nagaking.substack.com/p/deep-dive-curve-v2-parameters).
The shape of the Liquidity Bonding Curve is governed by two parameters: `A` which is also present in Stableswap, as well as a new parameter called `gamma`:
- **`A`**: controls liquidity concentration in the center of the bonding curve (the same as Stableswap, however `A` values are scaled up in Cryptoswap. An `A` of 10,000 in Cryptoswap is equivalent to an `A` of 1 in Stableswap
- **`gamma`**: controls whether liquidity drops off gradually or sharply away from the center of the bonding curve. In Stableswap `gamma` is equal to 1. Higher `gamma` pulls more liquidity in around the center of liquidity.
Below you can see how they affect the shape of the liquidity curve in practice (note that orange curve are equal in both charts):
As the image shows, a higher `A` means more liquidity is concentrated around the price at which it's center of liquidity, called the `price_scale`. Whereas a higher `gamma` means liquidity is spread wider.
Pricing is all about the balance of the pool, so let's look at how balance affects the value of the assets in the pool, based on `A` and `gamma`:
## Rebalancing
Assets within Cryptoswap pools are volatile, so prices and exchange rates are constantly changing. Cryptoswap's goal is to center most liquidity close to the current price, which allows more trading volume, and therefore more profit for LPs. So, as prices move, the algorithm must re-center or "rebalance" its liquidity to follow it. This process is handled carefully, because **rebalancing realizes impermanent loss**. To protect LPs, Cryptoswap only rebalances when two conditions are met:
1. The internal price must move beyond a minimum threshold, known as the **adjustment step**.
2. The cost of rebalancing must be less than 50% of the trading fees earned by LPs. **This core safeguard ensures that impermanent loss is only realized when it is sufficiently offset by trading profits**, helping to prevent the erosion of LP deposits from rebalancing fees over time.
!!! info "Internal EMA Price Oracle"
For added safety, rebalances are triggered not by the last price of the pool, but by an **Exponential Moving Average** (EMA) of all recent prices. This internal price oracle helps prevent manipulation of rebalances.
In the following examples, the EMA Price and current price are assumed to be the same. In reality, the EMA will lag the current price slightly.
Let's look at an example using a forex pool trading Euros (EUR) against US Dollars (USD):
1. Liquidity is initially balanced around the price of $1.10, which is also the current price. It looks and functions like a normal Stableswap pool.
2. A user swaps USD for EUR, and the EUR/USD price increases to $1.105, because of the slight imbalance. While price stays within Minimum Step range, it will continue functioning like Stableswap.
3. Another swap happens, generating more profit for LPs. This increases price to $1.11, outside adjustment step range. A rebalance can now occur, assuming the pool has enough profit to rebalance.
4. The pool can use up to half the generated profit from swaps to move to balance liquidity around around the new price of $1.11, automatically.
In this scenario, the pool performs a rebalance once the price hits the adjustment step, using up to 50% of its collected fees to cover the cost.
To make this clearer, here's how the [CVX/WETH pool](https://www.curve.finance/dex/ethereum/pools/cvxeth/deposit)'s liquidity changed over a month period, notice how the center of liquidity (`price_scale`) updates frequently when it's close to the oracle price, and much more slowly otherwise. Note that we've put the liquidity into discrete 0.5% range buckets below, so we can see a USD value for the liquidity available.
## Why Does Rebalancing Cost?
Rebalancing costs because you are offering your assets to be swapped in return for trading fees. As price increases, you are selling your assets. When you rebalance, you are rebuying your assets, but at a higher price, causing a loss. Let's look at a very simple example of a concentrated liquidity range AMM:
This example highlights two important takeaways about rebalancing:
* If the user had not rebalanced, they would have kept their initial asset value.
* If the price continued to increase without a rebalance, the user's impermanent loss would have been larger, as they would need to buy back assets at an even higher price to re-enter the liquidity range.
:::info "Cryptoswap Rebalancing"
Rebalancing is necessary, but both frequent and infrequent rebalancing can lead to significant losses.
Cryptoswap automates this process to strike a balance, only rebalancing when two conditions are met:
1. The price has changed more than the minimum amount
2. The rebalance costs less than 50% of the profit earned from swaps
This ensures LPs remain profitable and minimizes rebalances, while maintaining high liquidity depth for swappers.
:::
## Dynamic Fees
Cryptoswap and all new Stableswap pools feature **dynamic fees** that adjust to increase returns for LPs when their liquidity is in high demand.
Have a play below to see how the fee changes with the pool balance. Note: For an ETH/USD pool, a 70/30 value ratio means 70% of the total value is in ETH (e.g., \$700k) and 30% is in USD (e.g., \$300k), or vice versa.
## Cryptoswap Benefits
**1. Passive LPing and Decentralization**
Cryptoswap was built on the original cypherpunk ethos of DeFi: that anyone should be able to provide liquidity easily, passively, and profitably. Compared to protocols that require LPs to become active managers, Cryptoswap's design allows for broader participation, increasing the resilience of the ecosystem.
**2. Automatic Impermanent Loss Management**
The algorithm is designed to protect LPs from rebalancing losses (as much as possible). By only rebalancing when the fees earned are **more than double the cost**, it ensures that the act of locking in impermanent loss is itself profitable. This prevents the pool from "chasing" the price at a loss to LPs.
**3. Capital Efficiency**
This efficiency stands in contrast to classic AMMs with the `x*y=k` invariant, which use the $x \cdot y = k$ formula. In those models, liquidity is spread thinly across all possible prices (from zero to infinity). By concentrating liquidity around the current market price, Cryptoswap offers significantly lower slippage for traders and generates more fees for LPs from the same amount of capital.
## Stale Pools - How Cryptoswap Pools Can Become Stuck
A Cryptoswap pool's main safety feature is its refusal to rebalance at a loss to LPs. However, this can sometimes cause a pool to become **stuck**, meaning it has a **stale liquidity concentration** because the center of liquidity (`price_scale`) is very different from the current price. This can trigger a negative feedback loop during periods of high volatility:
As the market price moves away from the pool's center of liquidity, the available liquidity for traders decreases. This leads to fewer swaps and, consequently, lower fee generation. Without enough profit from fees, the pool cannot afford to rebalance and follow the price, leaving its liquidity stranded.
1. Another swap happens, generating more profit for LPs. This increases **price to $1.11, outside adjustment step range**. However, if the **pool hasn’t generated enough profit, a rebalance cannot occur**.
2. **Rebalances must cost less than 50% of profit** generated from swaps. So if the price continues increasing, there’s less liquidity available, and so less swaps happen and less profit is generated. Also, rebalancing becomes more expensive. So **rebalancing is unlikely unless price decreases or enough profit is generated.**
## Monitoring Liquidity Balance within Pools
To see how balanced liquidity is within a pool, navigate to the pool's page, for example, the [EURe/USDC pool on Arbitrum](https://www.curve.finance/dex/arbitrum/pools/factory-twocrypto-89/deposit). At the bottom of the pool details, click the `Advanced` tab. You will then see the following details:
In the image above, you can see two key parameters:
- **Price Scale**: This is the price at which liquidity was last rebalanced, also called the center of liquidity.
- **Price Oracle**: This is an Exponential Moving Average (EMA) of recent market prices. Rebalances are triggered by this price, not the current market price, which helps prevent manipulation of the rebalancing mechanism.
## How to Prevent Stale Liquidity within Pools
The best prevention for stale liquidity is **proper parameterization**.
Choosing a higher `gamma` and a lower `A` spreads liquidity across a wider price range. This makes a pool more resilient to volatility in two ways:
1. It ensures the pool can continue to facilitate trades and earn fees even during large price swings.
2. It makes the eventual rebalance cheaper because the liquidity is less concentrated.
For assistance with simulations to find reasonable parameters for your pool, refer to [Llamarisk](https://www.llamarisk.com/).
## Help! My Pool's Liquidity is Stale!
If your pool's liquidity becomes stale, you have three primary options:
1. **Change Pool Parameters:** Through a DAO vote, parameters can be gradually changed (a process called "ramping"). Reducing `A` and adjusting `gamma` will spread out liquidity, adding depth at the current price. If parameterization was not the root cause, these parameters can be ramped back to their original values once the pool recovers.
2. **Seed a New Pool:** This option is typically only viable for protocols that own most of the pool's liquidity (POL). It involves deploying a new pool with better parameters and "killing" the old gauge, if applicable.
3. **Wash Trade the Pool:** This involves generating high trading volume (often via flash loans) to create enough fee profit for the pool to rebalance. This requires careful coding and simulation, and is performed at a loss with no guarantee of a lasting fix, as another market swing can immediately undo the rebalance. This strategy should only be considered as a last resort.
## Why Not Use Stableswap with an External Oracle?
Since Stableswap is highly efficient around a single price and can be guided by an external oracle, many developers have considered using this design to price volatile asset pairs against each other.
While some protocols have attempted this, and it can work (e.g., [Spectra](https://spectra.finance/)'s pools), there are a few important considerations:
* **Rebalancing Costs:** Every time the oracle pushes a new price, the pool is forced to rebalance. For volatile assets, these frequent rebalances can accumulate into significant losses for LPs. Profitability depends on trading fees being high enough, or LPs being subsidized in other ways, such as with token emissions. However, for low-volatility assets (even USD/EUR volatility is too high), this technique can work well.
* **Oracle Dependency:** This design requires a high-quality oracle. A malfunctioning, manipulated, or delayed oracle could report an incorrect price, leading to substantial losses for LPs.
In contrast, Cryptoswap's **profit-aware rebalancing mechanisms** are designed specifically to mitigate these risks for highly volatile asset pairs.
---
## Understanding FXSwap
FXSwap is a specialized Automated Market Maker (AMM) designed for uncorrelated but low-volatility asset pairs (such as Forex pairs `crvUSD` and `EURC`). It leverages the mathematical efficiency of [Stableswap](understanding-stableswap.md) within the dynamic rebalancing framework of [Cryptoswap](understanding-cryptoswap.md), along with some new innovations.
As with all Curve AMM pools, providing liquidity in FXSwap is completely passive. All liquidity is full-range (no active range management required), so anyone can participate easily.
## Why FXSwap?
**Cryptoswap** pools are designed to be entirely self-sufficient, relying on trading volumes and swap fees to naturally rebalance liquidity around the market price. This model works well for assets where the Cryptoswap pool is the dominant liquidity pool for tokens, and where price discovery happens (e.g., CVX, YB, RSUP, etc).
However, for asset pairs where price discovery is mostly happening elsewhere, (e.g., forex, BTC, ETH, etc) the pool needs to quickly rebalance and stay balanced around the current market price. This is why **FXSwap** pools were created.
**FXSwap** does this by introducing **Refuels**. This mechanism allows projects to "fast-track" the rebalancing process using external incentives rather than waiting for accumulated trading profits. This ensures the pool remains efficient and balanced around the current price even during periods of low volatility.
:::info
**Note:** As FXSwap architecture is derived from these two predecessors, we recommend reviewing their documentation first. See [Stableswap explainer](understanding-stableswap.md) and [Cryptoswap explainer](understanding-cryptoswap.md).
:::
## How it works
FXSwap was created to provide efficient, fully passive forex-style liquidity on-chain. It achieves this by taking specific components from its predecessors:
- **Stableswap Invariant:** To keep spreads tight, FXSwap utilizes the Stableswap invariant for pricing. This is ideal for assets where liquidity should remain highly concentrated near the current oracle price.
- **Hybrid Rebalancing:** Like Cryptoswap, up to 50% of trading fees are used to rebalance liquidity. However, FXSwap introduces a **Refuelling mechanism**. Refuels act as an external buffer to cover rebalancing costs, protecting LP profitability and ensuring faster price alignment.
- **Dynamic Fees:** The pool utilizes a dynamic fee model, increasing fees during periods of high volatility and imbalance to protect LPs and capture value.
Let's look at how an FXSwap pool prioritizes these resources while moving liquidity:
You can see from the video above that if refuels are available, they are used first, however a small amount of profit is always consumed.
## Recommended Assets
Because FXSwap pools utilize donations to maintain efficiency, they are best suited for **highly liquid assets with lower relative volatility**.
It is generally not recommended to use FXSwap for primary price discovery. For example, YieldBasis utilizes FXSwap for its `BTC/crvUSD` pools (established assets tracking external prices), but uses a standard Cryptoswap pool for `YB/crvUSD` (the primary market for its governance token).
## Refuels
Refuels are LP tokens deposited into the pool specifically to subsidize rebalancing. The core premise is that liquidity is deepest when it's balanced, therefore, more balanced liquidity attracts higher volumes and profits for LPs; therefore, Refuels are an expense that is offset by higher aggregate returns for LPs.
Refuels are similar to market making fees, however instead of requiring trusting a third party, refuels are completely transparent for your team and LPs, keeping the pool balanced, and trustlessly optimizing liquidity depth and slippage for traders of your asset.
**Key Mechanics:**
- **Open Access:** Refuels can be added by anyone.
- **Vesting Period:** Deposits are initially locked and unlock linearly over a set duration (default is 7 days, configurable via `donation_duration`) to prevent immediate depletion.
- **Priority Usage:** Unlocked Refuels are the "first line of defense." They are consumed to recenter liquidity before the protocol taps into LP trading fees. This ensures the pool offers optimal swap rates faster, generating more volume and profits.
- **Efficiency:** The introduction of Refuels allows the pool to trigger rebalances on smaller price movements, keeping the peg tighter than standard pools.
### How much do Refuels cost?
Three main factors influence the cost of Refuels:
1. **Volatility:** Higher volatility requires more frequent rebalancing.
2. **Liquidity Concentration (`A`):** Higher `A` values create deeper liquidity, but increase the cost to move that liquidity when prices change.
3. **Swap Fees:** Higher volume generates more fees. Since 50% of fees are used for rebalancing, high-volume pools require fewer external Refuels.
**Case Study: YB Pools (BTC/crvUSD)**
*Data analyzed for ~45 days after TVL reached $100M.*
- **TVL:** ~$100M per pool
- **Total Fees Paid:** ~$86k / day
- **Swap Fees used to Rebalance:** ~$43k / day (50% of total fees)
- **Refuels Consumed:** ~$10k / day
- **LP Returns (Net):** ~$43k / day (~12% APY)
In this scenario, Refuels represent only **3.6% of TVL per year** (approx. 23% of total rebalancing costs), while enabling deep liquidity that created massive fee generation. This feeds into the FXSwap liquidity cycle:
### How Can I Refuel a Pool?
Refuels are deposited via the standard `add_liquidity` function. A community UI is available at: [crvhub.com/refuel](https://crvhub.com/refuel)
To set up **recurring automated refuels**, use the [**Donation Streamer**](./guides/donation-streamer.md) — deposit tokens once and have them streamed into the pool on a schedule.
### Can I add another Refuel while one is unlocking?
Yes. FXSwap pools track the total refuel amount within an unlock period (default 7 days) and calculate a continuous unlock rate.
**Example** (with 7 day unlock time):
1. **Day 0:** A \$700 refuel is added, unlock rate: \$100/day.
2. **Day 3:** \$300 has been consumed; \$400 remains.
3. **Day 3:** A new \$1400 refuel is added.
- **New Total:** The total refuels for this period is now \$700 (old) + \$1400 (new) = \$2100.
- **New Rate:** The total of \$2100 unlocked over 7 days is a rate of \$300/day, this is the new rate.
- **New State:** Total remaining is \$400 (old) + \$1400 (new) = \$1800.
7. **Day 4-9:** The \$1800 remaining refuels unlock at a rate of \$300/day.
### Can I use FXSwap without Refuels, or use Refuels only as a last resort instead of a Cryptoswap pool?
Yes, but be aware that FXSwap pools use the simpler Stableswap invariant (no `gamma`), so there is less fine tuning of the liquidity bonding curve available.
---
## Parameters
### Amplification Factor (`A`)
The amplification factor in FXSwap pools works similarly to the [amplification factor in Stableswap pools](understanding-stableswap.md#amplification-factor-a), with two distinctions:
1. **Center Price:** The liquidity centers around a variable called `price_scale` rather than a fixed 1.0 peg.
2. **Precision:** `A` values are scaled up. An `A` of **10,000** in FXSwap is equivalent to an `A` of **1** in Stableswap. This provides pool creators with 4 decimal places of precision for the liquidity curve.
Let's have a look at what this means in terms of balance in the pools and prices:
### Gamma (`gamma`)
Gamma is **unused** in FXSwap logic. While the parameter exists in the smart contract ABIs, it is retained solely for backward compatibility, allowing integrators to use existing Cryptoswap patterns to interface with FXSwap pools.
---
## Dynamic Fees
Dynamic fees adjust based on the pool's balance. **Fees are lower when a swap helps balance the pool, and higher when a swap unbalances it.** This is controlled by three parameters:
- **Mid Fee:** The base fee charged when the pool is perfectly balanced (e.g., 50/50 value ratio).
- **Out Fee:** The maximum fee, charged when the pool is entirely tilted toward one asset.
- **Fee Gamma**: Controls the steepness of the fee increase. A lower gamma results in a sharper rise from Mid Fee to Out Fee as imbalance grows.
Below you can see how these three parameters affect the shape of the dynamic fee that will be charged based on the value ratio of assets in a pool (e.g., \$550k in ETH and \$450k in crvUSD equals a 55/45 value ratio)
---
## Understanding Stableswap
## Historical Background
Curve’s Stableswap algorithm was invented by Michael Egorov, the founder of Curve, in November 2019.
It was introduced in the paper [“Stableswap: Efficient Mechanism for Stablecoin Liquidity”](../../../static/pdf/whitepapers/whitepaper_stableswap.pdf), which proposed a new type of Automated Market Maker (AMM) optimized for trading assets pegged to the same value — such as stablecoins (e.g., USDC/USDT) or tokenized versions of the same asset (e.g., wBTC/cbBTC).
The algorithm **combines features of both constant sum (fixed swap price) and constant product (`x*y=k`) curves**, allowing for **low-slippage trades near the peg while maintaining deep liquidity and arbitrage incentives when prices drift**.
:::info
In October 2023, the initial Stableswap implementation was reworked into **Stableswap-NG**, introducing crucial features to further enhance efficiency. More: Evolution Stableswap-NG
:::
## How it Works
Stableswap was designed for pools of similarly priced assets, such as stablecoins of the same denomination (e.g., USD), to concentrate liquidity around their pegged price (e.g., USDC/USDT at 1.0). Unlike concentrated liquidity AMMs (CLAMMs) where liquidity providers must actively set price ranges, **Stableswap's liquidity concentration is fully passive** — users simply deposit tokens, and the protocol automatically concentrates liquidity around the peg through a **single continuous bonding curve**. No range management or active position adjustments are required.
In this example most of the pool's liquidity (e.g., \$40M out of \$80M total) is concentrated in a very tight range around the \$1.00 price. Each block represents 1 million tokens (1M) of either crvUSD or USDC.
**Swap Example**:
1. A Stableswap pool has 40M of both crvUSD and USDC, the pool is perfectly balanced, so each asset is $1.
2. A user wants to swap \$20M USDC for crvUSD:
- The user's 20M USDC is deposited to the pool
- Approx. 19.98M crvUSD is withdrawn
- The price increases to 1.002 USDC per crvUSD
3. The pool is now imbalanced, it would only take a user selling 9M USDC to push the price up another 0.002. The imbalance also creates an **arbitrage opportunity**: traders are now incentivized to swap crvUSD back into the pool to get USDC at a slight discount, which naturally pushes the pool back toward a 50/50 balance. *Assuming the USDC can be redeemed for its underlying $1 of value*.
Stableswap pools function effectively even when heavily imbalanced. Depending on the **Amplification Coefficient** (`A`), pools can maintain close to 1:1 pricing even under significant imbalance. If the imbalance causes a price deviation from the peg, it creates an arbitrage opportunity. This incentivizes traders to rebalance the pool, with each swap generating fees for liquidity providers (LPs).
While the blocks offer a helpful visual, Stableswap's liquidity is more accurately represented by a bonding curve:
## Supported Asset Types
Stableswap pools support up to 8 assets in a single pool. These can be a range of different types:
- **Standard**: Normal stable assets. For a USD pool, this includes tokens like crvUSD or USDC. For a BTC pool, this includes WBTC, tBTC, or cbBTC.
- **Rebasing**: Assets where yield is distributed by automatically increasing the token balance in the user's wallet (e.g., stETH).
- **Oracle**: Assets whose underlying value increases relative to the peg, utilizing an external oracle to track the exchange rate. This is used for yield-bearing assets that are not ERC-4626 vaults (e.g., wstETH).
- **ERC-4626**: Also known as **Tokenized Vaults**. These tokens increase in value over time and utilize the standard `convertToAssets` function to track value. They function similarly to Oracle assets but require no configuration due to their standardized structure. *Note: these vaults should be strictly increasing in value over time.*
Curve allows these different asset types to be integrated into a single pool seamlessly. For example, a valid pool could contain:
- WETH (Standard)
- stETH (Rebasing)
- wstETH (Oracle)
- sfrxETH (ERC-4626 vault)
This configuration creates a highly efficient Stableswap pool. The integration process is streamlined and can be performed directly via the official Curve UI.
---
## Parameters
### Amplification Factor (`A`)
The shape of this liquidity bonding curve and the degree to which a pool can become imbalanced before the price significantly deviates from 1:1 is controlled by the **Amplification Factor** (`A`).
For a high-level understanding of `A`:
- **High `A` values** (e.g., 500–10,000) concentrate liquidity tightly around the peg. This provides deeper liquidity for swaps and allows pools to become very imbalanced before the price deviates significantly. The trade-off is that if an asset moves far from the peg, liquidity and pricing drop off sharply.
- **Lower `A` values** (e.g., 10–100) distribute liquidity more evenly. The price deviates more gradually from the peg as the pool becomes imbalanced, avoiding sharp cliffs.
By definition, the `A` parameter is a multiplier; it amplifies liquidity around the fair price (usually 1:1) compared to the [constant product formula (`x*y=k`)](https://www.reddit.com/r/ethereum/comments/55m04x/lets_run_onchain_decentralized_exchanges_the_way/)
We can observe this behavior in a theoretical pool containing **crvUSD** (pegged at exactly $1.00) and **newUSD**, where the price of newUSD fluctuates based on the ratio of assets in the pool:
### Offpeg Fee Multiplier and Dynamic Fees
The **Offpeg Fee Multiplier increases the pool swapping fee if the pool becomes imbalanced** (e.g., 90% newUSD and 10% crvUSD). The concept is straightforward:
- When pools become imbalanced, liquidity is in high demand, and LPs should be rewarded for remaining in the pool.
- Swaps that increase the imbalance should incur a higher fee than swaps that rebalance the pool.
The Offpeg Fee Multiplier represents the maximum factor by which the base fee can be multiplied. The **dynamic fee is calculated based on the average balance of the pool before and after the swap**; consequently, it is cheaper to perform a swap that balances the pool compared to one that increases imbalance.
The chart below demonstrates how this mechanism functions. Note that base fees and offpeg multipliers are fully customizable; the examples below illustrate a range of potential configurations:
---
## Basepools and Metapools
Basepools form a foundational liquidity layer on Curve, composed of widely-used, highly-liquid stablecoins. Anyone can permissionlessly build new liquidity pools - called Metapools - on top of a Basepool.
A **Metapool combines a asset with the LP tokens of an existing Basepool**. This structure allows Metapools to instantly benefit from the liquidity and stability provided by the underlying Basepool.
See the image below for a visual representation of the [Curve.fi Strategic USD Reserves Basepool](https://curve.finance/dex/ethereum/pools/factory-stable-ng-355/deposit/?ref=news.curve.finance) and the [DOLA Strategic Reserves Metapool](https://curve.finance/dex/ethereum/pools/factory-stable-ng-396/deposit/?ref=news.curve.finance):
In the above structure:
- **Basepool**: When USDC and USDT have equal value, liquidity will naturally balance to 50% USDC and 50% USDT.
- **Metapool**: When DOLA, USDC, and USDT all have equal value, liquidity will naturally balance to 50% DOLA, 25% USDC, and 25% USDT (via Basepool LP tokens).
#### Benefits of Basepools and Metapools
Curve’s Basepool-Metapool architecture offers distinct benefits for liquidity providers (LPs), stablecoin issuers, and traders:
- **Deep Liquidity & Low Slippage**: Metapools directly benefit from the liquidity of Basepools, resulting in lower slippage even for newly issued or less liquid stablecoins.
- **Risk Isolation**: LPs in Basepools aren’t directly exposed to tokens within the Metapools, minimizing their risk. Metapools are also isolated from each other; the only shared risk is de-pegging risk of Basepool assets, which is why adding new Basepools requires a DAO vote.
- **Positive Liquidity Loop (Reflexivity)**: Deposits into Metapools also increase liquidity within Basepools. This creates a positive feedback loop that enhances overall liquidity and trading efficiency across all pools.
- **Built-in Yield Generation**: Basepool LP tokens continuously earn trading fees. Since Metapools hold these LP tokens, Metapool LPs automatically receive a portion of the Basepool’s trading yield, providing immediate organic returns even before trading volumes pick up.
- **Seamless Stablecoin Integration**: Stablecoin issuers can permissionlessly pair their tokens with a Basepool, accelerating their token’s liquidity and adoption within the Curve ecosystem.
#### Basepool List
| Network | Basepool |
| :--- | :--- |
| **Ethereum** | [**DAI/USDC/USDT**](https://curve.finance/dex/#/ethereum/pools/3pool/deposit) |
| **Ethereum** | [**PYUSD/USDC**](https://curve.finance/dex/#/ethereum/pools/factory-stable-ng-43/deposit) |
| **Ethereum** | [**FRAX/USDC**](https://curve.finance/dex/#/ethereum/pools/fraxusdc/deposit) |
| **Ethereum** | [**USDC/USDT**](https://curve.finance/dex/#/ethereum/pools/factory-stable-ng-355/deposit) |
| **Arbitrum** | [**USDC.e/USD₮**](https://curve.finance/dex/#/arbitrum/pools/2pool/deposit) |
| **Arbitrum** | [**FRAX/USDC.e**](https://curve.finance/dex/#/arbitrum/pools/factory-v2-41/deposit) |
| **Optimism** | [**DAI/USDC.e/USDT**](https://curve.finance/dex/#/optimism/pools/3pool/deposit) |
| **Gnosis** | [**WXDAI/USDC/USDT**](https://curve.finance/dex/xdai/pools/3pool/deposit/) |
---
## Vault Assets & Assets with Oracles
These asset types are to allow the assets with underlying accruing interest to be added and work natively in a Stableswap pool. The vault or oracle let's the pool know the true value of the underlying asset against other assets in the pool, so as it accrues interest, the pool stays balanced around the true value price of each asset.
:::tip-green
Swiss Stake, the service provider developing Curve's technology, offers consulting services to guide protocols and asset issuers in the design and integration of price feed oracles for StableSwap liquidity pools. Additionally, Swiss Stake provides thorough audits of client-implemented oracle solutions to ensure correctness and security. Inquiries: [inquiries@curve.finance](mailto:inquiries@curve.finance).
:::
Tho achieve this, the Stablswap pool needs to shift it's center of liquidity (balanced price) over time as the oracle or vault asset accrues interest. Let's have a look at how Stableswap deals with this in practice. Here is the live [crvUSD/sUSDe pool](https://www.curve.finance/dex/ethereum/pools/factory-stable-ng-169/deposit) over the past year, note how as the fundamental value of sUSDe increases, Stableswap changes the center of liquidity to this new price:
---
## Oracles
### Specification Example
While ERC-4626 vaults configure their oracles automatically, the **Oracle** asset type requires manual setup. This allows any token to be used, provided an accompanying oracle is specified that reports the asset’s **value per token** relative to the pool’s base unit (must return **18-decimal precision**).
For example, in a WETH/wstETH pool, the oracle reads the `stEthPerToken()` function. This tells the pool that **1 wstETH represents a fixed amount of underlying ETH**, ensuring liquidity is centered around the correct value (for example, ~1.12 ETH) rather than 1.0.
**Configuration Example (wstETH):**
* **Token:** [0x7f39…](https://etherscan.io/token/0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0) (wstETH)
* **Oracle:** [0x7f39…](https://etherscan.io/token/0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0) (same contract)
* **Function:** `stEthPerToken()`
---
### Best Practices for Oracles
When integrating an oracle, the safety of the pool depends entirely on the oracle’s resistance to manipulation and the behavior of its updates.
**Best Practices:**
:::tip-green
* **Prefer value-accruing assets**: Assets whose value per token increases slowly and predictably (such as most ERC-4626 vaults or liquid staking derivatives, e.g., wstETH, scrvUSD) are best suited for oraclized Stableswap pools.
* **Use rate smoothing or limits**: If the underlying asset value can change, the oracle should apply smoothing or enforce a maximum change per update so that large jumps are spread over time. See [The Sandwich Rule](#1-the-oracle-sandwich-2-fee-rule).
* **Ensure consistent units**: Oracle outputs must be normalized to the same base unit as the rest of the pool (for example, ETH or USD) and returned with exactly 18 decimals.
:::
**Dangers:**
:::danger
* **Never Use Spot Prices:** Do not use the spot price from any DeFi pool, these are highly manipulatable. TWAPs/MAs of the spot price can be OK.
* **Never use withdrawal simulations**: Functions such as `calc_withdraw_one_coin()` or similar balance-based simulations are easily manipulable and must never be used as oracle inputs.
* **Never Use Spot Balances:** Do not rely on spot token balances to calculate value.
* **Avoid low-liquidity sources**: Do not derive an oracle from a market with less value than the Curve pool it secures.
:::
---
## Oracle Risks & Limitations
Since Stableswap centers liquidity around a specific value `p`, any update to that value directly shifts the pool’s pricing. If oracle updates are too large or too fast, liquidity providers will incur guaranteed losses.
### 1. The Oracle Sandwich (2× Fee Rule)
The most critical risk in Stableswap-NG oracles is the "Oracle Sandwich." When an oracle updates the price `p` on-chain, the pool's internal exchange rate shifts instantly.
If the price shift is larger than the cost of swapping (fees), an attacker can guarantee a profit and cause a loss for LPs by:
1. **Front-running:** Buying the asset *before* the oracle update.
2. **Oracle Update:** The oracle transaction executes, shifting the price up.
3. **Back-running:** Selling the asset *after* the update at the new higher price.
To prevent this, you must adhere to the **Safe Limit Rule**:
$$
\Delta P_{oracle} \le 2 \times Fee_{pool}
$$
**The oracle price used by the pool should never change by more than 2x the pool fee in a single block.** For example, if your base pool fee is 0.04%, the oracle should not shift by more than 0.08% per block. If the market moves 5%, your oracle contract should "smooth" this movement over many blocks.
### 2. Dynamic Fees Do Not Protect Against Oracle Attacks
Stableswap-NG includes dynamic fees that increase when token balances become imbalanced. These fees are **not designed to protect against oracle-driven price shifts**.
Oracle updates change the **value** of balances, not the **token amounts**. As a result, the pool may appear perfectly balanced to the fee logic while still being exploitable.
### 3. Unsuitability for Volatile Assets
Stableswap-NG is optimized for assets whose relative value changes slowly. It is not recommended for volatile asset pairs.
Frequent oracle updates force the pool to repeatedly re-center its liquidity, which mathematically causes liquidity providers to sell the appreciating asset at a discount. Over time, this leads to persistent LP losses unless offset by higher fee generation or incentives.
For volatile pairs, prefer **[Cryptoswap](understanding-cryptoswap.md)** or **[FXSwap](understanding-fxswap.md)** pools, which are designed specifically for such use cases.
:::info
Read more about best oracle practices and risks here: [MixBytes: Safe StableSwap-NG Deployment: How to Avoid Risks from Volatile Oracles](https://mixbytes.io/blog/safe-stableswap-ng-deployment-how-to-avoid-risks-from-volatile-oracles)
:::
:::tip-green
Swiss Stake, the service provider developing Curve's technology, offers consulting services to guide protocols and asset issuers in the design and integration of price feed oracles for StableSwap liquidity pools. Additionally, Swiss Stake provides thorough audits of client-implemented oracle solutions to ensure correctness and security. Inquiries: [inquiries@curve.finance](mailto:inquiries@curve.finance).
:::
---
## The Backbone of DeFi
Curve enables **endless possibilities** to be used or built on top of. Whether it is simply deploying a liquidity pool or lending market, or building an entirely new stack on top of Curve. Its **incredible neutrality and permissionless nature** do not discriminate or restrict any market participants, and they welcome everyone to use, build, and improve Curve.
Curve is the definitive platform for building deep, sustainable onchain liquidity for your asset. Whether you're launching a new token, scaling an existing one, or creating innovative DeFi products, Curve provides the complete infrastructure you need:
## Curve's Chain Presence
Curve runs on **multiple of networks** to meet users and builders where they are. Full DEX deployments deliver the complete Curve experience (gauges, CRV emissions, and full frontend support). **Curve Lite** exists so new rollups have the possibility to launch with production‑grade swapping from day one — automatically rolling out Curve’s core DEX stack (permissionless Stableswap/Cryptoswap factories), direct frontend integration, and CurveDAO ownership/fees/CRV emissions.
## Three Core Pillars, One Unified Platform
Curve is built on three core pillars that work together seamlessly:
### Curve DEX
The core business with which Curve started. It pioneered the exchange of stable-like assets with the **Stableswap algorithm** created back in 2020. To this day, it remains the most efficient, battle-tested, and reliable algorithm, despite other projects calling it a "soon to be obsolete" innovation. Algorithms for volatile pairs, the **Cryptoswap algorithm**, came along later in 2021.
Why Curve over other protocols? Because Curve is your swiss army knife providing:
- **Passive Liquidity Provision** - no range-setting, rebalancing, or manual upkeep required; the LP's liquidity is always in range and used for trading, so LPs always earn trading fees.
- **Specialized and suitable algorithms** for every asset type - not only from pegged stablecoins to volatile crypto pairs but also different token standards like ERC-20, ERC-4626, Rebasing, and more. While other AMMs rob the LPs of the rebases, Curve gives them where they belong: **to the LPs.**
- **Fully Permissionless** - deploy pools instantly without DAO approvals and **no deployment fees.**
- **Fully Onchain Built-in Oracles** - liquidity pools on Curve come with built-in EMA oracles; no need to pay extra for centralized, issue-prone oracles.
- **Routing Integration from Day 1** - liquidity pools are automatically picked up by leading aggregators like 1inch, CowSwap, and Paraswap.
### Llamalend
A permissionless lending system built on crvUSD, enabling sophisticated borrowing and lending strategies:
- **Liquidation Protections** - a new and novel liquidation mechanism which gives users more time and flexibility to react when things go south.
- **Isolated markets** - each lending market is isolated to keep risks as minimal as possible and allow for precise risk management.
- **High LTV ratios** - some of the highest loan-to-value ratios in DeFi.
### Gauges & Incentives
Curve's protocol mechanics makes it easy to bootstrap, maintain, and build liquidity for on-chain assets. Curve's gauge system provides a flexible incentive mechanism that enables you to bootstrap and sustain liquidity for your asset:
- **CRV Emissions** - Direct a portion of Curve's weekly CRV inflation to your pool through gauge weight voting, attracting more liquidity providers.
- **Permissionless Rewards** - Add your own token incentives instantly without governance approval to boost pool attractiveness.
- **Vote Incentives** - Provide rewards to veCRV holders to vote for your gauge, creating sustainable voting power.
- **Flexible Strategies** - Choose between temporary vote renting or permanent veCRV accumulation based on your needs.
- **Custom Combinations** - Mix CRV emissions, custom rewards, and vote incentives to create your perfect liquidity bootstrapping strategy.
---
## Getting Started
Ready to deploy a pool or lending market or proliferate your asset across DeFi? Here's how to get started:
---
## Bridging Tokens
Curve uses [LayerZero](https://layerzero.network/) to enable **cross-chain transfers of `CRV`, `crvUSD`, and `scrvUSD`** to other L1 blockchains such as `BinanceSmartChain`, `Avalanche`, and `Fantom`. This guide focuses on bridging using only blockchain explorers, reducing reliance on third-party interfaces and avoiding any additional fees they may charge.
This guide explains how to **bridge any of the three tokens from the Ethereum Mainnet to other L1 blockchains or vice versa**. The only requirements include having a wallet with the token to be bridged and ETH or the gas token of the L1, depending on the bridging direction, to cover transaction fees.
:::info Contract Addresses
This guide is applicable for bridging `CRV`, `crvUSD` and `scrvUSD` to other L1 blockchains. When following this guide, one needs to make sure to use the correct contract addresses depending on the token to be bridged.
:::
** CRV**
The contract addresses for bridges are mirrored meaning the bridge contract on Ethereum is the same as the one on the L1 blockchain. But the CRV token address is different for each chain.
- Ethereum: [`0xD533a949740bb3306d119CC777fa900bA034cd52`](https://etherscan.io/address/0xD533a949740bb3306d119CC777fa900bA034cd52)
- Binance Smart Chain: [`0x9996D0276612d23b35f90C51EE935520B3d7355B`](https://bscscan.com/address/0x9996D0276612d23b35f90C51EE935520B3d7355B)
- Avalanche: [`0xEEbC562d445F4bC13aC75c8caABb438DFae42A1B`](https://snowscan.xyz/address/0xEEbC562d445F4bC13aC75c8caABb438DFae42A1B)
- Fantom: [`0xE6c259bc0FCE25b71fE95A00361D3878E16232C3`](https://ftmscout.com/address/0xE6c259bc0FCE25b71fE95A00361D3878E16232C3)
| Chain | Bridge Contract Address | Etherscan Link |
| ------------------ | ---------------- | ---------------- |
| BinanceSmartChain Bridge | `0xC91113B4Dd89dd20FDEECDAC82477Bc99A840355` | [Ethereum](https://etherscan.io/address/0xC91113B4Dd89dd20FDEECDAC82477Bc99A840355), [BSC](https://bscscan.com/address/0xC91113B4Dd89dd20FDEECDAC82477Bc99A840355) |
| Avalanche Bridge | `0x5cc0144A511807608eF644c9e99B486124D1cFd6` | [Ethereum](https://etherscan.io/address/0x5cc0144A511807608eF644c9e99B486124D1cFd6), [Avalanche](https://snowscan.xyz/address/0x5cc0144A511807608eF644c9e99B486124D1cFd6) |
| Fantom Bridge | `0x7ce8aF75A9180B602445bE230860DDcb4cAc3E42` | [Ethereum](https://etherscan.io/address/0x7ce8aF75A9180B602445bE230860DDcb4cAc3E42), [Fantom](https://ftmscout.com/address/0x7ce8aF75A9180B602445bE230860DDcb4cAc3E42) |
---
** crvUSD**
The contract addresses for bridges are mirrored meaning the bridge contract on Ethereum is the same as the one on the L1 blockchain. But the crvUSD token address is different for each chain.
- Ethereum: [`0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E`](https://etherscan.io/address/0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E)
- Binance Smart Chain: [`0xe2fb3F127f5450DeE44afe054385d74C392BdeF4`](https://bscscan.com/address/0xe2fb3F127f5450DeE44afe054385d74C392BdeF4)
- Avalanche: [`0xCb7c161602d04C4e8aF1832046EE08AAF96d855D`](https://snowscan.xyz/address/0xCb7c161602d04C4e8aF1832046EE08AAF96d855D)
- Fantom: [`0xD823D2a2B5AF77835e972A0D5B77f5F5A9a003A6`](https://ftmscout.com/address/0xD823D2a2B5AF77835e972A0D5B77f5F5A9a003A6)
| Chain | Bridge Contract Address | Etherscan Link |
| ------------------ | ---------------- | ---------------- |
| BinanceSmartChain Bridge | `0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f` | [Ethereum](https://etherscan.io/address/0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f), [BSC](https://bscscan.com/address/0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f) |
| Avalanche Bridge | `0x26D01ce989037befd7Ff63837A86e2da32E7D7e2` | [Ethereum](https://etherscan.io/address/0x26D01ce989037befd7Ff63837A86e2da32E7D7e2), [Avalanche](https://snowscan.xyz/address/0x26D01ce989037befd7Ff63837A86e2da32E7D7e2) |
| Fantom Bridge | `0x76EAfda658C54548B460B3f190386699DE3827d8` | [Ethereum](https://etherscan.io/address/0x76EAfda658C54548B460B3f190386699DE3827d8), [Fantom](https://ftmscout.com/address/0x76EAfda658C54548B460B3f190386699DE3827d8) |
---
** scrvUSD**
The contract addresses for bridges are mirrored meaning the bridge contract on Ethereum is the same as the one on the L1 blockchain. But the crvUSD token address is different for each chain.
- Ethereum: [`0x0655977FEb2f289A4aB78af67BAB0d17aAb84367`](https://etherscan.io/address/0x0655977FEb2f289A4aB78af67BAB0d17aAb84367)
- Binance Smart Chain: [`0x0094Ad026643994c8fB2136ec912D508B15fe0E5`](https://bscscan.com/address/0x0094Ad026643994c8fB2136ec912D508B15fe0E5)
- Avalanche: [`0xA3ea433509F7941df3e33857D9c9f212Ad4A4e64`](https://snowscan.xyz/address/0xA3ea433509F7941df3e33857D9c9f212Ad4A4e64)
- Fantom: [`0x5191946500e75f0A74476F146dF7d386e52961d9`](https://ftmscout.com/address/0x5191946500e75f0A74476F146dF7d386e52961d9)
| Chain | Bridge Contract Address | Etherscan Link |
| ------------------ | ---------------- | ---------------- |
| BinanceSmartChain Bridge | `0xAE0666C978500f2C05784242B79B08C478Dd999c` | [Ethereum](https://etherscan.io/address/0xAE0666C978500f2C05784242B79B08C478Dd999c), [BSC](https://bscscan.com/address/0xAE0666C978500f2C05784242B79B08C478Dd999c) |
| Avalanche Bridge | `0x26E91B1f142b9bF0bB37e82959bA79D2Aa6b99b8` | [Ethereum](https://etherscan.io/address/0x26E91B1f142b9bF0bB37e82959bA79D2Aa6b99b8), [Avalanche](https://snowscan.xyz/address/0x26E91B1f142b9bF0bB37e82959bA79D2Aa6b99b8) |
| Fantom Bridge | `0x08132eA9b02750E118cF5F5C640B7c46A8E638E8` | [Ethereum](https://etherscan.io/address/0x08132eA9b02750E118cF5F5C640B7c46A8E638E8), [Fantom](https://ftmscout.com/address/0x08132eA9b02750E118cF5F5C640B7c46A8E638E8) |
:::
---
## Bridging tokens from Ethereum to an L1 blockchain
### Step 1: Approve the Bridge Contract to Spend Your Tokens
1. Navigate to the contract of the token you want to bridge on [Etherscan](https://etherscan.io/).
2. Connect your wallet by navigating to **`Contract` > `Write Contract`** and clicking the **`Connect to Web3`** option.
3. Look for the **`approve`** method and approve the according bridge contract as a spender.
- **`_spender`**: Enter `0x0A92Fd5271dB1C41564BD01ef6b1a75fC1db4d4f`, the bridge contract address. This address is the same for all tokens.
- **`_value`**: Specify the amount in 1e18 format (for example, for 100 crvUSD, enter `100000000000000000000`).
Again, to avoid manually entering the amount in 1e18 format, you can input the amount of tokens you wish to bridge and then append 18 zeros by using the **`+`** button.
2. Click **`Write`**. A transaction should pop up in your wallet which you need to sign to complete the approval.
---
### Step 2: Read Contract and Quote ETH Amount
1. Visit the bridge contract on Etherscan. This contract address is different depending on the token to be bridged and where it is being bridged to. This time, there is **no need to connect your wallet**.
2. Use function **`1. quote`** to determine the bridging cost.
The `quote` amount represents the cost (in ETH) of calling the bridge method in the [next step](#step-3-bridge-the-token-to-the-l1-blockchain). This does not include gas costs, which need to be paid on top of the quoted amount.
---
### Step 3: Bridge the token to the L1 blockchain
1. Access the bridge contract on Etherscan. This contract address is different depending on the token to be bridged and where it is being bridged to.
2. Connect your wallet by navigating to **`Contract` > `Write Contract`** and clicking the **`Connect to Web3`** option.
3. Navigate to method **`2. bridge`** and input your values. On this contract, there might be multiple methods with the same name. Make sure to select the one which looks like the one in the image down below (it should have three input parameters: `bridge`, `_amount` and `_receiver`).
- **`bridge`**: Enter the `ETH` amount quoted in [Step 2](#step-2-read-contract-and-quote-eth-amount). Ensure you enter the **amount denominated in Ether** (quoted amount / 1e18).
- **`_amount`**: Specify the amount of tokens to bridge in 1e18 format.
- **`_receiver`**: Enter the wallet you wish to receive the tokens to.
Alternatively, to avoid manually entering the amount in 1e18 format, you can input the amount of tokens you wish to bridge and then append 18 zeros by using the **`+`** button.
4. Click **`Write`**. A transaction should pop up in your wallet which you need to sign to complete the bridging process.
:::warning Warning
The bridging transaction will not be settled immediately. After completing these steps, it may take a few minutes for your tokens to be successfully bridged to the L1.
:::
---
## Bridging tokens from an L1 blockchain to Ethereum
### Step 1: Approve the Bridge Contract to Spend Your Tokens
1. Navigate to the token contract on the block explorer for the L1 network you want to bridge from. For example, to bridge crvUSD from BSC to Ethereum, you would need to navigate to the crvUSD token contract on BSCScan. All token addresses are listed in the table at the top of the page.
2. Connect your wallet by navigating to **`Contract` > `Write Contract`** and clicking the **`Connect to Web3`** option.
3. Look for the **`approve`** method and approve the according bridge contract as a spender.
- **`_spender`**: Enter the contract address of the bridge on the L1 blockchain.
- **`_value`**: Specify the amount in 1e18 format (for example, for 100 crvUSD, enter `100000000000000000000`).
Alternatively, to avoid manually entering the amount in 1e18 format, you can input the amount of tokens you wish to bridge and then append 18 zeros by using the **`+`** button.
4. Click **`Write`**. A transaction should pop up in your wallet which you need to sign to complete the approval.
---
### Step 2: Read Contract and Quote the Fee Amount
1. Visit the bridge contract on the L1 blockchain you want to bridge from.
2. Use function **`1. quote`** to determine the bridging cost.
The `quote` amount represents the cost (in the gas token of the L1 blockchain) of calling the bridge method in [Step 3](#step-3-bridge-tokens-to-ethereum). This does not include gas costs, which need to be paid additionally.
---
### Step 3: Bridge Tokens to Ethereum
1. Access the bridge contract on the L1 blockchain you want to bridge from.
2. Connect your wallet by navigating to **`Contract` > `Write Contract`** and clicking the **`Connect to Web3`** option.
3. Navigate to method **`2. bridge`** and input your values. On this contract, there might be multiple methods with the same name. Make sure to select the one which looks like the one in the image down below (it should have three input parameters: `bridge`, `_amount` and `_receiver`).
- **`bridge`**: Enter the ETH amount quoted in [Step 2](#step-2-read-contract-and-quote-the-fee-amount). Ensure you enter the **amount denominated in Ether** (quoted amount / 1e18).
- **`_amount`**: Specify the amount of tokens in 1e18 format.
- **`_receiver`**: Enter the wallet you wish to receive the tokens to.
Alternatively, to avoid manually entering the amount in 1e18 format, you can input the amount of tokens you wish to bridge and then append 18 zeros by using the **`+`** button.
4. Click **`Write`**. A transaction should pop up in your wallet which you need to sign to complete the bridging process.
:::warning Warning
The bridging transaction will not be settled immediately. After completing these steps, it may take a few minutes for your tokens to be successfully bridged to the L1.
:::
---
## Corss-Chain DAO Operations
All Curve governance and voting takes place on **Ethereum mainnet**. Once a vote concludes and passes, the proposal is automatically executed across all supported chains, including L2s and sidechains. This ensures that all Curve deployments remain synchronized and follow the same governance decisions.
To participate in governance, users need [veCRV tokens](../curve-tokens/vecrv.md) on Ethereum mainnet. For detailed information about the DAO and voting processes, see the [Governance Overview](../dao/overview.md) or explore the guides below:
---
## Supported Chains & Assets
While Ethereum remains Curve's primary network, Curve products and assets are available across multiple blockchains. Each network offers unique advantages in terms of transaction speed and costs. To use Curve on alternate networks, you'll typically need to bridge your funds from Ethereum using the target network's bridge.
## Ecosystem Components
The Curve ecosystem consists of five key components with varying cross-chain availability:
- [**Curve DEX** (Decentralized Exchange)](/user/dex/overview) - Available on many chains, and now deployable permissionlessly to any chain.
- [**Curve Lending (Llamalend)**](/user/llamalend/overview) - Available on Ethereum and selected L2s.
- [**Curve Assets** (CRV, crvUSD, scrvUSD)](/user/curve-tokens/crv) - Available on most chains through native bridges and LayerZero.
- **crvUSD Minting** - Ethereum-only.
- **CRV Rewards** - CRV inflation rewards are available to pools and llamalend markets on all chains that Curve supports, but the DAO must vote to add the market to the Gauge Controller (controls which markets can get rewards).
## Curve's Chain Presence
Curve runs on dozens of networks to meet users and builders where they are. Full DEX deployments deliver the complete Curve experience (gauges, CRV emissions and frontend analytics). Curve Lite exists so new rollups have the possibility to launch with production‑grade swapping from day one — automatically rolling out Curve’s core DEX stack (permissionless Stableswap/Cryptoswap factories), direct frontend integration and CurveDAO ownership/fees/CRV emissions. Llamalend adds credit markets on selected networks.
---
## CRV (Curve DAO Token)
The Curve token can be bridged across various chains, though it does not always retain full functionality. Locking CRV to obtain veCRV, as well as rewards voting for cross-chain gauges, must be conducted on the Ethereum blockchain.
| Network | Contract Address | Bridge |
| ------- | :--------------: | :----: |
| **Ethereum** | [0xD533a949740bb3306d119CC777fa900bA034cd52](https://etherscan.io/token/0xd533a949740bb3306d119cc777fa900ba034cd52) | --- |
| **Arbitrum** | [0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978](https://arbiscan.io/address/0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978) | [**Arbitrum Bridge**](https://bridge.arbitrum.io/) |
| **Optimism** | [0x0994206dfE8De6Ec6920FF4D779B0d950605Fb53](https://optimistic.etherscan.io/address/0x0994206dfe8de6ec6920ff4d779b0d950605fb53) | [**Optimism Bridge**](https://app.optimism.io/bridge) |
| **Base** | [0x8Ee73c484A26e0A5df2Ee2a4960B789967dd0415](https://basescan.org/address/0x8Ee73c484A26e0A5df2Ee2a4960B789967dd0415) | [**Base Bridge**](https://bridge.base.org/deposit) |
| **Polygon** | [0x172370d5Cd63279eFa6d502DAB29171933a610AF](https://polygonscan.com/address/0x172370d5cd63279efa6d502dab29171933a610af) | [**Polygon Bridge**](https://wallet.polygon.technology/bridge/) |
| **Fraxtal** | [0x331b9182088e2a7d6d3fe4742aba1fb231aecc56](https://fraxscan.com/address/0x331b9182088e2a7d6d3fe4742aba1fb231aecc56) | [**Fraxtal Bridge**](https://app.frax.finance/bridge) |
| **Sonic** | [0x5af79133999f7908953e94b7a5cf367740ebee35](https://sonicscan.org/address/0x5af79133999f7908953e94b7a5cf367740ebee35) | [Guide here](/user/cross-chain/bridging-tokens) |
| **Taiko** | [0x09413312b263fD252C16e592A45f4689F26cb79d](https://taikoscan.io/address/0x09413312b263fD252C16e592A45f4689F26cb79d) | [**Taiko Bridge**](https://bridge.taiko.xyz/) |
| **Gnosis** | [0x712b3d230F3C1c19db860d80619288b1F0BDd0Bd](https://gnosisscan.io/address/0x712b3d230f3c1c19db860d80619288b1f0bdd0bd) | [**Gnosis Bridge**](https://bridge.gnosischain.com/) |
| **Fantom** | [0xE6c259bc0FCE25b71fE95A00361D3878E16232C3](https://ftmscout.com/address/0xE6c259bc0FCE25b71fE95A00361D3878E16232C3) | [Guide here](/user/cross-chain/bridging-tokens) |
| **BSC** | [0x9996D0276612d23b35f90C51EE935520B3d7355B](https://bscscan.com/address/0x9996D0276612d23b35f90C51EE935520B3d7355B) | [Guide here](/user/cross-chain/bridging-tokens) |
| **Avalanche** | [0xEEbC562d445F4bC13aC75c8caABb438DFae42A1B](https://snowscan.xyz/address/0xEEbC562d445F4bC13aC75c8caABb438DFae42A1B) | [Guide here](/user/cross-chain/bridging-tokens) |
| **Kava** | [0x7736C61F00c72e868AA9904c9063e8445A1eF5DD](https://kavascan.com/address/0x7736C61F00c72e868AA9904c9063e8445A1eF5DD) | [Guide here](/user/cross-chain/bridging-tokens) |
| **Etherlink** | [0x004A476B5B76738E34c86C7144554B9d34402F13](https://explorer.etherlink.com/address/0x004A476B5B76738E34c86C7144554B9d34402F13) | [Guide here](/user/cross-chain/bridging-tokens) |
| **X-Layer** | [0x3d5320821bfca19fb0b5428f2c79d63bd5246f89](https://web3.okx.com/explorer/x-layer/address/0x3d5320821bfca19fb0b5428f2c79d63bd5246f89) | [**X-Layer Bridge**](https://www.okx.com/xlayer/bridge) |
---
## crvUSD (Curve Stablecoin)
crvUSD was first introduced in May 2023 on the Ethereum blockchain. For now, this stablecoin can be minted exclusively on the Ethereum mainnet.
[Understanding crvUSD](/user/curve-tokens/crvusd)
*Despite being launched on Ethereum, crvUSD can be bridged to various chains:*
| Chain | crvUSD Token Address | Official Bridge |
| ----------------------------- | :------------------: | :-------------: |
| **Ethereum** | [0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E](https://etherscan.io/token/0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E) | --- |
| **Arbitrum** | [0x498Bf2B1e120FeD3ad3D42EA2165E9b73f99C1e5](https://arbiscan.io/address/0x498Bf2B1e120FeD3ad3D42EA2165E9b73f99C1e5) | [Arbitrum Bridge](https://bridge.arbitrum.io/?destinationChain=arbitrum-one&sourceChain=ethereum) |
| **Optimism** | [0xc52d7f23a2e460248db6ee192cb23dd12bddcbf6](https://optimistic.etherscan.io/address/0xc52d7f23a2e460248db6ee192cb23dd12bddcbf6) | [Optimism Bridge](https://app.optimism.io/bridge/deposit) |
| **Base** | [0x417Ac0e078398C154EdFadD9Ef675d30Be60Af93](https://basescan.org/address/0x417Ac0e078398C154EdFadD9Ef675d30Be60Af93) | [Base Bridge](https://bridge.base.org/deposit) |
| **Polygon** | [0xc4Ce1D6F5D98D65eE25Cf85e9F2E9DcFEe6Cb5d6](https://polygonscan.com/address/0xc4Ce1D6F5D98D65eE25Cf85e9F2E9DcFEe6Cb5d6) | [Polygon Bridge](https://wallet.polygon.technology/) |
| **X-Layer** | [0xda8f4eb4503acf5dec5420523637bb5b33a846f6](https://web3.okx.com/explorer/x-layer/address/0xda8f4eb4503acf5dec5420523637bb5b33a846f6) | [X-Layer Bridge](https://www.okx.com/xlayer/bridge) |
| **Fraxtal** | [0xB102f7Efa0d5dE071A8D37B3548e1C7CB148Caf3](https://fraxscan.com/address/0xB102f7Efa0d5dE071A8D37B3548e1C7CB148Caf3) | [Fraxtal Bridge](https://app.frax.finance/bridge) |
| **Sonic** | [0x7FFf4C4a827C84E32c5E175052834111B2ccd270](https://sonicscan.org/address/0x7FFf4C4a827C84E32c5E175052834111B2ccd270) | [Sonic Bridge](https://bridge.soniclabs.com/) |
| **Taiko** | [0xc8F4518ed4bAB9a972808a493107926cE8237068](https://taikoscan.io/address/0xc8F4518ed4bAB9a972808a493107926cE8237068) | [Taiko Bridge](https://bridge.taiko.xyz/) |
| **Gnosis** | [0xaBEf652195F98A91E490f047A5006B71c85f058d](https://gnosisscan.io/address/0xaBEf652195F98A91E490f047A5006B71c85f058d) | [Gnosis Bridge](https://bridge.gnosischain.com/) |
| **Fantom** | [0xD823D2a2B5AF77835e972A0D5B77f5F5A9a003A6](https://ftmscout.com/address/0xD823D2a2B5AF77835e972A0D5B77f5F5A9a003A6) | [Guide here](/user/cross-chain/bridging-tokens) |
| **BSC** | [0xe2fb3F127f5450DeE44afe054385d74C392BdeF4](https://bscscan.com/address/0xe2fb3F127f5450DeE44afe054385d74C392BdeF4) | [Guide here](/user/cross-chain/bridging-tokens) |
| **Mantle** | [0x0994206dfe8de6ec6920ff4d779b0d950605fb53](https://mantlescan.xyz/address/0x0994206dfe8de6ec6920ff4d779b0d950605fb53) | [Mantle Bridge](https://app.mantle.xyz/bridge) |
| **zk-Sync** | [0x43cd37cc4b9ec54833c8ac362dd55e58bfd62b86](https://explorer.zksync.io/address/0x43cd37cc4b9ec54833c8ac362dd55e58bfd62b86) | [ZKsync Portal](https://portal.zksync.io/bridge) |
| **Avalanche** | [0xCb7c161602d04C4e8aF1832046EE08AAF96d855D](https://snowscan.xyz/address/0xCb7c161602d04C4e8aF1832046EE08AAF96d855D) | [Guide here](/user/cross-chain/bridging-tokens) |
| **Kava** | [0x98B4029CaBEf7Fd525A36B0BF8555EC1d42ec0B6](https://kavascan.com/address/0x98B4029CaBEf7Fd525A36B0BF8555EC1d42ec0B6) | [Guide here](/user/cross-chain/bridging-tokens) |
---
## scrvUSD (Savings-crvUSD)
Savings crvUSD (scrvUSD) was first introduced in November 2024 on Ethereum.
[Understanding scrvUSD](/user/curve-tokens/scrvusd)
*Despite being launched on Ethereum, scrvUSD can be bridged to various chains:*
| Chain | scrvUSD Token Address | Official Bridge |
| ----------------------------- | :------------------: | :-------------: |
| **Ethereum** | [0x0655977FEb2f289A4aB78af67BAB0d17aAb84367](https://etherscan.io/address/0x0655977FEb2f289A4aB78af67BAB0d17aAb84367) | - |
| **Arbitrum** | [0xEfB6601Df148677A338720156E2eFd3c5Ba8809d](https://arbiscan.io/address/0xEfB6601Df148677A338720156E2eFd3c5Ba8809d) | [Arbitrum Bridge](https://bridge.arbitrum.io/?destinationChain=arbitrum-one&sourceChain=ethereum) |
| **Optimism** | [0x289f635106d5b822a505b39ac237a0ae9189335b](https://optimistic.etherscan.io/address/0x289f635106d5b822a505b39ac237a0ae9189335b) | [Superbridge](https://superbridge.app/base) |
| **Base** | [0x646a737b9b6024e49f5908762b3ff73e65b5160c](https://basescan.org/address/0x646a737b9b6024e49f5908762b3ff73e65b5160c) | [Superbridge](https://superbridge.app/base) |
| **Fraxtal** | [0xaB94C721040b33aA8b0b4D159Da9878e2a836Ed0](https://fraxscan.com/address/0xaB94C721040b33aA8b0b4D159Da9878e2a836Ed0) | [Fraxtal Bridge](https://app.frax.finance/bridge) |
| **Sonic** | [0xb5f0edecff09081354db252ceec000b213186fac](https://sonicscan.org/address/0xb5f0edecff09081354db252ceec000b213186fac) | [Sonic Bridge](https://bridge.soniclabs.com/) |
| **Taiko** | [0x8c74d11da5E6cE0498cd280F9209dFB0C5e048A7](https://taikoscan.io/address/0x8c74d11da5E6cE0498cd280F9209dFB0C5e048A7) | [Taiko Bridge](https://bridge.taiko.xyz/) |
| **Fantom** | [0x5191946500e75f0A74476F146dF7d386e52961d9](https://ftmscout.com/address/0x5191946500e75f0A74476F146dF7d386e52961d9) | [Guide here](/user/cross-chain/bridging-tokens) |
| **BSC** | [0x0094Ad026643994c8fB2136ec912D508B15fe0E5](https://bscscan.com/address/0x0094Ad026643994c8fB2136ec912D508B15fe0E5) | [Guide here](/user/cross-chain/bridging-tokens) |
| **Avalanche** | [0xA3ea433509F7941df3e33857D9c9f212Ad4A4e64](https://snowscan.xyz/address/0xA3ea433509F7941df3e33857D9c9f212Ad4A4e64) | [Guide here](/user/cross-chain/bridging-tokens) |
| **XDC** | [0x3d8EADb739D1Ef95dd53D718e4810721837c69c1](https://xdcscan.io/address/0x3d8EADb739D1Ef95dd53D718e4810721837c69c1) | [Guide here](/user/cross-chain/bridging-tokens) |
---
## Using Curve on L2s
Using Curve products on L1s and L2s works exactly the same way as on Ethereum. However, some products might not be supported on every chain (such as Llamalend). To see which products are supported on each chain, see [Supported Chains & Features](supported-chains.md). All these products can be used by simply switching to the desired chain in the top right of the UI.
Curve offers these two main products:
## DEX (Decentralized Exchange)
The Curve DEX is available on many chains, with more becoming available all the time. Providing liquidity to pools and creating new pools works the same way across all chains. Both [**Stableswap**](/protocol/pool/overview#stableswap-pool) and [**Cryptoswap**](/protocol/pool/overview#cryptoswap) pools are available on all chains where Curve has been deployed.
**Fee Distribution**: Fees from swaps on all chains are split 50/50 between LPs and the Curve DAO. These fees are automatically bridged back to Ethereum for distribution.
**CRV Rewards**: CRV inflation rewards are available on L2s and alt-L1s, but they are delayed by one week. When users vote to send CRV rewards to a pool on an L2 or alt-L1, the rewards must first accrue on Ethereum in a [Root Gauge](https://docs.curve.fi/liquidity-gauges-and-minting-crv/xchain-gauges/RootGauge/) for one week, before being bridged to the [Child Gauge](https://docs.curve.fi/liquidity-gauges-and-minting-crv/xchain-gauges/ChildGauge/) and distributed over the following week.
### Curve-Core
[Curve-Core](https://github.com/curvefi/curve-core) is a trustless solution for chain operators to deploy a minimal Curve DEX instance to new chains. This tool enables faster deployment with reduced overhead by providing:
- **Trustless Pool Creation through Factories** - including Stableswap and Cryptoswap pools
- **Swap Router** - so users don't have to manually select pools to swap through
- **Reward Contracts** - so any reward token can be easily streamed to LPs
- **Governance Contracts** - ensuring cross-chain operations like parameter updates remain fully under DAO control
## Llamalend
Llamalend is currently available on five chains: **Ethereum**, **Arbitrum**, **Fraxtal**, **Sonic**, and **Optimism**.
**CRV Rewards**: CRV inflation rewards are available on L2s and alt-L1s, but they are delayed by one week. When users vote to send CRV rewards to a Llamalend market on an L2 or alt-L1, the rewards must first accrue on Ethereum in a [Root Gauge](https://docs.curve.fi/liquidity-gauges-and-minting-crv/xchain-gauges/RootGauge/) for one week, before being bridged to the [Child Gauge](https://docs.curve.fi/liquidity-gauges-and-minting-crv/xchain-gauges/ChildGauge/) and distributed over the following week.
---
## CRV
CRV is the native token of the Curve ecosystem. It was launched in August 2020 on Ethereum and has a fixed maximum supply of approximately 3.03 billion tokens. CRV is mainly used for:
- Rewarding users who provide liquidity in liquidity pools and lending markets
- Participating in Curve governance, earning a share of Curve's revenue, and boosting LP rewards by locking CRV into vote-escrowed CRV (veCRV). See the [veCRV page](../vecrv/what-is-vecrv.md) for more information
CRV tokens are constantly and slowly put into circulation following an emission schedule and are distributed to Liquidity Providers (LPs). The issuance rate decreases over time, reducing by 16% each August (see the [stats section](#current-crv-stats) below for this year's exact date). A portion of the initial supply (1.3B) was distributed with vesting schedules that have now finished as of August 2024. The majority of the remaining 1.7B CRV is emitted to liquidity providers gradually over the next approximately 200 years.
---
## Current CRV Stats
Current CRV stats directly fetched from
## Tokenomics
The Community (Emissions) represents a dedicated supply that is emitted over time as rewards for liquidity providers depending on [gauge weights](../dao/gauge-weights.md). Allocations for the Core Team, Early Users, Community Reserve, Investors, and Employees have all fully unlocked.
## Emission Schedule
The chart below shows the cumulative release of CRV tokens over time, including vesting schedules for different allocation categories.
:::info
The chart shows the cumulative release of CRV tokens over time. Community emissions decrease by 16% each August, while all vesting schedules have completed as of August 2024. The majority of remaining CRV will be emitted to liquidity providers over the next ~200 years.
:::
---
## FAQ
### What is veCRV?
veCRV stands for vote-escrowed CRV (locked CRV). You can get veCRV by locking your CRV tokens for a specified period, ranging from 1 week to 4 years. The longer you lock, the more veCRV you receive. See the [veCRV page](vecrv) for the benefits of locking CRV for veCRV, and how locking works.
### How is CRV Distributed?
Each week veCRV holders vote for which pools and lending markets they want the weekly minted CRV to be distributed to. The CRV is split based on how many votes each pool/lending market receives. This is called [Gauge voting](/user/dao/gauge-weights). See the page for more information.
### Are there any upcoming team, investor, or insider unlocks?
No, all unlocks have already occurred. The final unlock (team vesting) finished in August 2024. All remaining CRV tokens will be rewarded to LPs over the next approximately 200 years.
### What is the current inflation rate of CRV?
The current inflation rate of CRV is 4.96%.
---
## crvUSD(Curve-tokens)
crvUSD is a fully **decentralized, censorship-resistant, and un-freezable** USD stablecoin created by Curve. Unlike centralized stablecoins like USDC or USDT, which are mostly backed by t-bills, crvUSD is fully backed by crypto assets and is controlled by smart contracts and the Curve DAO. Its home is Ethereum, but crvUSD can be bridged to most EVM-compatible chains.
crvUSD can be obtained in two ways: by borrowing it using crypto assets like ETH or BTC as collateral (which lets you access liquidity without selling your crypto) or by simply buying it.
---
## Current crvUSD Stats
Current crvUSD stats directly fetched from
---
## FAQ
### What can I do with crvUSD?
You can do many things with crvUSD: from simply holding it to earning yield or spending it for real-life expenses.
### Can I earn yield with crvUSD?
Yes, you can earn rewards with your crvUSD on Curve in a few ways:
- **Deposit into [scrvUSD](./scrvusd.md)**: Earn yield with the savings version of crvUSD
- **Supply it to a lending market**: Provide your crvUSD to borrowers and earn interest on it
- **Provide liquidity to a crvUSD trading pool**: Add crvUSD to a liquidity pool and earn trading fees
### What chains is crvUSD available on?
crvUSD is primarily minted on Ethereum, but it can be bridged and used on many other chains. See [crvUSD Chain Deployment Addresses](/user/cross-chain/supported-chains) for a full list.
---
## Guide: Claiming Pre-CRV Airdrop
# Claiming the Pre-CRV Liquidity Provider Airdrop
Before the CRV token [launched](https://etherscan.io/tx/0x5dc4a688b63cea09bf4d73a695175b77572792a2e2b3656297809ad3596d4bfe) in August 2020, early liquidity providers were already supplying assets to Curve pools. To reward these early supporters, a vesting contract was deployed that distributes CRV tokens to eligible addresses. The vesting period has fully ended, meaning all allocated tokens are now immediately claimable.
:::info
The vesting contract still holds over **11 million CRV** in unclaimed tokens. If you provided liquidity to Curve pools before CRV launched (August 2020), you may have tokens waiting for you.
:::
**Contract address:** [`0x575CCD8e2D300e2377B43478339E364000318E2c`](https://etherscan.io/address/0x575CCD8e2D300e2377B43478339E364000318E2c)
---
## Step 1: Check Your Eligibility
First, check if your address has any CRV to claim.
1. Go to the [contract on Etherscan](https://etherscan.io/address/0x575CCD8e2D300e2377B43478339E364000318E2c#readContract)
2. Find the **`balanceOf`** function (function #4) and enter your wallet address
3. Click **"Query"**
If the result is **`0`**, your address is not eligible or has already claimed. If the result is a non-zero number, you have CRV to claim. The value is in wei (18 decimals), so divide by $10^{18}$ to get the amount in CRV. For example, `1000000000000000000` = 1 CRV.
:::tip
You can also check **`initial_locked`** to see the original amount allocated to your address, and **`total_claimed`** to see how much you've already claimed.
:::
---
## Step 2: Claim Your CRV
Once you've confirmed you have a balance, you can claim your tokens.
1. Go to the [contract's Write tab on Etherscan](https://etherscan.io/address/0x575CCD8e2D300e2377B43478339E364000318E2c#writeContract)
2. Click **"Connect Wallet"** and connect the eligible wallet
3. Find the **`claim`** function (function #6, the one with no inputs)
4. Click **"Write"**
5. Confirm the transaction in your wallet
6. Once the transaction confirms, your CRV tokens will be in your wallet
:::note
Gas fees apply as this is an Ethereum mainnet transaction. Make sure you have enough ETH in your wallet to cover gas costs.
:::
---
## FAQ
### How do I know if I provided liquidity before CRV launched?
CRV launched on **August 13, 2020**. If you deposited into any Curve pool before that date, you may be eligible. The easiest way to check is to simply query `balanceOf` with your address as described in Step 1.
### Is there a deadline to claim?
No. The tokens remain in the contract indefinitely until claimed. There is no expiry.
---
## scrvUSD
scrvUSD is short for **savings-crvUSD**, and it functions like a **bank savings account for crvUSD**. It is similar to other staked versions of USD stablecoins like sDAI, sUSDS, sUSDe, etc. Users can "stake" their crvUSD to receive scrvUSD, or buy it directly from the market.
The yield comes from receiving a share of the crvUSD borrowing revenue. Yield accrues automatically to scrvUSD holders, increasing the value of their tokens over time. It's important to understand that the underlying crvUSD is not rehypothecated elsewhere—they remain idle within the vault, minimizing risk.
---
## Current scrvUSD Stats
Current scrvUSD stats directly fetched from
---
## FAQ
### What is scrvUSD?
scrvUSD is the savings version of crvUSD. By staking crvUSD, you receive scrvUSD tokens that automatically accrue yield over time from crvUSD borrowing revenue.
### How does scrvUSD earn yield?
scrvUSD earns yield from a share of the crvUSD borrowing revenue. The yield accrues automatically to scrvUSD holders, increasing the value of their tokens over time.
### Is my crvUSD safe when staked as scrvUSD?
Yes. The underlying crvUSD is not rehypothecated elsewhere—they remain idle within the vault, minimizing risk.
### Can I convert scrvUSD back to crvUSD?
Yes, you can convert scrvUSD back to crvUSD at any time. The exchange rate reflects the accrued yield.
---
## veCRV
veCRV stands for vote-escrowed CRV (locked CRV). You receive veCRV by locking your CRV tokens for a period between 1 week and 4 years. **The longer the lock, the more veCRV you receive.**
:::warning
veCRV is not transferable. If you lock CRV into veCRV, the only way to get it back is by waiting for your lock to expire.
:::
Holding veCRV has four main benefits:
1. [**Revenue Share**](../vecrv/revenue): Receive a share of the weekly fee revenue Curve generates
2. [**Gauge weight voting**](../dao/gauge-weights): Decide which pools and Llamalend markets receive the weekly CRV inflation
3. [**Voting**](../dao/proposals): Vote on governance proposals
4. [**Boosting**](../vecrv/boosting): Boost your own CRV rewards by up to 2.5x if you provide liquidity to a pool or lending market
For more on how locking works and how rewards and boosting are calculated, see the [main veCRV documentation](../vecrv/what-is-vecrv).
---
## Current veCRV Stats
Current veCRV stats directly fetched from
---
## FAQ
### Can I send my veCRV to a different wallet?
No, veCRV cannot be transferred or traded. It stays in the wallet that locked the CRV.
### Can I lock CRV through a multisig wallet or smart contract?
Yes, CRV can now be locked to veCRV by any wallet, multisig, or smart contract. Previously, smart contracts needed to be whitelisted, but this was removed by a [DAO vote in May 2025](https://www.curve.finance/dao/ethereum/proposals/1062-ownership/).
### Can I withdraw my CRV before my lock ends?
No, once locked, your CRV cannot be withdrawn until your chosen lock-up period expires.
---
## Community Fund & Treasury
## Community Fund
Curve initially distributed around 151M CRV to a Community Fund fully controlled by the DAO, intended for use in emergencies or as rewards for community-led initiatives such as grants. Funds can be allocated via a governance proposal and can only be distributed through a linear vesting over a minimum duration of one year.
The Community Fund is deployed on Ethereum at [`0xe3997288987e6297ad550a69b31439504f513267`](https://etherscan.io/address/0xe3997288987e6297ad550a69b31439504f513267).
:::info Looking for a Grant?
If you are building something which in our view deserves a grant from Curve, please feel free to reach out in the dedicated Telegram channel: [Curve Ecosystem Grants](https://t.me/curve_grants) or create a proposal on the [Governance Forum].
:::
## Treasury
On June 27, 2025, the Curve DAO voted to establish a dedicated Treasury, allocating **10% of DAO revenue** to it. These funds belong to the DAO and remain entirely under its control. With a successful governance vote, they can be used for any purpose the DAO chooses, such as development, research, risk assessment, bad debt insurance, bug bounties, audits, or anything else the DAO deems appropriate. Check the governance forum post [here](https://gov.curve.finance/t/activate-the-fee-allocator-and-redirect-10-of-revenue-to-community-fund/10676).
In comparison to the community fund, assets do not require a one-year vesting period, which makes the usage of these funds much more convenient.
The Treasury is deployed on Ethereum at [`0x6508ef65b0bd57eabd0f1d52685a70433b2d290b`](https://etherscan.io/address/0x6508ef65b0bd57eabd0f1d52685a70433b2d290b).
---
## Emergency DAO
The EmergencyDAO is a **5-of-9 multisig** authorized for emergency interventions. To keep crvUSD safe from any unknowns and ensure strong checks and balances, the Emergency DAO's authority has been expanded to include shared control with the DAO over several critical risk parameters to ensure fast, safe responses to market stress. These expanded permissions were granted through [proposal 1252](https://www.curve.finance/dao/ethereum/proposals/1252-ownership?ref=news.curve.finance).
:::important Security Design
The Emergency DAO is designed very conservatively and **cannot move or withdraw any user funds**. Its permissions are strictly limited to reducing risk from unexpected events through parameter adjustments and pausing mechanisms. The Emergency DAO cannot access user funds under any circumstances.
:::
## Scope of the Emergency DAO
- **Peg Stability Reserve (PSR)**: Pause the Peg Stabilization Reserve's associated contracts to stop them from depositing or withdrawing crvUSD (this pause does not affect the pool level).
- **Mint Factory:** Reduce, but never increase, debt ceilings for any component relying on crvUSD minting (including YieldBasis, FlashMinter, etc.).
- **Controller (Llamalend markets):** Adjust AMM fees, modify monetary policy, update the liquidity-mining callback, and manage borrowing discounts (without triggering liquidation for any users).
- **Lending Vaults:** Set deposit limits.
## Deployments
The original EmergencyDAO is deployed on :logos-ethereum: Ethereum at [`0x467947EE34aF926cF1DCac093870f613C96B1E0c`](https://etherscan.io/address/0x467947EE34aF926cF1DCac093870f613C96B1E0c).
Additionally, the Emergency DAO multisig is deployed at `0x6d447e544D01a59cb0774763bf15526574CffFeD` on all chains where Curve is active (except Moonbeam, which is incompatible with Safe).
## EmergencyDAO Members
The EmergencyDAO consists of the following members:
| Name | Telegram Handle |
| --------------- | --------------------- |
| `banteg` | `Yearn, @banteg` |
| `Calvin` | `@calchulus` |
| `C2tP` | `Convex, @c2tp_eth` |
| `Darly Lau` | `@Daryllautk` |
| `Ga3b_node` | `@ga3b_node` |
| `Naga King` | `@nagakingg` |
| `Peter MM` | `@PeterMm` |
| `Addison` | `@addisonthunderhead` |
| `Quentin Milne` | `StakeDAO, @Kii_iu` |
---
## Gauge Weights
## What are Gauge Weights
Each week on Curve, a fixed amount of CRV is emitted and distributed to liquidity providers. Where that CRV flows is decided by the gauge weight system.
veCRV holders vote to assign weights to different gauges, which are smart contracts that track deposits of LP tokens or vault shares. The more votes a gauge receives, the more CRV it distributes to depositors (stakers) over the week. **Gauge weights are updated every Thursday at 00:00 UTC**, and rewards are continuously distributed proportionally based on their share of the total staked value.
:::info Gauge Weight Example
**Thursday 00:00 UTC**: A snapshot is taken for this week's gauge weights. Pool A's gauge receives 10% of all veCRV votes. As a result, 10% of the weekly CRV inflation is allocated to Pool A's gauge and distributed to users who stake the LP token of that pool in the gauge throughout the week.
:::
To check the current gauge weights across all gauges, visit the [Curve DAO Gauge Section](https://www.curve.finance/dao/ethereum/gauges/) and connect your wallet to load the data.
Below the chart is a table listing each gauge. Users can click on a gauge to view analytics like past relative weights, how much CRV it's receiving this week, and other details.
---
## Voting for Gauge Weight
Users can start voting for gauge weights as soon as they have voting power in the form of veCRV. If a user freshly locks CRV, they will not be able to take part in the current gauge weight voting cycle and must wait for the next week (updates every Thursday 00:00 UTC). A user's votes on gauge weights are not final and can be adjusted. You can change your vote on each gauge every 10 days. **Your votes are not reset every Thursday at 00:00 UTC - if you don't change your weight, your voting power allocation will simply remain the same.**
Since gauge weight voting is fully on-chain, each vote requires signing a transaction and paying gas fees.
To get started, simply navigate to `DAO -> Gauges -> Voting` and connect your wallet:
If you have already allocated some or all of your voting power, your vote weight distribution will show up:
### Voting For the First Time
If you are voting for the first time, your used voting power starts at 0%. You can distribute up to 100% of your voting power across as many gauges as you wish. You don't need to allocate the full 100% - you can use as much or as little as you want.
To vote for your first gauge, click the blue "Add Gauge" button and select the gauge you want to vote for. You can search by name or gauge address.
After selecting the gauge, input the percentage of your voting power you want to assign, click the blue `Vote` button, and sign the transaction in your wallet to cast the vote.
You can vote for as many gauges as you wish and use up to 100% of your voting power. Your overview page will update accordingly after each vote.
### Updating Your Gauge Weights
If you've already voted for a gauge and want to adjust your allocation, you can do so at any time — but with one important restriction: You can only **change your vote on a specific gauge once every 10 days**.
To update your current weights:
1. Click on the gauge you want to update
2. Enter the new percentage you want to allocate (any value between 0% and your available voting power)
3. Click the `Update Vote` button
4. Sign the transaction in your wallet
The UI shows you the exact date and time when you'll be able to update your vote on each gauge again. In this example, the user needs to wait until June 27th to adjust the weight.
:::info **Example**
You previously allocated **80%** of your voting power to the reUSD/scrvUSD gauge. You can reduce it to **60%**, and immediately reallocate the freed-up **20%** to other gauges.
However, if you later want to reduce your vote on reUSD/scrvUSD again (e.g., from 60% to 38%), you'll need to wait 10 days from your last change to that specific gauge.
:::
## Gauge Weights on L2s
While gauge voting takes place only on Ethereum, Curve has built a permissionless system to allow gauges on other chains (L2s and sidechains) to receive gauge weights and CRV emissions. Here's how it works:
1. **Proxy Gauges**: A "proxy" gauge is deployed on Ethereum for each L2/sidechain gauge
2. **Voting**: veCRV holders vote on these proxy gauges just like regular gauges
3. **Bridging**: After the full epoch's CRV emissions have been streamed to the proxy gauge, they are bridged to the corresponding chain
4. **Distribution**: The bridged CRV is then distributed to users who have staked LP tokens in the actual L2/sidechain gauge
:::info **Important Note**
Gauge emissions on L2 and sidechain gauges are **delayed by about one week** compared to Ethereum gauges. This is because CRV must first be fully streamed to the proxy gauge over the 7-day epoch before it can be bridged. Once streaming is complete, bridging is usually very fast.
:::
---
## FAQ
### Getting Started & Prerequisites
**What is veCRV & why do I need it to vote?**
[veCRV (vote-escrowed CRV)](../vecrv/what-is-vecrv.md) is a token you get when you lock your CRV tokens for a specific period. The longer you lock, the more veCRV you receive. You need veCRV to participate in gauge weight voting because it represents your voting power in the Curve DAO.
**What if I add more CRV or extend my lock?**
If you increase your veCRV balance (by locking more CRV or extending the lock), your vote power increases — but you must **re-vote** to apply the new power.
**What's the minimum amount of veCRV I need to vote?**
There's no minimum amount of veCRV required to vote. However, the more veCRV you have, the more voting power you'll have. Even a small amount of veCRV allows you to participate in the governance process.
**Do I need to stake LP tokens to vote on gauges?**
No, you don't need to stake LP tokens to vote on gauges. Gauge voting and LP staking are separate activities. You can vote on gauges with just your veCRV tokens. However, to earn rewards from the gauges you vote for, you would need to stake LP tokens in those specific pools.
### Voting Mechanics
**Can I vote for multiple gauges at once?**
Yes! You can vote for as many gauges as you want, but your total voting allocation cannot exceed 100% of your voting power. You can distribute your votes across multiple gauges (e.g., 40% to gauge A, 30% to gauge B, 30% to gauge C).
**How often can I change my vote?**
You can update your voting weights **once every 10 days per gauge**.
**How often are gauge weights updated?**
Gauge weights are recalculated every **Thursday at 00:00 UTC** based on all active votes. CRV emissions are then distributed according to these updated weights.
**What happens if I don't use all 100% of my voting power?**
If you don't use all 100% of your voting power, the unused portion simply goes unused. You're not penalized for this, but you're missing out on potential influence. You can always come back later to allocate the remaining voting power.
**When do my votes take effect?**
Your votes take effect at the next weekly update, which happens every **Thursday at 00:00 UTC**. If you vote on Wednesday, your votes will be active starting Thursday. If you vote on Friday, you'll need to wait until the following Thursday.
**What happens if I try to vote on a gauge before the 10-day cooldown expires?**
The UI will prevent you from voting on that gauge. If you attempt to do so, the transaction will fail and you'll see an error message. You'll need to wait until the 10-day cooldown period expires before you can modify your vote on that specific gauge again.
**Do my votes carry over to the next week if I don't change them?**
Yes! Your votes automatically carry over to the next week. You don't need to re-vote every week unless you want to change your allocations.
**Should I reset my gauge votes before re-voting?**
No. Resetting your votes (setting your votes to 0%) will trigger a 10-day cooldown for those gauges. To update your vote, just re-cast it — no reset needed. E.g. if you want to decrease your vote for a gauge from 80% to 60%, don't do `80 -> 0 -> 60`. Simply do `80 -> 60`.
:::warning
Resetting gauge weights before re-voting will block you from voting again for 10 days on those same gauges.
Always **re-vote directly** unless you're intentionally changing your selection.
:::
### Rewards & Benefits
**How do I earn rewards from gauge voting?**
Gauge voting itself doesn't directly earn you rewards. However, by voting on gauges, you're helping direct CRV emissions to specific pools. To actually earn rewards, you need to stake your LP tokens in the pools that have gauges you voted for.
**Do I earn CRV just by voting, or do I need to stake LP tokens too?**
You need to stake LP tokens to earn CRV rewards. Voting on gauges only directs where the CRV emissions go - it doesn't earn you rewards directly. You must stake LP tokens in the pools you voted for to receive the CRV rewards.
**When do I receive rewards from my gauge votes?**
Rewards are distributed continuously throughout the week to users who have staked LP tokens in the gauges. The rewards you receive depend on your share of the total staked value in that gauge. You can claim your rewards at any time through the Curve interface.
---
## Voting
All voting, whether it is voting for a proposal or voting for gauge weights, is done on **Ethereum** and every veCRV holder can take part. Just because all votes happen on Ethereum does not imply that only gauges on Ethereum can receive CRV emissions. For more information, see: [Cross-Chain Governance](./overview.md#cross-chain-governance).
---
## Overview(Dao)
The Curve DAO is the decentralized governance system that controls all aspects of Curve Finance. All contracts and decisions on Curve are in the hands of the DAO, governed solely by its holders through a system pioneered by Curve called **veTokenomics**. Members receive governance power by locking CRV for veCRV tokens. The longer a user locks their tokens up for, the more voting power they receive. This reduces governance attacks and helps align incentives so the DAO passes proposals which benefit the protocol long term.
**Governance Mechanics**
- [**Locking CRV:**](../vecrv/how-to-lock.md) Users must lock CRV to participate in DAO governance and access protocol benefits.
- [**Proposals:**](./proposals.md) These are DAO votes that last 7 days and can contain all sorts of different actions. Votes pass if they meet the Quorum and Min support thresholds. Anyone with veCRV can vote on proposals, and holders of at least 2,500 veCRV can create votes.
- [**Gauge Weights:**](./gauge-weights.md) Each week veCRV holders vote on where the weekly CRV emissions will go. Gauges distribute CRV emissions to LPs in DEX pools or suppliers to lending markets.
The [governance discussion forum](https://gov.curve.finance/) allows users to post and discuss current proposals, future proposals, or ideas.
Users can access the Curve DAO dashboard at https://curve.finance/dao. This dashboard provides an overview of all current and closed votes. Each proposal should have a corresponding topic on the Curve governance forum, accessible at https://gov.curve.finance/.
Ready to participate in Curve governance? Start by [locking your CRV tokens](../vecrv/how-to-lock.md) to receive veCRV and gain voting power. Then visit the [governance forum](https://gov.curve.finance/) to join discussions, stay informed about current proposals and start voting on proposals on [https://www.curve.finance/dao/ethereum/proposals/](https://www.curve.finance/dao/ethereum/proposals/).
## Cross-Chain Governance
Just because Curve is operational on multiple networks, this does not come along with a centralization of powers. All L1 and L2 deployments are fully in control of the DAO. To make changes on chains other than Ethereum, an on-chain vote needs to be created on Ethereum whose outcomes are broadcasted to the corresponding chain.
---
## Governance Proposals
## What are Governance Proposals
Governance proposals are the primary mechanism for making changes to the Curve protocol. veCRV holders can create and vote on proposals that affect protocol parameters, add new features, or make other important decisions. All proposals are open for voting for **7 days** and require specific quorum and support thresholds to pass.
Curve DAO proposals interface showing active and completed proposals
To view all current and past proposals, visit the [Curve DAO Proposals page](https://www.curve.finance/dao/ethereum/proposals/) and connect your wallet to see your voting options.
## Proposal Types
[Curve DAO proposals](https://www.curve.finance/dao/ethereum/proposals/) fall into three main categories:
**Ownership Vote** – The most common proposal type, used for a wide range of DAO matters such as gaining control over protocol fees, whitelisting new gauges, changing pool parameters, adding crvUSD mint markets, or implementing protocol upgrades. Requires **30% quorum** and **51% minimum support** to pass.
**Parameter Vote** – Used to modify parameters on older liquidity pools, such as fees, amplification factors, or other pool-level settings. Requires **15% quorum** and **60% minimum support** to pass. This type of vote is rarely used in todays Curve governance as most operations are now handled through ownership votes.
:::info Quorum
In the Curve DAO, only "Yes" votes count towards quorum.
:::
## Voting on Proposals
Voting on proposals always happens on Ethereum. Anyone who **already held veCRV when the vote began** can participate in voting. Since voting is fully on-chain, each vote requires signing a transaction and paying gas fees, so consider the current gas costs before voting.
### Voting Power and Decay
Voting decay is a safety measure to stop manipulation of proposals at the last minute by whales, to give all DAO members time to react to large votes in a timely manner. Your veCRV counts for **100% during the first 3.5 days**, then linearly decreases to **0% by the deadline**.
> *Note: Gauge-weight votes are exempt from this decay and retain full power for the entire weekly epoch.*
### How to Vote
To vote on a proposal, navigate to the [Curve DAO Proposals page](https://www.curve.finance/dao/ethereum/proposals/) and select the relevant proposal. Make sure your wallet is connected to see your voting options.
Each proposal card displays:
- **User** - Your connected wallet address
- **Voting Power at Snapshot** – Your veCRV balance at the time the proposal was created
- **Current Voting Power** – Your current voting power (decays if proposal is older than 3.5 days)
To cast your vote:
1. Click **"Vote For"** or **"Vote Against"** depending on your preference
2. Sign the transaction in your wallet
3. Your vote is now recorded on-chain
## Creating Proposals
Creating a governance proposal involves three main stages:
#### 1. Ideation & Discussion
- Post a draft in the [governance forum](https://gov.curve.finance/) using the standard [proposal template](https://gov.curve.finance/t/template-governance-proposal/)
- Gather feedback from the community in [Discord](https://discord.gg/twUngQYz85) or [Telegram](https://t.me/curvefi)
- Refine your proposal based on community input
#### 2. Create the Vote
To create an on-chain vote, you need **2,500 veCRV**. You can either:
- Lock up CRV tokens to get veCRV
- Ask someone from the community to create the vote for you
#### 3. Vote & Execute
After the 7-day voting period, if the proposal passes successfully, it needs to be executed. This can be done directly via the UI using the `Execute` button.
:::info
Need help creating your on-chain vote? Reach out to the Curve community on [Telegram](https://t.me/curvefi) — we're happy to assist.
If you're looking to whitelist a gauge, check out the [Gauge Whitelisting Guide](/protocol/gauge/whitelisting-gauge) for a step-by-step walkthrough.
:::
---
## FAQ
### Getting Started & Prerequisites
**What is veCRV and why do I need it to vote?**
[veCRV (vote-escrowed CRV)](../vecrv/what-is-vecrv.md) is a token you get when you lock your CRV tokens for a specific period. You need veCRV to participate in governance voting because it represents your voting power in the Curve DAO.
**What's the minimum amount of veCRV I need to vote?**
There's no minimum amount of veCRV required to vote on proposals. However, the more veCRV you have, the more voting power you'll have. Even a small amount of veCRV allows you to participate in governance.
**How much veCRV do I need to create a proposal?**
You need **2,500 veCRV** to create an on-chain governance proposal. This requirement helps ensure that proposals come from committed community members.
**Should I vote on every proposal?**
While you can vote on every proposal, it's better to vote thoughtfully on proposals you understand rather than voting on everything. Quality participation is more valuable than quantity.
**How can I stay informed about new proposals?**
Monitor the [Curve DAO page](https://www.curve.finance/dao/ethereum/proposals/), join the [Discord](https://discord.gg/twUngQYz85) or [Telegram](https://t.me/curvefi) communities, and follow the [governance forum](https://gov.curve.fi/).
### Voting Mechanics
**When can I vote on a proposal?**
You can only vote if you **already held veCRV when the vote began**. This prevents last-minute vote manipulation by large holders who might acquire veCRV just to swing a vote.
**How long do proposals stay open for voting?**
All proposals are open for voting for exactly **7 days** from when they are created.
**What happens if I don't vote?**
If you don't vote, your voting power is simply not counted. You're not penalized for abstaining, but you're missing out on the opportunity to influence protocol decisions.
**Can I change my vote after casting it?**
No, votes cannot be changed once cast. Make sure you're confident in your decision before signing the transaction.
### Voting Power & Decay
**Why does my voting power decay?**
Voting power decays during the final 3.5 days of a proposal to prevent last-minute vote manipulation by large holders. This encourages early participation and thoughtful voting.
**How does the voting power decay work?**
Your veCRV counts for **full 100% during the first 3.5 days**, then linearly falls to **0% by the deadline**. This means voting early maximizes your influence.
**Does voting power decay affect gauge voting?**
No, gauge-weight votes are exempt from this decay. They retain full power for the entire weekly epoch.
### Proposal Execution
**Who can execute a passed proposal?**
Anyone can execute a passed proposal after the timelock expires. You don't need veCRV to execute proposals.
**What if a proposal fails?**
Failed proposals cannot be executed and the proposed changes do not take effect. The proposal creator can submit a new proposal with modifications if desired.
---
## Contract Deployments(User)
---
## Guide: Claiming LP Rewards
To claim your earned `CRV` and other token rewards, go to your pool's page, and then the `Withdraw/Claim` tab on the left, then `Claim Rewards`, you should see a box like the following:
You may see 2 claim buttons, or just a single one, depending on the rewards you have to claim:
- `Claim CRV`: This will claim ***only*** `CRV` rewards.
- `Claim Rewards`: This will claim everything ***except*** any `CRV` rewards.
By confirming the transactions within your wallet, you will claim each separate reward. Congratulations on your earnings!
---
## Guide: Depositing to a Pool
The UI makes depositing to a pool easy. Follow along below.
## 1. Go to the pools overview
First of all go to the pools overview page here:
You will then all the different pools on the network you are currently connected to, if you would like to see the pools for a different network, you can change this in the top right hand corner. You should see a screen like the following:
## 2. Choose your desired pool
The pools overview above shows a list of all the available pools on that network, you can also search for an asset you would like to deposit, e.g., `crvUSD`, `CRV`, etc.
In this example we will be depositing to the `crvUSD/USDC` pool. You can search for it on the overview page and click it to see the details of this pool and deposit, or click here: [`crvUSD/USDC` Pool](https://www.curve.finance/dex/ethereum/pools/factory-crvusd-0/deposit)
## 3. Deposit & Stake - Choose Amounts
In this example we will be doing `Deposit & Stake` as a single transaction. However, depositing and staking are two different actions, and can be done separately:
- **Deposit**: Depositing is depositing your assets into the pool. When you do this you receive LP tokens in return, and start earning trading fees from swaps.
- **Staking**: Stake your LP tokens in the pool's reward gauge, this means you can earn CRV or other token rewards, if available.
More info on how each of these actions work in the [Understanding Rewards](/user/dex/understanding-rewards#summary-how-to-earn-different-rewards) page.
Now on the `crvUSD/USDC` pool page, we can see the deposit menu to the left and choose `deposit & stake` and choose how much we'd like to deposit, here we are depositing $10k `crvUSD`:
Note that we don't have to deposit both `crvUSD` and `USDC` to the pool.
## 4. Deposit & Stake - Complete Transactions
To complete the `Deposit & Stake`, you'll likely need to perform two separate transactions:
1. **Approve**: Before swapping a token for the first time, you must grant the Curve smart contract permission to interact with it. This is a standard security step in DeFi. Click `Approve` and confirm the transaction in your wallet. You only need to do this once per token.
2. **Deposit & Stake**: Once the approval transaction is confirmed, the button will change to `Deposit & Stake`. Click it and confirm the final transaction in your wallet to execute the trade.
## 5. Confirmation
After a few seconds, the transactions should be confirmed on the blockchain. The interface will confirm to show your transactions were successful:
Then underneath the chart, where it shows the Pool Details, there will be a new tab called `Your Details`, click on this to see your deposit details:
Here we can see our details
- **9,805 LP Tokens**: Our $10k `crvUSD` was converted to 9,805 LP tokens, which are currently staked.
- **We now have `USDC` and `crvUSD`**: We own a share of assets in the pool, so that means we have 4,606 `USDC` and 5,392 `crvUSD`. As users swap in this pool, these numbers will change, and the total should increase because of earned swap fees, becoming worth more than $10k in time.
- **Earning 3.41% `CRV` APR**: This means we will earn $341 of `CRV` if we stake for a year and the interest rate stays the same.
- **Boost is 1x**: we don't have any [veCRV](/user/vecrv/what-is-vecrv), so our boost is 1x.
---
## Guide: How to Swap Assets
Swapping on Curve is easy thanks to the Curve Swap Router, follow along below.
## 1. Go to Curve Swap
First, head to [Curve Swap](https://www.curve.finance/dex/ethereum/swap/):
## 2. Connect Wallet
If your wallet isn't already connected, you'll find the `CONNECT WALLET` button in the top-right corner, and choose your desired network, Ethereum is the default.
You should then see the swap interface in the center of your screen:
## 3. Select Your Tokens and Amount
Use the top box (shown as `ETH` above) to select the token you want to sell and the bottom box for the token you want to buy (shown as `WETH` in the image above). Then, enter the amount you wish to trade.
Let's walk through an example of swapping `2000 USDC` for `crvUSD`. After entering these details, the interface updates to show you a detailed quote for the trade:
Here’s what the detailed quote from the interface is telling us:
* **Exchange Rate**: For every `1 USDC` you swap, you will receive `1.00004 crvUSD`. This rate already includes trading fees.
* **Trade Route**: The Swap Router found the best price by routing your trade through two pools: first `USDC` → `USDT`, and then `USDT` → `crvUSD`.
* **Slippage Tolerance**: This is a safety feature that protects you from large price swings while your transaction is being processed. The default of `0.03%` means that in a worst-case scenario, the minimum you would receive is `0.99974 crvUSD` per `1 USDC`.
## 3. Approve and Swap
To complete the swap, you'll likely need to perform two separate transactions:
1. **Approve**: Before swapping a token for the first time, you must grant the Curve smart contract permission to interact with it. This is a standard security step in DeFi. Click `Approve` and confirm the transaction in your wallet. You only need to do this once per token.
2. **Swap**: Once the approval transaction is confirmed, the button will change to `Swap`. Click it and confirm the final transaction in your wallet to execute the trade.
## 4. Confirmation
After a few moments, the transaction will be confirmed on the blockchain. The interface will update to show your new `crvUSD` balance, confirming a successful swap:
Congratulations, you've successfully swapped tokens on Curve!
---
## Guide: How to Unstake & Withdraw
To withdraw from a pool, you may have to first `Unstake` your tokens if you have staked them. These are two separate actions, and unfortunately cannot be done together like `Deposit & Stake`.
## Unstaking
To unstake, go to the `Withdraw/Claim` tab on the left, then click `Unstake`:
You can choose to unstake a portion or all of your LP tokens. After choosing your desired amount, click the `Unstake` button and confirm the transaction within your wallet. Once successful you will receive confirmation that your tokens are now unstaked:
## Withdrawing
To withdraw, go to the `Withdraw/Claim` tab on the left, then click `withdraw`. You should then a menu like this:
Above we have chosen to withdraw all our LP tokens by clicking `MAX`, then we chose what assets we wanted:
- **One Coin**: Withdraw everything as just `crvUSD` or `USDC`
- **Balanced**: Each LP token is worth an equal share of the all the `crvUSD` and `USDC` in the pool. In our case, each LP token was worth 0.61 `USDC` and 0.41 `crvUSD`, so in total our balanced withdrawal of 9805 LP tokens would be 5989 `USDC` and 4047 `crvUSD`
- **Custom**: This lets you withdraw in any ratio you wish, for example, receiving 1000 `crvUSD` and the rest as `USDC`
Above we are going to withdraw our LP tokens as solely `crvUSD`, because that is what we initially deposited.
Click on `Withdraw` and confirm the withdrawal within your wallet. Once this is successful the UI will show a confirmation:
Congratulations you successfully provided liquidity to a Curve DEX pool!
You can see here that we initially deposited 10,000 `crvUSD`, but were able to withdraw 10,034 `crvUSD`, a profit of 34 `crvUSD` which was earned from swap fees.
---
## Providing Liquidity in Pools
Providing liquidity in pools is a great way to earn interest (yield) on your assets, and help Curve and DeFi at the same time.
Pools are groups of two or more assets. When you provide liquidity to a pool, you deposit your assets into that pool. This is called being an **LP** or **Liquidity Provider**. Traders can then trade between the assets within the pool. For example, if you are an LP in a `crvUSD/USDC` pool, the pool will have two assets: `crvUSD` and `USDC`. Traders can trade between `crvUSD` to `USDC` freely within the pool, and as an LP you earn fees from these swaps and other rewards.
If you're interested in all the different rewards you can earn as an LP, or what the numbers mean, see the page about understanding rewards: [Understanding Rewards](understanding-rewards.md).
:::warning Risks of Providing Liquidity
It's important to remember that providing liquidity to pools comes with risks. See the disclaimer here: [Pool Risks](../security/risks/pools.md)
:::
Below are links with guides on how to use the UI to deposit, withdraw, stake and claim your rewards.
---
## Overview(Dex)
A **D**ecentralized **Ex**change (DEX) is a trading platform that operates without a central company or authority, instead it runs on smart contracts with clear rules and behaviour. Unlike traditional exchanges like Coinbase or the New York Stock Exchange, a DEX allows users to trade digital assets on a blockchain like Ethereum.
This approach gives you complete control over your money. Here’s what that means for you:
* **You're in Control:** You are always in control of your assets. Even when you buy or sell them, your assets are never held by Curve, which protects you from issues like exchange hacks or companies halting trading. This is known as being **non-custodial**.
* **Automated and Transparent:** Trades on Curve are handled by **smart contracts**. These are programs that run on the blockchain and automatically execute trades according to their code. Every action is public and verifiable on the blockchain, ensuring a transparent and secure process.
## How Do Swaps Work? Liquidity Pools
To allow anyone to trade instantly, 24/7, Curve uses Pools, sometimes called Liquidity Pools. Instead of matching individual buyers and sellers (orderbook), users trade against a large pool of assets. All the pools available are shown on the [Pools page](https://www.curve.finance/dex/ethereum/pools/) for each network.
Each pool contains two or more tokens. When you want to swap one token for another, you add your token to the pool and take out the token you want. This system means there's always liquidity ready for your trade, so you don't have to wait for a buyer or seller to appear. In the example below Alice swaps 1 ETH for 0.99 stETH through the stETH/ETH pool.
*Note: that in real pools Alice would get much more than 0.99 stETH, probably closer to 0.9999 stETH.*
## Who Provides the Assets? Liquidity Providers (LPs)
The assets in these pools are supplied by other users, called **Liquidity Providers** (or LPs). Anyone can become an LP by depositing their assets into a pool. When there's more assets in a pool, traders get better prices.
In return for providing liquidity, LPs earn fees from every swap that happens in their pool, and many pools also offer CRV or other token rewards. This incentivizes users to fill the pools with assets, which in turn provides a better trading experience for everyone.
## Types of Pools - Stableswap and Cryptoswap Pools
Curve uses two different kinds of pools, each designed for specific combination of assets:
* **Stableswap Pools:** For assets that should trade at a similar price (e.g., USDC and USDT, or stETH and ETH).
* **Cryptoswap Pools:** For assets whose prices move independently of each other (e.g., ETH and WBTC, or ETH and USDT).
To learn more about how they work, see the [Stableswap vs. Cryptoswap page](./stableswap-vs-cryptoswap.md)
## Basepools and Metapools
Within Curve's Stableswap pools, there are also pool designs which are known as Basepools and Metapools:
- **Basepools**: These are foundational pools of liquidity on Curve. When you deposit to pool you get back an LP token representing your share of assets in that pool. For certain pools, think [3pool](https://www.curve.finance/dex/ethereum/pools/3pool/deposit/) (DAI/USDC/USDT) and [Curve.fi Strategic USD Reserves](https://curve.finance/dex/ethereum/pools/factory-stable-ng-355/deposit/?ref=news.curve.finance) (USDT/USDC) these LP tokens are approved to be used as an asset in other pools. Any Stableswap pool can be approved to be a Basepool, with a successful DAO vote.
- **Metapools**: Metapools contain a Basepool LP token as one of their assets, as well as one or more different assets which aren't in the Basepool.
See the image below for a visual representation of the [Curve.fi Strategic USD Reserves Basepool](https://curve.finance/dex/ethereum/pools/factory-stable-ng-355/deposit/?ref=news.curve.finance) and the [DOLA Strategic Reserves Metapool](https://curve.finance/dex/ethereum/pools/factory-stable-ng-396/deposit/?ref=news.curve.finance):
## Pool Fees
Each **Stableswap** pool has its own set fee, with most ranging from **0.005% to 0.02%**. However, some pools, such as the Strategic Reserve pools, have a much lower fee of around **0.001%**. Most current and all new pools now feature **dynamic fees**. This mechanism automatically increases the fee when a swap would make an imbalanced pool even more so, thereby increasing liquidity providers' (LPs) profits during market volatility.
**Cryptoswap** pools have a broader fee range, typically higher than Stableswap pools, to account for impermanent loss and rebalancing costs. Fees can be as low as **0.05%** and go up to around **0.4%**. Cryptoswap pools also feature dynamic fees, which automatically increases the fee when swaps make the imbalances worse.
---
## Curve's DEX Philosophy
Curve has a simple design principle and philosophy:
- DeFi and markets are strongest when anyone and everyone can participate easily and passively.
- a DEX should be positive sum for all participants.
Because of these design principles, every pool on Curve is designed to be:
- **Great for Swappers**: The top priority is to offer users the best possible price when they swap. Curve designs its pools with low fees and deep liquidity to minimize slippage, even for very large trades.
- **Simple, Easy & Rewarding for Liquidity Providers (LPs)**: Providing liquidity is designed as a "set and forget" passive experience, removing the need for LPs to actively manage complex positions, orders or liquidity ranges, like most other DEXs today. LPs are also rewarded with trading fees from their pool and, in many cases, CRV and other token rewards.
- **Reliable for Asset Issuers**: For any asset to be useful, people need to be able to buy and sell it easily, especially during volatile times. Curve is built to be highly efficient and the most reliable, long-lasting liquidity around, and this has been proven during many events of market distress.
- **Aligned with the Curve DAO**: The Curve DAO receives a share of the trading fees generated from each pool. This creates a direct incentive for the community to govern all pools effectively. DAO members are motivated to optimize pools to attract more swappers and increase profitability for LPs and themselves, creating a positive feedback loop that benefits all participants.
---
## Stableswap and Cryptoswap Pools
Curve utilizes two specialized algorithms for its liquidity pools: **Stableswap** and **Cryptoswap**. Each is engineered to provide the best possible trading experience and returns for different types of crypto assets.
Here’s a quick comparison:
| Feature | Stableswap Pools | Cryptoswap Pools |
| :--- | :--- | :--- |
| **Asset Type** | Pegged-value assets (e.g., USDC/USDT, stETH/ETH), or stable assets with underlying yield (e.g., scrvUSD/USDT). | Volatile, uncorrelated assets (e.g., ETH/WBTC`, `WBTC/crvUSD). |
| **Number of Assets** | At least 2, with maximum of 8 | 2 or 3 |
| **Primary Goal** | Ultra-low slippage for assets that trade near a fixed ratio (usually 1:1). | Efficiently trade volatile assets while protecting LPs and maintaining pool balance. |
| **Mechanism** | Concentrates liquidity around a fixed peg (e.g., 1:1). The pool can become unbalanced (e.g., 90% USDC, 10% USDT) and still function efficiently, relying on arbitrage to restore balance. | Balances and concentrates liquidity around current price, with equal value in each asset. If prices change, it automatically rebalances liquidity if LP have generated enough to stay profitable. |
| **LP Strategy** | **Fully Passive.** LPs provide liquidity across the entire price range. This "set and forget" model contrasts with AMMs that require active management of liquidity positions. | **Fully Passive.** The algorithm automatically manages liquidity concentration and rebalancing, using accrued trading fees to cover the costs. |
## Stableswap Pools
The Stableswap algorithm is Curve's original innovation and the foundation of the protocol. It's designed to facilitate swaps between assets that are pegged to the same value, like two different stablecoins or a liquid staking derivative and its underlying asset.
The primary advantage is **incredibly deep liquidity** around the peg. This means traders can execute massive swaps with minimal price impact (slippage). For instance, in a healthy \$20M USDC/USDT pool, a \$1M swap would likely incur less than 0.01% slippage (receive \$999.9k after swapping), making it one of the most capital-efficient platforms for stable assets.
A key feature of Stableswap pools is that they are **fully passive**. Unlike most other AMMs where LPs must actively manage their price ranges, Curve LPs can simply deposit their assets and earn fees without further intervention. The pool provides liquidity across all possible prices, though it's most concentrated at the peg.
### Types of Assets in Stableswap Pools
Stableswap pools can handle a few different types of assets:
* **Pegged Assets**: Tokens that should trade at a 1:1 ratio, such as crvUSD/USDT or stETH/ETH.
* **Yield-Bearing Assets**: Assets that gradually appreciate against a base asset due to underlying yield, like sUSDe/USDT. The Stableswap math handles this slow-changing ratio gracefully.
* **Constant Ratio Assets**: A less common but powerful use case. For example, two tokenized gold assets for different weights, kgGOLD and ozGOLD, where 1 kgGOLD is always worth about 35 times more than 1 ozGOLD (1 kilogram (kg) = 35 ounces (oz)).
It's important to note that Stableswap pools are designed for these specific types of assets. Trying to use them for things they weren't designed for, like a forex pair (USDC and EURC), is a bad idea and can cause LPs to lose a lot of money. Cryptoswap was created to solve this very problem.
**Important Note:** Stableswap pools assume the peg is stable. If an asset permanently depegs or is exploited, LPs can incur significant losses as they will be left holding the devalued asset.
### How Providing Liquidity Works
### How does Stableswap Work?
The magic of Stableswap is its liquidity concentration. Let's visualize a crvUSD/USDC pool where each block represents \$1M of liquidity:
In this example most of the pool's liquidity (e.g., \$40M out of \$80M total) is concentrated in a very tight range around the \$1.00 price. Each block represents 1 million tokens (1M) of either crvUSD or USDC.
**Swap Example**:
1. A Stableswap pool has 40M of both crvUSD and USDC, the pool is perfectly balanced, so each asset is $1.
2. A user wants to swap \$20M USDC for crvUSD:
- The user's 20M USDC is deposited to the pool
- Approx. 19.98M crvUSD is withdrawn
- The price increases to 1.002 USDC per crvUSD
3. The pool is now imbalanced, it would only take a user selling 9M USDC to push the price up another 0.002. The imbalance also creates an **arbitrage opportunity**: traders are now incentivized to swap crvUSD back into the pool to get USDC at a slight discount, which naturally pushes the pool back toward a 50/50 balance.
---
## Cryptoswap Pools
Cryptoswap pools are Curve's solution for trading any pair of assets with **volatile or uncorrelated prices**. This includes everything from blue-chips like ETH/WBTC to forex pairs like EURC/USDC or a project's native token against ETH.
Unlike Stableswap, the Cryptoswap algorithm does not assume a fixed peg. Instead, its goal is to keep the **value of each asset in the pool balanced** (e.g., 50% ETH and 50% USDT by value). It achieves this through a novel internal oracle and an automated rebalancing mechanism.
Like their Stableswap counterparts, Cryptoswap pools are **fully passive and full-range**, making them simple and accessible for all LPs.
### How Providing Liquidity Works
### How Cryptoswap Works: Concentrating and Rebalancing
At its core, a Cryptoswap pool functions like a Stableswap pool concentrated around the `price_scale` (center of liquidity). As traders swap back and forth, the pool earns fees.
However, when the market price of the assets drifts significantly, the pool must move its liquidity concentration to match the new price. This is called **rebalancing**. To ensure this process is always profitable for LPs, it only happens if two conditions are met:
1. **Adjustment Step**: The price must move more than a set minimum amount (e.g., 0.5%). This prevents constant, unnecessary rebalancing in a choppy market. It is set by the `adjustment_step` parameter.
2. **Profitability Check**: The pool must have accrued enough trading fees to ensure the recent profits from swaps are more than double the cost of rebalancing. This guarantees that rebalancing doesn't cause a net loss for LPs.
Let's look at a simple USD/EUR forex pool example:
1. Liquidity is initially balanced around the price of $1.10, which is also the current price. It looks and functions like a normal Stableswap pool.
2. A user swaps USD for EUR, and the EUR/USD price increases to $1.105, because of the slight imbalance. While price stays within Minimum Step range, it will continue functioning like Stableswap.
3. Another swap happens, generating more profit for LPs. This increases price to $1.11, outside adjustment step range. A rebalance can now occur, assuming the pool has enough profit to rebalance.
4. The pool can use up to half the generated profit from swaps to move to balance liquidity around around the new price of $1.11, automatically.
---
## Swapping on Curve
Swapping on Curve is simple, secure, and as the main liquidity hub for many tokens, Curve gives you excellent swap rates.
Curve is a decentralized exchange, so **you are always in control of your funds**. Your assets never leave your personal wallet until a trade successfully completes. Every swap is protected from unexpected price changes. You're guaranteed to receive a specific minimum amount of tokens, or the transaction will not complete, leaving your funds untouched.
Here are the main ways you can swap on Curve:
1. **Use Curve Swap (Swap Router)**: This is the easiest and most recommended method. The Swap Router automatically finds the best possible price across all Curve pools. It also handles convenient extras, like:
* Depositing/withdrawing from the [scrvUSD savings vault](../curve-tokens/scrvusd.md).
* Wrapping/unwrapping `ETH` and `WETH` automatically.
Best of all, Curve charges **no frontend fee** for this service, meaning you often get a better final price here than anywhere else.
2. **Swap Directly Within a Pool**: For advanced users looking to minimize gas fees, you can trade directly inside a specific liquidity pool. For example, the [crvUSD/USDC pool's swap tab](https://www.curve.finance/dex/ethereum/pools/factory-crvusd-0/swap/). This doesn't search all pools for the best price, but can be cheaper for simple, direct swaps.
3. **Use a Swap Aggregator**: Curve is integrated into all popular swap aggregators like CoWSwap, 1inch, and Odos. If you frequently use an aggregator, you're likely already benefiting from Curve's liquidity without even knowing it\!
For a quick guide showing how to make a swap using the **Curve Swap Router**, see below.
---
## FAQ
### Does Curve have any frontend fees?
No, none of Curve products have any frontend fees. Curve does have a small pool fee, but this exists no matter where the swap happens, so very often the Curve Swap Router will give a better price than any aggregator because there are no extra fees.
### What are the fees for swapping on Curve?
When you swap, there are typically two types of fees:
1. **Trading Fee:** A small fee (often around 0.04%) is paid to the liquidity providers of the pool you are using. This fee is already included in the exchange rate you see in the UI.
2. **Gas Fee:** This is a fee you pay to the blockchain network (e.g., Ethereum) to process and secure your transaction. Curve does not control or receive any part of this fee.
Curve charges **no frontend fees**, meaning you often get a better final price here than on other platforms or aggregators.
### My transaction failed. Did I lose my funds?
No, your funds are safe. If a swap transaction fails, it means the trade did not happen, and your original tokens never left your wallet. The only thing you lose is the small gas fee you paid for the attempted transaction on the blockchain. Transactions usually fail as a safety measure, for instance, if the price moved beyond your set slippage tolerance while the trade was pending.
### Why do I have to 'Approve' a token before I can swap it?
The `Approve` transaction is a standard security feature across all of DeFi. It gives the Curve smart contract your permission to interact with *only that specific token* you want to swap. Think of it as giving a valet the key to your car, but not the keys to your entire house. It's a one-time security step you must complete for each new token you want to swap from.
### What is 'slippage' and what should I set it to?
Slippage is a safety setting that protects you from getting a much worse price than you expected. If you set slippage to 1%, you're telling Curve that you are willing to accept a final price that is up to 1% lower than what was quoted. If the price slips more than that while your transaction is pending, the transaction will fail, protecting your funds.
The default slippage values are low, and should normally work well. You should only increase the slippage if you're having issues with a highly volatile token/asset.
---
## Understanding Rewards
One of the primary ways to earn yield on Curve is by providing liquidity to a pool. When you deposit your assets, you become a liquidity provider (LP) and receive LP tokens in return. These tokens represent your share of the pool and are your key to earning rewards.
This page explains the different types of rewards you can earn and how they are displayed on the pool page. However, if you're looking for a guide on how to claim your earned rewards, see below.
If you are just looking for how to claim your earned CRV or other token rewards, see the guide below.
---
## How Rewards Are Displayed
Let's look at the standard pools overview page to understand the rewards you can earn.
Here is a breakdown of the reward types shown in the UI:
1. **Base vAPY (Trading Fees & Underlying Yield)**: This is the foundational yield you earn simply by holding a pool's LP tokens. It is composed of:
* **Trading Fees:** A share of the fees from all swaps in the pool. (e.g., 1a - USDC/USDT pool)
* **Underlying Yield (if applicable):** Some pools contain yield-bearing tokens (e.g., stETH, sUSDS). The interest from these assets is also included in the Base vAPY (e.g., 1b - ETH/stETH pool).
The figure shown is a variable Annual Percentage Yield (vAPY) based on the pool's recent daily activity (weekly figure shown on hover). It autocompounds, meaning the value of your deposits grows in value automatically over time.
2. **CRV Rewards**: Many pools offer additional rewards in the form of CRV tokens. To earn these, you must stake your LP tokens in the pool's gauge. The reward rate is shown as a range, as your earnings can be boosted by your [veCRV balance](https://www.google.com/search?q=../vecrv/boosting.md).
3. **Other Token Rewards**: Partner projects may provide their own tokens as extra incentives for liquidity providers. Like CRV, these rewards require you to stake your LP tokens in the gauge, however these can't be boosted.
4. **Points**: Some pools feature Points programs, which may qualify you for future benefits or airdrops. The requirements for earning points vary by project, so always check the specific details for each pool.
### What is a Gauge?
As mentioned above, a "gauge" is a smart contract where you stake your LP tokens to earn additional rewards like CRV and other tokens. Gauges are designed to distribute these reward tokens to LPs over time, incentivizing liquidity for specific pools.
Stakers accrue rewards in the gauge over time and can be claimed whenever they like.
### Summary: How to Earn Different Rewards
This table provides an at-a-glance summary of how to earn each type of reward.
| Action Required | Earn Base vAPY | Earn CRV Rewards | Earn Other Token Rewards | Earn Points |
|----------------------------------|-------------------|------------------|--------------------------|--------------------|
| Deposit to Pool (Hold LP Tokens) | ✅ | ❌ | ❌ | ❓\* |
| Stake LP Tokens in Gauge | ✅ | ✅ | ✅ | ❓\* |
| **Reward Mechanism** | **Autocompounds** | **Must be Claimed** | **Must be Claimed** | ❓\* |
*\*Points programs have unique criteria; some reward for depositing, others for staking. Always check the specific pool's requirements.*
---
## Earning Rewards
### Depositing to Pool
When you deposit your assets into a pool, you receive LP tokens. These tokens immediately begin earning the **Base vAPY**, which automatically compounds and increases the value of your LP tokens over time. You don't need to do anything else to earn this base yield.
### Staking LP Tokens in a Gauge
To earn additional rewards like **CRV** or **Other Tokens**, you must complete the extra step of staking your LP tokens in the pool's gauge. Once staked, you will begin accumulating these rewards, which you can claim at your convenience from the pool's "Claim" section.
---
## What is Curve?
Curve is a decentralized exchange (DEX) powered by automated market makers (AMMs) designed for efficient trading of stablecoins and volatile assets. Built on Ethereum and EVM-compatible chains, Curve delivers deep liquidity for traders and peace of mind for liquidity providers through passive and fully automated concentrated liquidity.
Additionally, Curve developed crvUSD, a decentralized CDP USD stablecoin, and Llamalend, a fully permissionless lending protocol, both powered by an innovative liquidation mechanism (LLAMMA) that carefully protects collateral in liquidation and provides more peace of mind for borrowers.
---
Check out the other sections of the documentation for technical and protocol information:
---
## Borrowing
## Why Borrow on Llamalend?
Borrowing on Llamalend allows users to access liquidity while maintaining exposure to their crypto assets. Instead of selling their holdings, users can use them as collateral to borrow crvUSD or other assets. Users borrowing on Llamalend benefit from:
- **Liquidation Protection**: If a user's loan health becomes very low, Llamalend automatically starts protecting their position by gradually converting their volatile collateral into stable crvUSD. This creates a safety buffer that reduces their exposure to price volatility
- **Maintain Exposure**: Keep crypto positions while accessing liquidity. Users don't have to sell their assets to get cash, preserving their potential upside
- **Leverage**: Use borrowed funds to increase market exposure or invest in additional opportunities
- **No Opening/Closing Fees**: Users just pay the interest rate while the loan remains open
- **No Minimum Loan Amounts**: Users can open a loan for any amount, there's no minimum
:::info
The liquidation protection is a very unique and novel feature and integral to understand when using Llamalend. For detailed information about liquidation protection and loan health monitoring, see [Liquidation Protection & Loan Health](/user/llamalend/guides/borrow/liquidation).
:::
## Borrow Rates
When users borrow crvUSD, they pay interest on their loan based on the current **borrow rate**. These dynamic interest rates help maintain the stability of crvUSD.
This borrow rate on Llamalend is **dynamic** and adjusts based on different market conditions, with interest charged every second. Users don't need to make manual interest payments—instead, the loan's debt automatically grows over time to account for the accrued interest. There are two types of markets in Llamalend, each with a different interest rate model:
### Mint Markets
The borrow rate in mint markets is influenced by two main factors: the price of crvUSD and the size of the Peg Stabilization Reserve (PSR). The rate mechanism is designed to help maintain crvUSD's $1.00 peg through economic incentives.
- **When crvUSD trades above \$1.00**: The borrow rate goes **down** to incentivize more users to borrow crvUSD. Users can then sell the borrowed crvUSD for other stablecoins or use it for leveraging positions. This creates sell pressure on crvUSD, pushing the price back down toward $1.00.
- **When crvUSD trades below \$1.00**: The borrow rate goes **up**, making it more expensive to maintain open loans. This encourages users to repay their debt, often requiring them to buy crvUSD (since they may have sold or used the borrowed crvUSD for other purposes). This creates buy pressure on crvUSD, pushing the price back up toward $1.00.
:::info
The PSR is another tool that helps keep crvUSD stabilized at \$1.00. When crvUSD trades above \$1.00, it creates more crvUSD to bring the price down. When crvUSD trades below $1.00, it removes crvUSD from circulation to bring the price up. The size of the reserve affects the borrow rate - a bigger reserve generally results in a lower borrow rate, while a smaller reserve means higher rates.
:::
### Lend Market Rates
In lending markets, the rate depends only on market utilization — the percentage of supplied crvUSD that is being borrowed. Unlike mint markets, the price of crvUSD does not influence the borrow rate. Each market has a minimum and maximum rate range. The higher the utilization, the higher the borrow rate.
Market utilization is calculated as:
$$
\text{utilization} = \frac{\text{amount borrowed}}{total supplied} \times 100
$$
When utilization is low, the rate stays close to the minimum and there's plenty of crvUSD available to borrow. As utilization increases, the rate increases toward the maximum and less crvUSD becomes available. At full utilization (100%), the rate reaches the maximum and there's no crvUSD left to borrow.
---
## FAQ
## Quick Start: Key Concepts
Before diving into the details, here are the essential concepts you need to understand:
**Liquidation Protection Range**: A price zone where your loan enters protection mode. If your collateral price falls into this range, the system automatically starts protecting your position.
**Health**: The most critical metric - represents how much buffer you have before full liquidation. Health decreases from price drops, conversion losses, and interest. When health reaches 0%, your loan is fully liquidated.
**Bands**: Small price ranges that make up your liquidation protection range. More bands = wider range = lower risk but lower LTV. Fewer bands = narrower range = higher risk but higher LTV.
**Liquidation Protection vs Full Liquidation**:
- **Liquidation Protection** = Your loan is being protected, you can still repay debt, position can recover
- **Full Liquidation** = Your loan is closed, collateral used to repay debt, position cannot be recovered
**Key Rule**: Monitor your health constantly. Health can decrease even when prices are rising if you're in liquidation protection due to conversion losses.
---
## Glossary
**LTV (Loan-To-Value Ratio)**: The ratio of your debt to your collateral value. Higher LTV = higher risk but more borrowing power.
**Liquidation Protection Range**: The price range where your loan enters protection mode. Defined by a higher and lower price of your collateral asset.
**Bands**: Small price ranges where collateral is grouped. The number of bands determines the width of your liquidation protection range.
**Health**: A percentage representing how close your loan is to full liquidation. Health = 0% means your loan can be fully liquidated.
**Full Liquidation**: When health reaches 0%, your loan is closed and collateral is used to repay debt.
**Mint Markets**: Markets where crvUSD is minted (created) when you borrow. Interest rates depend on crvUSD price, PSR size, and other parameters.
**Lending Markets**: Permissionless markets where you borrow from supplied liquidity. Interest rates depend solely on utilization of supplied assets.
**LLAMMA**: Lending-Liquidating AMM Algorithm - the mechanism that powers automatic collateral conversion.
---
## Liquidation Protection Range & Bands
### What is the Liquidation Protection Range?
A user's **liquidation protection range** is defined by a higher and a lower price of the collateral asset. Their loan enters liquidation protection if the market price of their collateral drops into this zone.
:::example
**Example:** ETH is currently trading at around \$3000. The liquidation protection range is between \$2000 and \$1500. If the price of ETH drops within this range, the loan enters liquidation protection.
:::
### How is the liquidation range defined?
The range's position (at which price it starts) and size (how big it is) are dependent on the following factors:
**Where the Range Sits (Position)** is set by the **Loan-To-Value Ratio** (LTV):
* A **higher LTV** moves the protection range **closer** to the current market price.
* A **lower LTV** keeps the range **further away**, giving users a larger safety margin before protection mode kicks in.
**The Width of the Range (Size)** is set by the number of **bands** users choose when opening a loan:
* **More bands** (e.g., 20-50) = **Wider** protection range = Lower risk.
* **Fewer bands** (e.g., 4-10) = **Narrower** protection range = Higher risk.
### What are Bands?
Bands are small price ranges where all user collateral is grouped together. The number of bands a loan uses can be set when creating a loan. Using bands makes Llamalend more efficient, because it can convert one piece of collateral for all users at a single time, instead of each user requiring their own separate conversion. The full liquidation range is made up by the number of bands used in the loan.
### How do I calculate my LTV?
LTV (Loan-To-Value Ratio) is calculated as:
**LTV = (Debt / Collateral Value) × 100%**
For example, if you have \$10,000 worth of ETH as collateral and borrow \$7,000 of crvUSD, your LTV is 70%.
You can see your current LTV in the [Llamalend UI](https://www.curve.finance/llamalend/ethereum/markets) when viewing your loan position.
### What's the maximum LTV I can use?
The maximum LTV depends on the collateral asset and market parameters:
- **BTC and ETH**: Up to 91% LTV
- **Yield-bearing and low-volatility tokens** (like sDAI or sUSDe): Up to 98% LTV
The exact maximum LTV for each market is determined by the number of bands and band width factor used when the market was created. More bands generally allow for higher LTV, but with a wider liquidation protection range.
### Can I change the number of bands after opening a loan?
No, the number of bands is set when you create your loan and cannot be changed afterward. If you want to use a different number of bands, you would need to close your current loan and open a new one with your preferred band configuration.
### Can I Move my Liquidation Range?
Under the right conditions, yes. First, the user has full influence on the initial liquidation range when creating the loan ([see above](#how-is-the-liquidation-range-defined)). The liquidation protection range can only be changed if a loan is currently not in liquidation protection. If a loan IS in liquidation protection, the only option is to fully repay the loan and create a new one or wait for prices to recover.
**Collateral Actions:**
- **Adding more collateral**: Pushes the liquidation ranges down (e.g., from $1000-$900 to $800-$700)
- **Removing collateral**: Pushes the liquidation range further up (e.g., from $1000-$900 to $1100-$1000)
**Debt Actions:**
- **Borrowing more**: Pushes the liquidation range further up
- **Repaying some debt**: Pushes the liquidation range down
- **Repay all debt**: Fully closes the loan (no liquidation range anymore)
**Rule of Thumb**: The higher the LTV of the loan gets, the closer the liquidation range gets to the current market price.
---
## Liquidation Protection & Health
### How Does Liquidation Protection work?
Liquidation protection is a mechanism which protects the collateral of your loan. Once a loan enters the [liquidation protection range](#what-is-the-liquidation-protection-range), the loan enters liquidation protection. In this price range, the system automatically converts your volatile collateral asset into stable crvUSD tokens when prices go down. This reduces the collateral exposure of the loan when collateral prices decline and preserves the total value of the collateral (minus some [losses](#what-are-the-losses-during-liquidation-protection)). If the price of the collateral asset goes up again, the system automatically converts back the previously obtained crvUSD back into the original collateral asset to restore the initial collateral composition.
TLDR: The system kind of derisks your collateral position on the way down and restores your exposure on the way up.
:::example
**Example:** Price of ETH is at \$3000 and liquidation protection range is between \$2000 and \$1500. ETH price now drops below \$2000 all the way to \$1750 and therefore enters liquidation protection range. The system now starts converting ETH into crvUSD. As the price moves through different bands, a portion of your collateralized ETH will be converted into crvUSD (the exact percentage depends on how far the price moves through your bands). Your loan is now backed by a mix of ETH and crvUSD which reduces the exposure to the volatile asset. Now, the price of ETH recovers and goes back to \$2000. In that case, the system automatically converts the crvUSD back into ETH. Once you exit liquidation protection once ETH goes above \$2000, your loan will be fully collateralized by ETH again.
:::
### When Does a Loan Enter Liquidation Protection?
A loan enters liquidation protection once the price of the collateral asset falls into the liquidation protection range.
### What's the Difference Between Liquidation Protection and Full Liquidation?
**Liquidation Protection** occurs when the collateral price falls into the liquidation protection range. During this phase:
- Your collateral is automatically converted between volatile assets and stable crvUSD to protect your position
- You cannot add/remove collateral or borrow more, but you can repay debt
- Your loan remains active and can recover if prices improve
- Health can still decrease due to losses from conversions and interest
**Full Liquidation** occurs when your loan's health reaches 0%. At this point:
- Your loan is completely closed
- Your collateral is used to repay the debt
- Any remaining collateral (if any) is returned to you
- You cannot recover the position - you would need to open a new loan
The key difference is that liquidation protection is a protective mechanism that gives you time and flexibility, while full liquidation is the final closure of your loan.
### What Can Users Do When Their Loan is in Liquidation Protection?
While a loan is in liquidation protection, the actions users can take are limited. The most important metric to watch is the loan's health. If a loan is NOT in liquidation protection, users have all possibilities and there are no restrictions at all.
**Restricted Actions:**
- **Collateral actions**: Users cannot add or remove any collateral
- **Borrowing**: Users cannot borrow any more funds
**Available Actions:**
- **Debt repayment**: Users can repay some or all of the debt
:::info
Only repaying all of the debt and therefore fully closing the loan will get users out of liquidation protection. Repaying some debt (even if it's 99% of all their debt) will only increase the health of the loan, but will not get the loan out of liquidation protection nor change the liquidation range.
:::
### What happens if I repay partial debt while in liquidation protection?
Repaying partial debt while in liquidation protection will:
- **Increase your health** - This is the main benefit, giving you more buffer before full liquidation
- **NOT get you out of liquidation protection** - Even repaying 99% of your debt won't exit liquidation protection mode
- **NOT change your liquidation range** - The range remains the same until you fully repay or prices recover
The only way to exit liquidation protection is to fully repay all debt and close the loan, or wait for the collateral price to rise above your liquidation protection range.
### Why Are Actions Restricted in Liquidation Protection Mode?
Users are not able to add or remove collateral because the loan is already in liquidation protection and the collateral is currently being protected through Llamalend. The nature of the system does not allow any collateral actions when the position is in liquidation range, as this is a unique situation where the collateral is being actively protected.
### How to get out of liquidation protection
Once in liquidation protection, there are only two ways to get out:
1. Fully repay your loan and create a new one
2. Wait for the collateral price to rise above your liquidation protection range
**Key Points:**
- Adding collateral is restricted
- Even repaying 99% of the debt will not get the loan out of liquidation protection
- Only full debt repayment and loan closure will exit liquidation protection mode
Reminder: collateral actions are prohibited in liquidation protection so adding more collateral to your loan is not an option.
:::info Recovery and Time Flexibility
**Important Understanding**: If prices recover while users are in liquidation protection, they can theoretically return to their original state (minus some losses due to the liquidation protection process). The system gives users more time to act because there's no instant liquidation, but there's actually no need to act at all.
**Key Point**: Users can theoretically stay in liquidation protection forever as long as they ensure their health stays above 0. The system will continue protecting their position automatically, and if market conditions improve, their position can recover without any manual intervention.
:::
:::warning Important Note
When collateral is swapped during liquidation protection, users lose health, regardless of whether prices are increasing or decreasing. So health can go to 0 even if prices are increasing. This is why monitoring health is crucial while in liquidation protection.
:::
### When Am I Liquidated (HEALTH = 0)
Unlike on other protocols, Curve does not have a specific price where a position is liquidated. Instead, a loan is liquidated once the health of the loan reaches 0%. There are different factors which decrease the health of a loan:
- Price going down
- Losses in Liquidation Protection
- Interest accumulation
:::info
To understand Loan Health and Losses in Liquidation Protection please check: [Losses & Loan Health](#losses--loan-health)
:::
To avoid a full liquidation which closes your loan, users are advised to either make sure to not enter liquidation range at all or constantly monitor health while being in liquidation protection and make sure it's not going to 0. More on how to properly use the system [here](#how-can-users-monitor-and-prevent-problems).
### How Is the System Automated?
When talking about "automatically converting between collateral asset and crvUSD", it's actually being done by arbitrageurs. Curve's liquidation mechanism works in a way that a small arbitrage opportunity is created to convert assets accordingly. So, the entire system is relying on arbitrage.
### How Quickly Does the System Convert Collateral?
The speed of collateral conversion depends on several factors:
- **Price movement speed**: Faster price movements may require quicker conversions
- **Number of bands**: More bands spread conversions over a wider range, allowing for more gradual conversions
- **Arbitrage efficiency**: How quickly arbitrageurs take advantage of the opportunities created by the system
The system creates arbitrage opportunities that incentivize conversions, but the actual speed depends on market participants taking these opportunities.
### What Happens If Arbitrageurs Don't Act?
Technically, arbitrageurs might not act if gas costs are very high and they prioritize other arbitrage opportunities first. In this case, nothing would really happen because your collateral is not being converted. Your position would remain in its current state until arbitrageurs take the opportunity or market conditions change. This is a rare scenario, as arbitrage opportunities are typically taken quickly when profitable.
### What happens if the price goes below the liquidation protection range?
If the price goes lower than the Liquidation Protection range with positive health and fully converted collateral, users are completely safe from further price declines. While underneath the range, health will only decline from debt increasing from interest on the loan.
If users get here, it's normally best to repay the loan and reopen it, because there is a very high chance of liquidation from collateral conversion losses if they go back up through the Liquidation Protection range.
---
## Losses & Loan Health
### What is Loan Health and How Does It Decrease?
**Health is the most important metric to monitor** because it determines when a loan will be fully liquidated. Users should always keep track of their loan's health, regardless of market conditions.
**Health decreases due to:**
- **Collateral price drops**: Above the Liquidation Protection range, health will decrease as the range gets closer.
- **Losses in Liquidation Protection**: Selling the collateral asset for crvUSD and vice versa results in losses which decrease the health of the loan.
- **Borrowing more funds**: Taking on additional debt reduces health.
- **Interest accumulation**: Interest is paid on the debt you've taken on. Debt is accrued by constantly increasing your total debt, which can over time decrease your health (albeit very slowly).
For tips on monitoring and preventing health issues, see [How Can Users Monitor and Prevent Problems?](#how-can-users-monitor-and-prevent-problems).
### How is health calculated?
Health is calculated using a formula that takes into account your collateral value, debt amount, and liquidation discount:
**Health = (Estimated Collateral Value × (1 - Liquidation Discount) + Price Above Bands) / Debt - 1**
Where:
- **Estimated Collateral Value**: An estimation of how much crvUSD you would have after converting all collateral through your bands in liquidation protection
- **Liquidation Discount**: A safety margin applied during liquidation calculations
- **Price Above Bands**: The value of collateral above your highest band
- **Debt**: Your current debt amount
Health is displayed as a percentage in the UI. Health > 0% means your loan is safe. Health = 0% means your loan can be fully liquidated.
You can view your current health percentage in the [Llamalend UI](https://www.curve.finance/llamalend/ethereum/markets) when viewing your loan position.
### What's a safe health percentage to maintain?
While there's no universal "safe" health percentage, here are some general guidelines:
- **Above 50%**: Generally considered safe, but still monitor regularly
- **30-50%**: Moderate risk - consider taking action to improve health
- **10-30%**: High risk - strongly recommended to repay debt or add collateral (if not in liquidation protection)
- **Below 10%**: Critical - immediate action required to avoid full liquidation
- **0%**: Full liquidation can occur
**Important**: If you're in liquidation protection, health can decrease even when prices are rising due to conversion losses. Always monitor health closely while in liquidation protection.
### Can I see my current health percentage in the UI?
Yes! You can view your loan position, health percentage, LTV, and liquidation protection range directly in the [Llamalend UI](https://www.curve.finance/llamalend/ethereum/markets). Simply connect your wallet and navigate to your active loan position.
### Band Choice Impact
- **More bands = Bigger Range = Fewer losses**: Spreading collateral across many bands creates a wide protection range. This gives the system more time to gradually convert collateral during a price drop, which typically results in **smaller losses**.
- **Fewer bands = Smaller Range = Higher losses**: Fewer bands concentrates collateral over a very small protection range. This works fine when prices move very slowly. However, a sudden price drop can force the system to sell collateral very quickly, leading to **higher losses** and increasing the risk of full liquidation.
Learn more about bands and how to customize them when creating a position [here](./guides/borrow/custom-bands.md).
:::info Do losses always occur?
No. Losses occur **only** when a position is within the liquidation protection zone. As long as a position remains outside this zone, no losses are incurred, regardless of market conditions.
:::
:::warning Important Note
These losses are not temporary - they are permanent reductions in collateral value that occur during the liquidation protection process. The key is to monitor a position's health and take action before losses accumulate too much.
:::
### What are the Losses During Liquidation Protection?
When a position enters the liquidation protection, losses occur due to the constant change of collateral composition. The system gradually converts volatile collateral into stable crvUSD and vice versa. This results in losses.
The exact amount of losses is hard to predict as it depends on external factors like market volatility and liquidity. However, the most significant factor is one users can control: **the number of bands they select for their loan.**
---
## Loan Management
### How do I close my loan?
To close your loan, you need to fully repay all of your debt. Here's how:
1. Go to the [Llamalend UI](https://www.curve.finance/llamalend/ethereum/markets)
2. Connect your wallet
3. Navigate to your loan position
4. Click "Repay" and repay the full amount of your debt
5. Once all debt is repaid, your loan will be closed and you'll receive your collateral back
**Note**: If you're in liquidation protection, you can only repay debt - you cannot add collateral. Once you fully repay, the loan closes and you exit liquidation protection.
### What happens if I don't repay my loan?
If you don't repay your loan, interest will continue to accrue on your debt, gradually increasing your total debt amount. This will slowly decrease your health over time.
If your health reaches 0%, your loan will be eligible for full liquidation. At that point, anyone can repay your debt and claim your collateral. Your loan will be closed and you'll lose your collateral (minus any amount returned if there's excess after repaying the debt).
**Important**: Always monitor your loan's health to avoid this scenario. Consider setting up the [Telegram Bot](https://news.curve.fi/llamalend-telegram-bot/) for automated monitoring.
### Can I Have Multiple Loans?
One wallet can only have one loan per market. However, you can open multiple loans across multiple markets. For example, you could have one loan on Ethereum and another on Arbitrum, or one loan for ETH collateral and another for a different collateral asset.
### Are There Any Fees?
No fees at all besides the borrow rate. There are no opening fees, no closing fees, and no minimum loan amounts. You only pay interest on the debt you borrow.
### Can I use the borrowed crvUSD for anything?
Yes! Once you borrow crvUSD, it's yours to use however you want. Common use cases include:
- **Yield farming**: Use borrowed crvUSD to earn yield in other DeFi protocols
- **Leverage trading**: Increase your exposure to assets
- **Working capital**: Access liquidity without selling your collateral
- **Arbitrage opportunities**: Take advantage of price differences across protocols
- **Any other DeFi activity**: The borrowed crvUSD is a standard ERC-20 token you can use anywhere
Remember: You'll need to repay the debt (plus interest) eventually, so make sure you have a plan for repayment.
---
## Markets & Collateral
### What collateral assets are supported?
Llamalend supports various collateral assets across different markets. Markets can be either:
- **Mint Markets**: Where you borrow crvUSD against collateral (like ETH, BTC, etc.)
- **Lending Markets**: Permissionless markets where any asset can be used as collateral or borrowable token (as long as crvUSD is one of them)
To see all available markets and supported collateral assets, visit the [Llamalend UI](https://www.curve.finance/llamalend/ethereum/markets).
### What's the difference between mint markets and lending markets?
**Mint Markets**:
- crvUSD is **minted** (created) when you borrow
- Interest rates depend on crvUSD price, Peg Stabilization Reserve (PSR) size, and other parameters
- Used for borrowing crvUSD against collateral
**Lending Markets**:
- Assets are **borrowed from supplied liquidity** (not minted)
- Interest rates depend **solely on utilization** of supplied assets
- Permissionless - anyone can create markets for any asset (with crvUSD) if there's a proper oracle
- Can be used for borrowing crvUSD or other assets, or lending assets to earn yield
Both market types use the same liquidation protection mechanism and health system.
---
## Interest & Rates
### How are interest rates determined?
Interest rates work differently depending on the market type:
**Mint Markets**:
- Interest rates depend on multiple factors:
- crvUSD price (peg stability)
- Size of the Peg Stabilization Reserve (PSR)
- Market utilization
- Other protocol parameters
**Lending Markets**:
- Interest rates depend **solely on utilization** of supplied assets
- Higher utilization = higher interest rates
- Lower utilization = lower interest rates
- This creates a dynamic rate that adjusts based on supply and demand
You can see current interest rates for each market in the [Llamalend UI](https://www.curve.finance/llamalend/ethereum/markets).
### How often is interest charged?
Interest is charged **continuously every second** by gradually increasing your total debt amount. This means your debt grows slightly every second, which slowly decreases your health over time.
The interest rate you see (e.g., 5% APY) is the annualized rate. The actual amount charged per second is calculated from this annual rate.
---
## How To Use The System
### How Can Users Monitor and Prevent Problems?
While Curve's liquidation mechanism offers greater flexibility and safety, it is still crucial to monitor the health of a loan. **Positions can be fully liquidated once health reaches 0.** To avoid this outcome, it is highly recommended to monitor loan status regularly and take action when needed.
You can view your loan position, health, and liquidation protection range directly in the [Llamalend UI](https://www.curve.finance/llamalend/ethereum/markets).
To simplify monitoring, a dedicated [Telegram Bot](https://news.curve.fi/llamalend-telegram-bot/) has been developed to continuously track and monitor loan positions.
**Bot Features:**
- **Multi-Address Monitoring**: Track multiple wallet addresses simultaneously
- **Cross-Chain Coverage**: Monitor positions across Ethereum, Arbitrum, Fraxtal, and Optimism
- **Automated Health Checks**: Perform regular position checks every 5–10 minutes
- **Prompt Alerts**: Receive notifications when a loan enters liquidation protection or becomes eligible for full liquidation
- **On-Demand Information**: Access current on-chain data for monitored positions
Check out this article for more information on how to set up the Telegram Bot: [Llamalend Telegram Bot](https://news.curve.fi/llamalend-telegram-bot/)
---
## Advanced Topics
### What happens underneath the Liquidation Protection Range?
If the price goes lower than the Liquidation Protection range with positive health and fully converted collateral, users are completely safe from further price declines. While underneath the range, health will only decline from debt increasing from interest on the loan.
If users get here, it's normally best to repay the loan and reopen it, because there is a very high chance of liquidation from collateral conversion losses if they go back up through the Liquidation Protection range.
---
## Opening a Loan with Custom Bands
To follow this guide, make sure **"Advanced Mode"** is enabled in your user settings.
Before diving in, please make sure you're familiar with the concept of bands. You can read more about them [here](/user/llamalend/liquidation-protection/how-it-works).
This guide is very similar to the [Beginner: Open & Close](./open-and-close.md) tutorial. The only difference is that you'll be able to set a **custom number of bands** — which defaults to 10 in non-advanced mode. Everything else works exactly the same.
Make sure you understand the **consequences and benefits** of using more or fewer bands. You'll find a quick summary further down in this guide, or read more [here](/user/llamalend/liquidation-protection/how-it-works).
## Customizing Bands
Enter the amount of collateral you want to use and choose how much crvUSD to borrow. Additionally, you can adjust the number of bands for the loan by moving the slider left or right.
- **Minimum bands:** 4
- **Maximum bands:** 50
## Effects of Number of Bands
The maximum LTV (Loan-To-Value ratio — the maximum amount you can borrow against your collateral) is heavily influenced by the number of bands selected when opening a loan.
In general:
- The **fewer** bands you use, the **higher** your borrowing power (LTV)
- The **more** bands you use, the **lower** your potential losses during soft-liquidation
This creates a trade-off between **maximum borrowing capacity** and **protection from liquidation losses**.
If you're actively monitoring your loan and plan to repay or close it before entering soft-liquidation, using **fewer bands** may allow you to borrow more efficiently.
If you prefer a more conservative approach, using **more bands** will help reduce potential losses if liquidation occurs.
Ultimately, it's a trade-off between **risk** and **borrowing power**, and you should choose the configuration that best fits your goals and risk tolerance.
Maximum LTV is calculated using the following formula:
```math
\text{Maximum LTV} = 100\% - \text{Loan Discount} - \frac{100\% \times N}{2 \times \text{Band Width Factor}}
```
Where:
- `Loan Discount` is different for each market based on the volatility of the underlying collateral asset, e.g., for the ETH minting market its **9%**, for sreUSD lending market its **2%**.
- `Band Width Factor` varies by market and is visible in the UI, it controls how large bands are, a larger `Band Width Factor` means narrower bands
- `N` is the **number of bands** selected for the loan
---
## Using Leverage
:::warning Differences in Leverage Support
The Curve UI generally supports **one-click leverage** for loans. This means users can directly "loop" their positions with a single click, without manually repeating the borrowing and buying process.
While one-click leverage is available for most markets, there may still be some **discrepancies** depending on the market. As a rule of thumb:
If one-click leverage is supported, a second tab labeled **"Leverage"** will appear next to the standard **"Create Loan"** tab.
:::
- **Newer markets** support leverage through **aggregators**, which can route through external liquidity sources for more efficient execution.
- **Older markets** (especially the early deployed mint markets) rely solely on Curve's internal liquidity, which can result in higher price impact — especially when leveraging large amounts.
## Opening a Leveraged Loan
When creating a leveraged loan, users can choose to deposit either the **collateral token** or **crvUSD** *(depositing crvUSD is only possible on newer markets)*.
If **crvUSD** is selected, it will be automatically swapped into the collateral asset before leveraging up the position.
After that, simply select the amount of **crvUSD** to borrow.
Of course, you can also adjust the number of **bands (N)** used for the position.
After setting these parameters, the UI will display an overview containing all relevant details about your leveraged position:
The overview includes:
- **Total leverage** used in the position
- **Expected** collateral amount after looping, including aggregator route, **average swap price**, and **price impact**
- **Loan details** – Standard information such as **Health**, and the following extra information if `Advanced Mode` is enabled: **Band range**, **Number of bands (N)**, **Post-loan borrow rate** and **Loan-to-Value ratio (LTV)**
After opening a loan, you will see the details of your loan in the loan details tab:
## Leveraging Up
To increase your leverage you can simply go back to the `LEVERAGE` tab and borrow more using the `BORROW AMOUNT` box.
## Leveraging Down
To reduce your leverage you need to repay debt from your wallet in the `LOAN` tab under `REPAY`.
Otherwise, you must fully [close your loan](leveraged-loans.md#closing-a-leveraged-loan), and [open a new loan](./leveraged-loans.md#opening-a-leveraged-loan).
## Closing a Leveraged Loan
To repay (or **deleverage**) your loan, you can either use your **collateral balance**, or repay directly using **crvUSD** or the **collateral token** from your wallet *(repaying with either asset is only supported on newer markets)*.
Once the repayment parameters are selected, the UI will update to show the latest stats and expected state of your loan after repayment.
:::info Closing a Leveraged Loan
When closing a leveraged loan by repaying with collateral, you will receive your collateral back in the borrowed token (crvUSD above), instead of your original collateral (sDOLA above).
This is because the system swaps all collateral (sDOLA above) to repay the debt (crvUSD), and sends you back all remaining assets.
To receive back your original collateral (sDOLA above) you must repay your debt from your wallet.
:::
---
## What to Do During Liquidation
If your loan enters liquidation protection, you have more time and flexibility to act. You have two main options:
- **Close your loan**: Stop further losses by closing the position
- **Keep your loan open**: Monitor your health closely to avoid reaching 0%
## Close Your Loan
You can close your loan in two ways:
- **Repay your full debt**: [See how to do that here.](./open-and-close.md#fully-repaying)
- **Self-liquidate**: Only available when in liquidation range. Closes your position without liquidation penalty.
## How to Get Out of Liquidation Protection
**Important**: You cannot exit liquidation protection by repaying debt, because the liquidation range does not move when actions happen in liquidation (see the [loan management table](./loan-management.md#loan-management-overview)).
You can only get out of liquidation protection by:
1. **Repay the full loan and create a new one**
2. **Wait for price recovery**: If the collateral price moves above or below the liquidation range, you'll exit liquidation protection
:::warning Monitor Your Health
Watch your health closely - if it reaches 0%, your loan will be fully liquidated.
:::
## Keep Your Loan Open
If you choose to keep your position open, monitor your loan health closely to avoid reaching 0%.
Use the [Llamalend Telegram bot](https://news.curve.fi/llamalend-telegram-bot/) to track your loan health and receive alerts.
---
## Managing a Loan
Prerequisite for this page is that you already have an existing loan: [Open and Close Loan](open-and-close.md)
## Loan Management Overview
The actions available to you depend on whether your loan is in **liquidation protection** or not. Understanding this distinction is crucial for effective loan management.
| Action | Available When | Effect on Liquidation Range |
|--------|----------------|----------------------------|
| **Add collateral** | Only when NOT in liquidation | ⬇️ Moves range DOWN (safer) |
| **Remove collateral** | Only when NOT in liquidation | ⬆️ Moves range UP (riskier) |
| **Borrow more** | Only when NOT in liquidation | ⬆️ Moves range UP (riskier) |
| **Repay debt (NOT in liquidation)** | When NOT in liquidation | ⬇️ Moves range DOWN (safer) |
| **Repay debt (IN liquidation)** | When IN liquidation | ➖ NO effect on range (only improves health) |
| **Self-liquidate** | Only when IN liquidation | ➖ No effect (closes loan) |
:::info Key Understanding
- **Liquidation range changes** only happen when your loan is NOT in liquidation protection
- **Repaying debt** behaves differently: it moves the liquidation range when NOT in liquidation, but has NO effect on the range when IN liquidation
- **Collateral and borrowing actions** are only possible when NOT in liquidation, so they always affect the liquidation range
:::
## Adding More Collateral
:::warning Adding Collateral while being in Liquidation is not possible
Adding collateral is not possible when your loan is in liquidation. To get out of liquidation, see [here].
:::
To add more collateral to your loan, navigate to the `Collateral` and `Add collateral` tab and select the amount of collateral tokens to add:
**Effect of Adding More Collateral**
Adding more collateral to your loan has a **positive effect on its health and liquidation range**. It increases your loan's health and moves the liquidation range further down, keeping your position further away from liquidation.
:::info Important
This action is only possible when NOT in liquidation, so it will always change your liquidation range.
:::
## Removing Collateral
:::warning Removing Collateral while being in Liquidation is not possible
Removing collateral from your loan is not possible when you are in liquidation. To get out of liquidation, see [here].
:::
To remove collateral from your loan, navigate to the `Collateral` and `Remove collateral` tab and select the amount of collateral tokens to remove. The UI will show you how much collateral you can remove to still support the current debt.
**Effect of Removing Collateral**
Removing collateral from your loan has a **negative effect on its health and liquidation range**. It decreases your loan's health and moves the liquidation range further up, bringing you closer to liquidation.
:::info Important
This action is only possible when NOT in liquidation, so it will always change your liquidation range.
:::
---
## Borrow More
:::warning Borrowing more in Liquidation
Borrowing more crvUSD in liquidation is not possible. To get out of liquidation, see [here].
:::
To borrow more assets, specify the amountyou want to borrow. You also have the option to add more collateral at the same time — allowing you to increase both your debt and your collateral in a single transaction.
**Effect of Borrowing More**
Borrowing more crvUSD is only possible if the loan is not in liquidation. Borrowing more crvUSD will decrease the loan's health and move up the liquidation range, bringing you closer to liquidation.
:::info Important
This action is only possible when NOT in liquidation, so it will always change your liquidation range.
:::
## Repaying Debt
Debt can be repaid fully or partially. Repayment is always possible, whether the loan is in liquidation or not, but it will have different effects on the liquidation range.
### Partial Repayment while NOT in Liquidation
Repaying debt when the loan is not in liquidation improves the health of the loan and **moves the liquidation range down**, bringing the loan further away from liquidation.
:::info Important
When NOT in liquidation, repaying debt will change your liquidation range.
:::
### Partial Repayment while IN Liquidation
Repaying debt while being in liquidation will increase your health, but **NOT move the liquidation range**. No matter how much you repay, it will never move your liquidation range up or down but will only increase your health.
:::warning Liquidation Range will NOT move!
Your liquidation range will not move if you repay debt while you're in liquidation — not even if you repay 99% of your debt. This is a key difference from repaying when NOT in liquidation.
:::
### Full Repayment
see here: [Close Loan](./open-and-close.md#fully-repaying).
---
## Open and Close Loan
## Open a new Loan
### Select a Market
The UI provides various filtering methods to find the most suitable market.
### Specify Loan Parameters
To create a loan, enter the amount of collateral you want to use and choose how much crvUSD to borrow. If its your first time creating a loan within this market, you will need to sign two transactions.
1. approval
2. create loan
The UI will provide an overview of the price range ([liquidation protection range](/user/llamalend/liquidation-protection/how-it-works)), health of the loan, borrow rate, etc.
Additionally, there is a chart which displays the price of the collateral token (blue line) and your liquidation range (orange area).
### View Your Position
After a loan has been created, all your position details show in a single card. The "Get alerts" button on the top redirects you to a telegram bot which lets you monitor your position.
---
## Closing Your Loan
To fully close your loan, you need to repay all of your debt. There are two options:
1. Fully repay your loan
2. Self-liquidate your loan if its in liquidation protection mode
### Fully Repaying
Navigate to the "Repay" tab and select how to repay your debt. You have the following options:
- **Repay from Collateral**: Repay your debt using your current collateral. The system will sell as much collateral as needed to repay the debt and return the remaining balance to your wallet.
- **Repay from Wallet**: Repay your debt using assets directly from your wallet. You can use either the collateral token or the debt token.
Alternatively, you can check the "Repay in full and close loan" box for a streamlined process.
### Self Liquidate
Another option to close your loan is to self-liquidate when your loan is in liquidation protection mode. This is essentially the same as repaying the full debt with crvUSD from your wallet.
---
## Claiming Rewards
Rewards can only be claimed if the lending market offers additional rewards like CRV emissions or other external rewards. The base rate for lending assets is automatically accrued and reflected in the increasing value of your vault shares - over time, you'll be able to withdraw more assets than you originally deposited.
To claim rewards:
1. Navigate to the market you want to claim from
2. Go to the **Claim Rewards** tab under **Withdraw**
3. Confirm the transaction
:::tip Important
If you plan to claim rewards before withdrawing all your assets from the market, make sure to unstake the assets first to avoid leaving any dust rewards behind.
:::
---
## Depositing Assets
## Select a Market
The UI provides a granular filter system to help you find the right lending market for your assets. Lending markets labled with the "Mint" can't be supplied to.
When hovering over the supply rate, a tooltip will appear explaining the composition of the total rate (base yield, CRV emissions, etc.).
## Depositing Assets
Input the amount of assets you wish to deposit. If this is your first time interacting with this market, you will need to sign two transactions:
1. **Approve tokens** - Allow the contract to access your tokens
2. **Deposit tokens** - Transfer your tokens into the lending market
The UI displays important information including:
- **Expected vault shares** - Represent your ownership of lent assets
- **Lending APR** - The rate you will earn on your assets
- **Estimated transaction fee** - Gas cost for the transaction
## Staking Vault Shares
If the lending market offers additional rewards, you can earn them by staking your vault shares. Unstaked shares only earn the base supply rate.
To stake your assets:
1. Switch to the **Stake** tab
2. Set the amount of vault shares to stake
3. Confirm the transaction
After staking, your position will start earning extra rewards like CRV emissions. Learn how to claim rewards in the [Claiming Rewards](/user/llamalend/guides/supply/claim-rewards) guide.
## Position Overview
Your lending position displays key information including:
- **Current supply rate** - The APR you're currently earning
- **Amount supplied** - Total assets deposited in the market
- **Total vault shares** - Your ownership stake in the lending pool
---
## Withdrawing Assets
## Unstaking Assets
If you have staked your vault shares to earn additional rewards, you need to unstake them before you can withdraw your underlying assets. Staked shares are deposited into an additional contract and cannot be directly withdrawn - they must first be converted back to unstaked vault shares.
To unstake your assets:
1. Navigate to the **Unstake** tab under **Withdraw**
2. Select the amount of assets to unstake
3. Confirm the transaction
## Withdrawing Assets
You can withdraw your assets from the lending market using the **Withdraw** tab. Withdrawn assets will be sent directly to your wallet.
To withdraw your assets:
1. Navigate to the **Withdraw** tab
2. Select the amount of assets to withdraw
3. Confirm the transaction
---
## Liquidation Protection & Loan Health
## Making Volatile Markets Manageable
Volatile markets are one of the biggest risks when taking out onchain loans. Prices can collapse in seconds, and on many lending platforms, a single sharp move can liquidate a user before they even have time to respond.
Llamalend's **liquidation protection** solves this problem.
Instead of liquidating a position the moment it reaches a specific price, Llamalend gives borrowers **time, flexibility, and breathing room**. Even during the extremest market swings, the system often manages to keep a loan alive automatically.
This turns the liquidation experience on its head. A sudden, irreversible liquidation becomes a **gradual and predictable process**. With Llamalend, borrowers can:
- navigate extreme conditions with much higher resilience
- increase their odds of surviving flash crashes and sudden volatility
- adjust or repay their loan before it is too late
- remain safe even when the market briefly dips below their "liquidation price"
- potentially avoiding surprise liquidation spikes
---
## Quick Reference
| Concept | What It Means |
|---------|---------------|
| **Liquidation Protection** | Your loan enters liquidation protection mode when collateral price drops into the liquidation protection range. The system gradually converts your collateral to protect you, and automatically converts back when prices recover. |
| **Health** | A value showing how close you are to full liquidation. Think of it like a fuel gauge: when it hits 0, your loan is closed. |
| **Liquidation Protection Range** | A price zone (e.g., ETH \$3,000-$2,500). When price enters this zone, liquidation protection activates. |
| **Full Liquidation** | When health reaches 0, your loan is fully closed. |
| **Bands** | Small price ranges that make up your protection range. More bands = wider range = lower risk. |
| **Protection ≠ Liquidation** | Being in liquidation protection doesn't mean you lost all your collateral (although losses occur) — it means the system is actively defending it. |
| **Losses in Liquidation Protection** | Losses from conversions reduce your total collateral value permanently. If you enter with 10 ETH and exit protection later, you'll have less than 10 ETH (10 ETH minus losses). This reduction doesn't recover even if prices fully recover. |
:::important
**The Golden Rule**: Monitor your health constantly. As long as it's above 0, you're protected from full liquidation.
For answers to common questions, see the [FAQ](../faq.md).
:::
## The Simple Idea
:::info
**Analogy for Liquidation Protection:**
Other protocols are like a trapdoor: if your collateral price hits a certain point, the floor drops out and you instantly lose everything with no warning. Liquidation protection on Curve acts like a safety net that gradually activates as you fall, catching you over a range of prices and giving you time to react and recover.
Think of the safety net as having two parts: the **liquidation protection range** is the net itself (the price zone where liquidation protection activates), and **health** is how strong and intact the net remains. As long as your health stays above 0, the net is there to catch you. When health reaches 0, the net is gone and you're fully liquidated.
:::
Liquidation protection in Llamalend works differently from systems that use a fixed liquidation price. There is **no single price** at which your loan suddenly disappears. A position is only liquidated when its **health reaches 0**. Important notice: in Llamalend, health is not a direct correlation of LTV.
To prevent health from collapsing to zero during volatility, every loan is given a **liquidation range**, defined by two price points. When the market price enters this range, the system begins **gradually adjusting your collateral** to keep the loan stable.
The liquidation range is defined by:
- the **Liquidation Threshold**, which is the price below which liquidation begins — essentially the start of the liquidation range.
- the **Bottom of the Liquidation Range**, where collateral has been fully converted. Important: this is **not** the price at which a position becomes fully liquidated.
So, what happens when the price of the collateral falls within the liquidation range?
- **When prices fall**, the system automatically and **gradually sells off parts of your collateral asset (for example ETH) for crvUSD**. This reduces exposure to the falling asset and helps preserve the value supporting loans.
- **When prices rise** again, the system uses the **previously obtained crvUSD to buy back the initial collateral**, restoring part of the original asset balance.
These adjustments happen continuously and automatically (no need for user interaction) while the price moves up and down inside the liquidation range. Instead of a sudden liquidation at a single price, the loan is stabilized through **small, ongoing conversions** that give users more time to act.
However, these **conversions come with a cost**. Because the system needs to incentivize arbitrage traders to perform them, each conversion incurs a slight **loss**. When the market moves up and down inside the range, these **losses accumulate and gradually reduce your health**. **These losses happen in both directions, on the way down and on the way back up**. The more volatility and the less liquidity in the liquidation zone, the faster health gets eroded. More about these losses here: [Understanding Losses](#understanding-losses).
But as long as **health stays above 0**, the loan survives. Only when health reaches 0 — regardless of the current price — is the position fully liquidated.
The good news is that users have full control over the health of the loan. For details on what actions you can take, see [I'm in Liquidation Protection. What Now?](#im-in-liquidation-protection-what-now).
---
## Understanding Health: Your Safety Measurement
Health is like a fuel gauge for your loan. It shows how much buffer you have before full liquidation.
- **High health (e.g., 10 or higher)**: You have a large safety buffer
- **Medium health (e.g., 5)**: Moderate risk. Consider taking action
- **Low health (e.g., sub 2)**: Critical: immediate action needed
- **0 health**: Full liquidation occurs
Health decreases from four main factors:
1. **Price drops**: When your collateral becomes less valuable
2. [**Losses in liquidation protection**](#understanding-losses): When you're in liquidation protection and collateral swaps occur
3. **Interest**: Charged continuously every second (very slowly)
4. **Borrowing more or removing collateral**: Taking on additional debt or removing collateral obviously decreases your health as well
:::important
Even if price is rising, health can still fall while you're inside the protection range because losses from conversions continue until you exit the range completely. A rising price does not guarantee improving health unless price moves above the full protection range.
:::
Health can be monitored:
- **In the UI**: View your health in the [Llamalend UI](https://www.curve.finance/llamalend/ethereum/markets)
- **Telegram Bot**: Get automated alerts via the [Llamalend Telegram Bot](https://news.curve.fi/llamalend-telegram-bot/)
---
## How It Works: Two Stages
### Stage 1: Liquidation Protection
When your collateral price drops into the liquidation protection range (see [The Simple Idea](#the-simple-idea) for how the range is defined), your position enters **liquidation protection**.
**What happens:**
- The system gradually converts your volatile collateral (ETH) into stable crvUSD as prices drop, and converts crvUSD back into ETH as prices recover
- This protects you from further price drops
- Losses occur while in protection (see [Understanding Losses](#understanding-losses))
- **Restricted**: You cannot add collateral or borrow more while in liquidation protection
The range is determined by your Loan-To-Value Ratio (LTV) and the number of bands you selected when opening your loan. For example, if ETH is trading at \$3,000, your liquidation protection range might be between \$3,200 (liquidation threshold) and \$2,900 (bottom of the range).
The illustration below is a real example where the price of the collateral (ETH) dropped into the liquidation range between \$3,200 and \$2,900 where the collateral protection was active. As can be seen, the loan was not fully liquidated because the health always stayed above zero. Once the health of the position approached closer to 0, the user repaid some debt to increase it again to avoid full liquidation.
:::example
For a more detailed illustration which shows how the collateral of the loan is actually converted, see here: [How the System Works (Technical Details)](#how-the-system-works-technical-details).
:::
This loan continuously entered and exited liquidation protection and stayed in it for quite some time (around 4 hours). The user constantly monitored its health and repaid some debt as soon as health got closer to 0.
### Stage 2: Full Liquidation
Full liquidation only happens when your **health reaches 0**, not at a fixed price. At this point, your loan is completely closed and your collateral is lost. As long as health stays above 0, you're protected from full liquidation.
Full liquidation can still happen during a price recovery if health is already critically low when inside the range.
The illustration below shows how a full liquidation works. The position entered liquidation protection where losses started occurring. Because the health of the loan reached 0 eventually, the position was fully liquidated. The user could have avoided it by repaying some debt to increase the health.
This liquidation occurred during an extremely volatile market event. BTC price dropped around 15% in an hour. Because Llamalend uses smooth oracles, prices did not drop as sharply compared to the general market. Even though the position ended up being fully liquidated, liquidation protection gave the user around 40 minutes (200 blocks) to repay some debt to increase health again.
---
## I'm in Liquidation Protection. What Now?
Being in liquidation protection doesn't mean you're liquidated. The system is actively protecting your position. Monitor your health constantly. As long as it stays above 0, you're protected from full liquidation.
### What You Can Do
**Available Actions:**
- **Repay partial debt**: Increases health but doesn't exit protection
- **Repay full debt**: Closes loan and exits protection
**Repaying 99% of your debt does NOT exit liquidation protection. Only full repayment does.**
**Restricted Actions:**
- **Add/remove collateral**: Not possible in protection
- **Borrow more**: Not possible in protection
### How to Exit
You have two options:
1. **Wait for price recovery**: Price must rise above your protection range
2. **Fully repay debt**: Close the loan and open a new one
Repaying debt improves health but doesn't change the protection range boundaries. The range only adjusts when you're not in protection.
For more details, see the [FAQ](/user/llamalend/faq#how-to-get-out-of-liquidation-protection).
---
## What Happens in Different Scenarios?
### Scenario 1: Price Above Protection Range
ETH at \$3,200, protection range is \$3,000-\$2,500
- **Safe**: No losses in liquidation protection
- **Warning**: Health still decreases if ETH price drops or from interest
- **Full control**: You can add/remove collateral, borrow more
### Scenario 2: Price Inside Protection Range
ETH at \$2,750, protection range is \$3,000-\$2,500
- **Warning**: You're in liquidation protection
- **Warning**: System is converting ETH to crvUSD to protect you (as price drops)
- **Warning**: Losses occur while in protection (see [Understanding Losses](#understanding-losses))
- **Warning**: Health decreases from both price drops AND losses in liquidation protection
- **Recovery**: If price recovers, system automatically converts crvUSD back to ETH, helping restore your position
- **Restricted**: You cannot add/remove collateral or borrow more
- **Available**: You can still repay debt to improve health
**Why losses still occur during recovery:** Until price climbs above the protection range, up-and-down price movements cause repeated conversions, which accumulate losses.
### Scenario 3: Price Below Protection Range
ETH at \$2,400, protection range is \$3,000-\$2,500. This is a special case where your loan was fully protected while moving through the entire liquidation range. At this point, all of your collateral would have been converted to crvUSD.
- **Protected**: No more losses in liquidation protection (if fully converted)
- **Protected**: Protected from further ETH price declines (because your entire collateral is now crvUSD)
Being below the protection range does NOT mean you're "safe". It means your entire position is now in crvUSD.
- **Warning**: Health only decreases from interest
- **Warning**: If price recovers back into range, you re-enter protection
- **Warning**: If price stays far below the range for a long time, interest alone can eventually push health to 0
### Scenario 4: Health Reaches 0
- **Liquidated**: Your loan is fully liquidated
- **Closed**: Loan is closed, collateral used to repay debt
- **Final**: Cannot recover the position
---
## Understanding Losses
While in liquidation protection, you'll incur losses from conversions. This is the cost of protection, but it's much better than instant liquidation.
**Important: Losses reduce your total collateral value permanently.** If your loan enters liquidation protection with 10 ETH as collateral, stays in protection and takes losses, then exits protection, your total collateral will be less than 10 ETH (10 ETH minus the losses incurred during protection). This reduction in collateral value is permanent and does not recover even if prices fully recover.
**Losses depend on:**
- **Market volatility**: More volatility = more conversions = more losses
- **Time in range**: Longer time = more accumulated losses
- **Number of bands**: More bands typically mean fewer losses
- **Sideways volatility**: Repeated up-and-down price movement inside the range causes multiple conversions and increases losses
:::info
Losses in liquidation protection only occur when you're inside the protection range. Outside the range, no losses in liquidation protection occur, though health still decreases from price drops and interest. However, once losses have occurred, your total collateral value remains reduced even after exiting protection.
:::
For more on losses, see the [FAQ](/user/llamalend/faq#what-are-the-losses-during-liquidation-protection).
---
## How the System Works (Technical Details)
### The Conversion Mechanism
The conversion process is powered by **LLAMMA** (Lending-Liquidating AMM Algorithm). Here's how it works technically:
- Collateral is deposited into **price bands** (small price ranges)
- As price moves through bands, collateral in those bands gets converted
- Each conversion involves a small discount to incentivize arbitrageurs, which creates the losses mentioned earlier
See how the collateral composition of bands changes based on the collateral price:
### Why Losses Occur
Losses occur because the system offers collateral at a small discount to incentivize arbitrageurs to perform the conversions. This discount ensures swaps happen, but means you receive slightly less value than market price. For more details on losses, see [Understanding Losses](#understanding-losses).
---
## Common Misconceptions
- **"Being in liquidation protection means I'm liquidated."** → False. Being in liquidation protection means the system is defending your position, not that you've lost everything. However, your total collateral value will shrink due to losses (see [Understanding Losses](#understanding-losses)).
- **"If price goes up, health always goes up."** → False. Health can decrease even when prices are rising if you're still inside the protection range, because losses from conversions continue.
- **"Repaying part of my debt lets me exit protection."** → False. Only full repayment exits liquidation protection. Repaying 99% of your debt only increases health but doesn't exit protection.
- **"Below the protection range means I'm safe."** → False. While below the range, health only decreases from interest, but if price stays far below for a long time, interest alone can eventually push health to 0.
- **"Losses stop when price goes up."** → False. Losses continue until you exit the protection range completely. Up-and-down price movements inside the range cause repeated conversions and accumulate losses.
---
For answers to common questions, see the [FAQ](/user/llamalend/faq).
---
## How to properly use Liquidation Protection
Making correct use of liquidation protection can make your borrowing experience a lot more relaxing, provided you follow a certain strategy.
This section showcases two different strategies: a conservative and aggressive approach, on how to use liquidation protection:
## Conservative Approach: Treat Entry as Your Liquidation Price
This approach is **recommended for most users**: Treat the entry point into liquidation protection as your effective liquidation price. This gives you a comfortable safety buffer and you will almost never face unexpected full liquidation.
**What to do:**
- **Before entering protection**: Repay some debt or add more collateral to push the range further away
- **If already in protection**: Fully repay and open a new position.
---
## Aggressive Approach: Stay in Protection
You can choose to stay in liquidation protection, but this approach includes **higher risk and requires active monitoring**.
Once a position enters liquidation protection, the protection mechanism incurs losses which continuously reduce your loan's health, regardless of whether the price moves up or down within the liquidation protection range. These losses occur during every conversion, so even if the price goes up and your position appears to be recovering, the accumulated losses from the protection mechanism can still outweigh the health gained from price appreciation. This means positions can very well be fully liquidated even if the collateral price is rising within the liquidation protection range, if the incurred losses are bigger than the health gained through the price appreciation. If health reaches 0%, the loan is fully liquidated. While the system will automatically help restore your position if prices recover above the range (crvUSD converts back to collateral), your total collateral value will still be reduced by accumulated losses.
While in liquidation protection, you face strict restrictions: you cannot add or remove collateral, and you cannot move the liquidation protection range once you're inside it. Even repaying 99% of your debt will not get the position out of liquidation protection or move the range. Your only available actions are to repay debt to improve health (the only action possible while in protection) or wait for price recovery above the liquidation protection range to automatically exit.
This strategy requires constant vigilance. You must continuously monitor your loan's health, and if it approaches zero, you must repay some debt to improve it. Remember: repaying debt improves health but does not move the liquidation protection range or exit protection—only full repayment closes the position.
This loan continuously entered and exited liquidation protection and stayed in it for quite some time (around 4 hours). The user constantly monitored its health and repaid some debt as soon as health got closer to 0%.
---
## Llamalend
Llamalend is Curve's non-custodial lending infrastructure allowing users to borrow crvUSD against their crypto assets or lend crvUSD to markets. The infrastructure is fully built on crvUSD and therefore each market has to contain crvUSD as a token (either as the borrow token or the collateral token).
There are two distinct ways to use Llamalend:
Llamalend combines several key features:
- **Liquidation Protection:** By borrowing on Curve, users benefit from built-in liquidation protection, giving users more peace of mind when borrowing and potentially more time to repay loans when markets crash.
- **Highest LTV across DeFi:** Allows borrowing up to 91% LTV against BTC and ETH and up to 98% against yield-bearing and low-volatility tokens like sDAI or sUSDe. Perfect for looping.
- **Isolated Markets:** All markets are isolated, one-way markets where each market only has one collateral token and one borrowable token. This design eliminates risk of contamination through depegged assets, keeps risk contained within each market, and makes it easier to understand your exposure. Unlike some other protocols where risks can spread across multiple assets, Llamalend's isolation ensures that problems in one market don't affect others. Collateral is not re-hypothecated.
- **Permissionless Markets**: No gatekeeping. Everyone can deploy lending markets.
## Interesting Reads
This is a collection of articles, news, and explainers about Llamalend covering the latest updates, features, and insights.
## Different Market Types
From a user interface and usage perspective, both products function identically. The primary difference is in how borrow rates are calculated between mint markets and lending markets. More here: [Borrowing: Borrow Rates](./borrowing.md#borrow-rates).
---
## Supplying
## Why Supply to Llamalend?
Supplying assets to Llamalend allows users to earn yield by lending their crypto assets to borrowers. Users supply assets to lending market vaults, where they become available for other users to borrow. Users supplying to Llamalend benefit from:
- **Passive & Uncomplicated Yield**: Earn interest from borrowers who pay rates based on market utilization. The interest borrowers pay is distributed to suppliers based on their share of the total supplied assets. The supply rate accumulates automatically, so no need to spend transaction fees on claiming it. Users may also earn staking incentives such as CRV rewards or external token rewards in addition to the supply rate, though these additional rewards need to be claimed.
- **Isolated Markets**: All markets are isolated and one-way, meaning each market is self-contained with just one type of collateral and one borrowable token. Because markets are separate, problems in one market won't spread to others, reducing risk
- **No Lockups**: Users can withdraw their supplied assets at any time, as long as there are enough unborrowed assets available in the vault.
- **Permissionless Markets**: The isolated market design makes it safe to allow anyone to create lending markets, providing more opportunities for suppliers
## Supply Yields
When users supply assets to Llamalend lending markets, they earn yield from multiple potential sources: **supply rate** and **staking incentives**.
### Supply Rate
The supply rate is always earned when supplying assets to lending markets. This rate is **dynamic** and directly tied to the borrow rates in each market. When borrowers pay interest on their loans, that interest is distributed to suppliers based on their share of the total supplied assets. The supply rate is calculated and accrued every second, automatically growing the user's supplied balance over time.
Supply rates in lending markets depend on market utilization — the percentage of supplied assets that are being borrowed. Each market has a minimum and maximum rate range. The higher the utilization, the higher both the borrow rate and the supply rate.
The supply rate is calculated as:
$$
\text{supply rate} = \frac{\text{total borrowed}}{\text{total supplied}} \times \text{borrow rate}
$$
When utilization is low, supply rates are lower and there's plenty of liquidity available for borrowers. As utilization increases, both borrow rates and supply rates increase, providing higher returns for suppliers. At full utilization (100%), the supply rate reaches its maximum.
### Staking Incentives
In addition to the supply rate, users may also earn **staking incentives** when supplying to lending markets. Staking incentives can include CRV rewards distributed via gauge weights every week. If a lending market has gauge weight allocated to it, CRV tokens are emitted to that market, and users who supply assets there earn a share of these rewards based on their supply share.
Some lending markets may also offer additional staking incentives such as protocol points or external token rewards, depending on the specific market and its partnerships.
---
## Glossary
A comprehensive glossary of terms used throughout Curve Finance documentation.
## A
**Amplification Coefficient (A)** - A parameter that controls how tightly liquidity is concentrated around the peg in Stableswap pools. Higher values create a flatter curve with lower slippage near the peg.
**AMM (Automated Market Maker)** - A protocol that uses mathematical formulas to price assets and enable trading without traditional order books.
## B
**Basepool** - A core liquidity pool that other pools (metapools) can be paired with to improve capital efficiency.
**Bonding Curve** - The mathematical function that determines exchange rates between assets in a liquidity pool.
## C
**CRV** - The governance token of Curve Finance, used for voting and earning protocol fees.
**crvUSD** - Curve's native decentralized stablecoin that maintains its peg through dynamic interest rates and LLAMMA liquidation mechanisms.
**Cryptoswap** - Curve's AMM algorithm designed for volatile or uncorrelated assets that automatically rebalances liquidity around market prices.
## F
**FXSwap** - Curve's AMM designed for low-volatility uncorrelated pairs like forex, combining Stableswap efficiency with Cryptoswap's rebalancing, along with the refuelling mechanism.
## G
**Gauge** - A smart contract that manages CRV token emissions (or other token rewards) for liquidity providers in specific pools.
**Gauge Weight** - The proportion of CRV emissions allocated to a specific gauge, determined by veCRV holder votes.
## I
**Impermanent Loss** - The temporary loss in value that liquidity providers experience when asset prices diverge from their deposit ratio.
**Invariant** - The mathematical equation that must remain constant in an AMM, determining how trades affect pool balances and prices.
## L
**Liquidity Provider (LP)** - A user who deposits assets into a pool to enable trading and earn fees in return.
**LP Token** - An ERC-20 token representing a user's share of a liquidity pool, which can be redeemed for the underlying assets plus accumulated fees.
**LLAMMA (Lending-Liquidating AMM Algorithm)** - Curve's lending market AMM that gradually converts collateral to stablecoins as prices drop, with the goal of reducing losses for borrowers and giving them more time to act before their loans are closed.
**Llamalend** - Curve's permissionless lending platform that uses LLAMMA for risk management.
**Liquidation Protection** - LLAMMA's gradual collateral conversion process that avoids complete liquidation during price volatility. Previously called "soft liquidation."
## M
**Metapool** - A pool that pairs assets with an existing basepool, enabling efficient trading with multiple assets while reducing fragmentation.
## O
**Oracle** - A price feed mechanism. Curve pools have built-in oracles providing spot and EMA prices for other protocols to use.
## P
**Peg** - The target price relationship between two assets (e.g., 1:1 for stablecoins).
## S
**scrvUSD** - Savings crvUSD, an interest-bearing version of crvUSD that automatically accrues yield.
**Slippage** - The difference between expected and actual trade prices, typically larger for bigger trades relative to pool size.
**Soft Liquidation** - Old term for what is now called Liquidation Protection. See **Liquidation Protection**.
**Stableswap** - Curve's original AMM algorithm optimized for similarly-priced or pegged assets, providing extremely low slippage.
## V
**veCRV (Vote-Escrowed CRV)** - CRV tokens locked for voting that grant governance power, fee earnings, and gauge weight voting rights. Lock duration determines voting power.
**Volatility** - The degree of price fluctuation in an asset or asset pair.
---
## Useful Links
## Official
## Socials
## Analytics & Dashboards
## Tools
## Vote Incentives
## Ecosystem Protocols
---
## Whitepapers(Reference)
Curve Finance whitepapers covering core protocol, governance, and stablecoin mechanisms. Hover over the information icon to view detailed descriptions of each whitepaper.
---
## Security Audits
---
## Bug Bounty Program
# Bug Bounty
:::info
For security-related inquiries and vulnerability reports, please contact us at **security@curve.finance**.
We take security seriously and appreciate responsible disclosure.
:::
## Bug Bounty Payout
| Likelihood ↓ / Severity → | Low | Moderate | High |
| :-: | :-: | :-: | :-: |
| Almost Certain | $10,000 | $50,000 | $250,000 |
| Possible | $1,000 | $10,000 | $50,000 |
| Unlikely | $250 | $1,000 | $5,000 |
## Scope
Issues which can lead to **substantial loss of money**, critical bugs like a **broken liveness condition** or **irreversible loss of funds**.
## Disclosure Policy
- Let us know as soon as possible upon discovery of a potential security issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a third party.
## Exclusions
- Already known vulnerabilities.
- Vulnerabilities in front-end code not leading to smart contract vulnerabilities.
## Eligibility
- You must be the first reporter of the vulnerability.
- You must be able to verify a signature from the same address.
- Provide enough information about the vulnerability.
---
## Security Practices
Curve Finance prioritizes the security of its protocols and user funds above all else.
:::warning Always Do Your Own Research
While Curve implements extensive security measures, users should always conduct their own due diligence and understand the risks involved in DeFi protocols.
:::
## Comprehensive Security Audits
Curve conducts [extensive security audits](audits.md) across all protocol components:
- **Immutable contracts**: Core pool contracts are intentionally immutable
- **Multiple audit firms**: Trail of Bits, MixBytes, QuantStamp, ChainSecurity, and others
- **Public audit reports**: All audit reports are publicly available. See: [Security Audits](audits.md)
## Bug Bounty Program
Curve maintains a [bug bounty program](bug-bounty.md):
- **Up to $250,000** for critical vulnerabilities
- **Clear scope**: Focus on smart contract vulnerabilities that could lead to substantial loss of funds
- **Responsible disclosure**: Encourages security researchers to report issues responsibly
- **Transparent process**: Clear eligibility requirements and payout structure
:::info Bug Bounty Contact
Report security vulnerabilities to: **security@curve.finance**
:::
## Vyper
Curve's smart contracts have been written in **Vyper since day 1**. With very few exceptions, all contracts are written in Vyper. Curve developers actively contribute to Vyper to make it a better and more secure language and also fund Vyper development via a fundraising gauge, where users can vote for the gauge and the CRV emissions are "donated" to Vyper development.
Vyper's cleaner, more readable syntax tends to reduce the likelihood of bugs compared to Solidity. The language design prioritizes security and makes common vulnerabilities harder to introduce. Curve's ongoing contributions and funding help improve Vyper's security and functionality. For detailed information about Vyper's security improvements, see the [State of Vyper Security - September 2024](https://blog.vyperlang.org/posts/vyper-security/) blog post.
## Decentralized Governance & Multisig Removal
Curve is committed to true decentralization:
- **DAO-first approach**: The Curve DAO is governed solely by veCRV holders
- **No Multisig-Finance**: Almost no multisig controls, and if any exist, only for contracts not holding any funds
- **Emergency DAO**: Limited 5-of-9 multisig with very restricted emergency powers only
- **Community control and transparent governance**: All major protocol decisions require DAO approval through on-chain voting. All proposals and voting records are publicly available.
---
## crvUSD Risk Disclaimer
Curve stablecoin infrastructure enables users to mint crvUSD using a selection of crypto-tokenized collaterals (adding new ones is subject to DAO approval). Interacting with crvUSD doesn't come without risks. Before minting or taking exposure of crvUSD, it is best to research and understand the risks involved.
:::info
**crvUSD Audits** are available here: [Security Audits](../audits.md#stablecoin-and-lending)
:::
---
## CrvUSD Design Risks
### Soft-Liquidation and Hard-Liquidation
Collateralized debt positions are managed passively through arbitrage opportunities: if the collateral's price decreases, the system automatically sells off collateral to arbitrageurs in a ‘soft-liquidation mode’. If the collateral's price increases, the system recovers the collateral. This algorithm is designed to dynamically adjust the collateral backing each crvUSD in real-time, responding to fluctuating market conditions. While this approach is intended to mitigate the severity of traditional liquidations—a process where collateral becomes insufficient, leading to irreversible sales at potentially undervalued prices—it does not eliminate the inherent risk of collateral volatility. Additional information can be found on [LLAMMA Overview](/developer/crvusd/amm).
Borrowers in the crvUSD ecosystem are subject to specific risks associated with the liquidation process. It is crucial to understand that if the User’s collateral is placed into soft-liquidation mode, they are prohibited from withdrawing the collateral or augmenting their position with additional collateral. Entering soft-liquidation mode locks the collateral, removing the option to withdraw or add to it. In case market conditions suggest a strategic adjustment to the User’s position, they may face exacerbated risk due to such restrictions.
Users should be cautious about collateral management, as a sharp decline in the price of the collateral within a brief period can escalate losses, further deteriorating the health of the loan. Respectively, an increase in the collateral's value while in soft-liquidation can also cause “de-liquidation losses” - a situation where an appreciating market price for the collateral may negatively impact the loan’s health. During periods of high volatility and/or high Ethereum gas prices, arbitrage may be less efficient, causing losses incurred from soft liquidation to be exacerbated.
If the health of the loan falls to zero, the position is subject to hard liquidation, which is an irreversible process resulting in the loss of the collateral with no possibility of recovery or de-liquidation. This scenario underscores the critical need for risk management when using leverage. Leverage and collateral management should be approached with caution, reflecting a balanced assessment of potential gains against the risk of significant financial loss.
### Curve Pool EMA Oracles
Curve incorporates specialized on-chain Exponential Moving Average (EMA) oracles built into Stableswap-NG, Tricrypto-NG, and Twocrypto-NG Curve pool implementations. crvUSD markets derive price information from a select number of high TVL Curve pools. By utilizing the EMA smoothing methodology, oracles mitigate the impact of transient price fluctuations, aiming to reduce unnecessary losses caused by short-term market volatility or attempts to manipulate the oracle. Despite the manipulation-resistant design specification, Curve pool oracles may exhibit price distortions in certain scenarios that have the potential to result in missed or excessive liquidations. This may be a result of liquidity and volume migration to alternate venues that increase the risk of oracle manipulation. A detailed explanation of the aforementioned terms can be found in the [crvUSD Oracle documentation](/developer/crvusd/oracles/overview)
### Peg Stabilization Reserve (PSR)
crvUSD makes use of a Peg Stabilization Reserve (PSR) which consists of contracts authorized to deposit and withdraw crvUSD from a whitelisted Curve crvUSD Stableswappool up to a predefined debt cap. These contracts reference a subset of whitelisted stablecoins as a proxy for the intended USD price. Instability affecting any counterparty Reserve assets (e.g. USDT, USDC), which are also used to aggregate a USD price for crvUSD, may cause the Reserve to deposit all of its crvUSD into the pool in an attempt to rebalance. This creates a dependency on the Reserve counterparty assets that determines the stability of the crvUSD peg. An upgraded PegkeeperV2 design promises to alleviate this risk.
### Dynamic Interest Rates
The borrowing rate is algorithmically determined based on several factors, including:
- The crvUSD price as reported by an on-chain price aggregator contract
- The ratio of Reserve debt to total outstanding debt
- Several variables set by the Monetary Policy admin
Essentially, the borrow rate increases when the price of crvUSD goes lower and/or the proportion of Reserve debt to total debt reduces. This process is intended to dynamically regulate market behavior such that it reinforces the crvUSD peg. Changes to the Monetary Policy are authorized only by the Curve DAO. A [crvUSD simulation tool](https://github.com/0xreviews/crvusdsim) by 0xReviews allows Users to visualize the influence of these factors on the borrowing rate.
There may be assumptions in the Monetary Policy design that, in some circumstances, cause interest rates to produce undesired outcomes, and which may cause a sub-optimal experience for borrowers. In general, interest rates on borrowing may change dramatically in response to changing market circumstances, and may not reflect a borrower's expectations when they had opened their position.
---
## Market Risks
### Liquidity Risk
Users should be aware that ample crvUSD liquidity on exchange is necessary for facilitating liquidations. Circumstances leading to a reduction in the available crvUSD liquidity for liquidators are plausible. Such scenarios can significantly impact the proper functioning of the stablecoin market, particularly concerning the process of liquidation.
crvUSD relies on liquidity concentrated within particular pools used for the Peg Stabilization Reserve (PSR), which serve a dual purpose as both a source of liquidity and price feeds for crvUSD oracles. If the incentives for depositing crvUSD into these pools are insufficient, the liquidity shortfalls can result in missed liquidations or deflationary price spirals (cascading liquidations). This phenomenon occurs when initial liquidations fail to be executed effectively, leading to a domino effect of further liquidations and potentially rapid, significant decreases in asset prices.
### No Guarantee of Price Stability
The value of the crypto assets used as collateral for crvUSD is subject to high levels of volatility and unpredictability. The pricing of these assets may be extremely speculative and prone to rapid fluctuations. Such market characteristics can impact the stability of crvUSD’s value. While the LLAMMA algorithm aims to adjust collateral levels to support crvUSD’s value, there is no guarantee that these adjustments will always preserve stability, especially during periods of extreme market volatility.
### Depegging Risk
The blockchain ecosystem has witnessed instances where stablecoins experienced significant and prolonged periods of depegging from their intended value. Despite the innovative measures designed to uphold price stability, crvUSD is not exempt from the risk of depegging. Market volatility, shifts in regulatory landscapes, sudden and substantial changes in the value of collateral assets, or unforeseen technical issues can precipitate a departure from its pegged value.
---
## Technology Risk
### Smart Contract Risk
crvUSD relies on smart contracts, which are self-executing pieces of code. While these contracts are designed to be secure, there is a risk that they may contain vulnerabilities or bugs. Malicious actors could exploit these vulnerabilities, resulting in the loss of funds or other adverse consequences. Users need to conduct due diligence and review the smart contracts and security audit reports to assess the inherent risks.
Curve smart contracts have undergone multiple audits by reputable firms, including MixBytes and ChainSecurity, to enhance protocol security. While smart contract audits play an important role in good security practices to mitigate user risks, they don't eliminate all risks. Users should always exercise caution regardless of Curve's commitment to protocol security.
### No Loss Prevention
crvUSD and its underlying infrastructure are in an early stage of development, are inherently experimental, and carry a significant degree of risk. Engagement with crvUSD during this phase should be approached with the understanding that it may lead to partial or complete loss of funds. Users considering minting, redeeming, or utilizing crvUSD should be prepared for the possibility of encountering technical issues, bugs, or vulnerabilities that could impact the value of crvUSD or the safety of allocated crypto assets.
### General Blockchain Technology Risks
Engaging with crypto assets involves exposure to a range of technological risks inherent to the use of new and evolving technologies. Users must be aware of key risk factors (as outlined below) and consider their implications for crypto asset transactions.
1. **Irreversibility of Transactions**: Once executed, transactions in crypto assets cannot be reversed. Errors or accidental transactions cannot be easily rectified, potentially leading to permanent loss of assets.
2. **Anonymity**: The degree of anonymity provided by blockchain technology can complicate the tracing of funds and the identification of parties in transactions.
3. **Software Dependencies**: Crypto asset operations rely heavily on the flawless functioning of complex software, including wallets, smart contracts, and blockchain networks. Any defects, bugs, or vulnerabilities in software can impair access to or use of crypto assets, leading to potential losses.
4. **Cybersecurity Incidents**: The digital nature of crypto assets makes them a target for hackers, malicious actors, and other cybersecurity threats. Failures, hacks, exploits, protocol errors, and unforeseen vulnerabilities can compromise the security of assets, resulting in theft, loss, or unauthorized access.
5. **Operational Challenges**: The process of recording and settling transactions on a blockchain depends on the network's stability and performance. Disruptions, high transaction volumes, or network congestion can delay settlement times, affecting the liquidity and availability of assets.
---
*Disclaimer: The information provided within this context does not constitute financial, legal, or tax advice personalized to your specific circumstances. The content presented is for informational purposes only and should not be relied upon as a substitute for professional advice tailored to your individual needs. It is recommended that you seek the advice of qualified professionals regarding financial, legal, and tax matters before engaging in any activities on Curve.*
---
## Lending Risk Disclaimer
Curve Lending enables users to permissionlessly create and interact with isolated lending pairs composed of crvUSD, a decentralized stablecoin native to the Curve ecosystem, and various paired tokens. The notifications provided herein address risks associated with Curve Lending activities. The following list is not exhaustive.
Users wishing to acquaint themselves with a broader range of general risk disclosures are encouraged to read the [Curve Risk Disclosures for Liquidity Providers](/user/security/risks/pools). Users are also advised to review the public [audit reports](/developer/security/) to assess the security and reliability of the platform before engaging in any lending or borrowing activities.
:::info
**Lending Audits** are available here: [Security Audits](../audits.md#stablecoin-and-lending).
:::
---
## Permissionless Markets Risks
Curve Lending markets are permissionless, allowing anyone to create and customize markets with unique token pairs, a price oracle, and parameters that influence the LLAMMA liquidation algorithm and interest rate model. Given the protocol's permissionless nature, users should verify that the market has been instantiated with sensible parameters. Curve provides a [LLAMMA-simulator](https://github.com/curvefi/llamma-simulator) that can be referenced for finding optimal parameters.
There are several factors users should consider regarding attributes of the permissionless markets:
### 1. Unvetted Tokens
Curve Lending pairs consist of crvUSD and one other token, which may not undergo rigorous vetting due to the permissionless lending factory and lack of strict onboarding criteria. As a result, some tokens in Curve pools could be unvetted, introducing potential risks such as exchange rate volatility, smart contract vulnerabilities, and liquidity risks. Users should exercise caution and conduct their due diligence before interacting with any token on the platform.
### 2. Oracle Designation
Curve Lending markets by default use a Curve pool as the oracle, as long as the pool pair contains both tokens in the market and the pool is a Curve Tricrypto-NG, Twocrypto-NG or Stableswap-NG pool, which has manipulation-resistant oracles. However, this creates a dependency on the selected pool oracle, which may become unreliable due to market circumstances (e.g., liquidity migration) or technical bugs.
Alternatively, market deployers may designate a custom oracle, which can introduce additional trust assumptions or technical risks, and these custom oracles may need to be thoroughly vetted due to permissionless market deployment. Users should fully understand the oracle mechanism before interacting with a Curve Lending market.
### 3. Parameter Configuration
There are several parameters configurable by market deployers, including “A” (number of bands within the LLAMMA algorithm), fee on LLAMMA swaps, loan discount (Loan-To-Value), liquidation discount (RiskListItemquidation Threshold), and min/max borrow rate. Misconfigured AMM parameters may result in greater losses than necessary during liquidation and generally negatively impact user experience involving liquidation. Misconfigured borrow rates may prevent the market from adequately reflecting rates in the broader market, potentially leading to insufficient withdrawal liquidity for lenders. Users should be aware of market parameter configurations and ensure they are suitable for the underlying assets and anticipated market conditions.
### 4. Governance
The Curve Lending admin is the Curve DAO, a decentralized organization made up of veCRV tokenholders. Votes are required to make any change to the Curve Lending system, including individual markets. Votes undergo a 1-week vote period, requiring a 51% approval and a sufficient voter quorum to execute any on-chain actions. The DAO controls critical system functions in Curve Lending, including setting system contract implementations and configuring parameters such as min/max borrow rates, borrow discounts and AMM fees.
---
## Borrowing Risks
Borrowers can choose from various lending markets to borrow crvUSD against another asset or provide crvUSD as collateral. Markets are designated as one-way or two-way. In one-way markets, collateral cannot be lent out to other users. These assets serve solely as collateral to secure the loan and maintain the borrowing capacity within the protocol. Two-way markets allow collateral to be lent out, creating an opportunity for borrowers to earn interest.
### Soft and Hard Liquidation
Curve Lending uses a "soft" liquidation process powered by the LLAMMA algorithm. LLAMMA is a market-making contract that manages the liquidation and de-liquidation of collateral via arbitrageurs. This mechanism facilitates arbitrage between the collateral and borrowed asset in line with changes in market price, allowing a smoother liquidation process that strives to minimize user losses. Additional information can be found in the [LLAMMA Overview](/developer/crvusd/amm) docs.
Please consider the following risks when using the Curve Stablecoin infrastructure:
* If your collateral enters soft-liquidation mode, you can't withdraw it or add more collateral to your position.
* If the price of your collateral drops sharply over a short time interval, it can result in higher losses that may reduce your loan's health.
* If you are in soft-liquidation mode and the price of the collateral appreciates sharply, this can also result in de-liquidation losses. If your loan's health is low, collateral price appreciation can further reduce the loan's health, potentially triggering a hard liquidation.
* If the health of your loan drops to zero or below, your position will get hard-liquidated with no option of de-liquidation.
Borrowers should be aware that, while in soft liquidation, they essentially pay a fee to arbitrageurs in the form of favorable pricing. This will gradually erode the health of the position, especially during times of high volatility and, importantly, even when the market price of their collateral is increasing. This activity can decrease the position's health and cause it to undergo “hard” liquidation, whereby the collateral is sold off and the Borrower's position is closed. Borrowers are advised to monitor market conditions and actively manage their collateral to mitigate the liquidation risk. Borrowers should also be aware that if the loan's health falls below a certain threshold, hard liquidation could occur, leading to collateral loss.
### Interest Rates
The borrowing rate is algorithmically determined based on the utilization rate of the lending markets. It is calculated using a function that accounts for the spectrum of borrowing activity, ranging from conditions where no assets are borrowed (where the rate is set to a minimum) to conditions where all available assets are borrowed (where the rate is set to a maximum). The rates within the described monetary policy are subject to changes only by Curve DAO. More information on the interest rate model can be found in the Semi-log Monetary Policy docs.
## Lending Risks
When participating in lending activities on Curve Lending, Users may deposit crvUSD (or other assets designated for borrowing) into non-custodial Vaults that accrue interest from borrowers. There may also be the opportunity for additional CRV incentives by staking Vault tokens in a Gauge contract, pending DAO approval.
### Risk of Illiquidity
While these Vaults enable Users to supply liquidity and potentially earn returns, Users maintain the right to withdraw their assets at any time, so long as liquidity is available. There may be temporary or permanent states of illiquidity that prevent Lenders from fully or partially withdrawing their funds. This may result from diverse circumstances, including excessive borrow demand, a poorly configured interest rate model, a failure associated with the collateral asset, or a drastic reduction in incentives to a market. Similarly, there may be high volatility in the behavior of either lenders, borrowers, or both, which causes sharp swings in interest rates.
### Risk of Bad Debt
In extreme scenarios, Lenders may experience a shortfall through the accumulation of bad debt. This may occur if collateral prices fall sharply, especially in combination with network congestion that inhibits timely liquidation of positions. In such cases, Borrowers may need a financial motive to repay their debt, and Lenders may race to withdraw any available liquidity, saddling the Lenders remaining in the Vault with the shortfall.
Curve Lending is designed to minimize the risk of bad debt through over-collateralization and the LLAMMA liquidation algorithm. While over-collateralization and the LLAMMA algorithm act as risk mitigation tools, they do not fully insulate Lenders from the inherent risks associated with Curve Lending and assets in its markets, including smart contract vulnerabilities, market volatility, failures in economic models, and regulatory challenges that threaten product viability. Lenders are advised to understand their exposure to risks associated with the collateral asset in Vaults they choose to interact with and appreciate the possibility of experiencing partial or total loss.
---
## crvUSD Risks
Users should be mindful of risks associated with exposure to the crvUSD stablecoin:
* Investing in crvUSD carries inherent risks that could lead to partial or complete loss of your investment due to its experimental nature. You are responsible for understanding the risks of buying, selling, and using crvUSD and its infrastructure
* The value of crvUSD can fluctuate due to stablecoin market volatility or rapid changes in the liquidity of the stablecoin.
* crvUSD is exclusively issued by smart contracts, without an intermediary. However, the parameters that ensure the proper operation of the crvUSD infrastructure are subject to updates approved by Curve DAO. Users must stay informed about any parameter changes in the stablecoin infrastructure.
* crvUSD is not recognized as legal tender by any authority and is not guaranteed to be accepted for payments, subject to changing regulatory landscapes which may affect its legality and utility.
* Information provided by crvUSD front-end is solely for educational purposes and does not constitute any form of professional advice, leaving users solely responsible for ensuring actions meet their financial goals.
* Despite efforts to maintain price stability, crvUSD faces the risk of depegging due to market volatility, regulatory changes, or technological issues, potentially affecting its value.
* Users of crvUSD are exposed to various technological risks, including irreversible transactions, anonymity and security concerns, software dependency, cybersecurity threats, and operational and settlement risks, which can lead to potential asset loss.
* The continued development and functionality of the crvUSD protocol rely on developer contributions, with no guarantee of sustained involvement, posing a risk to its maintenance and scalability.
## General Financial Risks
### Volatility
Users should be aware that the prices of cryptocurrencies and tokens are highly volatile and subject to dramatic fluctuations due to their speculative nature and variable acceptance as a payment method. The market value of blockchain-based assets can significantly decline, potentially resulting in losses. Transactions within blockchain systems, including Ethereum Mainnet and others, may experience variable costs and speeds, affecting asset access and usability. Users are encouraged to develop their strategies for managing volatility.
### Financial Loss
Users should know that cryptocurrencies and tokens are highly experimental and carry significant risks. Engaging in lending and borrowing activities involves irreversible, final, and non-refundable transactions. Users must participate in these activities at their own risk, understanding that the potential for financial loss is substantial. Users are advised to carefully evaluate their lending and borrowing strategies, considering their personal circumstances and financial resources to determine the most suitable situation.
### Use of Financial Terms
Financial terms used in the context of Curve Lending, such as “debt,” “lend,” “borrow,” and similar, are meant for analogy purposes only. They draw comparisons between the operations of decentralized finance smart contracts and traditional finance activities, emphasizing the automated and deterministic nature of DeFi systems. These terms should not be interpreted in their traditional financial context, as DeFi transactions involve distinct mechanisms and risks. Users are encouraged to understand the specific meanings within the DeFi framework.
---
*Disclaimer: The information provided within this context does not constitute financial, legal, or tax advice personalized to your specific circumstances. The content presented is for informational purposes only and should not be relied upon as a substitute for professional advice tailored to your individual needs. It is recommended that you seek the advice of qualified professionals regarding financial, legal, and tax matters before engaging in any activities on Curve.*
---
## Liquidity Pools Risk Disclaimer
Providing liquidity on Curve doesn't come without risks. Before making a deposit, it is best to research and understand the risks involved.
:::info
**Liquidity Pool Audits** are available here: [Security Audits](../audits.md#dex)
:::
---
## Technology Risk
### Smart Contract Risk:
Curve relies on smart contracts, which are self-executing pieces of code. While these contracts are designed to be secure, there is a risk that they may contain vulnerabilities or bugs. Malicious actors could exploit these vulnerabilities, resulting in the loss of funds or other adverse consequences. It is essential for users to conduct due diligence and review the smart contracts and security audit reports to assess the inherent risks.
Curve smart contracts have undergone multiple audits by reputable firms including Trail of Bits, MixBytes, QuantStamp, and ChainSecurity to enhance protocol security. While smart contract audits play an important role in good security practices to mitigate user risks, they don't eliminate all risks. Users should always exercise caution regardless of Curve's commitment to protocol security.
### Immutability and Irreversibility of Transactions:
When you engage in transactions on Ethereum or EVM-compatible blockchains, it is important to understand that these transactions are immutable and irreversible. Once a transaction is confirmed and recorded on the blockchain, it cannot be modified, reversed, or deleted. This means that if a user sends funds to an incorrect address or engage in a fraudulent transaction, it may not be possible to recover the funds. It is crucial to exercise caution, verify transaction details, and use secure wallets to minimize the risk of irreversible transactions.
---
## Counterparty Risk
### Access Control:
Curve pool smart contracts are intentionally designed to be immutable and noncustodial, meaning they cannot be upgraded and liquidity providers always retain full control of their funds. While this characteristic may limit protective actions in case of emergencies, it significantly strengthens user assurances about custody of their funds.
The Curve protocol is governed by a Decentralized Autonomous Organization (DAO) comprised of veCRV tokenholders that requires a 1-week vote period with 51% approval and a sufficient voter quorum to execute any actions. It controls critical system functions, including the implementation of new system contracts and the adjustment of system parameters.
The Curve Emergency Admin is a [5-of-9 multisig](https://etherscan.io/address/0x467947EE34aF926cF1DCac093870f613C96B1E0c), composed of Curve community members. It has restricted rights to undertake actions that do not directly impact users' funds, including canceling parameter changes authorized by the DAO and halting CRV emissions to a pool. Early pool implementations included a timelimited function to freeze swaps and deposits in case of emergency, but this precautionary function has since been deprecated in current pool implementations.
---
## Asset Risk
### Permanent Loss of a Peg:
Stablecoins and other derivative assets are designed to maintain a peg to a reference asset. If one of the pool assets drops below its peg, it effectively means that liquidity providers in the pool hold almost all their liquidity in that token. The depegged token may never recover as a result of a technical failure, insolvency, or other adverse situations.
If the token fails to regain its peg, liquidity providers will encounter losses proportional to the severity of the depeg. The potential permanent loss highlights the importance of thorough consideration and caution when participating in activities involving stablecoins and/or derivative assets.
### Impermanent Loss:
Providing liquidity to Curve pools may expose users to the risk of impermanent loss. Fluctuations in asset prices after supplying to a pool can result in losses when users remove liquidity. Before engaging in liquidity provision activities, users are advised to carefully evaluate the potential for impermanent loss and consider their own risk tolerance.
[Stableswap](/developer/amm/legacy/stableswap-overview) are designed to mitigate impermanent loss by pairing assets that are expected to exhibit mean-reverting behavior. This assumption may not hold true in every case, requiring diligent assessment when interacting with these pools.
[Cryptoswap V2](/developer/amm/legacy/cryptoswap-overview) pools are designed with an internal oracle to concentrate liquidity around the current market price. The algorithm attempts to offset the effects of impermanent loss by calculating fees generated by the pool and ensuring the pool is in profit before re-pegging. Impermanent loss may still occur in Cryptoswap V2 pools, particularly when the earned fees are insufficient to counterbalance the impact of re-pegging. This underscores the need for users to be attentive about the dynamics of these pools when making decisions about liquidity provision.
### Price Volatility:
Cryptocurrencies and ERC20 tokens have historically exhibited significant price volatility. They can experience rapid and substantial fluctuations in value, which may occur within short periods of time. The market value of any token may rise or fall, and there is no guarantee of any specific price stability.
The overall market dynamics, including price volatility, liquidity fluctuations, and broader economic factors, can impact the value of user funds when providing liquidity. Sudden market movements or unexpected events can result in losses that may be difficult to anticipate or mitigate.
### Unvetted Tokens:
Due to the permissionless pool factory and the absence of strict onboarding criteria, not every token included in Curve pools undergoes a detailed independent risk assessment. Curve pools may contain unvetted tokens that have uncertain value or potential fraudulent characteristics. The presence of unvetted tokens introduces potential risks, including exchange rate volatility, smart contract vulnerabilities, liquidity risks, and other unforeseen circumstances that could result in the loss of funds or other adverse consequences.
When participating as a liquidity provider in any pool, users should carefully assess the tokens' functionality, security audits, team credibility, community feedback, and other relevant factors to make informed decisions and mitigate potential risks associated with the pool assets.
### Pools with Lending Assets:
Due to composability within DeFi, it is possible for assets in Curve pools to be receipt tokens for deposits in third party lending platforms. Composability of this sort can amplify yields for liquidity providers, but it also exposes users to additional risks associated with the underlying lending protocol. Users interacting with pools that involve lending assets should be mindful of this additional risk and conduct due diligence on the associated lending protocol.
---
## Regulatory Risk
### Regulatory Uncertainty:
The regulatory landscape surrounding blockchain technology, DeFi protocols, tokens, cryptocurrencies, and related activities is constantly evolving, resulting in regulatory uncertainty. The lack of clear and consistent regulations may impact legal obligations, compliance requirements, and potential risks associated with the protocol activities.
---
*Disclaimer: The information provided within this context does not constitute financial, legal, or tax advice personalized to your specific circumstances. The content presented is for informational purposes only and should not be relied upon as a substitute for professional advice tailored to your individual needs. It is recommended that you seek the advice of qualified professionals regarding financial, legal, and tax matters before engaging in any activities on Curve.*
---
## scrvUSD Risk Disclaimer
Curve stablecoin infrastructure enables users to mint crvUSD using a selection of crypto-tokenized collaterals (adding new ones is subject to DAO approval). Interacting with crvUSD doesn't come without risks. Before minting or taking exposure of crvUSD, it is best to research and understand the risks involved.
:::info "Audits"
**scrvUSD Audits** are available here: [Security Audits](../audits.md#stablecoin-and-lending)
:::
---
## crvUSD Dependency
As scrvUSD is directly tied to crvUSD, it inherits all risks associated with the underlying token. Users should consider the potential for cascading impacts from issues affecting crvUSD mint markets, including but not limited to:
- **Technical Risk**: crvUSD is a permissionless stablecoin composed of many inter-related smart contracts that enable functionality such as minting from various collaterals, peg stability mechanisms, and liquidations processing. scrvUSD inherits all technical risks associated with the crvUSD protocol.
- **Collateral Dependency**: crvUSD is overcollateralized by an assortment of crypto assets whitelisted by the Curve DAO. Many collateral types are pegged assets that depend on an issuer or protocol to maintain their own stability. Problems associated with the underlying collateral may impact crvUSD's solvency.
- [**Peg Stabilization Reserve (PSR) Dependency**](/developer/crvusd/pegkeepers/overview): crvUSD maintains a soft peg to USD that is reinforced by programmatic mint/burn actions into designated pools used for the Reserve. This creates a dependency on the stability of third party stablecoins (e.g. USDT and USDC), which can impact crvUSD's stability by proxy.
- **Governance Risk**: The Curve DAO can modify code and change parameters within the crvUSD protocol. It is possible that bugs or malicious modifications are introduced to the protocol through governance actions.
Read the [crvUSD Risk Disclaimer](./crvusd.md) for a more detailed overview of risks related to crvUSD.
---
## Vault Smart Contract Risks
To obtain scrvUSD, users deposit crvUSD into a Savings Vault which make use of [Yearn V3 vaults](https://docs.yearn.fi/developers/v3/overview). While Yearn vaults have undergone extensive [audits](https://github.com/yearn/yearn-vaults-v3/tree/master/audits) and demonstrated resilience and security, users should be aware of the technical risks associated with interacting with smart contracts.
The vault may have undiscovered smart contract vulnerabilities, even in audited contracts. Flaws in the vault’s critical functions or underlying code could lead to unexpected or incorrect contract behavior, potentially resulting in financial losses or interruptions in the vault’s operations. A non-exhaustive list of technical failures may include:
- **Inflation Attacks**: An attacker could exploit weaknesses in the vault’s share issuance mechanism, enabling them to mint more shares than they are legitimately entitled to, leading to dilution of other users’ holdings.
- **Reentrancy Attacks**: Malicious actors could exploit reentrancy vulnerabilities by manipulating functions that interact with external contracts, such as token transfers or protocol integrations, potentially causing unexpected fund transfers or protocol malfunctions.
In case of an emergency, a privileged role can be assigned to call shutdown_vault() and prevent additional deposits to the vault. This is an irreversible action. Existing vault depositors are still able to withdraw from the vault in an emergency shutdown event, and are responsible for doing so to mitigate the risk of funds loss. This role and other vault roles are assigned by the Curve DAO, which requires an on-chain vote and 7 day timelock to enact any change.
---
## Interest Accrual Risks
The interest accrued to scrvUSD originates from crvUSD interest rate fees paid by borrowers taking out crvUSD loans. Interest on scrvUSD accrues passively and is designed to increase the underlying value of scrvUSD over time.
Users should recognize that the value increase is not guaranteed, nor does Curve guarantee a specific APY. The yield realized by holders depends on multiple factors. For example:
- **Revenue Dependency**: The proportion of revenue allocated to scrvUSD holders depends on the total crvUSD fees generated, which may fluctuate due to borrower demand, fee structure changes, or market conditions. A decline in borrower activity or revenue could reduce the interest accrued for scrvUSD holders.
- **Dynamic Proportion**: Realized yield is dynamic and depends on the share of the vault owned relative to the total revenue being distributed. Users should understand that variations in this ratio may directly affect their earned interest, leading to outcomes that differ from initial expectations. Factors such as global stablecoin yields and the risk premium of crvUSD exposure may cause fluctuations.
- **Rewards Rate Parameters**: Privileged addresses control parameters that determine the rewards distribution rate. This role is controlled by the Curve DAO, requiring an on-chain vote and 7 day timelock to enact any change. The DAO may also appoint a third party to control the distribution.
- **Rewards Proportion Parameters**: The owner of the Fee Splitter contract controls the max revenue share to scrvUSD, controlling the upper and lower bound of the scrvUSD yield allocation (e.g. allocating between 20% and 30% of the crvUSD yield). This role is controlled by the Curve DAO, requiring an on-chain vote and 7 day timelock to enact any change. The DAO may also appoint a third party to control the distribution.
---
## MEV and Revenue Distribution Risks
The accuracy of scrvUSD supply calculations is crucial for the proper distribution of protocol revenue among holders. Users should be aware that supply calculations may be subject to manipulation through MEV (Maximal Extractable Value) strategies, which could affect revenue distribution and, consequently, the actual yields received by scrvUSD holders. This can lead to discrepancies between expected and actually received yields for users. From a practical standpoint, when evaluating potential scrvUSD yields, users should consider that published APYs may differ from actual returns due to these factors.
scrvUSD accounts for MEV risk by incorporating protections that calculate a Time Weighted Average (TWA) of the ratio between deposited crvUSD in the Vault and total circulating supply of crvUSD. The TWA is meant to prevent MEV manipulation of the yield distribution rate. Misconfiguration of the TWA can allow MEV actions to become profitable. Any manipulation gain or loss is shared with all the other vault depositors until a new non-manipulated snapshot is taken.
---
## Cross-chain scrvUSD Risks
While scrvUSD can be bridged, cross-chain representations lack the method to return its continuously updating price. Therefore cross-chain scrvUSD incorporates a series of additional contracts that provide and validate Ethereum block hashes, using those to calculate and store the current rate.
There are operational requirements to reliably transmit the rate data, including to apply and prove block hashes. While these actions are permissionless, they may not be closely monitored or reliably executed. Assuming that cross-chain contracts are bug free, execution is programmatic and therefore safe; this point regarding monitoring is rather a liveness concern.
Furthermore, there is a trust assumption in the owner of the scrvUSD oracle which can update the prover address and rate limiting parameters. Due to these factors, the scrvUSD rate on chains other than mainnet may not accurately reflect the actual rate, either temporarily or permanently.
---
## Secondary Market Risks
scrvUSD can also be acquired on secondary markets outside of the primary access venue (and cross-chain scrvUSD is only available on secondary markets). As a consequence of certain market scenarios, scrvUSD may trade at a discount to its underlying crvUSD value. Users who sell scrvUSD at such a discounted price will realize losses relative to its intrinsic value. Secondary market dynamics are subject to external factors, including market sentiment, trading volume, and liquidity conditions, which may amplify these risks.
As scrvUSD is always readily redeemable for crvUSD directly from the vault contract, aside from a technical failure, users should never be required to swap at unfavorable rates on secondary markets. Situations may occur that DEX aggregators fail to route trades optimally, so users should always check that their aggregator service is quoting sensible exchange rates. Furthermore, being able to redeem crvUSD from an L2 requires the user to be able to bridge back to mainnet. Redemptions of cross-chain scrvUSD may incur additional gas costs and dependence on bridge availability.
---
*Disclaimer: The information provided within this context does not constitute financial, legal, or tax advice personalized to your specific circumstances. The content presented is for informational purposes only and should not be relied upon as a substitute for professional advice tailored to your individual needs. It is recommended that you seek the advice of qualified professionals regarding financial, legal, and tax matters before engaging in any activities on Curve.*
---
## Boosting CRV Rewards
:::info Prerequisites
This guide assumes you have already provided liquidity and are currently staking LP tokens on the DAO gauge. If you haven't done this yet, see our [DEX liquidity provision guide](../dex/liquidity.md) or [Llamalend supplying guide](../llamalend/supplying.md).
:::
One of the main incentives for holding CRV is the ability to boost rewards on provided liquidity. By locking CRV for veCRV, you not only gain voting power in the DAO and revenue share, but also earn a boost of up to **2.5x** on your liquidity rewards across Curve pools and lending markets.
## How Rewards Are Displayed
In Curve's interface, rewards are always displayed as a range showing the difference between unboosted rewards (for regular liquidity providers) and maximum boosted rewards (for veCRV holders). This range helps you understand the potential value of holding veCRV.
:::warning Important Note
Only **CRV rewards** are boosted by veCRV. External reward tokens (like tokens from other protocols) are distributed solely based on your liquidity share and are not affected by veCRV holdings.
:::
## Step 1: Calculate Required veCRV
The first step to getting rewards boosted is to determine how much veCRV you need. Each gauge has different requirements, meaning some pools are easier to boost than others. This depends on:
- The amount of veCRV others have locked
- The total value locked (TVL) in the gauge
- Your own liquidity position
Use the [boost calculator](https://dao-old.curve.finance/minter/calc) to determine your specific requirements:
## Step 2: Lock CRV for veCRV
After determining how much veCRV you need, visit the [CRV Locker](https://curve.fi/dao/ethereum/vecrv/create/) to create your lock. For detailed instructions on locking CRV, see [Locking CRV & Managing Locks](./how-to-lock.md).
## Step 3: Check Your Boost
After creating your lock, proceed to the desired pool page and click on **Your Details** as shown below. Under this tab you can see your current rewards boost.
### Troubleshooting Boost Updates
- ✅ **If the new boost is visible** after **'Current boost:'**, then no further action is required.
- ⚠️ **If the current boost hasn't updated**, try claiming CRV from each of the gauges where you have liquidity. This should trigger the boost update.
:::info Boost Update Timing
Boosts are only updated when you make a withdrawal, deposit, or claim from a liquidity gauge. This is why claiming rewards can help refresh your boost display.
:::
## How the Boost Formula Works
Your boost multiplier can be between 1 and 2.5. This means if you have a 2.5x boost and deposit \$10,000, your rewards are calculated as if you deposited $25,000. It depends on the following factors:
* **Your veCRV balance:** More veCRV means a higher potential boost.
* **The amount of liquidity you provide:** Your boost is calculated relative to your liquidity.
* **The total veCRV and liquidity in the gauge:** The boost mechanism balances your veCRV power against everyone else's in the Pool or Lending market's gauge.
By boosting, you're essentially getting a larger share of the new CRV tokens distributed. This incentivizes users to lock CRV, which in turn strengthens the Curve DAO's governance and promotes long-term alignment with the protocol.
### The Boost Formula
$$
B = \min\left(2.5, 0.4 + 2.5 \times \frac{\text{veCRV}_\text{user}}{\text{veCRV}_\text{total}} \times \frac{\text{Value}_\text{pool}}{\text{Value}_\text{user}}\right)
$$
**Formula Variables:**
- **$B$** = Your rewards boost (capped at 2.5x maximum)
- **$\text{Value}_\text{user}$** = Your deposited value in USD
- **$\text{Value}_\text{pool}$** = Total value in the pool's reward gauge in USD
- **$\text{veCRV}_\text{user}$** = Your veCRV amount (vote weight)
- **$\text{veCRV}_\text{total}$** = Total veCRV in the system ([check current amount](https://dao-old.curve.finance/minter/calc))
---
## FAQ(Vecrv)
# Frequently Asked Questions
## General Questions
### What is the minimum lock time for CRV?
The minimum lock time is **1 week**.
### What is the maximum lock time for CRV?
The maximum lock time is **4 years**.
### Can I transfer my veCRV tokens?
No, veCRV tokens are **non-transferable**. They are locked to your address and cannot be sent to other wallets.
### Can I have multiple locks?
No, you can only have **one active lock per address**. However, you can extend your existing lock or add more CRV to it.
### Can I withdraw my CRV before the lock expires?
No, CRV locks are **irreversible**. You can only withdraw your original CRV tokens after the lock period ends.
## Boosting Questions
### What is the maximum boost I can get?
The maximum boost is **2.5x** on your liquidity rewards.
### How do I check my current boost?
Visit the pool page and click on "Your Details" to see your current boost level.
### Why isn't my boost updating?
Boosts are only updated when you make a withdrawal, deposit, or claim from a liquidity gauge. Try claiming your CRV rewards to update the boost.
## Revenue Questions
### How often are rewards distributed?
Rewards are distributed **weekly** and can be claimed within 24 hours after Thursday 00:00 UTC.
### What token are rewards paid in?
Currently, rewards are paid in **crvUSD**. Previously, they were paid in 3CRV.
### Do I need to claim rewards every week?
No, rewards accumulate and can be claimed at any time without forfeiting any rewards.
## Technical Questions
### How is veCRV calculated?
veCRV is calculated using the formula: $veCRV = \frac{CRV_{locked} \times {locktime_{left}}}{4}$
### How does the boost formula work?
The boost formula is: $B = \min\left(2.5, 0.4 + 2.5 \times \frac{\text{veCRV}_\text{user}}{\text{veCRV}_\text{total}} \times \frac{\text{Value}_\text{pool}}{\text{Value}_\text{user}}\right)$
### Where can I find the total veCRV supply?
You can find the current total veCRV supply on the [calculator page](https://dao-old.curve.finance/minter/calc).
## Locking & Management Questions
### How do I lock my CRV tokens?
Visit the [CRV Locker](https://curve.fi/dao/ethereum/vecrv/create/) and connect your wallet. Enter the amount of CRV you want to lock and choose the duration (1 week to 4 years).
### Can I extend my lock duration?
Yes, you can extend your lock duration at any time. This resets your lock to a later expiry date and increases your veCRV balance.
### Can I add more CRV to my existing lock?
Yes, you can deposit additional CRV into your existing lock at any time. The new CRV will be locked until your current expiry date.
### How do I withdraw my CRV after the lock expires?
Once your lock period ends and your veCRV balance reaches zero, you can withdraw your original CRV tokens through the Curve UI under the "Withdraw" tab.
### What happens to my veCRV balance over time?
Your veCRV balance decays linearly as time passes, reflecting the decreasing time left on your lock. This means your voting power and benefits decrease gradually.
### How much veCRV do I get for locking CRV?
The amount depends on both the quantity of CRV locked and the lock duration. For example:
- 1 CRV locked for 4 years = 1 veCRV
- 1 CRV locked for 1 year = 0.25 veCRV
- 1 CRV locked for 6 months = 0.125 veCRV
## Governance Questions
### What voting power does veCRV give me?
veCRV gives you voting power in the Curve DAO, including on proposals and gauge weight votes that determine CRV emissions.
### How does veCRV prevent short-term manipulation?
By requiring users to commit their tokens for a set period, only those with long-term interest can influence important decisions. Voting power is directly tied to the duration of your commitment.
### Can I vote on multiple proposals?
Yes, you can vote on all active proposals with your veCRV balance. Your voting power is proportional to your veCRV amount.
## Revenue & Rewards Questions
### What sources generate revenue for veCRV holders?
- **Trading fees:** A portion of every swap fee on Curve
- **crvUSD interest:** Parts of the interest from Curve's stablecoin markets
### I locked last week, why didn't I receive any revenue?
You have to be locked for a FULL week before you are eligible, Thursday-Thursday. The system takes a snapshot on Thursday at 00:00 UTC to see who has veCRV, then distributes the weekly revenue to those users the following Thursday at 00:00 UTC.
### How is my revenue share calculated?
Your revenue share depends on your veCRV balance relative to the total veCRV supply at the time of distribution. The more veCRV you hold, the larger your share.
### When are rewards distributed?
Rewards are distributed weekly and can be claimed within 24 hours after Thursday 00:00 UTC.
### How do I claim my revenue?
Connect your wallet to the [Curve Dashboard](https://www.curve.finance/dex/ethereum/dashboard/) and click the blue "Claim crvUSD" button.
### Do I lose rewards if I don't claim weekly?
No, rewards accumulate and can be claimed at any time without forfeiting any rewards.
### What was the original reward token before crvUSD?
Previously, revenue was distributed using 3CRV tokens (LP tokens of the 3pool). The DAO later decided to switch to crvUSD.
---
## Locking CRV & Managing Locks
Locking your CRV tokens is the first step to participating in Curve governance and unlocking veCRV benefits. When you lock CRV, you receive veCRV based on the amount and duration of your lock. The longer you lock, the more veCRV you receive.
:::warning
Locking CRV is **not reversible**. veCRV is **non-transferable**, and you can only reclaim your CRV after the lock period ends. You can only have one active lock per address, but you can extend the lock or add more CRV at any time.
:::
## How to Lock CRV
Visit the official Curve Locker UI at [CRV Locker](https://curve.fi/dao/ethereum/vecrv/create/) and connect your wallet on the top right.
Then, simply add the amount of CRV tokens you want to lock up and choose the lock duration. The minimum is **1 week**, and the maximum is **4 years**. The amount of veCRV you receive increases with longer lock times.
After locking, you will receive veCRV in your wallet. But remember: Unlike other tokens, you will not be able to transfer them.
## Dashboard
The dashboard section ([Curve Dashboard](https://www.curve.finance/dex/ethereum/dashboard/)) provides a full overview on the users veCRV holdings such as their veCRV balance, total amount of CRV locked, unlock time and the claimable rewards.
## Managing Your Lock
When having an active lock, you can view your veCRV balance and lock details directly in the dashboard. Your veCRV will begin to decay as time passes, reflecting the decreasing time left on your lock.
If you want to increase your veCRV, you can either:
- **Extend your lock duration:** This resets your lock to a later expiry date, increasing your veCRV balance.
- **Add more CRV:** You can deposit additional CRV into your existing lock at any time. The new CRV will be locked until your current expiry date.
Remember, you can only have one active lock per address. If you try to lock again, you’ll be prompted to either extend or add to your existing lock.
## Withdrawing Your CRV
Once your lock period ends and your veCRV balance reaches zero, you can withdraw your original CRV tokens. This can be done through the same Curve UI under the "Withdraw" tab.
---
## Claiming veCRV Revenue Share
Holding veCRV not only gives you voting power in Curve governance, but also entitles you to a share of protocol revenue.
## How is Revenue Generated?
Curve collects fees from several sources:
- **Trading fees:** Every time users swap tokens on Curve, a portion of the trading fee is going to veCRV holders.
- **crvUSD interest:** Parts of the interest accrued from Curve’s stablecoin (crvUSD) markets is distributed to veCRV holders.
## How is Revenue Distributed?
All collected fees are converted to crvUSD and distributed among veCRV holders. The amount you receive depends on your veCRV balance relative to the total veCRV supply at the time of distribution. The more veCRV you hold, the larger your share of the revenue.
Rewards are distributed on a weekly basis and can be claimed each week within 24hrs after Thursday 00:00 UTC.
## Claiming Eligibility
Before you can claim veCRV revenue, you need to meet the following criteria:
1. **Have veCRV (locked CRV) in your wallet.**
If you hold tokens from 'liquid lockers' (like cvxCRV, sdCRV, or yCRV), these are derivatives of veCRV. You'll need to claim your revenue through that specific project's website. If you don't have veCRV, see the guide on how to lock CRV here: [Guide: Locking CRV](../vecrv/how-to-lock.md).
2. **You've held veCRV through a full revenue week (Thursday 00:00 UTC to Thursday 00:00 UTC).**
Curve distributes revenue based on weekly snapshots. If you acquire veCRV on a Friday, for example, your first claim will be available roughly 13 days later.
## How to Claim Your Revenue
Users can claim their rewards in the dashboard here: [Curve Dashboard](https://www.curve.finance/dex/ethereum/dashboard/)
Simply connect your wallet and claim your rewards by clicking the blue "Claim crvUSD" button. The rewards will be sent directly to your wallet. The rewards do not need to claimed on a weekly basis as all rewards accure and can be claimed at any point in time without forfeiting any rewards.
If you have claimable revenue, it will be visible as shown below:
:::info 3CRV as the original reward token
Before the DAO decided to have crvUSD as the reward token (June 20th, 2024), revenue was distributed using the 3CRV token which is the LP tokens of DeFi's biggest stablecoin pool (3pool).
:::
:::info Consider Re-locking CRV
Remember that your veCRV balance, and thus your rewards, are affected by how long is left on your CRV lock. Consider [re-locking your CRV](../vecrv/what-is-vecrv.md) to maintain your rewards.
:::
## Revenue Stats
All veCRV metrics and revenue stats can be viewed on the [analytics page](https://www.curve.finance/dao/ethereum/analytics/).
---
## What is veCRV?
**veCRV** stands for **vote-escrowed CRV**. It is a token users receive when they lock their CRV tokens for a period of time, which can range from a minimum of 1 week up to a maximum of 4 years. The longer users choose to lock their CRV, the more veCRV they receive in return.
:::warning
Unlike regular tokens, **veCRV is not transferrable**, and the **amount users hold gradually decreases** as their lock approaches its expiry date. The initial locked CRV can be withdrawn after the lock has ended.
:::
## Benefits of Holding veCRV
Holding veCRV unlocks several important benefits:
- [Earn Protocol Fees:](./revenue.md) veCRV holders receive a share of trading fees and a portion of interest from Curve's stablecoin markets.
- [Boosted CRV Rewards:](./boosting.md) If users provide liquidity to Curve pools, holding veCRV can boost their CRV rewards.
- [Governance:](./faq.md) veCRV gives users voting power in the Curve DAO, including on proposals and gauge weight votes that determine CRV emissions.
## How veCRV Works
When users lock their CRV, the amount of veCRV they receive depends on both the quantity of CRV locked and the length of the lock. For example, locking 1 CRV for the maximum period of 4 years grants users 1 veCRV, while locking the same amount for just 1 year gives users 0.25 veCRV. This relationship is defined by a simple formula:
$$
\text{veCRV} = \frac{\text{CRV locked} \times \text{Years until unlock}}{4}
$$
As time passes, users' veCRV balance decays linearly, reflecting the decreasing time left on their lock. Users can **only have one active lock per address**, but they are free to **add more CRV or extend the lock duration** at any time. Once the lock expires, users' veCRV balance reaches zero and they can withdraw all their originally locked CRV tokens.
## The Idea Behind Vote-Escrow
Curve pioneered the vote-escrow (ve) tokenomics model with the launch of veCRV in 2020, setting the standard for commitment-based governance in DeFi. This innovative approach has since been adopted by many other protocols, establishing ve-tokenomics as a dominant framework for aligning long-term incentives in decentralized systems.
The veCRV model builds sustainable value through commitment, not scarcity. By locking tokens for governance and rewards, veCRV creates a governance-driven ecosystem where long-term users are incentivized to participate, vote, and contribute to protocol success. Unlike buyback-and-burn models that treat holders as passive beneficiaries, veCRV empowers users to shape the protocol's direction while rewarding them in line with their commitment. The longer users lock their CRV, the greater their voting power, boosted rewards, and share of protocol revenue.
---
## Boosting CRV Rewards(Yield)
# veCRV - Boosting CRV rewards
While providing liquidity to Curve pools and lending to Llamalend markets already earns you trading fees and CRV rewards (if available), there's a powerful way to **maximize your CRV earnings**: by **boosting** them. Boosting allows you to **increase the amount of CRV you earn by up to 2.5x**.
Your current pool boosts can be seen on the DEX Dashboard, and you can calculate the boost your potential boost with the boost calculator, links below.
---
## What is Boosting?
Boosting is a mechanism tied to [**veCRV**](../vecrv/what-is-vecrv.md) (vote-escrowed CRV). To get veCRV, you **lock your CRV tokens** for a set period, ranging from one week to four years. The longer you lock your CRV, the more veCRV you receive.
Think of veCRV as your long-term commitment to the Curve ecosystem. In return for this commitment, the protocol rewards you with increased CRV emissions on your supplied liquidity. Essentially, with enough veCRV, you can earn up to **2.5 times** the CRV rewards you would normally receive from your LP tokens!
**Why Boost?**
Boosting is a key incentive for long-term participation in the Curve ecosystem. It allows active liquidity providers to significantly increase their CRV farming efficiency, making their contributions even more profitable.
---
## Guide: Checking Earned Interest
This guide shows how to check your earned interest while holding scrvUSD tokens on Ethereum mainnet.
:::info Important Note
While scrvUSD tokens exist and earn interest on all deployed networks, the deposit, withdrawal, and functions for the scrvUSD vault are exclusively available on the Ethereum mainnet. You can simply buy and sell scrvUSD from a curve pool and it will essentially have the same effect.
:::
## Step 1: Navigate to the scrvUSD Vault and Connect Your Wallet
Go to the official [scrvUSD vault page on Curve.finance](https://www.curve.finance/crvusd/ethereum/scrvUSD/):
## Step 2: Checking Your Earned Interest
You can easily see how much interest you've earned directly on the scrvUSD UI:
1. Navigate to the `Position Details` section.
2. Hover your mouse over the `Your crvUSD Staked` box.
This will reveal the current crvUSD value of your scrvUSD holdings. For instance, here we see our initial **953 scrvUSD (which acquired by depositing 1,000 crvUSD)** is now worth **1,016 crvUSD after 3 months**, representing a gain of **16 crvUSD, or 1.6%**.
*Value of 953 scrvUSD after 3 months*
---
## Guide: How to Deposit into scrvUSD
Before you can start earning interest by depositing into the scrvUSD savings vault, you'll **need to have crvUSD tokens on the Ethereum mainnet**.
If you don't have crvUSD, you can also swap directly into scrvUSD using [Curve Swap](https://www.curve.finance/dex/ethereum/swap). A guide for using Curve Swap is here: [Guide: How to Swap](../../dex/guides/swap.md)
:::info Important Note
While scrvUSD tokens exist and earn interest on all deployed networks, the deposit and withdrawal functions for the scrvUSD vault are exclusively available on the Ethereum mainnet. You can simply buy and sell scrvUSD from a curve pool and it will essentially have the same effect.
:::
## Step 1: Navigate to the scrvUSD Vault and Connect Your Wallet
Go to the official [scrvUSD vault page on Curve.finance](https://www.curve.finance/crvusd/ethereum/scrvUSD/):
Once on the page, connect your Web3 wallet by clicking the "Connect Wallet" button in the top right corner of the interface. After successfully connecting your wallet, you should see an interface similar to the following:
*Here we have 1000 crvUSD we can deposit, which can earn 6.9% APY*
## Step 2: Choose Your Deposit Amount
On the left you'll see the `Deposit` tab. Here, enter the amount of crvUSD you wish to deposit, or click `Max` to deposit all your crvUSD from your wallet.
After entering your amount, you'll see how many scrvUSD vault tokens you'll receive. In the example below, if we deposit **1,000 crvUSD**, we would receive **953 scrvUSD**. This is because scrvUSD is designed to continuously grow in value against crvUSD (1 scrvUSD = 1.049 crvUSD).
## Step 3: Approve & Deposit Your crvUSD
If this is your **first time depositing crvUSD into the scrvUSD vault**, you'll need to complete two separate transactions:
1. **Approve:** This transaction gives the scrvUSD vault permission to access and transfer your crvUSD tokens from your wallet.
2. **Deposit:** This second transaction actually sends your crvUSD into the vault, and in return, you'll receive scrvUSD tokens.
However, if you've deposited into the scrvUSD vault before, you'll only need to complete the **Deposit** transaction.
These transactions will appear sequentially in your Web3 wallet for you to review and sign. Once you've approved and sent both transactions, you'll see their progress displayed just below the deposit box on the interface:
## Congratulations! Your Deposit is Complete.
You've successfully deposited your crvUSD! In our example, you now hold **953.2 scrvUSD**. Remember, the scrvUSD token balance will not change, but the value of each scrvUSD token will automatically become worth *more* crvUSD over time, reflecting your earned interest.
---
---
## Guide: How to Withdraw from scrvUSD
This guide shows how to withdraw directly through the vault to receive crvUSD, however, you can also swap from scrvUSD directly into crvUSD or any other asset using [Curve Swap](https://www.curve.finance/dex/ethereum/swap). A guide for using Curve Swap is here: [Guide: How to Swap](../../dex/guides/swap.md)
:::info Important Note
While scrvUSD tokens exist and earn interest on all deployed networks, the deposit and withdrawal functions for the scrvUSD vault are exclusively available on the Ethereum mainnet. You can simply buy and sell scrvUSD from a curve pool and it will essentially have the same effect.
:::
## Step 1: Go to the scrvUSD Vault
You can withdraw your crvUSD from the vault at any time, there are **no delays or lock-ups**. Whenever you're ready to withdraw, simply navigate to the [scrvUSD vault page](https://www.curve.finance/crvusd/ethereum/scrvUSD/) on Curve.finance:
## Step 2: Select Your Withdrawal Amount
Once on the scrvUSD vault page, click on the `Withdraw` tab located on the left side of the interface.
Here, you'll see your current scrvUSD balance. Enter the amount of scrvUSD you wish to withdraw, or click `Max`:
Notice that the amount of scrvUSD tokens remains the same (e.g., **953 scrvUSD** from our example). However, because scrvUSD continuously grows in value, the initial **1,000 crvUSD** deposit can now be withdrawn as **1,022 crvUSD** after a few months, a profit of **22 crvUSD**!
## Step 3: Confirm and Complete Your Withdrawal
After you've selected your withdrawal amount, click the `Withdraw` button and confirm the transaction in your Web3 wallet.
Once this transaction is successfully confirmed on the blockchain, your crvUSD and earned interest will back in your wallet:
Congratulations on successfully withdrawing your crvUSD and realizing your earned yield!
---
## Earning Yield on Llamalend
Llamalend (Curve's lending infrastructure) lets you **earn yield by supplying assets** to lending markets. Doing so, other users can borrow your lent out assets and you'll earn interest on it.
Some lending markets also offer additional rewards, like CRV emissions, other tokens or points.
---
## How Lending on Llamalend Works
When you lend out your assets on Llamalend, you deposit them into a **lending market**. Each market lets users borrow one asset by using another as collateral, for example, borrowing crvUSD by using ETH as collateral. You earn interest according to the market's interest rate as assets are borrowed. You can withdraw your assets anytime, as long as there's enough available (unborrowed) in the pool.
Interest rates on Llamalend are **dynamic** and change based on supply and demand. High demand for borrowing means higher interest for lenders; low demand means lower interest.
## Earning Yield as a Lender
As a lender on Llamalend, your primary sources of yield comes from:
* **Borrowing Interest:** This is the direct interest paid by borrowers for using your supplied assets. This interest automatically accrues to your deposited assets, increasing their value over time.
* **CRV Rewards:** Similar to providing liquidity in swap pools, some Llamalend markets may offer additional CRV tokens as rewards for supplying assets.
* **Other Token Rewards and Points:** Protocols or asset issuers might also provide other tokens or points as additional rewards for lenders in specific markets. They often do so for bootstrapping their lending markets.
---
## Providing Liquidity in Pools(Yield)
One of the primary ways to **earn yield** on Curve is by providing liquidity to liquidity pools. When you deposit assets, you become a liquidity provider (LP) and, in return, receive LP tokens which represent your ownership stake in the pool's assets.
Users can withdraw their provided liquidity at any point in time, which burns the LP tokens again.
---
When you **deposit assets into a liquidity pool on Curve**, you become a **liquidity provider (LP)** which means you are providing your asset(s) in a pool for others to trade. Simply put, if you are e.g. providing USDT and USDC liquidity into a USDC/USDT pool, you provide liquidity for other traders to swap between USDC and USDT.
A liquidity provider can potentially earn in several ways:
- **Trading Fees:** Every time someone exchanges coins in the pool you provide liquidity to, a fee is charged. LPs get a share of these fees (typically 50%).
- **CRV Rewards:** Because of the way Curve works, some pools might receive CRV tokens as rewards. If that is the case and users want to earn these extra rewards, they need to stake their LP tokens in a so called [gauge](lp.md#what-is-a-gauge). You can claim these rewards whenever you want and you can unstake at any point in time. There is no lockup period.
- **Other Token Rewards:** Sometimes, projects or asset issuers give out extra token rewards to the LP's of their pools. You can earn these too by staking your LP tokens in the same gauge.
- **Points:** Some pools give you points for adding or staking your crypto. These points might get you extra perks or airdrops in the future, but check each project's rules to be sure.
All these rewards can add up, making liquidity providing potentially a great way to earn more from your crypto.
---
## FAQ
### What is a Gauge?
A "gauge" is a simple smart contract where you can stake your LP tokens to earn additional CRV and other token rewards. Gauges are designed to distribute these reward tokens evenly over time, effectively incentivizing liquidity provision to specific pools. Users can claim their earned rewards at any time from the gauge.
---
## Earning Yield on Curve
Curve offers many different ways to earn and grow using your assets, including:
- [Providing Liquidity to DEX Pools](lp.md):
By depositing your assets into a liquidity pool, you become a **liquidity provider (LP)**, and your assets are used to facilitate trades on Curve. In return, you earn a fee each time a trade happens. Additionally, you can stake your LP tokens for potentially more rewards or points.
- [Earning by Supplying on Llamalend](lending.md):
Llamalend, allows you to **earn interest by lending out assets** to borrowers. Simply deposit your assets into a lending market, and you'll start earning yield. Many markets also award CRV rewards and other incentives.
- [scrvUSD: The Savings Version of crvUSD](scrvusd.md):
scrvUSD offers a simple way to earn yield on **crvUSD**. These scrvUSD tokens automatically increase in value over time, as they accrue interest from the fees generated by crvUSD borrowing on Llamalend.
- **Earning with veCRV**:
veCRV, or vote-escrowed CRV (locked CRV) is a key component for maximizing your earning potential and participating in the Curve ecosystem. By locking your CRV tokens for a chosen period, you receive veCRV, which unlocks two powerful benefits:
- [Share of Curve's Protocol Revenue](revenue.md):
As a veCRV holder, you are entitled to a **share of the revenue generated on Curve**. A portion of the fees from DEX trades and crvUSD borrowing is collected and distributed weekly to veCRV holders. This allows you to directly benefit from the overall success across the Curve platform.
- [Boosting CRV Rewards (Up to 2.5x)](boosting.md):
If you're already earning CRV tokens from providing liquidity, you can potentially **increase these rewards up to 2.5 times** The more veCRV you hold and the longer you lock your CRV, the higher your boost will be on your eligible CRV earnings.
---
## veCRV Revenue Share
Users who lock their CRV for veCRV will earn a portion of the revenue generated on Curve. **Every week within 24hrs after Thursday 00:00 UTC, the previous week's revenue is being distributed to veCRV holders to claim.** Revenue is mainly generated from two sources:
1. **DEX Trades**: Each time a trade happens on Curve, a portion of trading fees are collected and distributed to veCRV holders.
2. **crvUSD**: When a user borrows crvUSD from a [minting market](https://curve.finance/crvusd), portions of the interest paid is going to veCRV holders.
You can check the current veCRV metrics, estimated earnings, and historical data using the button below, or here: [veCRV Analytics](https://www.curve.finance/dao/ethereum/analytics/)
---
## Savings Vault (scrvUSD)
scrvUSD stands for “savings-crvUSD,” and works like a bank savings account for crvUSD. It is similar to other staked versions of USD stablecoins, such as sDAI, sUSDS, sUSDe, etc. Users can either stake their crvUSD to receive scrvUSD, or buy it directly on the market. Yield accrues automatically, increasing the value of their tokens over time.
scrvUSD is designed with minimal risk in mind. The underlying crvUSD deposited into the vault is not loaned out or deployed into other liquidity pools. Instead, it remains securely within the vault, minimizing risk for scrvUSD holders.
---
## Guides
---
## Current scrvUSD Stats
Current CRV stats directly fetched from
## How scrvUSD Works: Earn Savings on Your crvUSD
Think of scrvUSD as a specialized savings account for your crvUSD stablecoins. When you deposit crvUSD, you receive scrvUSD tokens in return. Over time, your scrvUSD tokens will automatically increase in value, meaning each scrvUSD token will represent more crvUSD than what you deposited.
Here's how it works:
1. **Deposit crvUSD:** Send your crvUSD to the scrvUSD vault and receive scrvUSD tokens back.
2. **Hold scrvUSD:** Simply hold onto your scrvUSD tokens in your wallet. There's no need to stake or lock them.
3. **Earn Interest:** As you hold scrvUSD, its value against crvUSD steadily increases. This means your scrvUSD tokens are worth more crvUSD over time, reflecting the interest earned.
4. **Withdraw crvUSD:** You can withdraw your crvUSD at any time. You'll receive your initial crvUSD deposit plus all the interest you've earned.
### How Much Will You Earn?
The interest rate (or yield) for scrvUSD is variable and adjusts regularly, typically every few days. You can view the current and historical rates directly on the [scrvUSD UI](https://www.curve.finance/crvusd/ethereum/scrvUSD/).
For a detailed explanation of the different rate numbers you see on the interface, please refer to our FAQ: [What do the different APR/APY numbers on the UI mean?](scrvusd.md#what-do-the-different-aprapy-numbers-on-the-ui-mean)
Generally, the interest rate earned by scrvUSD holders is set to roughly match the average crvUSD borrowing rate from the previous week across all [crvUSD minting markets](https://www.curve.finance/crvusd/ethereum/markets/). However, if more than 50% of the total crvUSD supply is held as scrvUSD, the yield may be slightly diluted. You can see the current amount of crvUSD held as scrvUSD on our [Current scrvUSD Stats](../curve-tokens/scrvusd.md#current-scrvusd-stats) page.
### Where Does the Interest Come From?
The interest paid to scrvUSD holders originates from the fees paid by users who mint crvUSD against their collateral. These users pay a continuous borrowing interest rate as long as their loan is open.
scrvUSD receives up to 50% of these borrowing fees, the remaining portion going to the Curve DAO.
### What's the catch, why does Curve give out free money?
scrvUSD plays a crucial role in maintaining crvUSD's peg to $1. Its mechanics are designed to support the stability of the crvUSD stablecoin, making it an integral part of the wider crvUSD ecosystem. Further details on this aspect are explained below.
---
## FAQ
### Where do I go to deposit?
Click below:
### What do the different APR/APY numbers on the UI mean?
When you visit the [scrvUSD UI](https://www.curve.finance/crvusd/ethereum/scrvUSD/), you'll see a dashboard similar to this:
This interface displays several different numbers about the scrvUSD yield. Here's a breakdown of what each number means:
* **Current APY (Annual Percentage Yield):** This is current annual interest rate you would earn, taking into account the effect of **compounding** interest (where your earnings also start earning interest).
* **APR (Annual Percentage Rate):** This shows the current annual interest rate **without** factoring in compounding.
* **7-day MA APR (7-day Moving Average APR):** This is the average simple interest rate (APR, without compounding) calculated over the past 7 days. It helps to smooth out daily fluctuations.
* **Average APR:** This figure shows the average simple interest rate (APR, without compounding) over a user-selected time window. For example, if "1M" (1 month) is chosen in the bottom right corner of the UI, this number will reflect the average APR for the last month.
### Do the rewards mean I get more scrvUSD tokens over time?
No, you will always have the **same number of scrvUSD tokens**. What happens is that **each scrvUSD token becomes worth more crvUSD** as time goes on. So, when you sell your scrvUSD back, you get more crvUSD than you put in.
### Where does the yield come from?
The extra crvUSD you earn comes from **fees paid by people who borrow crvUSD**. A part of these fees is collected and given to scrvUSD holders like you through the system.
### Is scrvUSD locked when I deposit?
No, your scrvUSD is **never locked up**. You can take it out or swap it back to crvUSD whenever you want, without any waiting.
### Can the value of scrvUSD go down?
**No, not in the way you might think.** scrvUSD is designed to **always increase in value compared to crvUSD** because it's constantly earning yield.
However, if you decide to trade your scrvUSD for crvUSD (or another token) on a decentralized exchange (DEX) on any network, the actual price you get might be slightly less than its perfect theoretical value. This can happen because of how much crvUSD is available to trade at that moment (liquidity) or general market activity.
Also, it's always important to remember that while Curve uses the highest standards when creating its smart contracts, using any decentralized finance (DeFi) application, including scrvUSD, involves some risks. You can learn more about these risks here: [scrvUSD risks](../security/risks/scrvusd.md).
### I have scrvUSD on an L2, does it still earn interest?
Yes, absolutely! scrvUSD earns interest no matter which blockchain network it's on (like Ethereum, or an L2 such as Polygon or Arbitrum).
However, remember that the main scrvUSD vault for depositing and withdrawing is **only on Ethereum**. So, if your scrvUSD is on another network and you want to turn it back into crvUSD from the main vault, you'll need to either:
* Move your funds back to the Ethereum network (this is called "bridging").
* Or, you can simply swap your scrvUSD for crvUSD (or any other token) directly on Curve on the network you are currently on.