diff --git a/docs/developer/crvusd/amm.md b/docs/developer/crvusd/amm.md index b9238539..23fe840a 100644 --- a/docs/developer/crvusd/amm.md +++ b/docs/developer/crvusd/amm.md @@ -9,7 +9,7 @@ Each market deploys its own AMM from a blueprint contract. Source code is availa ::: :::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). +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 v1 markets. LlamaLend v2 uses a separately deployed AMM implementation; see the [v2 AMM reference](../llamalend-v2/amm.md). ::: :::info Getting familiar with LLAMMA diff --git a/docs/developer/crvusd/controller.md b/docs/developer/crvusd/controller.md index fa5eba37..fb53378e 100644 --- a/docs/developer/crvusd/controller.md +++ b/docs/developer/crvusd/controller.md @@ -15,13 +15,13 @@ Each market deploys its own Controller from a blueprint contract. Source code is ::: :::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. +This page documents **Controller V3** (Vyper 0.3.10), the current blueprint used for new crvUSD mint markets and **LlamaLend v1** markets. LlamaLend v2 uses a separate Vyper 0.4.3 `LendController`; see the [v2 reference](../llamalend-v2/lend-controller.md). 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** | +| **V3** | **0.3.10** | **weETH, cbBTC, LBTC + LlamaLend v1** | For full version details, see the [crvUSD Overview](./overview.md#controller--amm-versions). ::: @@ -36,9 +36,9 @@ For full version details, see the [crvUSD Overview](./overview.md#controller--am 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. +In LlamaLend v1 markets, one asset must be 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. LlamaLend v2 supports any ERC-20 pair. @@ -59,7 +59,7 @@ $$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. +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 LlamaLend v1 markets. ::: @@ -870,7 +870,7 @@ def deposit_range(user: address, amount: uint256, n1: int256, n2: int256): :::info[V3 Only] -Introduced in Controller V3 (Vyper 0.3.10). Available in weETH, cbBTC, LBTC markets and all Llamalend markets. +Introduced in Controller V3 (Vyper 0.3.10). Available in weETH, cbBTC, LBTC markets and LlamaLend v1 markets. ::: @@ -912,7 +912,7 @@ extra_health: public(HashMap[address, uint256]) :::info[V3 Only] -Introduced in Controller V3 (Vyper 0.3.10). Available in weETH, cbBTC, LBTC markets and all Llamalend markets. +Introduced in Controller V3 (Vyper 0.3.10). Available in weETH, cbBTC, LBTC markets and LlamaLend v1 markets. ::: @@ -960,7 +960,7 @@ def 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. +Introduced in Controller V3 (Vyper 0.3.10). Available in weETH, cbBTC, LBTC markets and LlamaLend v1 markets. ::: @@ -1001,7 +1001,7 @@ False :::info[V3 Only] -Introduced in Controller V3 (Vyper 0.3.10). Available in weETH, cbBTC, LBTC markets and all Llamalend markets. +Introduced in Controller V3 (Vyper 0.3.10). Available in weETH, cbBTC, LBTC markets and LlamaLend v1 markets. ::: diff --git a/docs/developer/crvusd/overview.md b/docs/developer/crvusd/overview.md index aea2a1e7..229f2d43 100644 --- a/docs/developer/crvusd/overview.md +++ b/docs/developer/crvusd/overview.md @@ -76,7 +76,7 @@ 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. +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 v1** adopted the V3 blueprint, so its markets share the same Controller/AMM version as the latest crvUSD markets. LlamaLend v2 uses a separate Vyper 0.4.3 modular architecture. ### Version Matrix @@ -87,7 +87,7 @@ The crvUSD mint markets use several iterations of the Controller and AMM (LLAMMA | **V3** | 0.3.10 | `0xe3e...415` ([Etherscan](https://etherscan.io/address/0xe3e3Fb7E9f48d26817b7210C9bD6B22744790415)) | `0x2B7...e9` ([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. +V3 is the **current blueprint** set on the Controller Factory. It is also the version used by [LlamaLend v1](../lending/overview.md) markets; see [LlamaLend v2](../llamalend-v2/overview.md) for the newer lending architecture. ::: ### Market → Version Mapping @@ -113,13 +113,13 @@ V3 is the **current blueprint** set on the Controller Factory. It is also the ve #### 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. +V3 is a significant upgrade that was developed alongside LlamaLend v1. The same blueprint is shared by crvUSD mint markets and LlamaLend v1 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. +- **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 v1's flexible token support. +- **`collect_fees()` disabled for lending**: In LlamaLend v1 markets, admin fees are zero and all interest goes to vault depositors. LlamaLend v2 has separately configurable admin percentages. - **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. +- **`check_lock` / `save_rate`**: New external helpers used by the Vault contract in LlamaLend v1. diff --git a/docs/developer/documentation-overview.md b/docs/developer/documentation-overview.md index bdcb1aa3..73405ff5 100644 --- a/docs/developer/documentation-overview.md +++ b/docs/developer/documentation-overview.md @@ -7,6 +7,12 @@ Additionally, Curve has launched its own stablecoin, **crvUSD**, and **Curve Len 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. +:::warning[LlamaLend v1 is deprecated] + +LlamaLend v1 markets are being phased out and no new v1 markets will be deployed. Use [LlamaLend v2](./llamalend-v2/overview.md) for all new markets and integrations. + +::: + :::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. @@ -41,9 +47,14 @@ Over-collateralized USD stablecoin powered by a unique liquidating algorithm ([L Savings version of crvUSD. An ERC-4626 compliant Vault that earns yield from crvUSD interest fees, with cross-chain oracle support. - + + +Permissionless isolated lending markets for any ERC-20 pair, with ERC-4626 vaults, a LendFactory, Configurator, and LLAMMA soft liquidations. + + + -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). +Historical reference for the earlier crvUSD-paired lending contracts. Use the v2 reference for new integrations. diff --git a/docs/developer/lending/contracts/controller-llamma.md b/docs/developer/lending/contracts/controller-llamma.md index 2445b560..2c5fd055 100644 --- a/docs/developer/lending/contracts/controller-llamma.md +++ b/docs/developer/lending/contracts/controller-llamma.md @@ -2,13 +2,19 @@ import DocCard, { DocCardGrid } from '@site/src/components/DocCard' # LLAMMA and Controller +:::warning + +This page documents the LlamaLend v1 Controller V3 and AMM contracts. For LlamaLend v2, use the [LendController](../../llamalend-v2/lend-controller.md) and [AMM](../../llamalend-v2/amm.md) references. + +::: + :::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). +LlamaLend v1 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**: diff --git a/docs/developer/lending/contracts/leverage.md b/docs/developer/lending/contracts/leverage.md index ec26b44f..3fe31930 100644 --- a/docs/developer/lending/contracts/leverage.md +++ b/docs/developer/lending/contracts/leverage.md @@ -1,4 +1,11 @@ # Building Leverage + +:::warning + +This page covers LlamaLend v1 leverage integrations. Validate v2 compatibility against the [LlamaLend v2 contracts](../../llamalend-v2/overview.md) before reusing a zap or callback flow. + +::: + There are multiple ways on how to create automated leverage for lending markets: - v1: using the [Curve Pools](../../crvusd/leverage/leverage-zap.md) diff --git a/docs/developer/lending/contracts/mp-overview.md b/docs/developer/lending/contracts/mp-overview.md index a5bef802..5ea3a2c4 100644 --- a/docs/developer/lending/contracts/mp-overview.md +++ b/docs/developer/lending/contracts/mp-overview.md @@ -1,6 +1,13 @@ import DocCard, { DocCardGrid } from '@site/src/components/DocCard' # Monetary Policies: Overview + +:::warning + +These are LlamaLend v1 monetary-policy contracts. LlamaLend v2 markets configure a policy through the [Configurator](../../llamalend-v2/configurator.md). + +::: + 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:* diff --git a/docs/developer/lending/contracts/oneway-factory.md b/docs/developer/lending/contracts/oneway-factory.md index 7ce23ab7..ca66b7ac 100644 --- a/docs/developer/lending/contracts/oneway-factory.md +++ b/docs/developer/lending/contracts/oneway-factory.md @@ -1,5 +1,11 @@ # OneWay Lending Factory +:::warning + +This is the LlamaLend v1 factory. For new LlamaLend v2 markets, use the [LendFactory reference](../../llamalend-v2/lend-factory.md). + +::: + import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; diff --git a/docs/developer/lending/contracts/oracle-overview.md b/docs/developer/lending/contracts/oracle-overview.md index 6a91fac7..c1728bd4 100644 --- a/docs/developer/lending/contracts/oracle-overview.md +++ b/docs/developer/lending/contracts/oracle-overview.md @@ -1,6 +1,13 @@ import DocCard, { DocCardGrid } from '@site/src/components/DocCard' # Lending Oracles: Overview + +:::warning + +These oracle contracts are documented for LlamaLend v1. For LlamaLend v2 market configuration, see the [Configurator](../../llamalend-v2/configurator.md). + +::: + *There are two main contracts for lending oracles:* diff --git a/docs/developer/lending/contracts/vault.md b/docs/developer/lending/contracts/vault.md index 3563181b..4629545a 100644 --- a/docs/developer/lending/contracts/vault.md +++ b/docs/developer/lending/contracts/vault.md @@ -1,5 +1,11 @@ # Vault +:::warning + +This page documents the LlamaLend v1 Vault. For the v2 ERC-4626 implementation, see the [LlamaLend v2 Vault reference](../../llamalend-v2/vault.md). + +::: + import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; diff --git a/docs/developer/lending/overview.md b/docs/developer/lending/overview.md index 669a0c91..2a658b5f 100644 --- a/docs/developer/lending/overview.md +++ b/docs/developer/lending/overview.md @@ -1,5 +1,12 @@ -# 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. +# LlamaLend v1: Legacy Overview + +:::warning + +This section documents **LlamaLend v1** contracts, including the crvUSD-pair requirement and the Vyper 0.3.x Controller/AMM design. LlamaLend v1 markets are being phased out: **no new v1 markets will be deployed**. Any new market must use [LlamaLend v2](../llamalend-v2/overview.md). + +::: + +LlamaLend v1 allowed 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. Existing 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. diff --git a/docs/developer/llamalend-v2/amm.md b/docs/developer/llamalend-v2/amm.md new file mode 100644 index 00000000..6226c01b --- /dev/null +++ b/docs/developer/llamalend-v2/amm.md @@ -0,0 +1,2289 @@ +# AMM (LLAMMA) + +The `AMM` contract implements **LLAMMA** — the Lending-Liquidating AMM Algorithm. It holds borrower collateral distributed across discretized **price bands** and performs **soft liquidation**: as the collateral price drops into a borrower's band range, the AMM gradually converts their collateral to the borrowed token. If the price recovers, the process reverses (**de-liquidation**). + +The AMM also serves as a regular DEX — arbitrageurs can swap between the collateral and borrowed tokens, which is the mechanism that drives soft liquidation. + +Each lending market has its own AMM instance. The associated [LendController](./lend-controller.md) is the AMM's `admin` and is the only address allowed to deposit/withdraw collateral. + +:::vyper[`AMM.vy`] + +The source code for the `AMM.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/AMM.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.4.3`. + +*Deployment addresses will be added once contracts are finalized.* + +::: + +Examples use Ethers v6 with an ABI generated from the pinned source linked in each panel. Replace every zero address and amount placeholder. Read-only calls use the verified Ethereum v2 reference market; execute a write only on a fork first. + + +--- + + +## Swaps + +### `exchange` + +::::description[`AMM.exchange(i: uint256, j: uint256, in_amount: uint256, min_amount: uint256, _for: address) -> uint256[2]`] + +Swaps `in_amount` of token `i` for token `j`. Token index 0 is the borrowed token, index 1 is the collateral token. + +| Input | Type | Description | +| --- | --- | --- | +| `i` | `uint256` | Input token index (0 = borrowed, 1 = collateral) | +| `j` | `uint256` | Output token index | +| `in_amount` | `uint256` | Amount of input token | +| `min_amount` | `uint256` | Minimum output amount (slippage protection) | +| `_for` | `address` | Address to receive the output tokens | + +Returns: `[in_amount_done, out_amount]` (`uint256[2]`). + +Emits: `TokenExchange` event. + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@nonreentrant +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) + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.exchange( + /* i: uint256 */ 0n, + /* j: uint256 */ 0n, + /* in_amount: uint256 */ 0n, + /* min_amount: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", +) +await tx.wait() +``` + + + +:::: + +### `exchange_dy` + +::::description[`AMM.exchange_dy(i: uint256, j: uint256, out_amount: uint256, max_amount: uint256, _for: address) -> uint256[2]`] + +Swaps to receive exactly `out_amount` of token `j`, paying at most `max_amount` of token `i`. + +| Input | Type | Description | +| --- | --- | --- | +| `i` | `uint256` | Input token index | +| `j` | `uint256` | Output token index | +| `out_amount` | `uint256` | Desired output amount | +| `max_amount` | `uint256` | Maximum input amount (slippage protection) | +| `_for` | `address` | Address to receive the output tokens | + +Returns: `[in_amount, out_amount_done]` (`uint256[2]`). `out_amount_done` can be less than `out_amount` when the AMM does not have enough liquidity. + +Emits: `TokenExchange` event. + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@nonreentrant +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 +@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 (borrowed) if we trade adiabatically down + @param user User the amount is calculated for + @param use_y Calculate amount of collateral if True and of borrowed 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: uint256 in range(MAX_TICKS_UINT): +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.exchange_dy( + /* i: uint256 */ 0n, + /* j: uint256 */ 0n, + /* out_amount: uint256 */ 0n, + /* max_amount: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `get_dy` + +::::description[`AMM.get_dy(i: uint256, j: uint256, in_amount: uint256) -> uint256: view`] + +Calculates the output amount for a given input amount (i.e., a swap quote). + +| Input | Type | Description | +| --- | --- | --- | +| `i` | `uint256` | Input token index | +| `j` | `uint256` | Output token index | +| `in_amount` | `uint256` | Input amount | + +Returns: expected output amount (`uint256`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +@nonreentrant +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 + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.get_dy( + /* i: uint256 */ 0n, + /* j: uint256 */ 0n, + /* in_amount: uint256 */ 0n, +) +``` + + + +:::: + +### `get_dx` + +::::description[`AMM.get_dx(i: uint256, j: uint256, out_amount: uint256) -> uint256: view`] + +Calculates the input amount needed to receive a given output amount. + +| Input | Type | Description | +| --- | --- | --- | +| `i` | `uint256` | Input token index | +| `j` | `uint256` | Output token index | +| `out_amount` | `uint256` | Desired output amount | + +Returns: required input amount (`uint256`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +@nonreentrant +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: IAMM.DetailedTrade = self._get_dxdy(i, j, out_amount, False) + assert trade.out_amount == out_amount + return trade.in_amount + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.get_dx( + /* i: uint256 */ 0n, + /* j: uint256 */ 0n, + /* out_amount: uint256 */ 0n, +) +``` + + + +:::: + +### `get_dxdy` + +::::description[`AMM.get_dxdy(i: uint256, j: uint256, in_amount: uint256) -> uint256[2]: view`] + +Returns both the input amount used and the output amount for a given swap. + +| Input | Type | Description | +| --- | --- | --- | +| `i` | `uint256` | Input token index | +| `j` | `uint256` | Output token index | +| `in_amount` | `uint256` | Input amount | + +Returns: `[in_amount_done, out_amount]` (`uint256[2]`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +@nonreentrant +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: IAMM.DetailedTrade = self._get_dxdy(i, j, in_amount, True) + return (out.in_amount, out.out_amount) + + +@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: ILMCallback = self._liquidity_mining_callback + collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = [] + + in_coin: IERC20 = BORROWED_TOKEN + out_coin: IERC20 = COLLATERAL_TOKEN + in_precision: uint256 = BORROWED_PRECISION +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.get_dxdy( + /* i: uint256 */ 0n, + /* j: uint256 */ 0n, + /* in_amount: uint256 */ 0n, +) +``` + + + +:::: + +### `get_dydx` + +::::description[`AMM.get_dydx(i: uint256, j: uint256, out_amount: uint256) -> uint256[2]: view`] + +Returns both the output amount and the input amount needed for a given desired output. + +| Input | Type | Description | +| --- | --- | --- | +| `i` | `uint256` | Input token index | +| `j` | `uint256` | Output token index | +| `out_amount` | `uint256` | Desired output amount | + +Returns: `[out_amount_done, in_amount]` (`uint256[2]`). `out_amount_done` can be less than `out_amount` when the AMM does not have enough liquidity. + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +@nonreentrant +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: IAMM.DetailedTrade = self._get_dxdy(i, j, out_amount, False) + return (out.out_amount, out.in_amount) + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.get_dydx( + /* i: uint256 */ 0n, + /* j: uint256 */ 0n, + /* out_amount: uint256 */ 0n, +) +``` + + + +:::: + +### `get_amount_for_price` + +::::description[`AMM.get_amount_for_price(_p: uint256) -> (uint256, bool): view`] + +Returns the amount and swap direction needed to move the AMM price to `_p`. + +| Input | Type | Description | +| --- | --- | --- | +| `_p` | `uint256` | Target price, scaled by `1e18` | + +Returns: `[amount, is_pump]`. If `is_pump` is true, swap borrowed token for collateral and `amount` is the required borrowed-token input. If false, swap collateral for borrowed token and `amount` is the required collateral-token input. + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +@nonreentrant +def get_amount_for_price(p: uint256) -> (uint256, bool): + """ + @notice Amount necessary to be exchanged to have the AMM at the final price `p` + @param p Target price to reach, in units of borrowed token per collateral multiplied by 1e18 + @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 + + fee: uint256 = max(self.fee, p_o[1]) + + for i: uint256 in range(MAX_TICKS_UINT + MAX_SKIP_TICKS_UINT): + 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 + dynamic_fee: uint256 = fee + not_empty: bool = x > 0 or y > 0 + + if not_empty: + y0 = self._get_y0(x, y, p_o[0], p_o_up) +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.get_amount_for_price( + /* _p: uint256 */ 0n, +) +``` + + + +:::: + + +--- + + +## Price and Band Info + +### `price_oracle` + +::::description[`AMM.price_oracle() -> uint256: view`] + +Returns the external oracle price, clamped to the maximum allowed relative change. + +Returns: oracle price (`uint256`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +def price_oracle() -> uint256: + """ + @notice Value returned by the external price oracle contract + @return Current price of collateral in units of borrowed token, multiplied by 1e18 + """ + return self._price_oracle_ro()[0] + + +@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) + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.price_oracle() +``` + + + +:::: + +### `price_oracle_contract` + +::::description[`AMM.price_oracle_contract() -> address: view`] + +Returns the address of the external price oracle contract. + +Returns: oracle address (`address`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@view +@external +def price_oracle_contract() -> IPriceOracle: + return self._price_oracle + + +old_p_o: uint256 +old_dfee: uint256 +prev_p_o_time: uint256 +PREV_P_O_DELAY: constant(uint256) = 2 * 60 # s = 2 min +MAX_P_O_CHG: constant(uint256) = 12500 * 10**14 # <= 2**(1/3) - max relative change to have fee < 50% + +bands_x: public(HashMap[int256, uint256]) +bands_y: public(HashMap[int256, uint256]) + +total_shares: HashMap[int256, uint256] +_user_shares: HashMap[address, IAMM.UserTicks] + + +_liquidity_mining_callback: ILMCallback + +# https://github.com/vyperlang/vyper/issues/4721 +@view +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.price_oracle_contract() +``` + + + +:::: + +### `get_p` + +::::description[`AMM.get_p() -> uint256: view`] + +Returns the current AMM price (the marginal price at the active band). + +Returns: current price (`uint256`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +@nonreentrant +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 _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] + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.get_p() +``` + + + +:::: + +### `get_base_price` + +::::description[`AMM.get_base_price() -> uint256: view`] + +Returns the base price used to calculate band boundaries, adjusted for accumulated interest rate. + +Returns: base price (`uint256`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@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 Base price in units of borrowed token per collateral, multiplied by 1e18 + """ + return self._base_price() + + +@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_result: uint256 = convert(math._wad_exp(power), uint256) + + assert exp_result > 1000 # dev: limit precision of the multiplier + return unsafe_div(self._base_price() * exp_result, WAD) + + +@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) +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.get_base_price() +``` + + + +:::: + +### `active_band` + +::::description[`AMM.active_band() -> int256: view`] + +Returns the index of the currently active band (the band where trading is happening). + +Returns: active band index (`int256`). + + + +The implementation is in [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.active_band() +await tx.wait() +``` + + + +:::: + +### `active_band_with_skip` + +::::description[`AMM.active_band_with_skip() -> int256: view`] + +Returns the active band, skipping over empty bands for gas efficiency. + +Returns: active band index (`int256`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +@nonreentrant +def active_band_with_skip() -> int256: + n0: int256 = self.active_band + n: int256 = n0 + min_band: int256 = self.min_band + for i: uint256 in range(MAX_SKIP_TICKS_UINT): + if n < min_band: + n = n0 - MAX_SKIP_TICKS + break + if self.bands_x[n] != 0: + break + n -= 1 + return n + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.active_band_with_skip() +``` + + + +:::: + +### `min_band` + +::::description[`AMM.min_band() -> int256: view`] + +Returns the lowest band index that has ever been used. + +Returns: minimum band index (`int256`). + + + +The implementation is in [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.min_band() +await tx.wait() +``` + + + +:::: + +### `max_band` + +::::description[`AMM.max_band() -> int256: view`] + +Returns the highest band index that has ever been used. + +Returns: maximum band index (`int256`). + + + +The implementation is in [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.max_band() +await tx.wait() +``` + + + +:::: + +### `p_current_up` + +::::description[`AMM.p_current_up(_n: int256) -> uint256: view`] + +Returns the upper price boundary of band `_n`, accounting for the current state of the band. + +| Input | Type | Description | +| --- | --- | --- | +| `_n` | `int256` | Band index | + +Returns: upper price boundary (`uint256`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```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) + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.p_current_up( + /* _n: int256 */ 0n, +) +``` + + + +:::: + +### `p_current_down` + +::::description[`AMM.p_current_down(_n: int256) -> uint256: view`] + +Returns the lower price boundary of band `_n`, accounting for the current state of the band. + +| Input | Type | Description | +| --- | --- | --- | +| `_n` | `int256` | Band index | + +Returns: lower price boundary (`uint256`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```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) + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.p_current_down( + /* _n: int256 */ 0n, +) +``` + + + +:::: + +### `p_oracle_up` + +::::description[`AMM.p_oracle_up(_n: int256) -> uint256: view`] + +Returns the upper boundary of band `_n` in the AMM's fixed price grid. Band numbers may be negative; higher band numbers correspond to lower prices, and positive bands lie to the left of the base price. + +| Input | Type | Description | +| --- | --- | --- | +| `_n` | `int256` | Band index | + +Returns: upper oracle price (`uint256`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```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) + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.p_oracle_up( + /* _n: int256 */ 0n, +) +``` + + + +:::: + +### `p_oracle_down` + +::::description[`AMM.p_oracle_down(_n: int256) -> uint256: view`] + +Returns the lower boundary of band `_n` in the AMM's fixed price grid. The boundaries are `p_base * ((A - 1) / A) ** n` and `p_base * ((A - 1) / A) ** (n + 1)` respectively. + +| Input | Type | Description | +| --- | --- | --- | +| `_n` | `int256` | Band index | + +Returns: lower oracle price (`uint256`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```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 _get_y0(x: uint256, y: uint256, p_o: uint256, p_o_up: uint256) -> uint256: + """ + @notice Calculate y0 for the invariant based on current liquidity in band. + The value of y0 has a meaning of amount of collateral when band has no borrowed tokens + but current price is equal to both oracle price and upper band price. + @param x Amount of borrowed in band + @param y Amount of collateral in band + @param p_o External oracle price + @param p_o_up Upper boundary of the band + @return y0 + """ + assert p_o != 0 + # solve: + # p_o * A * y0**2 - y0 * (p_oracle_up/p_o * (A-1) * x + p_o**2/p_oracle_up * A * y) - xy = 0 + b: uint256 = 0 + # p_o_up * unsafe_sub(A, 1) * x / p_o + A * p_o**2 / p_o_up * y / 10**18 + if x != 0: + b = unsafe_div(p_o_up * Aminus1 * x, p_o) + if y != 0: + b += unsafe_div(A * p_o**2 // p_o_up * y, 10**18) + if x > 0 and y > 0: + D: uint256 = b**2 + unsafe_div((unsafe_mul(4, A) * p_o) * y, 10**18) * x + return unsafe_div((b + self.sqrt_int(D)) * 10**18, unsafe_mul(unsafe_mul(2, A), p_o)) + else: + return unsafe_div(b * 10**18, unsafe_mul(A, p_o)) +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.p_oracle_down( + /* _n: int256 */ 0n, +) +``` + + + +:::: + +## User Position Info + +### `get_y_up` + +::::description[`AMM.get_y_up(_user: address) -> uint256: view`] + +Returns the amount of collateral token a user would have if the price goes up (full de-liquidation scenario). + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | User address | + +Returns: collateral amount (`uint256`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +@nonreentrant +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) + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.get_y_up( + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `get_x_down` + +::::description[`AMM.get_x_down(_user: address) -> uint256: view`] + +Returns the amount of borrowed token a user would have if the price goes down (full soft-liquidation scenario). + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | User address | + +Returns: borrowed token amount (`uint256`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +@nonreentrant +def get_x_down(user: address) -> uint256: + """ + @notice Measure the amount of x (borrowed) 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(user: address, is_sum: bool) -> DynArray[uint256, MAX_TICKS_UINT][2]: + """ + @notice A low-gas function to measure amounts of borrowed and collateral tokens which user currently owns + @param user User address + @param is_sum Return sum or amounts by bands + @return Amounts of (borrowed, 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: uint256 in range(MAX_TICKS_UINT): + 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)) +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.get_x_down( + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `get_sum_xy` + +::::description[`AMM.get_sum_xy(_user: address) -> uint256[2]: view`] + +Returns the total amounts of borrowed token and collateral token held in a user's bands. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | User address | + +Returns: `[sum_x (borrowed), sum_y (collateral)]` (`uint256[2]`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +@nonreentrant +def get_sum_xy(user: address) -> uint256[2]: + """ + @notice A low-gas function to measure amounts of borrowed and collateral tokens which user currently owns + @param user User address + @return Amounts of (borrowed, collateral) in a tuple + """ + xy: DynArray[uint256, MAX_TICKS_UINT][2] = self._get_xy(user, True) + return [xy[0][0], xy[1][0]] + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.get_sum_xy( + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `get_xy` + +::::description[`AMM.get_xy(_user: address) -> IController.UserTicks[]: view`] + +Returns the per-band breakdown of a user's position — the amount of borrowed and collateral tokens in each band. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | User address | + +Returns: array of per-band positions (`IController.UserTicks[]`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +@nonreentrant +def get_xy(user: address) -> DynArray[uint256, MAX_TICKS_UINT][2]: + """ + @notice A low-gas function to measure amounts of borrowed and collateral tokens by bands which user currently owns + @param user User address + @return Amounts of (borrowed, collateral) by bands in a tuple + """ + return self._get_xy(user, False) + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.get_xy( + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `read_user_tick_numbers` + +::::description[`AMM.read_user_tick_numbers(_user: address) -> int256[2]: view`] + +Returns the upper and lower band indices of a user's position. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | User address | + +Returns: `[n1, n2]` (`int256[2]`), where `n1 < n2` and the corresponding upper boundary is at a higher price than the lower boundary. + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +@nonreentrant +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_ticks(user: address, ns: int256[2]) -> DynArray[uint256, MAX_TICKS_UINT]: + """ + @notice Unpacks and reads user ticks (shares) for all the ticks user deposited into + @param user User address + @param ns Pair of band indices [n1, n2] defining the user's position range + @return Array of shares the user has + """ + ticks: DynArray[uint256, MAX_TICKS_UINT] = [] + size: uint256 = convert(ns[1] - ns[0] + 1, uint256) + for i: uint256 in range(MAX_TICKS_UINT // 2): + if len(ticks) == size: + break + tick: uint256 = self._user_shares[user].ticks[i] + ticks.append(tick & (2**128 - 1)) + if len(ticks) == size: + break + ticks.append(tick >> 128) + return ticks + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.read_user_tick_numbers( + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `has_liquidity` + +::::description[`AMM.has_liquidity(_user: address) -> bool: view`] + +Returns whether a user has any liquidity deposited in the AMM. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | User address | + +Returns: has liquidity (`bool`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +@nonreentrant +def has_liquidity(user: address) -> bool: + """ + @notice Check if `user` has any liquidity in the AMM + @param user Address of the user to check + @return True if the user has an active position, False otherwise + """ + return self._user_shares[user].ticks[0] != 0 + + +@internal +def save_user_shares(user: address, user_shares: DynArray[uint256, MAX_TICKS_UINT]): + ptr: uint256 = 0 + for j: uint256 in range(MAX_TICKS_UINT // 2): + if ptr >= len(user_shares): + break + tick: uint256 = user_shares[ptr] + ptr = unsafe_add(ptr, 1) + if len(user_shares) != ptr: + tick = tick | (user_shares[ptr] << 128) + ptr = unsafe_add(ptr, 1) + self._user_shares[user].ticks[j] = tick + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.has_liquidity( + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `bands_x` + +::::description[`AMM.bands_x(_n: int256) -> uint256: view`] + +Returns the total amount of borrowed token in band `_n`, normalized to 18 decimals (not the token's native decimals). + +| Input | Type | Description | +| --- | --- | --- | +| `_n` | `int256` | Band index | + +Returns: borrowed token amount in band (`uint256`). + + + +The implementation is in [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.bands_x( + /* _n: int256 */ 0n, +) +await tx.wait() +``` + + + +:::: + +### `bands_y` + +::::description[`AMM.bands_y(_n: int256) -> uint256: view`] + +Returns the total amount of collateral token in band `_n`, normalized to 18 decimals (not the token's native decimals). + +| Input | Type | Description | +| --- | --- | --- | +| `_n` | `int256` | Band index | + +Returns: collateral token amount in band (`uint256`). + + + +The implementation is in [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.bands_y( + /* _n: int256 */ 0n, +) +await tx.wait() +``` + + + +:::: + +### `user_shares` + +::::description[`AMM.user_shares(_user: address) -> UserTicks: view`] + +Returns a user's share information across their deposited bands. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | User address | + +Returns: user tick/share info (`UserTicks`). + + + +The implementation is in [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.user_shares( + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +await tx.wait() +``` + + + +:::: + + +--- + + +## Rate and Fee + +### `rate` + +::::description[`AMM.rate() -> uint256: view`] + +Returns the current interest rate stored in the AMM, set by the controller from the monetary policy. + +Returns: current rate (`uint256`). + + + +The implementation is in [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.rate() +await tx.wait() +``` + + + +:::: + +### `get_rate_mul` + +::::description[`AMM.get_rate_mul() -> uint256: view`] + +Returns the current rate multiplier — a cumulative value that tracks how much the base price has shifted due to interest accrual. + +Returns: rate multiplier (`uint256`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```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 _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 Base price in units of borrowed token per collateral, multiplied by 1e18 + """ + return unsafe_div(BASE_PRICE * self._rate_mul(), 10**18) + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.get_rate_mul() +``` + + + +:::: + +### `fee` + +::::description[`AMM.fee() -> uint256: view`] + +Returns the base AMM swap fee (the minimum fee before any dynamic component). + +Returns: fee (`uint256`). + + + +The implementation is in [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.fee() +await tx.wait() +``` + + + +:::: + + +--- + + +## Admin Methods + +These functions are restricted to the AMM's `admin`, which is the LendController. + +### `set_admin` + +::::description[`AMM.set_admin(_admin: address)`] + +:::guard[Guarded Method] +This function is only callable by the current `admin` (the LendController). +::: + +Transfers AMM admin to a new address. + +| Input | Type | Description | +| --- | --- | --- | +| `_admin` | `address` | New admin address | + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```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 + tkn.max_approve(BORROWED_TOKEN, _admin) + tkn.max_approve(COLLATERAL_TOKEN, _admin) + + +@internal +@pure +def sqrt_int(_x: uint256) -> uint256: + """ + @notice Wrapping isqrt builtin because otherwise it will be repeated every time instead of calling + @param _x Square root's input in "normal" units, e.g. sqrt_int(1) == 1 + @return Integer square root of _x + """ + return isqrt(_x) + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.set_admin( + /* _admin: address */ "0x0000000000000000000000000000000000000000", +) +await tx.wait() +``` + + + +:::: + +### `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` (LendController). It is called internally when a loan is created or collateral is added. +::: + +Deposits collateral for a user across bands `_n1` to `_n2`. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | User address | +| `_amount` | `uint256` | Collateral amount | +| `_n1` | `int256` | Upper band index | +| `_n2` | `int256` | Lower band index | + +Emits: `Deposit` event. + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@nonreentrant +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: ILMCallback = self._liquidity_mining_callback + + # Autoskip bands if we can + for i: uint256 in range(MAX_SKIP_TICKS_UINT + 1): + if n1 > n0: + if i != 0: + self.active_band = n0 + break +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.deposit_range( + /* _user: address */ "0x0000000000000000000000000000000000000000", + /* _amount: uint256 */ 0n, + /* _n1: int256 */ 0n, + /* _n2: int256 */ 0n, +) +await tx.wait() +``` + + + +:::: + +### `withdraw` + +::::description[`AMM.withdraw(_user: address, _frac: uint256) -> uint256[2]`] + +:::guard[Guarded Method] +This function is only callable by the `admin` (LendController). It is called internally during repayment and liquidation. +::: + +Withdraws a fraction of a user's position from the AMM. A `_frac` of `10**18` means full withdrawal. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | User address | +| `_frac` | `uint256` | Fraction to withdraw (scaled by 1e18) | + +Returns: `[withdrawn_borrowed, withdrawn_collateral]` (`uint256[2]`). + +Emits: `Withdraw` event. + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@nonreentrant +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 [borrowed, collateral] withdrawn + """ + assert msg.sender == self.admin + assert frac <= 10**18 + + lm: ILMCallback = self._liquidity_mining_callback + + ns: int256[2] = self._read_user_tick_numbers(user) + n: int256 = ns[0] + old_user_shares: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns) + user_shares: DynArray[uint256, MAX_TICKS_UINT] = old_user_shares + 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: uint256 in range(MAX_TICKS_UINT): + 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) +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.withdraw( + /* _user: address */ "0x0000000000000000000000000000000000000000", + /* _frac: uint256 */ 0n, +) +await tx.wait() +``` + + + +:::: + +### `set_rate` + +::::description[`AMM.set_rate(_rate: uint256) -> uint256`] + +:::guard[Guarded Method] +This function is only callable by the `admin` (LendController). +::: + +Sets the interest rate in the AMM and returns the updated rate multiplier. + +| Input | Type | Description | +| --- | --- | --- | +| `_rate` | `uint256` | New rate value | + +Returns: updated rate multiplier (`uint256`). + +Emits: `SetRate` event. + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@nonreentrant +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 IAMM.SetRate(rate=rate, rate_mul=rate_mul, time=block.timestamp) + return rate_mul + + +@internal +def _set_fee(_fee: uint256): + assert _fee >= MIN_FEE and _fee <= MAX_FEE # dev: fee is out of bounds + self.fee = _fee + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.set_rate( + /* _rate: uint256 */ 0n, +) +await tx.wait() +``` + + + +:::: + +### `set_fee` + +::::description[`AMM.set_fee(_fee: uint256)`] + +:::guard[Guarded Method] +This function is only callable by the `admin` (LendController). +::: + +Sets the swap fee. + +| Input | Type | Description | +| --- | --- | --- | +| `_fee` | `uint256` | New fee value | + +Emits: `SetFee` event. + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@nonreentrant +def set_fee(fee: uint256): + """ + @notice Set AMM fee + @param fee Fee where 1e18 == 100% + """ + assert msg.sender == self.admin + self._set_fee(fee) + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.set_fee( + /* _fee: uint256 */ 0n, +) +await tx.wait() +``` + + + +:::: + +### `set_price_oracle` + +::::description[`AMM.set_price_oracle(_price_oracle: address)`] + +:::guard[Guarded Method] +This function is only callable by the `admin` (LendController). +::: + +Sets the external price oracle contract. + +| Input | Type | Description | +| --- | --- | --- | +| `_price_oracle` | `address` | New oracle address | + +Emits: `SetPriceOracle` event. + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@nonreentrant +def set_price_oracle(_price_oracle: IPriceOracle): + """ + @notice Set a new price oracle contract. Can only be called by admin (Controller) + @param _price_oracle New price oracle contract + """ + assert msg.sender == self.admin + self._price_oracle = _price_oracle + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.set_price_oracle( + /* _price_oracle: address */ "0x0000000000000000000000000000000000000000", +) +await tx.wait() +``` + + + +:::: + +### `set_callback` + +::::description[`AMM.set_callback(_liquidity_mining_callback: address)`] + +:::guard[Guarded Method] +This function is only callable by the `admin` (LendController). +::: + +Sets the liquidity mining callback (gauge) address. + +| Input | Type | Description | +| --- | --- | --- | +| `_liquidity_mining_callback` | `address` | Callback address | + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@nonreentrant +def set_callback(liquidity_mining_callback: ILMCallback): + """ + @notice Set a gauge address with callbacks for liquidity mining for collateral + @param liquidity_mining_callback Gauge address + """ + assert msg.sender == self.admin # dev: admin only + self._liquidity_mining_callback = liquidity_mining_callback + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.set_callback( + /* _liquidity_mining_callback: address */ "0x0000000000000000000000000000000000000000", +) +await tx.wait() +``` + + + +:::: + + +--- + + +## Other Methods + +### `coins` + +::::description[`AMM.coins(_i: uint256) -> address: view`] + +Returns the token address at index `_i` (0 = borrowed token, 1 = collateral token). + +| Input | Type | Description | +| --- | --- | --- | +| `_i` | `uint256` | Token index (0 or 1) | + +Returns: token address (`address`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +def coins(i: uint256) -> address: + return [BORROWED_TOKEN.address, COLLATERAL_TOKEN.address][i] + + +@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. + @param p Current 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 + # 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: +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.coins( + /* _i: uint256 */ 0n, +) +``` + + + +:::: + +### `A` + +::::description[`AMM.A() -> uint256: view`] + +Returns the band-width factor. Band width is approximately `1/A`. + +Returns: band-width factor (`uint256`). + + + +The implementation is in [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.A() +await tx.wait() +``` + + + +:::: + +### `admin` + +::::description[`AMM.admin() -> address: view`] + +Returns the admin address (the LendController). + +Returns: admin address (`address`). + + + +The implementation is in [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, signer) +const tx = await contract.admin() +await tx.wait() +``` + + + +:::: + +### `can_skip_bands` + +::::description[`AMM.can_skip_bands(_n_end: int256) -> bool: view`] + +Returns whether bands up to `_n_end` can be skipped (are empty) during traversal. + +| Input | Type | Description | +| --- | --- | --- | +| `_n_end` | `int256` | Target band index | + +Returns: whether bands can be skipped (`bool`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@external +@view +@nonreentrant +def can_skip_bands(n_end: int256) -> bool: + """ + @notice Check that we have no liquidity between active_band and `n_end` + @param n_end Band index to check up to (not inclusive) + @return True if no liquidity exists between active_band and n_end, False otherwise + """ + n: int256 = self.active_band + for i: uint256 in range(MAX_SKIP_TICKS_UINT): + 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 + return True + raise "Too deep" + # 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) + + +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.can_skip_bands( + /* _n_end: int256 */ 0n, +) +``` + + + +:::: + +### `liquidity_mining_callback` + +::::description[`AMM.liquidity_mining_callback() -> address: view`] + +Returns the liquidity mining callback (gauge) address. + +Returns: callback address (`address`). + + + +Excerpt from [`AMM.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/AMM.vy): + +```vyper +@view +@external +def liquidity_mining_callback() -> ILMCallback: + return self._liquidity_mining_callback + + +@deploy +def __init__( + _borrowed_token: IERC20, + _borrowed_precision: uint256, + _collateral_token: IERC20, + _collateral_precision: uint256, + _A: uint256, + _sqrt_band_ratio: uint256, + _log_A_ratio: int256, + _base_price: uint256, + _fee: uint256, + _admin_fee: uint256, + _price_oracle: IPriceOracle, + ): + """ + @notice LLAMMA constructor + @param _borrowed_token Token which is being borrowed + @param _borrowed_precision Precision of borrowed token: we pass it because we want the blueprint to fit into bytecode + @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 DEPRECATED, left for backward compatibility + @param _price_oracle External price oracle which has price() and price_w() methods + which both return current price of collateral multiplied by 1e18 + """ + BORROWED_TOKEN = _borrowed_token + BORROWED_PRECISION = _borrowed_precision + COLLATERAL_TOKEN = _collateral_token +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xbf6f64b741164c26023f97faaea8e02453c27442', AMMAbi, provider) +const result = await contract.liquidity_mining_callback() +``` + + + +:::: diff --git a/docs/developer/llamalend-v2/configurator.md b/docs/developer/llamalend-v2/configurator.md new file mode 100644 index 00000000..3c9627c6 --- /dev/null +++ b/docs/developer/llamalend-v2/configurator.md @@ -0,0 +1,403 @@ +# Configurator + +The `Configurator` is the permissioned administrative entry point for LlamaLend v2 markets. It changes parameters across a market's controller, AMM, and view contracts; lenders, borrowers, and liquidators do not interact with it in normal operation. + +Each Configurator has a `default_admin`. The default administrator can assign a custom administrator for an individual controller. A market-setting call then accepts either the default administrator or that controller's custom administrator; changing the default administrator or custom assignments remains restricted to the default administrator. + +:::vyper[`Configurator.vy`] + +The implementation is [`Configurator.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Configurator.vy), written in Vyper `0.4.3`. + +The deployment registry lists the Configurator at [`0x6065858d0eF0AA240DFdf6f1A0B2ae34B41f49bC`](https://etherscan.io/address/0x6065858d0eF0AA240DFdf6f1A0B2ae34B41f49bC) on Ethereum and [`0xd36c590531cAF5F620C57Faf5827Ce8E7f6E5Bec`](https://optimistic.etherscan.io/address/0xd36c590531cAF5F620C57Faf5827Ce8E7f6E5Bec) on Optimism. A read-only Ethereum RPC check confirmed deployed code and a nonzero `default_admin`. + +::: + +Examples use Ethers v6 with an ABI generated from the pinned source. They are administrator transaction templates: replace every placeholder and execute them on a fork before any governance or multisig proposal. + +```ts +import { Contract } from 'ethers' + +const CONFIGURATOR = '0x6065858d0eF0AA240DFdf6f1A0B2ae34B41f49bC' +const configurator = new Contract(CONFIGURATOR, ConfiguratorAbi, signer) +``` + +--- + +## Administration + +### `set_custom_admin` + +::::description[`Configurator.set_custom_admin(_controller: address, _admin: address)`] + +Sets the administrator for one controller. Passing the zero address removes that controller-specific assignment, so future market-setting calls fall back to `default_admin`. + +| Input | Type | Description | +| --- | --- | --- | +| `_controller` | `address` | Controller whose authorization is being configured | +| `_admin` | `address` | New per-controller administrator, or zero address to reset | + +Emits: `SetCustomAdmin`. + + + +Excerpt from [`Configurator.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Configurator.vy): + +```vyper +@external +def set_custom_admin(_controller: IController, _admin: address): + self._check_admin() + self.admins[_controller] = _admin + log IConfigurator.SetCustomAdmin(controller=_controller.address, admin=_admin) +``` + + + + + +```ts +const tx = await configurator.set_custom_admin(controller, marketAdmin) +await tx.wait() +``` + + + +:::: + +### `set_owner` + +::::description[`Configurator.set_owner(_new_default_admin: address)`] + +Changes the default administrator. This does not overwrite existing per-controller custom administrator assignments. + +| Input | Type | Description | +| --- | --- | --- | +| `_new_default_admin` | `address` | New default administrator | + +Emits: `SetDefaultAdmin`. + + + +```vyper +@external +def set_owner(_new_default_admin: address): + self._check_admin() + self.default_admin = _new_default_admin + log IConfigurator.SetDefaultAdmin(new_default_admin=_new_default_admin) +``` + + + + + +```ts +const tx = await configurator.set_owner(newDefaultAdmin) +await tx.wait() +``` + + + +:::: + +### `default_admin` and `admins` + +::::description[`Configurator.default_admin() -> address: view`] + +Returns the Configurator's default administrator. `Configurator.admins(_controller) -> address: view` returns the custom administrator stored for a controller, or the zero address when none is set. + + + +```vyper +default_admin: public(address) +admins: public(HashMap[IController, address]) +``` + + + + + +```ts +const [defaultAdmin, customAdmin] = await Promise.all([ + configurator.default_admin(), + configurator.admins(controller), +]) +``` + + + +:::: + +--- + +## Controller configuration + +The following calls require the default administrator or the controller's custom administrator. + +### `set_borrowing_discounts` + +::::description[`Configurator.set_borrowing_discounts(_controller: address, _loan_discount: uint256, _liquidation_discount: uint256)`] + +Sets the loan and liquidation discounts used by the controller. Both values use `10**18 = 100%`; the liquidation discount must be positive and strictly less than the loan discount, which must be less than 100%. + + + +```vyper +@external +def set_borrowing_discounts( + _controller: IController, _loan_discount: uint256, _liquidation_discount: uint256 +): + self._check_authorized(_controller) + assert _liquidation_discount > 0 + assert _loan_discount < WAD and _loan_discount > _liquidation_discount + extcall _controller.configure( + _loan_discount, _liquidation_discount, IMonetaryPolicy(SKIP_CONFIG_ADDRESS), + SKIP_CONFIG_ADDRESS, SKIP_CONFIG_UINT256, IPriceOracle(SKIP_CONFIG_ADDRESS), + ILMCallback(SKIP_CONFIG_ADDRESS), + ) +``` + + + + + +```ts +const tx = await configurator.set_borrowing_discounts( + controller, + loanDiscount, + liquidationDiscount, +) +await tx.wait() +``` + + + +:::: + +### `set_monetary_policy` + +::::description[`Configurator.set_monetary_policy(_controller: address, _monetary_policy: address)`] + +Updates a controller's monetary-policy contract, then saves the controller rate. + + + +```vyper +@external +def set_monetary_policy(_controller: IController, _monetary_policy: IMonetaryPolicy): + self._check_authorized(_controller) + assert _monetary_policy.address != SKIP_CONFIG_ADDRESS + extcall _controller.configure( + SKIP_CONFIG_UINT256, SKIP_CONFIG_UINT256, _monetary_policy, + SKIP_CONFIG_ADDRESS, SKIP_CONFIG_UINT256, IPriceOracle(SKIP_CONFIG_ADDRESS), + ILMCallback(SKIP_CONFIG_ADDRESS), + ) + extcall _controller.save_rate() +``` + + + + + +```ts +const tx = await configurator.set_monetary_policy(controller, monetaryPolicy) +await tx.wait() +``` + + + +:::: + +### `set_view` + +::::description[`Configurator.set_view(_controller: address, _view_blueprint: address)`] + +Deploys and configures a new controller-view instance from the supplied blueprint. The blueprint must be nonzero and cannot be the configuration sentinel address. + + + +```vyper +@external +def set_view(_controller: IController, _view_blueprint: address): + self._check_authorized(_controller) + assert _view_blueprint != empty(address) + extcall _controller.configure( + SKIP_CONFIG_UINT256, SKIP_CONFIG_UINT256, IMonetaryPolicy(SKIP_CONFIG_ADDRESS), + _view_blueprint, SKIP_CONFIG_UINT256, IPriceOracle(SKIP_CONFIG_ADDRESS), + ILMCallback(SKIP_CONFIG_ADDRESS), + ) +``` + + + + + +```ts +const tx = await configurator.set_view(controller, controllerViewBlueprint) +await tx.wait() +``` + + + +:::: + +--- + +## Lending-market configuration + +### `set_borrow_cap` + +::::description[`Configurator.set_borrow_cap(_controller: address, _borrow_cap: uint256)`] + +Sets a lending controller's borrow cap in units of its borrowed token. A new lending market starts with a borrow cap of zero, so borrowing remains disabled until an authorized administrator raises it. + + + +```vyper +@external +def set_borrow_cap(_controller: ILendController, _borrow_cap: uint256): + self._check_authorized(IController(_controller.address)) + assert _borrow_cap != SKIP_CONFIG_UINT256 + extcall _controller.configure_lend(_borrow_cap, SKIP_CONFIG_UINT256) +``` + + + + + +```ts +const tx = await configurator.set_borrow_cap(lendController, borrowCap) +await tx.wait() +``` + + + +:::: + +### `set_admin_percentage` + +::::description[`Configurator.set_admin_percentage(_controller: address, _admin_percentage: uint256)`] + +Sets the share of interest assigned to the administrator. `_admin_percentage` uses `10**18 = 100%` and cannot exceed 100%. + + + +```vyper +@external +def set_admin_percentage(_controller: ILendController, _admin_percentage: uint256): + self._check_authorized(IController(_controller.address)) + assert _admin_percentage <= WAD + extcall _controller.configure_lend(SKIP_CONFIG_UINT256, _admin_percentage) +``` + + + + + +```ts +const tx = await configurator.set_admin_percentage(lendController, adminPercentage) +await tx.wait() +``` + + + +:::: + +--- + +## AMM configuration + +### `set_price_oracle` + +::::description[`Configurator.set_price_oracle(_controller: address, _price_oracle: address, _max_deviation: uint256)`] + +Updates the AMM price oracle after validating it. `_max_deviation` is WAD-scaled and may be at most 50%; `max_value(uint256)` explicitly skips the deviation check. The Configurator calls `price_w()` and `price()` on the new oracle before applying the change. + + + +```vyper +@external +def set_price_oracle( + _controller: IController, _price_oracle: IPriceOracle, _max_deviation: uint256 +): + self._check_authorized(_controller) + assert _max_deviation <= MAX_ORACLE_PRICE_DEVIATION or _max_deviation == max_value(uint256) + extcall _price_oracle.price_w() + new_price: uint256 = staticcall _price_oracle.price() + # The remaining implementation compares the existing oracle before configuration. +``` + + + + + +```ts +const tx = await configurator.set_price_oracle(controller, priceOracle, maxDeviation) +await tx.wait() +``` + + + +:::: + +### `set_callback` + +::::description[`Configurator.set_callback(_controller: address, _cb: address)`] + +Sets the AMM liquidity-mining callback. Pass the zero address to remove the callback; the configuration sentinel address is rejected. + + + +```vyper +@external +def set_callback(_controller: IController, _cb: ILMCallback): + self._check_authorized(_controller) + assert _cb.address != SKIP_CONFIG_ADDRESS + extcall _controller.configure( + SKIP_CONFIG_UINT256, SKIP_CONFIG_UINT256, IMonetaryPolicy(SKIP_CONFIG_ADDRESS), + SKIP_CONFIG_ADDRESS, SKIP_CONFIG_UINT256, IPriceOracle(SKIP_CONFIG_ADDRESS), _cb, + ) +``` + + + + + +```ts +const tx = await configurator.set_callback(controller, liquidityMiningCallback) +await tx.wait() +``` + + + +:::: + +### `set_amm_fee` + +::::description[`Configurator.set_amm_fee(_controller: address, _fee: uint256)`] + +Sets the AMM fee through the controller configuration path. The fee is WAD-scaled and the controller/AMM configuration enforces its permitted range. + + + +```vyper +@external +def set_amm_fee(_controller: IController, _fee: uint256): + self._check_authorized(_controller) + assert _fee != SKIP_CONFIG_UINT256 + extcall _controller.configure( + SKIP_CONFIG_UINT256, SKIP_CONFIG_UINT256, IMonetaryPolicy(SKIP_CONFIG_ADDRESS), + SKIP_CONFIG_ADDRESS, _fee, IPriceOracle(SKIP_CONFIG_ADDRESS), ILMCallback(SKIP_CONFIG_ADDRESS), + ) +``` + + + + + +```ts +const tx = await configurator.set_amm_fee(controller, ammFee) +await tx.wait() +``` + + + +:::: diff --git a/docs/developer/llamalend-v2/integration-guide.md b/docs/developer/llamalend-v2/integration-guide.md new file mode 100644 index 00000000..b5bf7c7e --- /dev/null +++ b/docs/developer/llamalend-v2/integration-guide.md @@ -0,0 +1,140 @@ +# Vault Integration Guide + +Llamalend v2 vaults are standard **ERC4626** vaults, so integrating them is straightforward — any protocol or aggregator that supports ERC4626 can plug in directly. + + +--- + + +## Depositing and Withdrawing + +Use the standard ERC4626 methods on the [Vault](./vault.md) contract: + +- **`deposit(assets, receiver)`** — deposit borrowed tokens, receive vault shares +- **`mint(shares, receiver)`** — mint exact shares, pull required assets +- **`withdraw(assets, receiver, owner)`** — withdraw exact assets, burn required shares +- **`redeem(shares, receiver, owner)`** — burn exact shares, receive corresponding assets + +All preview functions (`previewDeposit`, `previewMint`, `previewWithdraw`, `previewRedeem`) and limit functions (`maxDeposit`, `maxMint`, `maxWithdraw`, `maxRedeem`) are implemented per the ERC4626 spec. + +**On-chain (Vyper):** + +```vyper +from ethereum.ercs import IERC20 +from ethereum.ercs import IERC4626 + +vault: IERC4626 = IERC4626(vault_addr) +asset: IERC20 = IERC20(staticcall vault.asset()) + +# Deposit 1000 USDC into the vault +amount: uint256 = 1000 * 10 ** 6 +extcall asset.approve(vault_addr, amount) +shares: uint256 = extcall vault.deposit(amount, msg.sender) + +# Withdraw all — burn shares, receive assets +assets: uint256 = extcall vault.redeem(shares, msg.sender, msg.sender) +``` + +**Off-chain (Python / ape):** + +```python +vault = Contract(vault_address) +asset = Contract(vault.asset()) + +amount = 1000 * 10 ** asset.decimals() +asset.approve(vault.address, amount, sender=account) +vault.deposit(amount, account, sender=account) +``` + + +--- + + +## Supply Caps + +Each vault has a DAO-configurable `max_supply` that limits total deposits. Integrators should check available capacity before depositing: + +- **`maxDeposit(receiver)`** — returns the maximum assets depositable, accounting for the supply cap +- **`maxMint(receiver)`** — returns the maximum shares mintable, accounting for the supply cap + +If `max_supply` is set to `0`, there is no cap (unlimited deposits). Deposits that would push `totalSupply` above the cap will revert. + +```vyper +# Check remaining capacity before depositing +max_assets: uint256 = staticcall vault.maxDeposit(msg.sender) +assert amount <= max_assets, "exceeds supply cap" +extcall vault.deposit(amount, msg.sender) +``` + + +--- + + +## Dead Shares + +On initialization, the vault mints 1000 shares to the zero address. This is a standard defense against [ERC4626 inflation attacks](https://docs.openzeppelin.com/contracts/5.x/erc4626#inflation-attack), where an attacker front-runs the first depositor to manipulate `pricePerShare` and steal funds. + +Because of this, `pricePerShare` does not start at exactly `1e18` — it starts marginally above it. For most integrations this difference is negligible, but protocols that assume a fresh vault has a 1:1 asset-to-share ratio should account for the offset. + +```vyper +# Use convertToShares / convertToAssets instead of assuming 1:1 +shares: uint256 = staticcall vault.convertToShares(assets) +assets: uint256 = staticcall vault.convertToAssets(shares) +``` + + +--- + + +## Yield Accrual + +Yield accrues passively through the vault's rising `pricePerShare` — no claiming or staking required. Useful view functions: + +- **`pricePerShare()`** — current share price (scaled to 1e18), increases over time as borrowers pay interest +- **`lend_apr()`** — annualized yield for lenders after utilization and admin fees (scaled to 1e18) +- **`borrow_apr()`** — current annualized borrow rate (scaled to 1e18) + +```python +vault = Contract(vault_address) + +price = vault.pricePerShare() # e.g. 1_023_000_000_000_000_000 (1.023) +lend = vault.lend_apr() # e.g. 52_000_000_000_000_000 (5.2%) +borrow = vault.borrow_apr() # e.g. 78_000_000_000_000_000 (7.8%) +``` + + +--- + + +## Liquidity Considerations + +Withdrawals are limited by available liquidity — tokens currently lent out to borrowers cannot be withdrawn until repaid. The `maxWithdraw` and `maxRedeem` functions account for this automatically. Integrators building on top of the vaults should handle partial withdrawal availability gracefully. + +```vyper +available: uint256 = staticcall vault.maxWithdraw(msg.sender) + +if available < desired_amount: + # Only withdraw what's available now + extcall vault.withdraw(available, msg.sender, msg.sender) +else: + extcall vault.withdraw(desired_amount, msg.sender, msg.sender) +``` + + +--- + + +## Market Discovery + +Use the [LendFactory](./lend-factory.md) to enumerate available markets and find vault addresses programmatically. + +```python +factory = Contract(factory_address) + +n = factory.market_count() +for i in range(n): + market = factory.markets(i) + vault_addr = market.vault + vault = Contract(vault_addr) + print(f"Market {i}: {vault.name()}, APR: {vault.lend_apr() / 1e16:.2f}%") +``` diff --git a/docs/developer/llamalend-v2/lend-controller-view.md b/docs/developer/llamalend-v2/lend-controller-view.md new file mode 100644 index 00000000..1105dcb1 --- /dev/null +++ b/docs/developer/llamalend-v2/lend-controller-view.md @@ -0,0 +1,819 @@ +# LendControllerView + +The `LendControllerView` is a **stateless view helper** contract that the [LendController](./lend-controller.md) delegates read-only computations to. It extends the base `ControllerView` module with lending-specific logic — primarily making `max_borrowable` and `tokens_to_shrink` aware of **borrow caps** and **available balance**. + +Each lending market deploys its own `LendControllerView` instance alongside the controller. + +:::vyper[`LendControllerView.vy`] + +The source code for the `LendControllerView.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/lending/LendControllerView.vy). The base view module is [`ControllerView.vy`](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/ControllerView.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.4.3`. + +*Deployment addresses will be added once contracts are finalized.* + +::: + +Examples use Ethers v6 with an ABI generated from the pinned source linked in each panel. Replace every zero address and amount placeholder. Read-only calls use the verified Ethereum v2 reference market. + + +--- + + +## Lending-Specific Views + +These functions override the base `ControllerView` to account for borrow caps and available liquidity. + +### `max_borrowable` + +::::description[`LendControllerView.max_borrowable(_d_collateral: uint256, _N: uint256, _user: address = empty(address)) -> uint256: view`] + +Returns the maximum amount that can be borrowed for a given collateral and band count. Unlike the base implementation, this caps the result by the **borrow cap** and **available balance** in the vault. + +| Input | Type | Description | +| --- | --- | --- | +| `_d_collateral` | `uint256` | Collateral amount | +| `_N` | `uint256` | Number of bands | +| `_user` | `address` | Borrower address (for existing loan context) | + +Returns: max borrowable amount, capped by market limits (`uint256`). + + + +Excerpt from [`lending/LendControllerView.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendControllerView.vy): + +```vyper +@external +@view +def max_borrowable( + _d_collateral: uint256, + _N: uint256, + _user: address = empty(address), +) -> uint256: + """ + @notice Natspec for this function is available in its controller contract + """ + user_state: uint256[4] = core._user_state(_user) + if user_state[1] > 0: # Can't borrow in soft-liquidation + return 0 + + N: uint256 = _N + if user_state[3] > 0: # The user has the position + N = user_state[3] + + return crv_math.sub_or_zero( + core._max_borrowable(user_state[0] + _d_collateral, N, self._get_cap() + user_state[2] , _user), + user_state[2], + ) + + +``` + + + + + +```ts +const contract = new Contract('0xcda563f85388e621e7d810387e5afdac5d395e2b', LendControllerViewAbi, provider) +const result = await contract.max_borrowable( + /* _d_collateral: uint256 */ 0n, + /* _N: uint256 */ 0n, + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `tokens_to_shrink` + +::::description[`LendControllerView.tokens_to_shrink(_user: address, _d_collateral: uint256 = 0) -> uint256: view`] + +Returns the amount of debt tokens needed to "shrink" a user's position by the given collateral amount. Accounts for lending market constraints. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | Borrower address | +| `_d_collateral` | `uint256` | Collateral to remove | + +Returns: tokens needed (`uint256`). + + + +Excerpt from [`lending/LendControllerView.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendControllerView.vy): + +```vyper +@external +@view +def tokens_to_shrink(_user: address, _d_collateral: uint256 = 0) -> uint256: + """ + @notice Natspec for this function is available in its controller contract + """ + return core._tokens_to_shrink(_user, self._get_cap(), _d_collateral) + +``` + + + + + +```ts +const contract = new Contract('0xcda563f85388e621e7d810387e5afdac5d395e2b', LendControllerViewAbi, provider) +const result = await contract.tokens_to_shrink( + /* _user: address */ "0x0000000000000000000000000000000000000000", + /* _d_collateral: uint256 */ 0n, +) +``` + + + +:::: + + +--- + + +## Health Previews + +These functions are exported from the base `ControllerView` module and compute health predictions for various operations. + +### `create_loan_health_preview` + +::::description[`LendControllerView.create_loan_health_preview(_collateral: uint256, _debt: uint256, _N: uint256, _for: address, _full: bool) -> int256: view`] + +Previews the health that would result from creating a loan with the given parameters. + +| Input | Type | Description | +| --- | --- | --- | +| `_collateral` | `uint256` | Collateral amount | +| `_debt` | `uint256` | Debt amount | +| `_N` | `uint256` | Number of bands | +| `_for` | `address` | Borrower address | +| `_full` | `bool` | Full (pessimistic) health | + +Returns: predicted health (`int256`). + + + +Excerpt from [`ControllerView.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/ControllerView.vy): + +```vyper +@external +@view +def create_loan_health_preview( + _collateral: uint256, + _debt: uint256, + _N: uint256, + _for: address, + _full: bool, +) -> int256: + """ + @notice Natspec for this function is available in its controller contract + """ + assert _debt > 0, "debt==0" + n1: int256 = staticcall CONTROLLER.calculate_debt_n1(_collateral, _debt, _N, _for) + ld: uint256 = self._liquidation_discount() + + return self._calc_full_health(_collateral, _debt, _N, n1, ld, _full) + + +@internal +@view +def _add_collateral_borrow_health_preview( + _collateral: uint256, + _debt: uint256, + _for: address, + _full: bool, + _remove: bool, +) -> int256: + """ + @notice Natspec for this function is available in its controller contract + """ + debt: uint256 = self._debt(_for) + ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) + N: uint256 = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256) + xy: uint256[2] = staticcall AMM.get_sum_xy(_for) + + assert debt > 0, "debt==0" + assert xy[0] == 0, "Underwater" +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xcda563f85388e621e7d810387e5afdac5d395e2b', LendControllerViewAbi, provider) +const result = await contract.create_loan_health_preview( + /* _collateral: uint256 */ 0n, + /* _debt: uint256 */ 0n, + /* _N: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", + /* _full: bool */ false, +) +``` + + + +:::: + +### `borrow_more_health_preview` + +::::description[`LendControllerView.borrow_more_health_preview(_collateral: uint256, _debt: uint256, _for: address, _full: bool) -> int256: view`] + +Previews health after borrowing more. + +| Input | Type | Description | +| --- | --- | --- | +| `_collateral` | `uint256` | Additional collateral | +| `_debt` | `uint256` | Additional debt | +| `_for` | `address` | Borrower address | +| `_full` | `bool` | Full (pessimistic) health | + +Returns: predicted health (`int256`). + + + +Excerpt from [`ControllerView.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/ControllerView.vy): + +```vyper +@external +@view +def borrow_more_health_preview( + _collateral: uint256, + _debt: uint256, + _for: address, + _full: bool, +) -> int256: + """ + @notice Natspec for this function is available in its controller contract + """ + if _debt == 0: + return staticcall CONTROLLER.health(_for, _full) + return self._add_collateral_borrow_health_preview( + _collateral, _debt, _for, _full, False + ) + + +``` + + + + + +```ts +const contract = new Contract('0xcda563f85388e621e7d810387e5afdac5d395e2b', LendControllerViewAbi, provider) +const result = await contract.borrow_more_health_preview( + /* _collateral: uint256 */ 0n, + /* _debt: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", + /* _full: bool */ false, +) +``` + + + +:::: + +### `add_collateral_health_preview` + +::::description[`LendControllerView.add_collateral_health_preview(_collateral: uint256, _for: address, _full: bool) -> int256: view`] + +Previews health after adding collateral. + +| Input | Type | Description | +| --- | --- | --- | +| `_collateral` | `uint256` | Collateral to add | +| `_for` | `address` | Borrower address | +| `_full` | `bool` | Full (pessimistic) health | + +Returns: predicted health (`int256`). + + + +Excerpt from [`ControllerView.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/ControllerView.vy): + +```vyper +@external +@view +def add_collateral_health_preview( + _collateral: uint256, + _for: address, + _full: bool, +) -> int256: + """ + @notice Natspec for this function is available in its controller contract + """ + if _collateral == 0: + return staticcall CONTROLLER.health(_for, _full) + return self._add_collateral_borrow_health_preview(_collateral, 0, _for, _full, False) + + +``` + + + + + +```ts +const contract = new Contract('0xcda563f85388e621e7d810387e5afdac5d395e2b', LendControllerViewAbi, provider) +const result = await contract.add_collateral_health_preview( + /* _collateral: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", + /* _full: bool */ false, +) +``` + + + +:::: + +### `remove_collateral_health_preview` + +::::description[`LendControllerView.remove_collateral_health_preview(_collateral: uint256, _for: address, _full: bool) -> int256: view`] + +Previews health after removing collateral. + +| Input | Type | Description | +| --- | --- | --- | +| `_collateral` | `uint256` | Collateral to remove | +| `_for` | `address` | Borrower address | +| `_full` | `bool` | Full (pessimistic) health | + +Returns: predicted health (`int256`). + + + +Excerpt from [`ControllerView.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/ControllerView.vy): + +```vyper +@external +@view +def remove_collateral_health_preview( + _collateral: uint256, + _for: address, + _full: bool, +) -> int256: + """ + @notice Natspec for this function is available in its controller contract + """ + if _collateral == 0: + return staticcall CONTROLLER.health(_for, _full) + return self._add_collateral_borrow_health_preview( + _collateral, 0, _for, _full, True + ) + + +``` + + + + + +```ts +const contract = new Contract('0xcda563f85388e621e7d810387e5afdac5d395e2b', LendControllerViewAbi, provider) +const result = await contract.remove_collateral_health_preview( + /* _collateral: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", + /* _full: bool */ false, +) +``` + + + +:::: + +### `repay_health_preview` + +::::description[`LendControllerView.repay_health_preview(_d_collateral: uint256, _d_debt: uint256, _for: address, _shrink: bool, _full: bool) -> int256: view`] + +Previews health after repaying debt. + +| Input | Type | Description | +| --- | --- | --- | +| `_d_collateral` | `uint256` | Collateral amount removed through the callback | +| `_d_debt` | `uint256` | Debt to repay | +| `_for` | `address` | Borrower address | +| `_shrink` | `bool` | Whether the repayment shrinks a soft-liquidated position | +| `_full` | `bool` | Full (pessimistic) health | + +Returns: predicted health (`int256`). + + + +Excerpt from [`ControllerView.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/ControllerView.vy): + +```vyper +@external +@view +def repay_health_preview( + _d_collateral: uint256, + _d_debt: uint256, + _for: address, + _shrink: bool, + _full: bool, +) -> int256: + """ + @notice Natspec for this function is available in its controller contract + """ + ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) + debt: uint256 = self._debt(_for) + active_band: int256 = staticcall AMM.active_band_with_skip() + + assert debt > 0, "Loan doesn't exist" + + if ns[0] > active_band or _shrink: # re-deposit + xy: uint256[2] = staticcall AMM.get_sum_xy(_for) + d_debt: uint256 = _d_debt + xy[0] + assert d_debt > 0, "No coins to repay" + assert debt > d_debt, "Repay amount is too high" + debt = unsafe_sub(debt, d_debt) + + collateral: uint256 = xy[1] + assert collateral > _d_collateral, "Can't remove more collateral than user has" + collateral = unsafe_sub(collateral, _d_collateral) + + if _shrink: + assert ns[1] >= active_band + MIN_TICKS, "Can't shrink" + + N: uint256 = convert(unsafe_add(unsafe_sub(ns[1], max(ns[0], active_band + 1)), 1), uint256) + n1: int256 = staticcall CONTROLLER.calculate_debt_n1(collateral, debt, N, _for) + + return self._calc_full_health(collateral, debt, N, n1, self._liquidation_discount(), _full) + else: + assert _d_debt > 0, "No coins to repay" +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xcda563f85388e621e7d810387e5afdac5d395e2b', LendControllerViewAbi, provider) +const result = await contract.repay_health_preview( + /* _d_collateral: uint256 */ 0n, + /* _d_debt: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", + /* _shrink: bool */ false, + /* _full: bool */ false, +) +``` + + + +:::: + +### `liquidate_health_preview` + +::::description[`LendControllerView.liquidate_health_preview(_user: address, _caller: address, _frac: uint256, _full: bool) -> int256: view`] + +Previews health after a partial liquidation. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | Borrower address | +| `_caller` | `address` | Address that would submit the liquidation | +| `_frac` | `uint256` | Fraction of the position to liquidate, where `10**18` is 100% | +| `_full` | `bool` | Full (pessimistic) health | + +Returns: predicted health (`int256`). + + + +Excerpt from [`ControllerView.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/ControllerView.vy): + +```vyper +@external +@view +def liquidate_health_preview( + _user: address, + _caller: address, + _frac: uint256, + _full: bool, +) -> int256: + """ + @notice Natspec for this function is available in its controller contract + """ + assert _frac < WAD, "frac >= 100%" + debt: uint256 = self._debt(_user) + ns: int256[2] = staticcall AMM.read_user_tick_numbers(_user) + active_band: int256 = staticcall AMM.active_band_with_skip() + + approval: bool = self._has_approval(_user, _caller) + health_limit: uint256 = 0 + ld: uint256 = 0 + if approval: + ld = self._liquidation_discount() + else: + assert staticcall CONTROLLER.health(_user, True) < 0, "Not enough rekt" + ld = self._liquidation_discounts(_user) + health_limit = ld + f_remove: uint256 = core._get_f_remove(_frac, health_limit) + + x_eff: uint256 = staticcall AMM.get_x_down(_user) * (WAD - f_remove) // WAD + debt = debt * (WAD - _frac) // WAD + health: int256 = self._calc_health(x_eff, debt, ld) + + if _full and ns[0] > active_band: + xy: uint256[2] = staticcall AMM.get_sum_xy(_user) + collateral: uint256 = xy[1] * (WAD - f_remove) // WAD + p0: uint256 = staticcall AMM.p_oracle_up(ns[0]) + p_diff: uint256 = crv_math.sub_or_zero(staticcall AMM.price_oracle(), p0) + + if p_diff > 0: +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xcda563f85388e621e7d810387e5afdac5d395e2b', LendControllerViewAbi, provider) +const result = await contract.liquidate_health_preview( + /* _user: address */ "0x0000000000000000000000000000000000000000", + /* _caller: address */ "0x0000000000000000000000000000000000000000", + /* _frac: uint256 */ 0n, + /* _full: bool */ false, +) +``` + + + +:::: + + +--- + + +## Position Info + +### `user_state` + +::::description[`LendControllerView.user_state(_user: address) -> uint256[4]: view`] + +Returns the full state of a user's loan: `[collateral, borrowed, debt, N]`. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | Borrower address | + +Returns: `[collateral, borrowed, debt, N]` (`uint256[4]`). + + + +Excerpt from [`ControllerView.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/ControllerView.vy): + +```vyper +@external +@view +def user_state(_user: address) -> uint256[4]: + """ + @notice Natspec for this function is available in its controller contract + """ + return self._user_state(_user) + + +@internal +@view +def _max_p_base(_amm: IAMM, _logn_a_ration: int256) -> uint256: + """ + @notice Calculate max base price including skipping bands + """ + p_oracle: uint256 = staticcall _amm.price_oracle() + # Should be correct unless price changes suddenly by MAX_P_BASE_BANDS+ bands + n1: int256 = math._wad_ln( + convert(staticcall _amm.get_base_price() * WAD // p_oracle, int256) + ) + if n1 < 0: + n1 -= ( + _logn_a_ration - 1 + ) # This is to deal with vyper's rounding of negative numbers + n1 = unsafe_div(n1, _logn_a_ration) + MAX_P_BASE_BANDS + n_min: int256 = staticcall _amm.active_band_with_skip() + n1 = max(n1, n_min + 1) + p_base: uint256 = staticcall _amm.p_oracle_up(n1) + + for _: uint256 in range(MAX_SKIP_TICKS + 1): + n1 -= 1 + if n1 <= n_min: + break + p_base_prev: uint256 = p_base + p_base = staticcall _amm.p_oracle_up(n1) + if p_base > p_oracle: + return p_base_prev + return p_base +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xcda563f85388e621e7d810387e5afdac5d395e2b', LendControllerViewAbi, provider) +const result = await contract.user_state( + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `user_prices` + +::::description[`LendControllerView.user_prices(_user: address) -> uint256[2]: view`] + +Returns the upper and lower price bounds of a user's collateral bands. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | Borrower address | + +Returns: `[price_upper, price_lower]` (`uint256[2]`). + + + +Excerpt from [`ControllerView.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/ControllerView.vy): + +```vyper +@external +@view +def user_prices(_user: address) -> uint256[2]: # Upper, lower + """ + @notice Natspec for this function is available in its controller contract + """ + assert staticcall AMM.has_liquidity(_user) + ns: int256[2] = staticcall AMM.read_user_tick_numbers(_user) # ns[1] > ns[0] + return [ + staticcall AMM.p_oracle_up(ns[0]), staticcall AMM.p_oracle_down(ns[1]) + ] + + +@internal +@view +def _user_state(_user: address) -> uint256[4]: + """ + @notice Natspec for this function is available in its controller contract + """ + xy: uint256[2] = empty(uint256[2]) + N: uint256 = 0 + debt: uint256 = self._debt(_user) + if debt > 0: + xy = staticcall AMM.get_sum_xy(_user) + ns: int256[2] = staticcall AMM.read_user_tick_numbers(_user) # ns[1] > ns[0] + N = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256) + + return [xy[1], xy[0], debt, N] + + +``` + + + + + +```ts +const contract = new Contract('0xcda563f85388e621e7d810387e5afdac5d395e2b', LendControllerViewAbi, provider) +const result = await contract.user_prices( + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `users_to_liquidate` + +::::description[`LendControllerView.users_to_liquidate(_from: uint256 = 0, _limit: uint256 = 0) -> IController.Position[]: view`] + +Returns a paginated list of liquidatable positions. + +| Input | Type | Description | +| --- | --- | --- | +| `_from` | `uint256` | Starting index | +| `_limit` | `uint256` | Maximum positions to return | + +Returns: array of liquidatable positions (`IController.Position[]`). + + + +Excerpt from [`ControllerView.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/ControllerView.vy): + +```vyper +@external +@view +def users_to_liquidate( + _from: uint256 = 0, _limit: uint256 = 0 +) -> DynArray[IController.Position, 1000]: + """ + @notice Natspec for this function is available in its controller contract + """ + return self.users_with_health( + CONTROLLER, + _from, + _limit, + 0, + False, + empty(address), + True, + ) + + +``` + + + + + +```ts +const contract = new Contract('0xcda563f85388e621e7d810387e5afdac5d395e2b', LendControllerViewAbi, provider) +const result = await contract.users_to_liquidate( + /* _from: uint256 */ 0n, + /* _limit: uint256 */ 0n, +) +``` + + + +:::: + +### `min_collateral` + +::::description[`LendControllerView.min_collateral(_debt: uint256, _N: uint256, _user: address = empty(address)) -> uint256: view`] + +Returns the minimum collateral required for a given debt amount and number of bands. + +| Input | Type | Description | +| --- | --- | --- | +| `_debt` | `uint256` | Desired debt | +| `_N` | `uint256` | Number of bands | +| `_user` | `address` | User address, needed only when the user has nonzero extra health | + +Returns: minimum collateral (`uint256`). + + + +Excerpt from [`ControllerView.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/ControllerView.vy): + +```vyper +@external +@view +def min_collateral( + _debt: uint256, _N: uint256, _user: address = empty(address) +) -> uint256: + """ + @notice Natspec for this function is available in its controller contract + """ + # Add N**2 to account for precision loss in multiple bands, e.g. N / (y/N) = N**2 / y + assert _N <= MAX_TICKS_UINT and _N >= MIN_TICKS_UINT + return unsafe_div( + unsafe_div( + _debt + * unsafe_mul(WAD, BORROWED_PRECISION) // self._max_p_base(AMM, LOGN_A_RATIO) + * WAD // self._get_y_effective( + WAD, _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, + ) + * WAD, + WAD - 10**14, + ) + + +@internal +@view +def _tokens_to_shrink(_user: address, _cap: uint256, _d_collateral: uint256) -> uint256: + active_band: int256 = staticcall AMM.active_band_with_skip() + ns: int256[2] = staticcall AMM.read_user_tick_numbers(_user) + + if ns[0] > active_band: + return 0 + + assert ns[1] >= active_band + MIN_TICKS, "Can't shrink" +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xcda563f85388e621e7d810387e5afdac5d395e2b', LendControllerViewAbi, provider) +const result = await contract.min_collateral( + /* _debt: uint256 */ 0n, + /* _N: uint256 */ 0n, + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: diff --git a/docs/developer/llamalend-v2/lend-controller.md b/docs/developer/llamalend-v2/lend-controller.md new file mode 100644 index 00000000..032074a6 --- /dev/null +++ b/docs/developer/llamalend-v2/lend-controller.md @@ -0,0 +1,2249 @@ +# LendController + +The `LendController` is the **primary user-facing contract** for each lending market. Borrowers interact with it to create loans, manage collateral, borrow more, repay, and get liquidated. It wraps the core `controller.vy` module — which contains all shared loan logic — and extends it with lending-specific features: **borrow caps**, **vault token transfers**, and **available balance tracking**. + +Each lending market gets its own `LendController` instance, deployed by the [LendFactory](./lend-factory.md). + +:::vyper[`LendController.vy`] + +The source code for the `LendController.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/lending/LendController.vy). The core module is [`controller.vy`](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/controller.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.4.3`. + +*Deployment addresses will be added once contracts are finalized.* + +::: + +Examples use Ethers v6 with an ABI generated from the pinned source linked in each panel. Replace every zero address and amount placeholder. Read-only calls use the verified Ethereum v2 reference market; execute a write only on a fork first. + +:::info + +The `LendController` exports most of its functions from the `controller.vy` module. Functions documented below include both module-exported functions and lending-specific extensions. The module pattern means the core loan logic is shared with `MintController` (used for crvUSD mint markets). + +::: + + +--- + + +## Loan Management + +### `create_loan` + +::::description[`LendController.create_loan(_collateral: uint256, _debt: uint256, _N: uint256, _for: address = msg.sender, _callbacker: address = empty(address), _calldata: Bytes[CALLDATA_MAX_SIZE] = b"")`] + +Creates a new loan by depositing collateral and borrowing tokens. The collateral is distributed across `_N` bands in the AMM. + +| Input | Type | Description | +| --- | --- | --- | +| `_collateral` | `uint256` | Amount of collateral to deposit | +| `_debt` | `uint256` | Amount of borrowed tokens to receive | +| `_N` | `uint256` | Number of bands to distribute collateral across (4–50) | +| `_for` | `address` | Address that owns the new loan | +| `_callbacker` | `address` | Optional callback contract for leveraged operations | +| `_calldata` | `Bytes[CALLDATA_MAX_SIZE]` | Data passed to the callback contract | + +Emits: `Borrow` event. + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +def create_loan( + _collateral: uint256, + _debt: uint256, + _N: uint256, + _for: address = msg.sender, + _callbacker: address = empty(address), + _calldata: Bytes[CALLDATA_MAX_SIZE] = b"", +): + """ + @notice Create loan but pass borrowed tokens to a callback first so that it can build leverage + @param _collateral Amount of collateral to use + @param _debt Borrowed asset 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 + @param _callbacker Address of the callback contract + @param _calldata Any data for callbacker + """ + assert self._has_approval(_for) + + more_collateral: uint256 = 0 + if _callbacker != empty(address): + tkn.transfer(BORROWED_TOKEN, _callbacker, _debt) + # If there is any unused debt, callbacker can send it to the user + cb: IController.CallbackData = self._execute_callback( + _callbacker, + CALLBACK_DEPOSIT, + _for, + 0, + _collateral, + _debt, + _calldata, + ) + assert cb.borrowed == 0 # dev: Not available + more_collateral = cb.collateral + + total_collateral: uint256 = _collateral + more_collateral +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, signer) +const tx = await contract.create_loan( + /* _collateral: uint256 */ 0n, + /* _debt: uint256 */ 0n, + /* _N: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", + /* _callbacker: address */ "0x0000000000000000000000000000000000000000", + /* _calldata: Bytes[CALLDATA_MAX_SIZE] */ 0n, +) +await tx.wait() +``` + + + +:::: + +### `borrow_more` + +::::description[`LendController.borrow_more(_collateral: uint256, _debt: uint256, _for: address = msg.sender, _callbacker: address = empty(address), _calldata: Bytes[CALLDATA_MAX_SIZE] = b"")`] + +Adds additional collateral and/or borrows more tokens against an existing loan. + +| Input | Type | Description | +| --- | --- | --- | +| `_collateral` | `uint256` | Additional collateral to deposit (can be 0) | +| `_debt` | `uint256` | Additional debt to take on (can be 0) | +| `_for` | `address` | Address that owns the loan | +| `_callbacker` | `address` | Optional callback contract for leveraged operations | +| `_calldata` | `Bytes[CALLDATA_MAX_SIZE]` | Data passed to the callback contract | + +Emits: `Borrow` event. + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +def borrow_more( + _collateral: uint256, + _debt: uint256, + _for: address = msg.sender, + _callbacker: address = empty(address), + _calldata: Bytes[CALLDATA_MAX_SIZE] = b"", +): + """ + @notice Borrow more borrowed tokens while adding more collateral using a callback (to leverage more) + @param _collateral Amount of collateral to add + @param _debt Amount of borrowed asset debt to take + @param _for Address to borrow for + @param _callbacker Address of the callback contract + @param _calldata Any data for callbacker + """ + if _debt == 0: + return + assert self._has_approval(_for) + + more_collateral: uint256 = 0 + if _callbacker != empty(address): + tkn.transfer(BORROWED_TOKEN, _callbacker, _debt) + # If there is any unused debt, callbacker can send it to the user + cb: IController.CallbackData = self._execute_callback( + _callbacker, + CALLBACK_DEPOSIT, + _for, + 0, + _collateral, + _debt, + _calldata, + ) + assert cb.borrowed == 0 # dev: Not available + more_collateral = cb.collateral + + rate_mul: uint256 = self._add_collateral_borrow( + _collateral + more_collateral, _debt, _for, False +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, signer) +const tx = await contract.borrow_more( + /* _collateral: uint256 */ 0n, + /* _debt: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", + /* _callbacker: address */ "0x0000000000000000000000000000000000000000", + /* _calldata: Bytes[CALLDATA_MAX_SIZE] */ 0n, +) +await tx.wait() +``` + + + +:::: + +### `add_collateral` + +::::description[`LendController.add_collateral(_collateral: uint256, _for: address = msg.sender)`] + +Adds collateral to an existing loan without borrowing more. The borrower must call it directly or authorize the caller. + +| Input | Type | Description | +| --- | --- | --- | +| `_collateral` | `uint256` | Amount of collateral to add | +| `_for` | `address` | Address of the borrower to add collateral for | + +Emits: `Borrow` event. + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +def add_collateral(_collateral: uint256, _for: address = msg.sender): + """ + @notice Add extra collateral to avoid bad liquidations + @param _collateral Amount of collateral to add + @param _for Address to add collateral for + """ + if _collateral == 0: + return + assert self._has_approval(_for) + self._add_collateral_borrow(_collateral, 0, _for, False) + tkn.transfer_from(COLLATERAL_TOKEN, msg.sender, AMM.address, _collateral) + self._save_rate() + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, signer) +const tx = await contract.add_collateral( + /* _collateral: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", +) +await tx.wait() +``` + + + +:::: + +### `remove_collateral` + +::::description[`LendController.remove_collateral(_collateral: uint256, _for: address = msg.sender)`] + +Removes collateral from an existing loan. Reverts if the resulting health would be too low. + +| Input | Type | Description | +| --- | --- | --- | +| `_collateral` | `uint256` | Amount of collateral to remove | +| `_for` | `address` | Address that owns the loan; the caller must be authorized | + +Emits: `RemoveCollateral` event. + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +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._has_approval(_for) + self._add_collateral_borrow(_collateral, 0, _for, True) + tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, _for, _collateral) + self._save_rate() + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, signer) +const tx = await contract.remove_collateral( + /* _collateral: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", +) +await tx.wait() +``` + + + +:::: + +### `repay` + +::::description[`LendController.repay(_wallet_d_debt: uint256, _for: address = msg.sender, _max_active_band: int256 = max_value(int256), _callbacker: address = empty(address), _calldata: Bytes[CALLDATA_MAX_SIZE] = b"", _shrink: bool = False)`] + +Repays debt for a loan. If `_wallet_d_debt` exceeds the current debt, only the outstanding debt is repaid. The borrower must call it directly or authorize the caller. + +| Input | Type | Description | +| --- | --- | --- | +| `_wallet_d_debt` | `uint256` | Amount of debt to repay from the wallet (use `max_value(uint256)` to repay all) | +| `_for` | `address` | Address of the borrower to repay for | +| `_max_active_band` | `int256` | Maximum active band accepted for the repayment | +| `_callbacker` | `address` | Optional callback contract | +| `_calldata` | `Bytes[CALLDATA_MAX_SIZE]` | Data passed to the callback contract | +| `_shrink` | `bool` | Whether to shrink a soft-liquidated position | + +Emits: `Repay` event. + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +def repay( + _wallet_d_debt: uint256, + _for: address = msg.sender, + _max_active_band: int256 = max_value(int256), + _callbacker: address = empty(address), + _calldata: Bytes[CALLDATA_MAX_SIZE] = b"", + _shrink: bool = False +): + """ + @notice Repay debt (partially or fully) + @param _wallet_d_debt The amount of debt to repay from user's wallet. + If it's max_value(uint256) or just 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 _callbacker Address of the callback contract + @param _calldata Any data for callbacker + @param _shrink Whether shrink soft-liquidated part of the position or not + """ + assert self._has_approval(_for) + debt: uint256 = 0 + rate_mul: uint256 = 0 + debt, rate_mul = self._debt(_for) + self._check_loan_exists(debt) + xy: uint256[2] = staticcall AMM.get_sum_xy(_for) + + cb: IController.CallbackData = empty(IController.CallbackData) + if _callbacker != empty(address): + xy = extcall AMM.withdraw(_for, WAD) + tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, _callbacker, xy[1]) + cb = self._execute_callback( + _callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, _calldata + ) + + d_debt: uint256 = min(min(_wallet_d_debt, debt) + xy[0] + cb.borrowed, debt) + assert d_debt > 0 # dev: no coins to repay + + if d_debt == debt: +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, signer) +const tx = await contract.repay( + /* _wallet_d_debt: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", + /* _max_active_band: int256 */ 0n, + /* _callbacker: address */ "0x0000000000000000000000000000000000000000", + /* _calldata: Bytes[CALLDATA_MAX_SIZE] */ 0n, + /* _shrink: bool */ false, +) +await tx.wait() +``` + + + +:::: + +### `liquidate` + +::::description[`LendController.liquidate(_user: address, _min_x: uint256, _frac: uint256 = 10**18, _callbacker: address = empty(address), _calldata: Bytes[CALLDATA_MAX_SIZE] = b"")`] + +Liquidates an unhealthy loan (health < 0). The caller repays the debt and receives the remaining collateral. Supports partial liquidation if the position is partially in soft liquidation. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | Address of the borrower to liquidate | +| `_min_x` | `uint256` | Minimum amount of collateral to receive (slippage protection) | +| `_frac` | `uint256` | Fraction of the position to liquidate, where `10**18` is 100% | +| `_callbacker` | `address` | Optional callback contract | +| `_calldata` | `Bytes[CALLDATA_MAX_SIZE]` | Data passed to the callback contract | + +Emits: `Liquidate` event. + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +def liquidate( + _user: address, + _min_x: uint256, + _frac: uint256 = 10**18, + _callbacker: address = empty(address), + _calldata: Bytes[CALLDATA_MAX_SIZE] = b"", +): + """ + @notice Perform a bad liquidation (or self-liquidation) of user if health is not good + @param _user Address of the user to liquidate + @param _min_x Minimal amount of borrowed asset to receive (to avoid liquidators being sandwiched) + @param _frac Fraction to liquidate; 100% = 10**18 + @param _callbacker Address of the callback contract + @param _calldata Any data for callbacker + """ + approval: bool = self._has_approval(_user) + liquidation_discount: uint256 = self.liquidation_discounts[_user] + debt: uint256 = 0 + rate_mul: uint256 = 0 + debt, rate_mul = self._debt(_user) + + health_before: int256 = self._health(_user, debt, True, liquidation_discount) + health_limit: uint256 = 0 + if not approval: + assert health_before < 0, "Not enough rekt" + health_limit = liquidation_discount + + final_debt: uint256 = debt + assert _frac <= WAD, "frac>100%" + debt = unsafe_div(debt * _frac + (WAD - 1), WAD) + assert debt > 0 + final_debt = unsafe_sub(final_debt, debt) + + # If liquidating entire debt, ensure full collateral withdrawal + f_remove: uint256 = self._get_f_remove(_frac, health_limit) + if final_debt == 0: + f_remove = WAD +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, signer) +const tx = await contract.liquidate( + /* _user: address */ "0x0000000000000000000000000000000000000000", + /* _min_x: uint256 */ 0n, + /* _frac: uint256 */ 0n, + /* _callbacker: address */ "0x0000000000000000000000000000000000000000", + /* _calldata: Bytes[CALLDATA_MAX_SIZE] */ 0n, +) +await tx.wait() +``` + + + +:::: + + +--- + + +## Loan Info + +### `debt` + +::::description[`LendController.debt(_user: address) -> uint256: view`] + +Returns the current debt of a user, including accrued interest. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | Borrower address | + +Returns: current debt (`uint256`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +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] + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.debt( + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `health` + +::::description[`LendController.health(_user: address, _full: bool = False) -> int256: view`] + +Returns the health of a user's loan. Health > 0 means the loan is safe; health < 0 means it can be liquidated. If `_full` is true, the calculation also accounts for an oracle-price difference above the user's highest band when the position is not in soft liquidation. If false, it calculates health as for a user in soft-liquidation mode. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | Borrower address | +| `_full` | `bool` | If true, use full (pessimistic) health calculation | + +Returns: health value (`int256`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +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 + @param _user Address of the user + @param _full If True, use the full health calculation including bad debt discounts + @return Health value normalized to 1e18 + """ + return self._health( + _user, self._debt(_user)[0], _full, self.liquidation_discounts[_user] + ) + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.health( + /* _user: address */ "0x0000000000000000000000000000000000000000", + /* _full: bool */ false, +) +``` + + + +:::: + +### `loan_exists` + +::::description[`LendController.loan_exists(_user: address) -> bool: view`] + +Returns whether a loan exists for the given user. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | Borrower address | + +Returns: whether the loan exists (`bool`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +def loan_exists(_user: address) -> bool: + """ + @notice Check whether there is a loan of `user` in existence + @param _user Address of the user to check + @return True if the user has an active loan, False otherwise + """ + return self.loan[_user].initial_debt > 0 + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.loan_exists( + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `user_state` + +::::description[`LendController.user_state(_user: address) -> uint256[4]: view`] + +Returns the full state of a user's loan: `[collateral, borrowed, debt, N]`. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | Borrower address | + +Returns: `[collateral, borrowed, debt, N]` (`uint256[4]`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +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, borrowed, debt, N) + """ + return staticcall self._view.user_state(_user) + + +@internal +def _configure( + _loan_discount: uint256, + _liquidation_discount: uint256, + _monetary_policy: IMonetaryPolicy, + _view_blueprint: address, + _fee: uint256, + _price_oracle: IPriceOracle, + _liquidity_mining_callback: ILMCallback, +): + if _loan_discount != SKIP_CONFIG_UINT256 and _liquidation_discount != SKIP_CONFIG_UINT256: + self.loan_discount = _loan_discount + self.liquidation_discount = _liquidation_discount + if _monetary_policy.address != SKIP_CONFIG_ADDRESS: + self._monetary_policy = _monetary_policy + if _view_blueprint != SKIP_CONFIG_ADDRESS: + view: address = create_from_blueprint( + _view_blueprint, + self, + SQRT_BAND_RATIO, + LOGN_A_RATIO, + AMM, + A, + COLLATERAL_TOKEN, + COLLATERAL_PRECISION, + BORROWED_TOKEN, + BORROWED_PRECISION, +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.user_state( + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `user_prices` + +::::description[`LendController.user_prices(_user: address) -> uint256[2]: view`] + +Returns the upper and lower price bounds of a user's collateral bands in the AMM. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | Borrower address | + +Returns: `[price_upper, price_lower]` (`uint256[2]`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +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) + """ + return staticcall self._view.user_prices(_user) + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.user_prices( + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `users_to_liquidate` + +::::description[`LendController.users_to_liquidate(_from: uint256 = 0, _limit: uint256 = 0) -> IController.Position[]: view`] + +Returns a list of user positions that can be liquidated, paginated. + +| Input | Type | Description | +| --- | --- | --- | +| `_from` | `uint256` | Starting index | +| `_limit` | `uint256` | Maximum number of positions to return | + +Returns: array of liquidatable positions (`IController.Position[]`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +def users_to_liquidate( + _from: uint256 = 0, _limit: uint256 = 0 +) -> DynArray[IController.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 + """ + return staticcall self._view.users_to_liquidate(_from, _limit) + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.users_to_liquidate( + /* _from: uint256 */ 0n, + /* _limit: uint256 */ 0n, +) +``` + + + +:::: + +### `tokens_to_liquidate` + +::::description[`LendController.tokens_to_liquidate(_user: address, _frac: uint256 = 10**18) -> uint256: view`] + +Returns the amount of borrowed tokens needed to fully liquidate a user's position. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | Borrower address | +| `_frac` | `uint256` | Fraction to liquidate, where `10**18` is 100% | + +Returns: tokens needed for liquidation (`uint256`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +def tokens_to_liquidate(_user: address, _frac: uint256 = WAD) -> uint256: + """ + @notice Calculate the amount of borrowed asset 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 borrowed asset needed + """ + assert _frac <= WAD, "frac>100%" + health_limit: uint256 = 0 + if not self._has_approval(_user): + health_limit = self.liquidation_discounts[_user] + borrowed: uint256 = unsafe_div( + (staticcall AMM.get_sum_xy(_user))[0] + * self._get_f_remove(_frac, health_limit), + WAD, + ) + debt: uint256 = unsafe_div(self._debt(_user)[0] * _frac + (WAD - 1), WAD) + + return crv_math.sub_or_zero(debt, borrowed) + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.tokens_to_liquidate( + /* _user: address */ "0x0000000000000000000000000000000000000000", + /* _frac: uint256 */ 0n, +) +``` + + + +:::: + +### `total_debt` + +::::description[`LendController.total_debt() -> uint256: view`] + +Returns the total outstanding debt across all loans in this market, including accrued interest. + +Returns: total debt (`uint256`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +@reentrant +def total_debt() -> uint256: + """ + @notice Total debt of this controller + @dev Marked as reentrant because used by monetary policy + @return Total outstanding debt with accrued interest + """ + return self._get_total_debt() + + +@internal +@pure +def _get_y_effective( + _collateral: uint256, + _N: uint256, + _discount: uint256, + _SQRT_BAND_RATIO: uint256, + _A: 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 with 18 decimals 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%) + @param _SQRT_BAND_RATIO Square root of the band ratio sqrt((A-1)/A) at 1e18 base + @param _A Band width factor + @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 +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.total_debt() +``` + + + +:::: + +### `n_loans` + +::::description[`LendController.n_loans() -> uint256: view`] + +Returns the total number of active loans. + +Returns: number of loans (`uint256`). + + + +The implementation is in [`lending/LendController.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendController.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, signer) +const tx = await contract.n_loans() +await tx.wait() +``` + + + +:::: + +### `loans` + +::::description[`LendController.loans(_index: uint256) -> address: view`] + +Returns the borrower address at a given loan index. + +| Input | Type | Description | +| --- | --- | --- | +| `_index` | `uint256` | Loan index | + +Returns: borrower address (`address`). + + + +The implementation is in [`lending/LendController.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendController.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, signer) +const tx = await contract.loans( + /* _index: uint256 */ 0n, +) +await tx.wait() +``` + + + +:::: + +### `loan_ix` + +::::description[`LendController.loan_ix(_user: address) -> uint256: view`] + +Returns the index of a user's loan in the loans array. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | Borrower address | + +Returns: loan index (`uint256`). + + + +The implementation is in [`lending/LendController.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendController.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, signer) +const tx = await contract.loan_ix( + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +await tx.wait() +``` + + + +:::: + + +--- + + +## Health Previews + +These functions allow UIs to show the health impact of operations before the user submits a transaction. + +### `create_loan_health_preview` + +::::description[`LendController.create_loan_health_preview(_collateral: uint256, _debt: uint256, _N: uint256, _for: address, _full: bool) -> int256: view`] + +Previews the health of a loan that would result from calling `create_loan`. + +| Input | Type | Description | +| --- | --- | --- | +| `_collateral` | `uint256` | Collateral amount | +| `_debt` | `uint256` | Debt amount | +| `_N` | `uint256` | Number of bands | +| `_for` | `address` | Borrower address | +| `_full` | `bool` | Full (pessimistic) health | + +Returns: predicted health (`int256`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +def create_loan_health_preview( + _collateral: uint256, + _debt: uint256, + _N: uint256, + _for: address, + _full: bool, +) -> int256: + """ + @notice Calculates health after calling create_loan with the same params + @param _collateral Amount of collateral to use (from wallet + callback). + Note: the collateral amount coming from the callback should be included. + @param _debt Borrowed asset 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 of the user the loan is being created for + @param _full Whether it's a 'full' health or not + @return Signed health value + """ + return staticcall self._view.create_loan_health_preview( + _collateral, _debt, _N, _for, _full + ) + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.create_loan_health_preview( + /* _collateral: uint256 */ 0n, + /* _debt: uint256 */ 0n, + /* _N: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", + /* _full: bool */ false, +) +``` + + + +:::: + +### `borrow_more_health_preview` + +::::description[`LendController.borrow_more_health_preview(_collateral: uint256, _debt: uint256, _for: address, _full: bool) -> int256: view`] + +Previews the health after borrowing more. + +| Input | Type | Description | +| --- | --- | --- | +| `_collateral` | `uint256` | Additional collateral | +| `_debt` | `uint256` | Additional debt | +| `_for` | `address` | Borrower address | +| `_full` | `bool` | Full (pessimistic) health | + +Returns: predicted health (`int256`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +def borrow_more_health_preview( + _collateral: uint256, + _debt: uint256, + _for: address, + _full: bool, +) -> int256: + """ + @notice Calculates health after calling borrow_more with the same params + @param _collateral Amount of collateral to add (from wallet + callback). + Note: the collateral amount coming from the callback should be included. + @param _debt Amount of borrowed asset debt to take + @param _for Address to borrow for + @param _full Whether it's a 'full' health or not + @return Signed health value + """ + return staticcall self._view.borrow_more_health_preview( + _collateral, _debt, _for, _full + ) + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.borrow_more_health_preview( + /* _collateral: uint256 */ 0n, + /* _debt: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", + /* _full: bool */ false, +) +``` + + + +:::: + +### `add_collateral_health_preview` + +::::description[`LendController.add_collateral_health_preview(_collateral: uint256, _for: address, _full: bool) -> int256: view`] + +Previews the health after adding collateral. + +| Input | Type | Description | +| --- | --- | --- | +| `_collateral` | `uint256` | Collateral to add | +| `_for` | `address` | Borrower address | +| `_full` | `bool` | Full (pessimistic) health | + +Returns: predicted health (`int256`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +def add_collateral_health_preview( + _collateral: uint256, + _for: address, + _full: bool, +) -> int256: + """ + @notice Calculates health after calling add_collateral with the same args + @param _collateral Amount of collateral to add + @param _for The user to add collateral for + @param _full Whether it's a 'full' health or not + @return Signed health value + """ + return staticcall self._view.add_collateral_health_preview(_collateral, _for, _full) + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.add_collateral_health_preview( + /* _collateral: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", + /* _full: bool */ false, +) +``` + + + +:::: + +### `remove_collateral_health_preview` + +::::description[`LendController.remove_collateral_health_preview(_collateral: uint256, _for: address, _full: bool) -> int256: view`] + +Previews the health after removing collateral. + +| Input | Type | Description | +| --- | --- | --- | +| `_collateral` | `uint256` | Collateral to remove | +| `_for` | `address` | Borrower address | +| `_full` | `bool` | Full (pessimistic) health | + +Returns: predicted health (`int256`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +def remove_collateral_health_preview( + _collateral: uint256, + _for: address, + _full: bool, +) -> int256: + """ + @notice Calculates health after calling remove_collateral with the same args + @param _collateral Amount of collateral to remove + @param _for Address to remove collateral from + @param _full Whether it's a 'full' health or not + @return Signed health value + """ + return staticcall self._view.remove_collateral_health_preview( + _collateral, _for, _full + ) + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.remove_collateral_health_preview( + /* _collateral: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", + /* _full: bool */ false, +) +``` + + + +:::: + +### `repay_health_preview` + +::::description[`LendController.repay_health_preview(_d_collateral: uint256, _d_debt: uint256, _for: address, _shrink: bool, _full: bool) -> int256: view`] + +Previews the health after repaying debt. + +| Input | Type | Description | +| --- | --- | --- | +| `_d_collateral` | `uint256` | Collateral amount removed through the callback | +| `_d_debt` | `uint256` | Debt to repay | +| `_for` | `address` | Borrower address | +| `_shrink` | `bool` | Whether the repayment shrinks a soft-liquidated position | +| `_full` | `bool` | Full (pessimistic) health | + +Returns: predicted health (`int256`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +def repay_health_preview( + _d_collateral: uint256, + _d_debt: uint256, + _for: address, + _shrink: bool, + _full: bool, +) -> int256: + """ + @notice Calculates health after calling repay with the same params + @dev Works only for partial repay, reverts for full repay + @param _d_collateral Amount of collateral to remove (goes to callback) + @param _d_debt The amount of debt to repay (from wallet + callback). + Note: the borrowed amount coming from the callback should be included. + @param _for The user to repay the debt for + @param _shrink Whether shrink soft-liquidated part of the position or not + @param _full Whether it's a 'full' health or not + @return Signed health value + """ + return staticcall self._view.repay_health_preview( + _d_collateral, _d_debt, _for, _shrink, _full + ) + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.repay_health_preview( + /* _d_collateral: uint256 */ 0n, + /* _d_debt: uint256 */ 0n, + /* _for: address */ "0x0000000000000000000000000000000000000000", + /* _shrink: bool */ false, + /* _full: bool */ false, +) +``` + + + +:::: + +### `liquidate_health_preview` + +::::description[`LendController.liquidate_health_preview(_user: address, _caller: address, _frac: uint256, _full: bool) -> int256: view`] + +Previews what a user's health would be after liquidation. + +| Input | Type | Description | +| --- | --- | --- | +| `_user` | `address` | Borrower address | +| `_caller` | `address` | Address that would submit the liquidation | +| `_frac` | `uint256` | Fraction of the position to liquidate, where `10**18` is 100% | +| `_full` | `bool` | Full (pessimistic) health | + +Returns: predicted health (`int256`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +def liquidate_health_preview( + _user: address, + _caller: address, + _frac: uint256, + _full: bool, +) -> int256: + """ + @notice Calculates health after calling liquidate with the same args + @param _user User to liquidate + @param _caller Address from which liquidate tx is going to be sent. + Depending on this address liquidation_discount will be changed or not. + @param _frac Fraction to liquidate; 100% = 10**18 + @param _full Whether it's a 'full' health or not + @return Signed health value + """ + return staticcall self._view.liquidate_health_preview( + _user, _caller, _frac, _full + ) + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.liquidate_health_preview( + /* _user: address */ "0x0000000000000000000000000000000000000000", + /* _caller: address */ "0x0000000000000000000000000000000000000000", + /* _frac: uint256 */ 0n, + /* _full: bool */ false, +) +``` + + + +:::: + + +--- + + +## Lending-Specific + +These functions are unique to `LendController` and not part of the shared `controller.vy` module. + +### `available_balance` + +::::description[`LendController.available_balance() -> uint256: view`] + +Returns the amount of borrowed tokens currently available for new loans — i.e., tracked lender deposits minus amounts lent out. Direct token donations are not included in this accounting. + +Returns: available balance (`uint256`). + + + +Excerpt from [`lending/LendController.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendController.vy): + +```vyper +@external +@view +@reentrant +def available_balance() -> uint256: + return self._available_balance + + +# https://github.com/vyperlang/vyper/issues/4721 +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.available_balance() +``` + + + +:::: + +### `borrow_cap` + +::::description[`LendController.borrow_cap() -> uint256: view`] + +Returns the maximum total debt allowed for this market. It is initialized to 0, which prevents new borrowing; the LendConfigurator must raise it above 0 (for example, through a DAO action) before borrowing can be enabled. + +Returns: borrow cap (`uint256`). + + + +The implementation is in [`lending/LendController.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendController.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, signer) +const tx = await contract.borrow_cap() +await tx.wait() +``` + + + +:::: + +### `vault` + +::::description[`LendController.vault() -> address: view`] + +Returns the address of the associated ERC4626 vault. + +Returns: vault address (`address`). + + + +Excerpt from [`lending/LendController.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendController.vy): + +```vyper +@external +@view +@reentrant +def vault() -> address: + """ + @notice Address of the vault + @return Address of the vault contract + """ + return VAULT.address + + +@deploy +def __init__( + _vault: IVault, + _amm: IAMM, + _borrowed_token: IERC20, + _collateral_token: IERC20, + _monetary_policy: IMonetaryPolicy, + _loan_discount: uint256, + _liquidation_discount: uint256, + _view_blueprint: address, + _configurator: core.IConfigurator, +): + """ + @notice Lend Controller constructor + @param _vault Address of the vault this controller belongs to + @param _amm AMM address (already deployed from blueprint) + @param _borrowed_token Token which is being borrowed + @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 _view_blueprint Address of the controller view blueprint + @param _configurator Address of the configurator contract + """ + VAULT = _vault + +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.vault() +``` + + + +:::: + +### `configure_lend` + +::::description[`LendController.configure_lend(_borrow_cap: uint256, _admin_percentage: uint256)`] + +:::guard[Guarded Method] +This function is only callable by the LendConfigurator contract. Governance normally invokes it through the configurator's administrative methods. +::: + +Configures the market's borrow cap and admin-fee share. Either parameter can be left unchanged by passing the protocol's `SKIP_CONFIG_UINT256` sentinel. + +| Input | Type | Description | +| --- | --- | --- | +| `_borrow_cap` | `uint256` | New maximum total debt (0 disables new borrowing) | +| `_admin_percentage` | `uint256` | New admin-fee share, scaled so that `10**18` is 100% | + +Updates the supplied configuration values. + + + +Excerpt from [`lending/LendController.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendController.vy): + +```vyper +@external +def configure_lend(_borrow_cap: uint256, _admin_percentage: uint256): + assert msg.sender == core.CONFIGURATOR.address, "Only configurator" + if _borrow_cap != core.SKIP_CONFIG_UINT256: + self.borrow_cap = _borrow_cap + if _admin_percentage != core.SKIP_CONFIG_UINT256: + rate_mul: uint256 = staticcall core.AMM.get_rate_mul() + core._update_total_debt(0, rate_mul, False) + core.admin_percentage = _admin_percentage + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, signer) +const tx = await contract.configure_lend( + /* _borrow_cap: uint256 */ 0n, + /* _admin_percentage: uint256 */ 0n, +) +await tx.wait() +``` + + + +:::: + +## Calculations + +### `max_borrowable` + +::::description[`LendController.max_borrowable(_d_collateral: uint256, _N: uint256, _user: address = empty(address)) -> uint256: view`] + +Returns the maximum amount that can be borrowed given a collateral amount and number of bands, respecting the borrow cap and available balance. + +| Input | Type | Description | +| --- | --- | --- | +| `_d_collateral` | `uint256` | Collateral amount | +| `_N` | `uint256` | Number of bands | +| `_user` | `address` | Borrower address (for existing loan context) | + +Returns: max borrowable amount (`uint256`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +def max_borrowable( + _d_collateral: uint256, _N: uint256, _user: address = empty(address) +) -> uint256: + """ + @notice Calculation of maximum which can be borrowed. + Returns the maximum additional amount to be borrowed in case the user has a position. + @param _d_collateral Collateral amount against which to borrow + (additional amount if the user has a position) + @param _N Number of bands to have the deposit into (to be ignored if the user has a position) + @param _user User to calculate the value for + (can be zero address for create_loan if the user doesn't have extra_health) + @return Maximum (additional) amount of borrowed asset to borrow + """ + return staticcall self._view.max_borrowable(_d_collateral, _N, _user) + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.max_borrowable( + /* _d_collateral: uint256 */ 0n, + /* _N: uint256 */ 0n, + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `min_collateral` + +::::description[`LendController.min_collateral(_debt: uint256, _N: uint256, _user: address = empty(address)) -> uint256: view`] + +Returns the minimum collateral required for a given debt amount and number of bands. + +| Input | Type | Description | +| --- | --- | --- | +| `_debt` | `uint256` | Desired debt amount | +| `_N` | `uint256` | Number of bands | +| `_user` | `address` | User address, needed only when the user has nonzero extra health | + +Returns: minimum collateral required (`uint256`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +def min_collateral( + _debt: uint256, _N: uint256, _user: address = empty(address) +) -> uint256: + """ + @notice Calculation of minimum collaterl amount required for given debt amount + @param _debt The amount of debt for which calculation should be done + @param _N number of bands to have the deposit into + @param _user User to calculate the value for (only necessary for nonzero extra_health) + @return Minimum amount of collateral asset to provide + """ + return staticcall self._view.min_collateral(_debt, _N, _user) + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.min_collateral( + /* _debt: uint256 */ 0n, + /* _N: uint256 */ 0n, + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `calculate_debt_n1` + +::::description[`LendController.calculate_debt_n1(_collateral: uint256, _debt: uint256, _N: uint256, _user: address = empty(address)) -> int256: view`] + +Calculates the upper band number (`n1`) for a loan with the given parameters. Used to determine where collateral will be placed in the AMM. + +| Input | Type | Description | +| --- | --- | --- | +| `_collateral` | `uint256` | Collateral amount | +| `_debt` | `uint256` | Debt amount | +| `_N` | `uint256` | Number of bands | +| `_user` | `address` | User address, needed only when the user has nonzero extra health | + +Returns: upper band number (`int256`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +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 + """ + assert _N > MIN_TICKS_UINT - 1 # dev: Need more ticks + assert _N < MAX_TICKS_UINT + 1 # dev: Need less ticks + return self._calculate_debt_n1(_collateral, _debt, _N, _user) + + +@internal +@view +def _calculate_debt_n1( + _collateral: uint256, + _debt: uint256, + _N: uint256, + _user: address, +) -> int256: + assert _debt > 0 # dev: No loan + n0: int256 = staticcall AMM.active_band() + p_base: uint256 = staticcall 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( +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.calculate_debt_n1( + /* _collateral: uint256 */ 0n, + /* _debt: uint256 */ 0n, + /* _N: uint256 */ 0n, + /* _user: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + + +--- + + +## Configuration + +### `configure` + +::::description[`LendController.configure(_loan_discount: uint256, _liquidation_discount: uint256, _monetary_policy: address, _view_blueprint: address, _fee: uint256, _price_oracle: address, _liquidity_mining_callback: address)`] + +:::guard[Guarded Method] +This function is only callable by the LendConfigurator contract. +::: + +Updates shared-controller settings. Governance normally invokes it through the LendConfigurator rather than calling the controller directly. Parameters that should remain unchanged use the protocol's `SKIP_CONFIG_UINT256` or `SKIP_CONFIG_ADDRESS` sentinel, as applicable. + +| Input | Type | Description | +| --- | --- | --- | +| `_loan_discount` | `uint256` | Loan discount for LTV calculations | +| `_liquidation_discount` | `uint256` | Discount used in liquidation calculations | +| `_monetary_policy` | `address` | Monetary policy contract | +| `_view_blueprint` | `address` | Controller-view blueprint | +| `_fee` | `uint256` | Base AMM fee | +| `_price_oracle` | `address` | Price oracle contract | +| `_liquidity_mining_callback` | `address` | Liquidity-mining callback contract | + +Updates the supplied configuration values. + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +def configure( + _loan_discount: uint256, + _liquidation_discount: uint256, + _monetary_policy: IMonetaryPolicy, + _view_blueprint: address, + _fee: uint256, + _price_oracle: IPriceOracle, + _liquidity_mining_callback: ILMCallback, +): + assert msg.sender == CONFIGURATOR.address # dev: Only configurator + self._configure( + _loan_discount, + _liquidation_discount, + _monetary_policy, + _view_blueprint, + _fee, + _price_oracle, + _liquidity_mining_callback, + ) + + + +@internal +@view +def _admin_fees() -> uint256: + return self._stored_admin_fees + self._preview_total_debt(staticcall AMM.get_rate_mul(), self._total_debt)[1] + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.configure( + /* _loan_discount: uint256 */ 0n, + /* _liquidation_discount: uint256 */ 0n, + /* _monetary_policy: address */ "0x0000000000000000000000000000000000000000", + /* _view_blueprint: address */ "0x0000000000000000000000000000000000000000", + /* _fee: uint256 */ 0n, + /* _price_oracle: address */ "0x0000000000000000000000000000000000000000", + /* _liquidity_mining_callback: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + + +--- + + +## Other Methods + +### `amm` + +::::description[`LendController.amm() -> address: view`] + +Returns the AMM (LLAMMA) address for this market. + +Returns: AMM address (`address`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +@reentrant +def amm() -> IAMM: + """ + @notice Address of the AMM + @return Address of the AMM contract + """ + return AMM + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.amm() +``` + + + +:::: + +### `monetary_policy` + +::::description[`LendController.monetary_policy() -> address: view`] + +Returns the current monetary policy address. + +Returns: monetary policy address (`address`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +@reentrant +def monetary_policy() -> IMonetaryPolicy: + """ + @notice Address of the monetary policy + @return Address of the monetary policy contract + """ + return self._monetary_policy + + +approval: public(HashMap[address, HashMap[address, bool]]) +extra_health: public(reentrant(HashMap[address, uint256])) + +loan: HashMap[address, IController.Loan] +liquidation_discounts: public(HashMap[address, uint256]) +_total_debt: IController.Loan + +# Enumerate existing loans +loans: public(address[2**64 - 1]) +# Position of the loan in the list +loan_ix: public(HashMap[address, uint256]) +# Number of nonzero loans +n_loans: public(uint256) + + +# cumulative amount of borrowed assets ever lent +lent: uint256 +# cumulative amount of borrowed assets ever repaid +repaid: uint256 +# cumulative amount of borrowed assets ever collected as admin fees +collected: uint256 + + +# Admin fees yet to be collected. Goes to zero when collected. +_stored_admin_fees: uint256 +admin_percentage: public(uint256) + +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.monetary_policy() +``` + + + +:::: + +### `liquidation_discount` + +::::description[`LendController.liquidation_discount() -> uint256: view`] + +Returns the current liquidation discount. + +Returns: liquidation discount (`uint256`). + + + +The implementation is in [`lending/LendController.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendController.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, signer) +const tx = await contract.liquidation_discount() +await tx.wait() +``` + + + +:::: + +### `loan_discount` + +::::description[`LendController.loan_discount() -> uint256: view`] + +Returns the current loan discount. + +Returns: loan discount (`uint256`). + + + +The implementation is in [`lending/LendController.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendController.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, signer) +const tx = await contract.loan_discount() +await tx.wait() +``` + + + +:::: + +### `admin_fees` + +::::description[`LendController.admin_fees() -> uint256: view`] + +Returns the amount of uncollected admin fees. + +Returns: admin fees (`uint256`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +@reentrant +def admin_fees() -> uint256: + """ + @notice Pending admin fees which can be claimed if the controller has enough balance + @return Amount of pending admin fees in borrowed token units + """ + return self._admin_fees() + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.admin_fees() +``` + + + +:::: + +### `admin_percentage` + +::::description[`LendController.admin_percentage() -> uint256: view`] + +Returns the current admin fee percentage, scaled so that `10**18` is 100%. + +Returns: admin percentage (`uint256`). + + + +The implementation is in [`lending/LendController.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendController.vy). This public ABI entry is compiler-generated or inherited; inspect the linked contract source and interfaces for its implementation. + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, signer) +const tx = await contract.admin_percentage() +await tx.wait() +``` + + + +:::: + +### `collect_fees` + +::::description[`LendController.collect_fees()`] + +Collects accumulated admin fees and sends them to the fee receiver set in the factory. + +Emits: `CollectFees` event. + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +def collect_fees() -> uint256: + """ + @notice Collect the fees charged as interest. + @return Amount of fees collected and transferred to the fee receiver + """ + rate_mul: uint256 = staticcall AMM.get_rate_mul() + self._update_total_debt(0, rate_mul, False) + + # self._stored_admin_fees == self.admin_fees() after _update_total_debt + pending_admin_fees: uint256 = self._stored_admin_fees + self.collected += pending_admin_fees + self._stored_admin_fees = 0 + + extcall VIRTUAL.on_borrowed_token_transfer_out(pending_admin_fees) + tkn.transfer(BORROWED_TOKEN, staticcall FACTORY.fee_receiver(), pending_admin_fees) + + self._save_rate() + log IController.CollectFees(amount=pending_admin_fees) + + return pending_admin_fees + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, signer) +const tx = await contract.collect_fees() +await tx.wait() +``` + + + +:::: + +### `save_rate` + +::::description[`LendController.save_rate()`] + +Updates the stored interest rate from the monetary policy. This is called automatically during loan operations but can also be called externally to keep the rate fresh. + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +def save_rate(): + """ + @notice Save current rate + """ + self._save_rate() + + +@internal +@view +def _debt(_user: address) -> (uint256, uint256): + """ + @notice Get the value of debt and rate_mul without changing the state + @param _user User address + @return (debt, rate_mul) + """ + rate_mul: uint256 = staticcall AMM.get_rate_mul() + loan: IController.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 = math._ceil_div(loan.initial_debt * rate_mul, loan.rate_mul) + return (debt, rate_mul) + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.save_rate() +``` + + + +:::: + +### `version` + +::::description[`LendController.version() -> String[10]: view`] + +Returns the contract version string (e.g., `"2.0.0-lend"`). + +Returns: version string (`String[10]`). + + + +Excerpt from [`lending/LendController.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendController.vy): + +```vyper +@external +@view +@reentrant +def version() -> String[10]: + """ + @notice Version of this controller + @return Version string of the form "-lend" + """ + return concat(core.version, "-lend") + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.version() +``` + + + +:::: + +### `factory` + +::::description[`LendController.factory() -> address: view`] + +Returns the factory that deployed this controller. + +Returns: factory address (`address`). + + + +Excerpt from [`Controller.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/Controller.vy): + +```vyper +@external +@view +@reentrant +def factory() -> IFactory: + """ + @notice Address of the factory + @return Address of the factory contract + """ + return FACTORY + + +``` + + + + + +```ts +const contract = new Contract('0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0', LendControllerAbi, provider) +const result = await contract.factory() +``` + + + +:::: diff --git a/docs/developer/llamalend-v2/lend-factory.md b/docs/developer/llamalend-v2/lend-factory.md new file mode 100644 index 00000000..87753d0b --- /dev/null +++ b/docs/developer/llamalend-v2/lend-factory.md @@ -0,0 +1,792 @@ +# LendFactory + +The `LendFactory` is the entry point for **creating new one-way lending markets**. It deploys a triplet of contracts — [Vault](./vault.md), [LendController](./lend-controller.md), and [AMM](./amm.md) — from blueprint implementations stored in an internal registry. Each market is fully isolated. + +The factory uses Snekmate's [`ownable`](https://github.com/pcaversaccio/snekmate) module for access control and [`pausable`](https://github.com/pcaversaccio/snekmate) for emergency pause functionality. + +:::vyper[`LendFactory.vy`] + +The source code for the `LendFactory.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/lending/LendFactory.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.4.3`. + +*Deployment addresses will be added once contracts are finalized.* + +::: + +Examples use Ethers v6 with an ABI generated from the pinned source linked in each panel. Replace every zero address and amount placeholder. Read-only calls use the verified Ethereum v2 reference factory; execute a write only on a fork first. + + +--- + + +## Market Creation + +### `create` + +::::description[`LendFactory.create(_borrowed_token: address, _collateral_token: address, _A: uint256, _fee: uint256, _loan_discount: uint256, _liquidation_discount: uint256, _price_oracle: address, _monetary_policy: address, _supply_limit: uint256) -> address[3]`] + +Deploys a new lending market by creating a Vault, LendController, and AMM from their respective blueprints. Validates all parameters, initializes the triplet, and registers the market. + +| Input | Type | Description | +| --- | --- | --- | +| `_borrowed_token` | `address` | Token that lenders deposit and borrowers receive | +| `_collateral_token` | `address` | Token used as collateral by borrowers | +| `_A` | `uint256` | Amplification coefficient — determines band width (~1/A) | +| `_fee` | `uint256` | Swap fee for the AMM | +| `_loan_discount` | `uint256` | Maximum discount for LTV calculation | +| `_liquidation_discount` | `uint256` | Discount at which liquidation can occur | +| `_price_oracle` | `address` | Initialized price oracle contract | +| `_monetary_policy` | `address` | Initialized monetary policy contract | +| `_supply_limit` | `uint256` | Maximum supply cap for the vault | + +Returns: array of three addresses — `[vault, controller, amm]` (`address[3]`). + +Emits: `NewVault` event. + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +def create( + _borrowed_token: IERC20, + _collateral_token: IERC20, + _A: uint256, + _fee: uint256, + _loan_discount: uint256, + _liquidation_discount: uint256, + _price_oracle: IPriceOracle, + _monetary_policy: IMonetaryPolicy, + _supply_limit: uint256, +) -> address[3]: + """ + @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%). + Bounds are enforced by the AMM: MIN_FEE <= _fee <= min(WAD * MIN_TICKS / _A, 10%) + @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 _monetary_policy Monetary policy contract to set the borrow rate + @param _supply_limit Supply cap + @return Addresses of the deployed [vault, AMM, controller] contracts + """ + pausable._require_not_paused() + assert _borrowed_token != _collateral_token, "Same token" + assert _A >= MIN_A and _A <= MAX_A, "Wrong A" + assert _liquidation_discount > 0, "liquidation discount = 0" + assert _loan_discount < WAD, "loan discount >= 100%" + assert _loan_discount > _liquidation_discount, "loan discount <= liquidation discount" + + A_ratio: uint256 = 10**18 * _A // (_A - 1) + + # Validate price oracle + p: uint256 = (staticcall _price_oracle.price()) + assert p > 0 # dev: price oracle returned zero +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, signer) +const tx = await contract.create( + /* _borrowed_token: address */ "0x0000000000000000000000000000000000000000", + /* _collateral_token: address */ "0x0000000000000000000000000000000000000000", + /* _A: uint256 */ 0n, + /* _fee: uint256 */ 0n, + /* _loan_discount: uint256 */ 0n, + /* _liquidation_discount: uint256 */ 0n, + /* _price_oracle: address */ "0x0000000000000000000000000000000000000000", + /* _monetary_policy: address */ "0x0000000000000000000000000000000000000000", + /* _supply_limit: uint256 */ 0n, +) +await tx.wait() +``` + + + +:::: + + +--- + + +## Market Registry + +### `market_count` + +::::description[`LendFactory.market_count() -> uint256: view`] + +Returns the total number of lending markets created by the factory. + +Returns: number of markets (`uint256`). + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +@view +def market_count() -> uint256: + return len(self._vaults) + +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, provider) +const result = await contract.market_count() +``` + + + +:::: + +### `markets` + +::::description[`LendFactory.markets(_n: uint256) -> Market: view`] + +Returns the market info struct for a given market index. + +| Input | Type | Description | +| --- | --- | --- | +| `_n` | `uint256` | Market index (0-based) | + +Returns: market struct containing the vault, controller, AMM, collateral-token, borrowed-token, price-oracle, and monetary-policy addresses (`Market`). + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +@view +@reentrant +def markets(_n: uint256) -> ILendFactory.Market: + """ + @notice Get market data for market at index `_n` + @param _n Index of the market + @return Market struct containing vault, controller, amm, tokens, price oracle and monetary policy addresses + """ + vault: IVault = self._vaults[_n] + controller: IController = staticcall vault.controller() + amm: IAMM = staticcall vault.amm() + + return ILendFactory.Market( + vault=vault, + controller=controller, + amm=amm, + collateral_token=staticcall vault.collateral_token(), + borrowed_token=staticcall vault.borrowed_token(), + price_oracle=staticcall amm.price_oracle_contract(), + monetary_policy=staticcall controller.monetary_policy(), + ) + + +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, provider) +const result = await contract.markets( + /* _n: uint256 */ 0n, +) +``` + + + +:::: + +### `coins` + +::::description[`LendFactory.coins(_vault_id: uint256) -> IERC20[2]: view`] + +Returns the borrowed and collateral token addresses for a given vault. + +| Input | Type | Description | +| --- | --- | --- | +| `_vault_id` | `uint256` | Vault index | + +Returns: array of `[borrowed_token, collateral_token]` (`IERC20[2]`). + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +@view +@reentrant +def coins(_vault_id: uint256) -> IERC20[2]: + vault: IVault = self._vaults[_vault_id] + return [staticcall vault.borrowed_token(), staticcall vault.collateral_token()] + + +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, provider) +const result = await contract.coins( + /* _vault_id: uint256 */ 0n, +) +``` + + + +:::: + +### `vaults_index` + +::::description[`LendFactory.vaults_index(_vault: IVault) -> uint256: view`] + +Returns the index of a vault in the factory registry. Reverts if the vault is not registered. + +| Input | Type | Description | +| --- | --- | --- | +| `_vault` | `IVault` | Vault address to look up | + +Returns: vault index (`uint256`). + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +@view +@reentrant +def vaults_index(_vault: IVault) -> uint256: + """ + @notice Get the index of a vault in the markets array + @param _vault Address of the vault + @return Index of the vault + """ + return self._vaults_index[_vault] - 2**128 + + +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, provider) +const result = await contract.vaults_index( + /* _vault: IVault */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + + +--- + + +## Blueprints + +### `amm_blueprint` + +::::description[`LendFactory.amm_blueprint() -> address: view`] + +Returns the current AMM blueprint address used for deploying new markets. + +Returns: blueprint address (`address`). + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +@view +def amm_blueprint() -> address: + """ + @notice Get the address of the AMM blueprint + @return Address of the AMM blueprint + """ + return blueprint_registry.get(AMM_BLUEPRINT_ID) + + +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, provider) +const result = await contract.amm_blueprint() +``` + + + +:::: + +### `controller_blueprint` + +::::description[`LendFactory.controller_blueprint() -> address: view`] + +Returns the current controller blueprint address. + +Returns: blueprint address (`address`). + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +@view +def controller_blueprint() -> address: + """ + @notice Get the address of the controller blueprint + @return Address of the controller blueprint + """ + return blueprint_registry.get(CONTROLLER_BLUEPRINT_ID) + + +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, provider) +const result = await contract.controller_blueprint() +``` + + + +:::: + +### `vault_blueprint` + +::::description[`LendFactory.vault_blueprint() -> address: view`] + +Returns the current vault blueprint address. + +Returns: blueprint address (`address`). + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +@view +def vault_blueprint() -> address: + """ + @notice Get the address of the vault blueprint + @return Address of the vault blueprint + """ + return blueprint_registry.get(VAULT_BLUEPRINT_ID) + + +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, provider) +const result = await contract.vault_blueprint() +``` + + + +:::: + +### `controller_view_blueprint` + +::::description[`LendFactory.controller_view_blueprint() -> address: view`] + +Returns the current controller view blueprint address. + +Returns: blueprint address (`address`). + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +@view +def controller_view_blueprint() -> address: + """ + @notice Get the address of the controller view blueprint + @return Address of the controller view blueprint + """ + return blueprint_registry.get(CONTROLLER_VIEW_BLUEPRINT_ID) + + + +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, provider) +const result = await contract.controller_view_blueprint() +``` + + + +:::: + + +--- + + +## Fee Receivers + +### `fee_receiver` + +::::description[`LendFactory.fee_receiver(_controller: address) -> address: view`] + +Returns the fee receiver for a given controller. If a custom fee receiver is set, it returns that; otherwise, it returns the default fee receiver. + +| Input | Type | Description | +| --- | --- | --- | +| `_controller` | `address` | Controller address | + +Returns: fee receiver address (`address`). + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +@view +def fee_receiver(_controller: address = msg.sender) -> address: + """ + @notice Get fee receiver who earns interest from admin fees + @dev This function is called by controllers without specifying the + first argument to get their fee receiver. + @param _controller Address of the controller + @return Address of the fee receiver + """ + custom_fee_receiver: address = self.fee_receivers[_controller] + return custom_fee_receiver if custom_fee_receiver != empty(address) else self.default_fee_receiver + + +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, provider) +const result = await contract.fee_receiver( + /* _controller: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `set_default_fee_receiver` + +::::description[`LendFactory.set_default_fee_receiver(_fee_receiver: address)`] + +:::guard[Guarded Method by [Snekmate 🐍](https://github.com/pcaversaccio/snekmate)] +This function is only callable by the `owner` of the contract. +::: + +Sets the default fee receiver for all markets that don't have a custom one. + +| Input | Type | Description | +| --- | --- | --- | +| `_fee_receiver` | `address` | New default fee receiver address | + +Emits: `SetFeeReceiver` event. + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +@reentrant +def set_default_fee_receiver(_fee_receiver: address): + """ + @notice Set default fee receiver who earns admin fees on + all controllers without a custom fee receiver + @param _fee_receiver Address of the receiver + """ + ownable._check_owner() + self._set_default_fee_receiver(_fee_receiver) + + +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, signer) +const tx = await contract.set_default_fee_receiver( + /* _fee_receiver: address */ "0x0000000000000000000000000000000000000000", +) +await tx.wait() +``` + + + +:::: + +### `set_custom_fee_receiver` + +::::description[`LendFactory.set_custom_fee_receiver(_controller: address, _fee_receiver: address)`] + +:::guard[Guarded Method by [Snekmate 🐍](https://github.com/pcaversaccio/snekmate)] +This function is only callable by the `owner` of the contract. +::: + +Sets a custom fee receiver for a specific market's controller. + +| Input | Type | Description | +| --- | --- | --- | +| `_controller` | `address` | Controller address | +| `_fee_receiver` | `address` | Custom fee receiver address | + +Emits: `CustomSetFeeReceiver` event. + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +@reentrant +def set_custom_fee_receiver(_controller: address, _fee_receiver: address): + """ + @notice Set fee receiver who earns admin fees for a specific controller + @dev Setting to zero address resets to default fee receiver + @param _controller Address of the controller + @param _fee_receiver Address of the receiver + """ + ownable._check_owner() + contract_info: ILendFactory.ContractInfo = self.check_contract[_controller] + assert contract_info.contract_type == ILendFactory.ContractType.CONTROLLER, "not a controller" + self.fee_receivers[_controller] = _fee_receiver + log ILendFactory.SetCustomFeeReceiver(controller=_controller, fee_receiver=_fee_receiver) + + +@internal +def _set_default_fee_receiver(_fee_receiver: address): + assert _fee_receiver != empty(address), "invalid receiver" + self.default_fee_receiver = _fee_receiver + log ILendFactory.SetFeeReceiver(fee_receiver=_fee_receiver) + + +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, signer) +const tx = await contract.set_custom_fee_receiver( + /* _controller: address */ "0x0000000000000000000000000000000000000000", + /* _fee_receiver: address */ "0x0000000000000000000000000000000000000000", +) +await tx.wait() +``` + + + +:::: + + +--- + + +## Contract Ownership + +### `admin` + +::::description[`LendFactory.admin() -> address: view`] + +Returns the current owner of the factory contract. + +Returns: owner address (`address`). + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +@view +@reentrant +def admin() -> address: + """ + @notice Get the admin of the factory + @dev Called `admin` for backwards compatibility + @return Address of the factory admin + """ + return ownable.owner + + +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, provider) +const result = await contract.admin() +``` + + + +:::: + +### `pause` + +::::description[`LendFactory.pause()`] + +:::guard[Guarded Method by [Snekmate 🐍](https://github.com/pcaversaccio/snekmate)] +This function is only callable by the `owner` of the contract. +::: + +Pauses the factory, preventing new market creation. + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +def pause(): + """ + @notice Pause new market creation + """ + ownable._check_owner() + pausable._pause() + + +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, signer) +const tx = await contract.pause() +await tx.wait() +``` + + + +:::: + +### `unpause` + +::::description[`LendFactory.unpause()`] + +:::guard[Guarded Method by [Snekmate 🐍](https://github.com/pcaversaccio/snekmate)] +This function is only callable by the `owner` of the contract. +::: + +Unpauses the factory, re-enabling market creation. + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +def unpause(): + """ + @notice Unpause the factory to allow new market creation + """ + ownable._check_owner() + pausable._unpause() + + +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, signer) +const tx = await contract.unpause() +await tx.wait() +``` + + + +:::: + + +--- + + +## Other Methods + +### `version` + +::::description[`LendFactory.version() -> String[5]: view`] + +Returns the contract version string. + +Returns: version (`String[5]`). + + + +Excerpt from [`lending/LendFactory.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/LendFactory.vy): + +```vyper +@external +@view +def version() -> String[5]: + return c.__version__ + + +``` + + + + + +```ts +const contract = new Contract('0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd', LendFactoryAbi, provider) +const result = await contract.version() +``` + + + +:::: diff --git a/docs/developer/llamalend-v2/overview.md b/docs/developer/llamalend-v2/overview.md new file mode 100644 index 00000000..312f2ce8 --- /dev/null +++ b/docs/developer/llamalend-v2/overview.md @@ -0,0 +1,191 @@ +import DocCard, { DocCardGrid } from '@site/src/components/DocCard' + +# Llamalend v2: Overview + +Llamalend v2 is a major refactor of Curve's lending infrastructure, built on the same [`curve-stablecoin`](https://github.com/curvefi/curve-stablecoin) codebase that powers crvUSD. The system enables **permissionless one-way lending markets** where lenders deposit assets into ERC4626 vaults and borrowers take loans against collateral — with **LLAMMA-based liquidations protection** in order to protect borrowers from instant liquidation. + +v2 rewrites the core contracts in **Vyper 0.4.3**, leveraging its new **module system** to share loan logic between crvUSD mint markets and lending markets without code duplication. + +:::github[GitHub] + +Source code is available on [GitHub](https://github.com/curvefi/curve-stablecoin). The addresses below are taken from the documentation repository's current [`main` deployment registry](https://github.com/curvefi/docs/blob/main/static/deployments.json), not from the legacy LlamaLend registry. + +::: + +--- + +## Deployments + +The registry currently lists LlamaLend v2 deployments on Ethereum and Optimism. Addresses are chain-specific; do not reuse an address on another network. + +| Contract | Ethereum | Optimism | +| --- | --- | --- | +| LendFactory | [`0x8f6B…B0bd`](https://etherscan.io/address/0x8f6B56EC5ddF1F2691a1059f1D3cd97Ac9EaB0bd) | [`0x5F94…3640`](https://optimistic.etherscan.io/address/0x5F94073E3f51c1FFf92ffc6b4B06b7Af193B3640) | +| AMM blueprint | [`0xc8AC…cf41`](https://etherscan.io/address/0xc8AC252738E1Ece3f69CF77649C266c4E893cf41) | [`0xa6E2…103D`](https://optimistic.etherscan.io/address/0xa6E2E6A65059B3D0aCfEAfa9B42C0f9241Fc103D) | +| LendController blueprint | [`0x47b6…A75d`](https://etherscan.io/address/0x47b6dF6494aD62474cDF365B90a56C648778A75d) | [`0x8637…550E`](https://optimistic.etherscan.io/address/0x8637402cCd776A3991e04576DD24e00d9009550E) | +| LendControllerView blueprint | [`0x7259…B2C`](https://etherscan.io/address/0x7259efD886e3A717a9206C604E0156E720871B2C) | — | +| Vault blueprint | [`0x2c38…375b`](https://etherscan.io/address/0x2c3822264dcbd18d910C7834b1De8A70f368375b) | [`0x9dEe…F749`](https://optimistic.etherscan.io/address/0x9dEe3FcCEa37902F843e6E9c4AF0f158b192F749) | +| Configurator | [`0x6065…49bC`](https://etherscan.io/address/0x6065858d0eF0AA240DFdf6f1A0B2ae34B41f49bC) | [`0xd36c…5Bec`](https://optimistic.etherscan.io/address/0xd36c590531cAF5F620C57Faf5827Ce8E7f6E5Bec) | +| LeverageZap | [`0x5D84…b85`](https://etherscan.io/address/0x5D847c892891B503c3483D3Abbc2a23774279b85) | [`0xdbeB…584a`](https://optimistic.etherscan.io/address/0xdbeBDaE6f2D47B553B984E4091693824cf38584a) | + +On 2026-07-20, read-only RPC checks confirmed that both listed factories have code and return `version() = "2.0.0"`. The Ethereum factory reported two markets and Optimism reported three. Examples in this reference use Ethereum market `0` resolved from that factory: Vault `0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba`, Controller `0xc77d97cf01737eb7ace46cab7cd9f60ec51a40c0`, AMM `0xbf6f64b741164c26023f97faaea8e02453c27442`, and ControllerView `0xcda563f85388e621e7d810387e5afdac5d395e2b`. + +--- + + +## Architecture + +Each lending market consists of three contracts deployed as a triplet by the **LendFactory**: + +```mermaid +graph TB + subgraph Factory["LendFactory"] + direction TB + BP["Blueprint Registry"] + end + + subgraph Market["Lending Market (per pair)"] + direction TB + V["Vault (ERC4626)"] + C["LendController"] + AMM["AMM (LLAMMA)"] + CV["LendControllerView"] + end + + subgraph Ext["External"] + direction TB + Oracle["Price Oracle"] + MP["Monetary Policy"] + end + + Factory -- "deploys" --> V + Factory -- "deploys" --> C + Factory -- "deploys" --> AMM + + Configurator["Configurator"] -- "configures" --> C + Configurator -- "configures" --> AMM + + V -- "transfers borrowed tokens" --> C + C -- "manages collateral in" --> AMM + C -- "delegates view calls" --> CV + C -- "reads price" --> Oracle + C -- "reads rate" --> MP + AMM -- "reads price" --> Oracle + + Lender["Lender"] -- "deposit / withdraw" --> V + Borrower["Borrower"] -- "create_loan / repay / liquidate" --> C + Arb["Arbitrageur"] -- "exchange" --> AMM +``` + +The [LendFactory](./lend-factory.md) deploys new markets from blueprint contracts. Each market gets its own [Vault](./vault.md), [LendController](./lend-controller.md), and [AMM](./amm.md) instance, fully isolated from other markets. + +**Lenders** deposit the borrowed token (e.g., USDC, WETH) into the Vault — an ERC4626 vault that earns yield from borrower interest. The vault's `pricePerShare` increases over time as interest accrues. + +**Borrowers** interact with the LendController to create loans, add/remove collateral, borrow more, or repay. The controller enforces borrow caps and manages the debt accounting. + +**Collateral** is held in the AMM (LLAMMA), distributed across price bands. As the collateral price drops toward the liquidation range, the AMM gradually converts collateral to the borrowed token — this is **soft liquidation**. If the price recovers, the conversion reverses (**de-liquidation**). If a borrower's health drops below zero, anyone can call `liquidate()` on the controller to close the position. + +The [LendControllerView](./lend-controller-view.md) is a stateless helper that computes health previews, max borrowable amounts (respecting borrow caps), and other read-only calculations. + +The [Configurator](./configurator.md) is the permissioned administrative entry point for controller, AMM, monetary-policy, oracle, and lending-market settings. + + +--- + + +## New Features + +- **Any token pair** — lending markets are no longer required to include crvUSD as either the borrowed or collateral asset. Any ERC20-compliant token pair can be used to create a lending market. +- **Admin fees on lending markets** — v2 makes the per-market admin percentage configurable through the configurator's `configure_lend()` call. The factory can also set a controller-specific fee receiver, allowing revenue to be directed to a DAO, asset issuer, or curator. +- **Exit soft liquidation via repay** — calling `repay()` with `shrink=True` allows users to exit soft-liquidation by cutting the converted part of their position. `tokens_to_shrink()` indicates how many additional borrowed tokens are required (can be 0). +- **Per-operation health previews** — dedicated preview functions (`create_loan_health_preview`, `borrow_more_health_preview`, `add_collateral_health_preview`, `remove_collateral_health_preview`, `repay_health_preview`, `liquidate_health_preview`) replace the single `health_calculator()` from v1. +- **Merged extended methods** — all `*_extended` functions (e.g., `create_loan_extended`) have been merged into their base counterparts using Vyper keyword arguments, simplifying the ABI. + + +--- + + +## Security Improvements + +- **Vault balance accounting** — the ERC4626 Vault's internal accounting has been reworked. In v1, the balance value could be inflated, enabling the exploit vector behind the Resupply hack. The new accounting prevents this and makes it easier to build protocols on top of Llamalend. +- **Borrow caps** — per-market `borrow_cap`, configured through `configure_lend()`. It defaults to zero, so a new market cannot accept borrowing until its authorized configurator raises the cap. +- **Supply caps** — per-vault deposit limits (`max_supply`), configurable by the DAO. Limits the total assets that can be deposited by lenders, capping the market's exposure on both the lending and borrowing side. +- **Settable price oracle** — in v1, the price oracle was fixed at deployment, leading people to build proxy contracts as workarounds. v2 enshrines oracle upgradability at the protocol level with `set_price_oracle()` (DAO-gated). +- **Pausable factory** — the LendFactory can be paused via Snekmate's `pausable` module, preventing new market creation in emergencies while existing markets continue to operate normally. +- **NonReentrancy by default** — the Vyper 0.4.2 compiler flag `# pragma nonreentrancy on` makes all methods and public getters nonreentrant by default, removing the risk of forgetting a `@nonreentrant` decorator. +- **Reduced code duplication** — the shared `controller.vy` module replaces the duplicated ~2k+ LOC controller, eliminating divergence bugs between lending and mint market controllers. + + +--- + + +## Vyper Module System + +The biggest architectural change from v1. Previously, `Controller.vy` was a monolithic 2k+ LOC contract duplicated across crvUSD and lending with manual modifications. In v2, the contracts leverage Vyper 0.4.3's [module system](https://docs.vyperlang.org/en/latest/using-modules.html), which finally allows code reuse without duplication: + +- **`controller.vy`**: A Vyper module containing all core loan logic: debt tracking, health calculations, liquidation, fee collection, and rate accrual. The Llamalend market controllers are just a relatively small contract built on top of this module. +- **`LendController.vy`**: Declares `initializes: core` and `exports` the module's functions, making them part of its own external interface. It overrides virtual methods like `_on_debt_increased` to enforce borrow caps, and adds lending-specific state (vault, available balance). +- **`MintController.vy`**: For crvUSD — does the same but overrides virtual methods for minting/burning logic instead. + +This pattern means both systems share identical core logic with zero code duplication. Bug fixes to `controller.vy` automatically apply to both. The same approach is used throughout: `constants.vy` is a shared constants module, `blueprint_registry.vy` is a module imported by `LendFactory`, and external packages like [Snekmate](https://github.com/pcaversaccio/snekmate) (`ownable`, `pausable`) and `curve_std` (`token`, `math`, `ema`) are imported as modules. + +The upgrade to Vyper 0.4.3 also brings all recent compiler bug fixes shipped since 0.3.10. + +### Blueprint Registry + +v1's factory stored blueprint addresses directly. v2 introduces a `blueprint_registry` module — a whitelisted registry mapping string IDs (e.g., `"amm"`, `"ctrl"`, `"vault"`) to blueprint addresses, making blueprint management more structured. + +### Shared Constants + +Protocol-wide constants (version, tick limits, dead shares, WAD) are centralized in `constants.vy` and imported by all contracts, replacing scattered magic numbers. + + +--- + + +## Audits + +:::warning + +Audits are currently in progress and have not been publicly released yet. This section will be updated with links to the audit reports once they are finalized. + +::: + + +--- + + +## Contract Overview + + + + +Factory contract that **deploys new lending markets** from blueprints. Each market is a triplet of Vault + LendController + AMM. Manages the market registry, fee receivers, and pause functionality. + + + + +**ERC4626 vault** where lenders deposit the borrowed token to earn yield. Interest accrues through rising `pricePerShare`. Supports supply caps and dead shares protection against inflation attacks. + + + + +The **borrower-facing contract** for each market. Wraps the core `controller.vy` module and adds borrow caps, vault integration, and balance tracking. Handles loan creation, repayment, liquidation, and collateral management. + + + + +**Stateless view helper** that computes cap-aware max borrowable amounts, health previews, and user state queries. Delegates most logic to the base `ControllerView`. + + + + +**Permissioned market administration** for controllers and AMMs. Sets per-market administrators, borrow caps, interest allocation, oracle, fee, callback, policy, and view configuration. + + + + +The **Lending-Liquidating AMM** that holds collateral in discretized price bands. Performs soft liquidation by gradually converting collateral as prices drop, and de-liquidation when prices recover. + + + diff --git a/docs/developer/llamalend-v2/vault.md b/docs/developer/llamalend-v2/vault.md new file mode 100644 index 00000000..91811c78 --- /dev/null +++ b/docs/developer/llamalend-v2/vault.md @@ -0,0 +1,1625 @@ +# Vault + +The `Vault` is an **ERC4626-compliant vault** where lenders deposit the borrowed token (e.g., USDC, WETH) to earn yield from borrower interest. Each lending market has its own isolated vault instance deployed by the [LendFactory](./lend-factory.md). + +The vault's `pricePerShare` increases over time as borrowers pay interest. It implements the full ERC20 and ERC4626 interfaces, with additional Curve-specific methods for APR reporting and supply limits. + +The vault uses a **dead shares** mechanism (minting 1000 shares to the zero address on initialization) to protect against ERC4626 inflation attacks. + +:::vyper[`Vault.vy`] + +The source code for the `Vault.vy` contract can be found on [GitHub](https://github.com/curvefi/curve-stablecoin/blob/master/curve_stablecoin/lending/Vault.vy). The contract is written in [Vyper](https://vyperlang.org/) version `0.4.3`. + +*Deployment addresses will be added once contracts are finalized.* + +::: + +Examples use Ethers v6 with an ABI generated from the pinned source linked in each panel. Replace every zero address and amount placeholder. Read-only calls use the verified Ethereum v2 reference market; execute a write only on a fork first. + + +--- + + +## ERC4626 — Deposits and Withdrawals + +### `deposit` + +::::description[`Vault.deposit(_assets: uint256, _receiver: address) -> uint256`] + +Deposits the specified amount of borrowed tokens into the vault and mints shares to the receiver. + +| Input | Type | Description | +| --- | --- | --- | +| `_assets` | `uint256` | Amount of borrowed tokens to deposit | +| `_receiver` | `address` | Address to receive the minted vault shares | + +Returns: number of shares minted (`uint256`). + +Emits: `Deposit` event. + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +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 + @return Amount of shares minted + """ + controller: IController = self._controller + total_assets: uint256 = self._total_assets() + assert total_assets + _assets >= MIN_ASSETS, "Need more assets" + assert total_assets + _assets <= self.maxSupply, "Supply limit" + to_mint: uint256 = self._convert_to_shares(_assets, True, total_assets) + assert to_mint > 0, "Can't mint 0 shares" + + extcall ILendController(controller.address).on_borrowed_token_transfer_in(_assets) + tkn.transfer_from(self._borrowed_token, msg.sender, controller.address, _assets) + + self._mint(_receiver, to_mint) + extcall controller.save_rate() + log IERC4626.Deposit(sender=msg.sender, owner=_receiver, assets=_assets, shares=to_mint) + return to_mint + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, signer) +const tx = await contract.deposit( + /* _assets: uint256 */ 0n, + /* _receiver: address */ "0x0000000000000000000000000000000000000000", +) +await tx.wait() +``` + + + +:::: + +### `mint` + +::::description[`Vault.mint(_shares: uint256, _receiver: address) -> uint256`] + +Mints an exact number of vault shares by depositing the required amount of borrowed tokens. + +| Input | Type | Description | +| --- | --- | --- | +| `_shares` | `uint256` | Exact number of shares to mint | +| `_receiver` | `address` | Address to receive the minted shares | + +Returns: amount of assets deposited (`uint256`). + +Emits: `Deposit` event. + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +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 shares to mint + @param _receiver Optional receiver for the shares. If not specified - it's the sender + @return Amount of assets taken from the caller + """ + assert _shares > 0, "Can't mint 0 shares" + controller: IController = 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 total_assets + assets <= self.maxSupply, "Supply limit" + + extcall ILendController(controller.address).on_borrowed_token_transfer_in(assets) + tkn.transfer_from(self._borrowed_token, msg.sender, controller.address, assets) + + self._mint(_receiver, _shares) + extcall controller.save_rate() + log IERC4626.Deposit(sender=msg.sender, owner=_receiver, assets=assets, shares=_shares) + return assets + + +@internal +@view +def _available_balance() -> uint256: + return crv_math.sub_or_zero( + staticcall self._controller.available_balance(), + staticcall self._controller.admin_fees(), + ) + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.mint( + /* _shares: uint256 */ 0n, + /* _receiver: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `withdraw` + +::::description[`Vault.withdraw(_assets: uint256, _receiver: address, _owner: address) -> uint256`] + +Withdraws an exact amount of borrowed tokens by burning the required vault shares. + +| Input | Type | Description | +| --- | --- | --- | +| `_assets` | `uint256` | Exact amount of borrowed tokens to withdraw | +| `_receiver` | `address` | Address to receive the withdrawn tokens | +| `_owner` | `address` | Address whose shares are burned | + +Returns: number of shares burned (`uint256`). + +Emits: `Withdraw` event. + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +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 + @return Number of shares burned + """ + 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: IController = self._controller + self._burn(_owner, shares) + + extcall ILendController(controller.address).on_borrowed_token_transfer_out(_assets) + tkn.transfer_from(self._borrowed_token, controller.address, _receiver, _assets) + + extcall controller.save_rate() + log IERC4626.Withdraw(sender=msg.sender, receiver=_receiver, owner=_owner, assets=_assets, shares=shares) + return shares + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, signer) +const tx = await contract.withdraw( + /* _assets: uint256 */ 0n, + /* _receiver: address */ "0x0000000000000000000000000000000000000000", + /* _owner: address */ "0x0000000000000000000000000000000000000000", +) +await tx.wait() +``` + + + +:::: + +### `redeem` + +::::description[`Vault.redeem(_shares: uint256, _receiver: address, _owner: address) -> uint256`] + +Burns an exact number of vault shares and returns the corresponding borrowed tokens. + +| Input | Type | Description | +| --- | --- | --- | +| `_shares` | `uint256` | Exact number of shares to burn | +| `_receiver` | `address` | Address to receive the withdrawn tokens | +| `_owner` | `address` | Address whose shares are burned | + +Returns: amount of assets withdrawn (`uint256`). + +Emits: `Withdraw` event. + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +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 + @return Amount of assets sent to the receiver + """ + 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) + assert total_assets - assets_to_redeem >= MIN_ASSETS or total_assets == assets_to_redeem, "Need more assets" + self._burn(_owner, _shares) + controller: IController = self._controller + + extcall ILendController(controller.address).on_borrowed_token_transfer_out(assets_to_redeem) + tkn.transfer_from(self._borrowed_token, controller.address, _receiver, assets_to_redeem) + + extcall controller.save_rate() + log IERC4626.Withdraw(sender=msg.sender, receiver=_receiver, owner=_owner, assets=assets_to_redeem, shares=_shares) + return assets_to_redeem + + +# ERC20 methods + +@internal +def _approve(_owner: address, _spender: address, _value: uint256): + self.allowance[_owner][_spender] = _value + + log IERC20.Approval(owner=_owner, spender=_spender, value=_value) + + +@internal +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, signer) +const tx = await contract.redeem( + /* _shares: uint256 */ 0n, + /* _receiver: address */ "0x0000000000000000000000000000000000000000", + /* _owner: address */ "0x0000000000000000000000000000000000000000", +) +await tx.wait() +``` + + + +:::: + + +--- + + +## ERC4626 — Preview and Limits + +### `totalAssets` + +::::description[`Vault.totalAssets() -> uint256: view`] + +Returns the total amount of borrowed tokens managed by the vault, including tokens lent out to borrowers (tracked by the controller's debt accounting). + +Returns: total assets (`uint256`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +def totalAssets() -> uint256: + """ + @notice Total assets which can be lent out or be in reserve + @return Total amount of underlying assets managed by the vault + """ + return self._total_assets() + + +@internal +@view +def _convert_to_shares(_assets: uint256, _is_floor: bool = True, + _total_assets: uint256 = max_value(uint256)) -> uint256: + """ + @param _assets Amount of assets to convert + @param _is_floor If True, round down; if False, round up + @param _total_assets Override for total assets; uses current value if max_value(uint256) + @return Amount of shares equivalent to `_assets` + """ + 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 math._ceil_div(numerator, denominator) + + +@internal +@view +def _convert_to_assets(_shares: uint256, _is_floor: bool = True, + _total_assets: uint256 = max_value(uint256)) -> uint256: + """ + @param _shares Amount of shares to convert +# … the remaining implementation is linked above. +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.totalAssets() +``` + + + +:::: + +### `convertToShares` + +::::description[`Vault.convertToShares(_assets: uint256) -> uint256: view`] + +Returns the number of vault shares that would be minted for a given amount of assets. + +| Input | Type | Description | +| --- | --- | --- | +| `_assets` | `uint256` | Amount of assets | + +Returns: equivalent shares (`uint256`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +def convertToShares(_assets: uint256) -> uint256: + """ + @notice Returns the amount of shares which the Vault would exchange for the given amount of assets provided + @param _assets Amount of assets to convert + @return Amount of shares equivalent to _assets + """ + return self._convert_to_shares(_assets) + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.convertToShares( + /* _assets: uint256 */ 0n, +) +``` + + + +:::: + +### `convertToAssets` + +::::description[`Vault.convertToAssets(_shares: uint256) -> uint256: view`] + +Returns the amount of assets that a given number of shares is worth. + +| Input | Type | Description | +| --- | --- | --- | +| `_shares` | `uint256` | Amount of shares | + +Returns: equivalent assets (`uint256`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +def convertToAssets(_shares: uint256) -> uint256: + """ + @notice Returns the amount of assets that the Vault would exchange for the amount of shares provided + @param _shares Amount of shares to convert + @return Amount of assets equivalent to _shares + """ + return self._convert_to_assets(_shares) + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.convertToAssets( + /* _shares: uint256 */ 0n, +) +``` + + + +:::: + +### `previewDeposit` + +::::description[`Vault.previewDeposit(_assets: uint256) -> uint256: view`] + +Simulates a deposit and returns the number of shares that would be minted. Rounds down. + +| Input | Type | Description | +| --- | --- | --- | +| `_assets` | `uint256` | Amount of assets to deposit | + +Returns: shares that would be minted (`uint256`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +def previewDeposit(_assets: uint256) -> uint256: + """ + @notice Returns the amount of shares which can be obtained upon depositing assets + @param _assets Amount of assets to deposit + @return Amount of shares that would be minted for _assets + """ + return self._convert_to_shares(_assets) + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.previewDeposit( + /* _assets: uint256 */ 0n, +) +``` + + + +:::: + +### `previewMint` + +::::description[`Vault.previewMint(_shares: uint256) -> uint256: view`] + +Simulates a mint and returns the amount of assets required. Rounds up. + +| Input | Type | Description | +| --- | --- | --- | +| `_shares` | `uint256` | Number of shares to mint | + +Returns: assets required (`uint256`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +def previewMint(_shares: uint256) -> uint256: + """ + @notice Calculate the amount of assets which is needed to exactly mint the given amount of shares + @param _shares Number of shares to mint + @return Amount of assets required to mint exactly _shares + """ + return self._convert_to_assets(_shares, False) + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.previewMint( + /* _shares: uint256 */ 0n, +) +``` + + + +:::: + +### `previewWithdraw` + +::::description[`Vault.previewWithdraw(_assets: uint256) -> uint256: view`] + +Simulates a withdrawal and returns the number of shares that would be burned. Rounds up. + +| Input | Type | Description | +| --- | --- | --- | +| `_assets` | `uint256` | Amount of assets to withdraw | + +Returns: shares that would be burned (`uint256`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +def previewWithdraw(_assets: uint256) -> uint256: + """ + @notice Calculate number of shares which gets burned when withdrawing given amount of asset + @param _assets Amount of assets to withdraw + @return Number of shares that would be burned for _assets + """ + return self._convert_to_shares(_assets, False) + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.previewWithdraw( + /* _assets: uint256 */ 0n, +) +``` + + + +:::: + +### `previewRedeem` + +::::description[`Vault.previewRedeem(_shares: uint256) -> uint256: view`] + +Simulates a redemption and returns the amount of assets that would be returned. Rounds down. + +| Input | Type | Description | +| --- | --- | --- | +| `_shares` | `uint256` | Number of shares to redeem | + +Returns: assets that would be returned (`uint256`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +def previewRedeem(_shares: uint256) -> uint256: + """ + @notice Calculate the amount of assets which can be obtained by redeeming the given amount of shares + @param _shares Amount of shares to redeem + @return Amount of assets that would be received for _shares + """ + return self._convert_to_assets(_shares) + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.previewRedeem( + /* _shares: uint256 */ 0n, +) +``` + + + +:::: + +### `maxDeposit` + +::::description[`Vault.maxDeposit(_receiver: address) -> uint256: view`] + +Returns the maximum amount of assets that can be deposited for a given receiver, considering the supply limit. + +| Input | Type | Description | +| --- | --- | --- | +| `_receiver` | `address` | Receiver address | + +Returns: max depositable assets (`uint256`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +def maxDeposit(_receiver: address) -> uint256: + """ + @notice Maximum amount of assets which a given user can deposit + @param _receiver Address of the prospective depositor + @return Maximum depositable asset amount given the current supply cap + """ + max_supply: uint256 = self.maxSupply + if max_supply == max_value(uint256): + return max_supply + else: + assets: uint256 = self._total_assets() + return max(max_supply, assets) - assets + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.maxDeposit( + /* _receiver: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `maxMint` + +::::description[`Vault.maxMint(_receiver: address) -> uint256: view`] + +Returns the maximum number of shares that can be minted for a given receiver. + +| Input | Type | Description | +| --- | --- | --- | +| `_receiver` | `address` | Receiver address | + +Returns: max mintable shares (`uint256`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +def maxMint(_receiver: address) -> uint256: + """ + @notice Return maximum amount of shares which a given user can mint + @param _receiver Address of the prospective minter + @return Maximum mintable share amount given the current supply cap + """ + max_supply: uint256 = self.maxSupply + if max_supply == max_value(uint256): + return max_supply + else: + assets: uint256 = self._total_assets() + return self._convert_to_shares(max(max_supply, assets) - assets) + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.maxMint( + /* _receiver: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `maxWithdraw` + +::::description[`Vault.maxWithdraw(_owner: address) -> uint256: view`] + +Returns the maximum amount of assets that can be withdrawn by a given owner, limited by available liquidity. + +| Input | Type | Description | +| --- | --- | --- | +| `_owner` | `address` | Owner of the shares | + +Returns: max withdrawable assets (`uint256`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +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 + @param _owner Address of the share owner + @return Maximum withdrawable asset amount + """ + return min( + self._convert_to_assets(self.balanceOf[_owner]), + self._available_balance(), + ) + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.maxWithdraw( + /* _owner: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `maxRedeem` + +::::description[`Vault.maxRedeem(_owner: address) -> uint256: view`] + +Returns the maximum number of shares that can be redeemed by a given owner. + +| Input | Type | Description | +| --- | --- | --- | +| `_owner` | `address` | Owner of the shares | + +Returns: max redeemable shares (`uint256`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +def maxRedeem(_owner: address) -> uint256: + """ + @notice Calculate maximum amount of shares which a given user can redeem + @param _owner Address of the share owner + @return Maximum redeemable share amount given the owner's balance and available liquidity + """ + return min( + self._convert_to_shares(self._available_balance(), False), + self.balanceOf[_owner], + ) + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.maxRedeem( + /* _owner: address */ "0x0000000000000000000000000000000000000000", +) +``` + + + +:::: + +### `pricePerShare` + +::::description[`Vault.pricePerShare() -> uint256: view`] + +Returns the current price per share, scaled to 1e18. Increases over time as interest accrues. + +Returns: price per share (`uint256`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +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 + @dev pricePerShare can decrease if totalSupply reaches zero (e.g., when all shares are burned). + In this case, it resets to the initial value. + @param _is_floor If True, round down; if False, round up + @return Price of one share in asset tokens, normalized to 1e18 + """ + 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 = math._ceil_div(numerator, denominator) + assert pps > 0 + return pps + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.pricePerShare() +``` + + + +:::: + + +--- + + +## APR + +### `borrow_apr` + +::::description[`Vault.borrow_apr() -> uint256: view`] + +Returns the current annualized borrow rate as reported by the monetary policy, scaled to 1e18. + +Returns: borrow APR (`uint256`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +def borrow_apr() -> uint256: + """ + @notice Borrow APR (annualized and 1e18-based) + @return Current annualized borrow rate scaled by 1e18 + """ + return staticcall self._amm.rate() * (365 * 86400) + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.borrow_apr() +``` + + + +:::: + +### `lend_apr` + +::::description[`Vault.lend_apr() -> uint256: view`] + +Returns the current annualized lending rate — the effective yield lenders earn after accounting for utilization and admin fees, scaled to 1e18. + +Returns: lend APR (`uint256`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +def lend_apr() -> uint256: + """ + @notice Lending APR (annualized and 1e18-based), net of admin fees + @return Current annualized lending rate scaled by 1e18, after admin fee deduction + """ + debt: uint256 = staticcall self._controller.total_debt() + if debt == 0: + return 0 + + gross_apr: uint256 = staticcall self._amm.rate() * (365 * 86400) * debt // self._total_assets() + admin_pct: uint256 = staticcall ILendController(self._controller.address).admin_percentage() + return gross_apr * (c.WAD - admin_pct) // c.WAD + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.lend_apr() +``` + + + +:::: + + +--- + + +## Vault Info + +### `asset` + +::::description[`Vault.asset() -> address: view`] + +Returns the address of the underlying borrowed token (the ERC4626 `asset`). + +Returns: asset address (`address`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +def asset() -> IERC20: + """ + @notice Asset which is the same as borrowed_token + @return Address of the underlying asset token + """ + return self._borrowed_token + + +@internal +@view +def _total_assets() -> uint256: + return ( + staticcall self._controller.available_balance() + + staticcall self._controller.total_debt() + - staticcall self._controller.admin_fees() + ) + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.asset() +``` + + + +:::: + +### `borrowed_token` + +::::description[`Vault.borrowed_token() -> address: view`] + +Returns the borrowed token address. + +Returns: token address (`address`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +@reentrant +def borrowed_token() -> IERC20: + return self._borrowed_token + +_collateral_token: IERC20 +# https://github.com/vyperlang/vyper/issues/4721 +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.borrowed_token() +``` + + + +:::: + +### `collateral_token` + +::::description[`Vault.collateral_token() -> address: view`] + +Returns the collateral token address for this market. + +Returns: token address (`address`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +@reentrant +def collateral_token() -> IERC20: + return self._collateral_token + +_amm: IAMM +# https://github.com/vyperlang/vyper/issues/4721 +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.collateral_token() +``` + + + +:::: + +### `controller` + +::::description[`Vault.controller() -> address: view`] + +Returns the LendController address associated with this vault. + +Returns: controller address (`address`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +@reentrant +def controller() -> IController: + return self._controller + +_factory: IFactory +# https://github.com/vyperlang/vyper/issues/4721 +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.controller() +``` + + + +:::: + +### `amm` + +::::description[`Vault.amm() -> address: view`] + +Returns the AMM (LLAMMA) address associated with this vault. + +Returns: AMM address (`address`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +@reentrant +def amm() -> IAMM: + return self._amm + +_controller: IController +# https://github.com/vyperlang/vyper/issues/4721 +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.amm() +``` + + + +:::: + +### `factory` + +::::description[`Vault.factory() -> address: view`] + +Returns the factory that deployed this vault. + +Returns: factory address (`address`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +@reentrant +def factory() -> IFactory: + return self._factory + + +maxSupply: public(reentrant(uint256)) + +# ERC20 publics + +decimals: public(constant(uint8)) = 18 +name: public(reentrant(String[64])) +symbol: public(reentrant(String[34])) + +NAME_PREFIX: constant(String[16]) = 'Curve Vault for ' +SYMBOL_PREFIX: constant(String[2]) = 'cv' + +allowance: public(reentrant(HashMap[address, HashMap[address, uint256]])) +balanceOf: public(reentrant(HashMap[address, uint256])) +totalSupply: public(reentrant(uint256)) + +precision: uint256 + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.factory() +``` + + + +:::: + + +--- + + +## Admin + +### `set_max_supply` + +::::description[`Vault.set_max_supply(_max_supply: uint256)`] + +:::guard[Guarded Method] +This function is only callable by the `admin`, which is the factory owner. +::: + +Sets the maximum supply cap for the vault. When set, deposits that would push `totalSupply` above this limit are rejected. + +| Input | Type | Description | +| --- | --- | --- | +| `_max_supply` | `uint256` | New maximum supply cap (0 = unlimited) | + +Emits: `SetMaxSupply` event. + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +def set_max_supply(_max_supply: uint256): + """ + @notice Set maximum depositable supply + @param _max_supply New maximum supply cap in asset tokens + """ + assert msg.sender == staticcall self._factory.admin() or msg.sender == self._factory.address + self.maxSupply = _max_supply + log IVault.SetMaxSupply(max_supply=_max_supply) + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, signer) +const tx = await contract.set_max_supply( + /* _max_supply: uint256 */ 0n, +) +await tx.wait() +``` + + + +:::: + +### `admin` + +::::description[`Vault.admin() -> address: view`] + +Returns the admin of the vault, which is the owner of the factory contract. + +Returns: admin address (`address`). + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +@view +def admin() -> address: + return staticcall self._factory.admin() + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, provider) +const result = await contract.admin() +``` + + + +:::: + + +--- + + +## ERC20 + +The vault implements the full ERC20 interface for vault share tokens. + +### `transfer` + +::::description[`Vault.transfer(_to: address, _value: uint256) -> bool`] + +Transfers vault shares to another address. + +| Input | Type | Description | +| --- | --- | --- | +| `_to` | `address` | Recipient address | +| `_value` | `uint256` | Amount of shares to transfer | + +Returns: success (`bool`). + +Emits: `Transfer` event. + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +def transfer(_to: address, _value: uint256) -> bool: + """ + @notice Transfer tokens to `_to`. + @param _to The account to transfer tokens to. + @param _value The amount of tokens to transfer. + @return True on success + """ + self._transfer(msg.sender, _to, _value) + return True + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, signer) +const tx = await contract.transfer( + /* _to: address */ "0x0000000000000000000000000000000000000000", + /* _value: uint256 */ 0n, +) +await tx.wait() +``` + + + +:::: + +### `transferFrom` + +::::description[`Vault.transferFrom(_from: address, _to: address, _value: uint256) -> bool`] + +Transfers vault shares from one address to another, using the caller's allowance. + +| Input | Type | Description | +| --- | --- | --- | +| `_from` | `address` | Sender address | +| `_to` | `address` | Recipient address | +| `_value` | `uint256` | Amount of shares to transfer | + +Returns: success (`bool`). + +Emits: `Transfer` event. + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@external +def transferFrom(_from: address, _to: address, _value: uint256) -> bool: + """ + @notice Transfer tokens from one account to another. + @dev The caller needs to have an allowance from account `_from` greater than or + equal to the value being transferred. An allowance equal to the uint256 type's + maximum, is considered infinite and does not decrease. + @param _from The account which tokens will be spent from. + @param _to The account which tokens will be sent to. + @param _value The amount of tokens to be transferred. + @return True on success + """ + allowance: uint256 = self.allowance[_from][msg.sender] + if allowance != max_value(uint256): + self._approve(_from, msg.sender, allowance - _value) + + self._transfer(_from, _to, _value) + return True + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, signer) +const tx = await contract.transferFrom( + /* _from: address */ "0x0000000000000000000000000000000000000000", + /* _to: address */ "0x0000000000000000000000000000000000000000", + /* _value: uint256 */ 0n, +) +await tx.wait() +``` + + + +:::: + +### `approve` + +::::description[`Vault.approve(_spender: address, _value: uint256) -> bool`] + +Approves an address to spend vault shares on behalf of the caller. + +| Input | Type | Description | +| --- | --- | --- | +| `_spender` | `address` | Address to approve | +| `_value` | `uint256` | Allowance amount | + +Returns: success (`bool`). + +Emits: `Approval` event. + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@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. + @return True on success + """ + self._approve(msg.sender, _spender, _value) + return True + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, signer) +const tx = await contract.approve( + /* _spender: address */ "0x0000000000000000000000000000000000000000", + /* _value: uint256 */ 0n, +) +await tx.wait() +``` + + + +:::: + +### `increaseAllowance` + +::::description[`Vault.increaseAllowance(_spender: address, _add_value: uint256) -> bool`] + +Increases the allowance for a spender by a given amount. + +| Input | Type | Description | +| --- | --- | --- | +| `_spender` | `address` | Address whose allowance to increase | +| `_add_value` | `uint256` | Amount to add to the current allowance | + +Returns: success (`bool`). + +Emits: `Approval` event. + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@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. + @return True on success + """ + 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 + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, signer) +const tx = await contract.increaseAllowance( + /* _spender: address */ "0x0000000000000000000000000000000000000000", + /* _add_value: uint256 */ 0n, +) +await tx.wait() +``` + + + +:::: + +### `decreaseAllowance` + +::::description[`Vault.decreaseAllowance(_spender: address, _sub_value: uint256) -> bool`] + +Decreases the allowance for a spender by a given amount. + +| Input | Type | Description | +| --- | --- | --- | +| `_spender` | `address` | Address whose allowance to decrease | +| `_sub_value` | `uint256` | Amount to subtract from the current allowance | + +Returns: success (`bool`). + +Emits: `Approval` event. + + + +Excerpt from [`lending/Vault.vy` at `0d726b1`](https://github.com/curvefi/curve-stablecoin/blob/0d726b110555e9506f5bec7e4b04eaac7b2a7986/curve_stablecoin/lending/Vault.vy): + +```vyper +@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. + @return True on success + """ + 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 + + +``` + + + + + +```ts +const contract = new Contract('0x2b5a321c3cb1f33e1abecd047c2649d0b4c47eba', VaultAbi, signer) +const tx = await contract.decreaseAllowance( + /* _spender: address */ "0x0000000000000000000000000000000000000000", + /* _sub_value: uint256 */ 0n, +) +await tx.wait() +``` + + + +:::: diff --git a/docs/developer/resources/notebooks.md b/docs/developer/resources/notebooks.md index fb138951..a635fe31 100644 --- a/docs/developer/resources/notebooks.md +++ b/docs/developer/resources/notebooks.md @@ -41,7 +41,7 @@ After setting up these two secrets, the notebook can successfully be run directl ## Notebook List -### Curve Lending +### LlamaLend v1 (legacy) | Contract | Description | Link | | :-------: | ----------- | :-----: | diff --git a/docs/protocol/lending/guides/deploy-lending-market.md b/docs/protocol/lending/guides/deploy-lending-market.md index ff4405ad..b63ad906 100644 --- a/docs/protocol/lending/guides/deploy-lending-market.md +++ b/docs/protocol/lending/guides/deploy-lending-market.md @@ -1,22 +1,22 @@ --- id: deploy-lending-market -title: Deploying a Lending Market -sidebar_label: Deploying a Lending Market +title: Deploying a LlamaLend v1 Market (Legacy) +sidebar_label: Deploying a v1 Market (Legacy) --- import ThemedImage from '@theme/ThemedImage'; -:::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. +:::warning +This is historical documentation for **LlamaLend v1**. V1 markets are being phased out and **no new v1 markets will be deployed**. Its factories, `create_from_pool` flow, and crvUSD-pair requirement do not apply to v2. Any new market must use the [v2 LendFactory](/developer/llamalend-v2/lend-factory) and [Configurator](/developer/llamalend-v2/configurator) references. ::: -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. +The following describes the historical v1 deployment flow. Do not use it to deploy a market; new markets must use LlamaLend v2. **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:** +**Data that the v1 deployment flow required:** - Token addresses of the collateral and borrowable tokens - Simulated parameters (A, fee, loan_discount, liquidation_discount) @@ -26,11 +26,11 @@ Currently, there's no UI for deploying lending markets, but you can deploy via E For more information on how to acquire this information, see: [Oracles & Parameters](../oracles-and-parameters.md). -## Deploying via Etherscan +## Historical deployment 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: +The v1 factory contracts were deployed on the following chains: - **Ethereum**: [0xeA6876DDE9e3467564acBeE1Ed5bac88783205E0](https://etherscan.io/address/0xeA6876DDE9e3467564acBeE1Ed5bac88783205E0#writeContract#F1) - **Arbitrum**: [0xcaEC110C784c9DF37240a8Ce096D352A75922DeA](https://arbiscan.io/address/0xcaEC110C784c9DF37240a8Ce096D352A75922DeA#writeContract) @@ -38,9 +38,9 @@ First, locate the appropriate Factory contract on Etherscan for your target chai - **Optimism**: [0x5EA8f3D674C70b020586933A0a5b250734798BeF](https://optimistic.etherscan.io/address/0x5EA8f3D674C70b020586933A0a5b250734798BeF#writeContract) - **Sonic**: [0x30D1859DaD5A52aE03B6e259d1b48c4b12933993](https://sonicscan.org/address/0x30D1859DaD5A52aE03B6e259d1b48c4b12933993#writeContract) -## Deployment Methods +## Historical deployment methods -There are two deployment methods available: +The v1 factory offered two deployment methods: 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. diff --git a/docs/protocol/lending/oracles-and-parameters.md b/docs/protocol/lending/oracles-and-parameters.md index a238f6a4..8914d2d1 100644 --- a/docs/protocol/lending/oracles-and-parameters.md +++ b/docs/protocol/lending/oracles-and-parameters.md @@ -1,11 +1,17 @@ --- id: oracles-and-parameters -title: Oracles & Parameters -sidebar_label: Oracles & Parameters +title: LlamaLend v1 Oracles & Parameters (Legacy) +sidebar_label: v1 Oracles & Parameters (Legacy) --- import SemiLogChart from '@site/src/components/SemiLogChart'; +:::warning + +This guide documents **LlamaLend v1** oracle and parameter assumptions, including crvUSD-denominated collateral oracles. LlamaLend v2 allows any ERC-20 pair; its contract configuration is documented in the [v2 Configurator reference](/developer/llamalend-v2/configurator). + +::: + 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. @@ -18,7 +24,7 @@ Jump into their