# 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: ![Simple Stableswap Example](../assets/images/cryptoswap/stableswap-swap.png) 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: ![Stableswap Liquidity Curve](../assets/images/cryptoswap/stableswap-liquidity-curve.png) 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): ![Cryptoswap A and Gamma](../assets/images/cryptoswap/a_and_gamma.png) 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): ![Cryptoswap Liquidity & Rebalances](../assets/images/cryptoswap/cryptoswap-rebalances.png) 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: ![Rebalancing Loss](../assets/images/cryptoswap/rebalancing-loss.png) 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. ![Cryptoswap Stale Liquidity](../assets/images/cryptoswap/cryptoswap-stale-pools.png) ## 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: ![Cryptoswap Pool Details UI](../assets/images/cryptoswap/price-scale.png) 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. ![](pathname://./assets/images/block-oracle/blockhash_approach.png) ## 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. ![](pathname://./assets/images/block-oracle/storage_proof.png) ## 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 ![FastBridge Architecture](../assets/images/fast-bridge/architecture.png) --- ## 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"}] ``` ::: ![](../assets/images/fees/fee_splitter_flow.svg) --- ## 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.