-
Notifications
You must be signed in to change notification settings - Fork 410
Add a Custom Curve Pool Implementation Handling RAI #144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
stefanionescu
wants to merge
27
commits into
curvefi:master
Choose a base branch
from
reflexer-labs:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
218168b
feat: RAI pool, new meta pool customised to handle redemption rate
stobiewan a0ee346
test: RAI pool, add pool specific tests focused on moving redemption …
stobiewan 314dde1
docs: RAI pool, add temporary doc detailing changes made for RAI pool
stobiewan a6a29eb
Merge pull request #3 from stobiewan/pool_rai
stefanionescu f6cf407
Remove RAI readme
stefanionescu 6c5d52e
locking pragma for RAI pool
fabiohild 3104128
Merge pull request #4 from reflexer-labs/audit
stefanionescu f793ac7
changing state vars to uint256
fabiohild 291b16f
Uniting _rates instead of single vars for each rate
fabiohild 016c7a6
removing unreachable code on get_dy_underlying
fabiohild 6657b28
Improving invariant_check_virtual_price test on RAI integration tests
fabiohild 60824a3
Curve review points
fabiohild f6492b4
updating virtual_price_2 formula
fabiohild 0046953
include sqrt in the contract, remove donate_admin_fees
fabiohild 73397ec
fixing scaling issue on get_virtual_price_2
fabiohild d52df33
Merge pull request #5 from reflexer-labs/curve-review
stefanionescu d3654b7
Code: Raiust pool, Two Coin Pool with peggie & non-peggie
twpony d8799bf
Test: Raiust pool, add specific test for moving redemption price
twpony 43f0ae7
Test: Raiust pool, add specific test for moving redemption price
twpony 19365c5
Doc: Raiust pool, readme to describe implementation
twpony 9d44747
Merge pull request #7 from fs1028/master
fabiohild f1954a8
renaming RATES, adding virtual_price_2
fabiohild 763fa6f
removing temp README
fabiohild 087915e
Merge pull request #8 from reflexer-labs/gitcoin-submission-review
fabiohild b60d49c
Rename StableSwapRaiust.vy to StableSwapRaiUst.vy
stefanionescu c612bcf
Update README.md
stefanionescu 2297ab0
Update README.md
stefanionescu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| __pycache__ | ||
| .history | ||
| .hypothesis/ | ||
| .idea/ | ||
| build/ | ||
| reports/ | ||
| .venv/ | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,376 @@ | ||
| # @version 0.2.12 | ||
| """ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good |
||
| @title "Zap" Depositer for metapool | ||
| @author Curve.Fi | ||
| @license Copyright (c) Curve.Fi, 2020 - all rights reserved | ||
| @notice deposit/withdraw to Curve pool without too many transactions | ||
| """ | ||
|
|
||
| from vyper.interfaces import ERC20 | ||
|
|
||
|
|
||
| interface CurveMeta: | ||
| def add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256) -> uint256: nonpayable | ||
| def remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS]) -> uint256[N_COINS]: nonpayable | ||
| def remove_liquidity_one_coin(_token_amount: uint256, i: int128, min_amount: uint256) -> uint256: nonpayable | ||
| def remove_liquidity_imbalance(amounts: uint256[N_COINS], max_burn_amount: uint256) -> uint256: nonpayable | ||
| def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256: view | ||
| def calc_token_amount(amounts: uint256[N_COINS], deposit: bool) -> uint256: view | ||
| def base_pool() -> address: view | ||
| def coins(i: uint256) -> address: view | ||
|
|
||
| interface CurveBase: | ||
| def add_liquidity(amounts: uint256[BASE_N_COINS], min_mint_amount: uint256): nonpayable | ||
| def remove_liquidity(_amount: uint256, min_amounts: uint256[BASE_N_COINS]): nonpayable | ||
| def remove_liquidity_one_coin(_token_amount: uint256, i: int128, min_amount: uint256): nonpayable | ||
| def remove_liquidity_imbalance(amounts: uint256[BASE_N_COINS], max_burn_amount: uint256): nonpayable | ||
| def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256: view | ||
| def calc_token_amount(amounts: uint256[BASE_N_COINS], deposit: bool) -> uint256: view | ||
| def coins(i: uint256) -> address: view | ||
| def fee() -> uint256: view | ||
|
|
||
|
|
||
| N_COINS: constant(int128) = 2 | ||
| MAX_COIN: constant(int128) = N_COINS-1 | ||
| BASE_N_COINS: constant(int128) = 3 | ||
| N_ALL_COINS: constant(int128) = N_COINS + BASE_N_COINS - 1 | ||
|
|
||
| # An asset which may have a transfer fee (USDT) | ||
| FEE_ASSET: constant(address) = 0xdAC17F958D2ee523a2206206994597C13D831ec7 | ||
|
|
||
| FEE_DENOMINATOR: constant(uint256) = 10 ** 10 | ||
| FEE_IMPRECISION: constant(uint256) = 100 * 10 ** 8 # % of the fee | ||
|
|
||
|
|
||
| pool: public(address) | ||
| token: public(address) | ||
| base_pool: public(address) | ||
|
|
||
| coins: public(address[N_COINS]) | ||
| base_coins: public(address[BASE_N_COINS]) | ||
|
|
||
|
|
||
| @external | ||
| def __init__(_pool: address, _token: address): | ||
| """ | ||
| @notice Contract constructor | ||
| @param _pool Metapool address | ||
| @param _token Pool LP token address | ||
| """ | ||
| self.pool = _pool | ||
| self.token = _token | ||
| base_pool: address = CurveMeta(_pool).base_pool() | ||
| self.base_pool = base_pool | ||
|
|
||
| for i in range(N_COINS): | ||
| coin: address = CurveMeta(_pool).coins(i) | ||
| self.coins[i] = coin | ||
| # approve coins for infinite transfers | ||
| _response: Bytes[32] = raw_call( | ||
| coin, | ||
| concat( | ||
| method_id("approve(address,uint256)"), | ||
| convert(_pool, bytes32), | ||
| convert(MAX_UINT256, bytes32), | ||
| ), | ||
| max_outsize=32, | ||
| ) | ||
| if len(_response) > 0: | ||
| assert convert(_response, bool) | ||
|
|
||
| for i in range(BASE_N_COINS): | ||
| coin: address = CurveBase(base_pool).coins(i) | ||
| self.base_coins[i] = coin | ||
| # approve underlying coins for infinite transfers | ||
| _response: Bytes[32] = raw_call( | ||
| coin, | ||
| concat( | ||
| method_id("approve(address,uint256)"), | ||
| convert(base_pool, bytes32), | ||
| convert(MAX_UINT256, bytes32), | ||
| ), | ||
| max_outsize=32, | ||
| ) | ||
| if len(_response) > 0: | ||
| assert convert(_response, bool) | ||
|
|
||
|
|
||
| @external | ||
| def add_liquidity(_amounts: uint256[N_ALL_COINS], _min_mint_amount: uint256) -> uint256: | ||
| """ | ||
| @notice Wrap underlying coins and deposit them in the pool | ||
| @param _amounts List of amounts of underlying coins to deposit | ||
| @param _min_mint_amount Minimum amount of LP tokens to mint from the deposit | ||
| @return Amount of LP tokens received by depositing | ||
| """ | ||
| meta_amounts: uint256[N_COINS] = empty(uint256[N_COINS]) | ||
| base_amounts: uint256[BASE_N_COINS] = empty(uint256[BASE_N_COINS]) | ||
| deposit_base: bool = False | ||
|
|
||
| # Transfer all coins in | ||
| for i in range(N_ALL_COINS): | ||
| amount: uint256 = _amounts[i] | ||
| if amount == 0: | ||
| continue | ||
| coin: address = ZERO_ADDRESS | ||
| if i < MAX_COIN: | ||
| coin = self.coins[i] | ||
| meta_amounts[i] = amount | ||
| else: | ||
| x: int128 = i - MAX_COIN | ||
| coin = self.base_coins[x] | ||
| base_amounts[x] = amount | ||
| deposit_base = True | ||
| # "safeTransferFrom" which works for ERC20s which return bool or not | ||
| _response: Bytes[32] = raw_call( | ||
| coin, | ||
| concat( | ||
| method_id("transferFrom(address,address,uint256)"), | ||
| convert(msg.sender, bytes32), | ||
| convert(self, bytes32), | ||
| convert(amount, bytes32), | ||
| ), | ||
| max_outsize=32, | ||
| ) # dev: failed transfer | ||
| if len(_response) > 0: | ||
| assert convert(_response, bool) # dev: failed transfer | ||
| # end "safeTransferFrom" | ||
| # Handle potential Tether fees | ||
| if coin == FEE_ASSET: | ||
| amount = ERC20(FEE_ASSET).balanceOf(self) | ||
| if i < MAX_COIN: | ||
| meta_amounts[i] = amount | ||
| else: | ||
| base_amounts[i - MAX_COIN] = amount | ||
|
|
||
| # Deposit to the base pool | ||
| if deposit_base: | ||
| CurveBase(self.base_pool).add_liquidity(base_amounts, 0) | ||
| meta_amounts[MAX_COIN] = ERC20(self.coins[MAX_COIN]).balanceOf(self) | ||
|
|
||
| # Deposit to the meta pool | ||
| CurveMeta(self.pool).add_liquidity(meta_amounts, _min_mint_amount) | ||
|
|
||
| # Transfer meta token back | ||
| lp_token: address = self.token | ||
| lp_amount: uint256 = ERC20(lp_token).balanceOf(self) | ||
| assert ERC20(lp_token).transfer(msg.sender, lp_amount) | ||
|
|
||
| return lp_amount | ||
|
|
||
|
|
||
| @external | ||
| def remove_liquidity(_amount: uint256, _min_amounts: uint256[N_ALL_COINS]) -> uint256[N_ALL_COINS]: | ||
| """ | ||
| @notice Withdraw and unwrap coins from the pool | ||
| @dev Withdrawal amounts are based on current deposit ratios | ||
| @param _amount Quantity of LP tokens to burn in the withdrawal | ||
| @param _min_amounts Minimum amounts of underlying coins to receive | ||
| @return List of amounts of underlying coins that were withdrawn | ||
| """ | ||
| _token: address = self.token | ||
| assert ERC20(_token).transferFrom(msg.sender, self, _amount) | ||
|
|
||
| min_amounts_meta: uint256[N_COINS] = empty(uint256[N_COINS]) | ||
| min_amounts_base: uint256[BASE_N_COINS] = empty(uint256[BASE_N_COINS]) | ||
| amounts: uint256[N_ALL_COINS] = empty(uint256[N_ALL_COINS]) | ||
|
|
||
| # Withdraw from meta | ||
| for i in range(MAX_COIN): | ||
| min_amounts_meta[i] = _min_amounts[i] | ||
| CurveMeta(self.pool).remove_liquidity(_amount, min_amounts_meta) | ||
|
|
||
| # Withdraw from base | ||
| _base_amount: uint256 = ERC20(self.coins[MAX_COIN]).balanceOf(self) | ||
| for i in range(BASE_N_COINS): | ||
| min_amounts_base[i] = _min_amounts[MAX_COIN+i] | ||
| CurveBase(self.base_pool).remove_liquidity(_base_amount, min_amounts_base) | ||
|
|
||
| # Transfer all coins out | ||
| for i in range(N_ALL_COINS): | ||
| coin: address = ZERO_ADDRESS | ||
| if i < MAX_COIN: | ||
| coin = self.coins[i] | ||
| else: | ||
| coin = self.base_coins[i - MAX_COIN] | ||
| amounts[i] = ERC20(coin).balanceOf(self) | ||
| # "safeTransfer" which works for ERC20s which return bool or not | ||
| _response: Bytes[32] = raw_call( | ||
| coin, | ||
| concat( | ||
| method_id("transfer(address,uint256)"), | ||
| convert(msg.sender, bytes32), | ||
| convert(amounts[i], bytes32), | ||
| ), | ||
| max_outsize=32, | ||
| ) # dev: failed transfer | ||
| if len(_response) > 0: | ||
| assert convert(_response, bool) # dev: failed transfer | ||
| # end "safeTransfer" | ||
|
|
||
| return amounts | ||
|
|
||
|
|
||
| @external | ||
| def remove_liquidity_one_coin(_token_amount: uint256, i: int128, _min_amount: uint256) -> uint256: | ||
| """ | ||
| @notice Withdraw and unwrap a single coin from the pool | ||
| @param _token_amount Amount of LP tokens to burn in the withdrawal | ||
| @param i Index value of the coin to withdraw | ||
| @param _min_amount Minimum amount of underlying coin to receive | ||
| @return Amount of underlying coin received | ||
| """ | ||
| assert ERC20(self.token).transferFrom(msg.sender, self, _token_amount) | ||
|
|
||
| coin: address = ZERO_ADDRESS | ||
| if i < MAX_COIN: | ||
| coin = self.coins[i] | ||
| # Withdraw a metapool coin | ||
| CurveMeta(self.pool).remove_liquidity_one_coin(_token_amount, i, _min_amount) | ||
| else: | ||
| coin = self.base_coins[i - MAX_COIN] | ||
| # Withdraw a base pool coin | ||
| CurveMeta(self.pool).remove_liquidity_one_coin(_token_amount, MAX_COIN, 0) | ||
| CurveBase(self.base_pool).remove_liquidity_one_coin( | ||
| ERC20(self.coins[MAX_COIN]).balanceOf(self), i-MAX_COIN, _min_amount | ||
| ) | ||
|
|
||
| # Tranfer the coin out | ||
| coin_amount: uint256 = ERC20(coin).balanceOf(self) | ||
| # "safeTransfer" which works for ERC20s which return bool or not | ||
| _response: Bytes[32] = raw_call( | ||
| coin, | ||
| concat( | ||
| method_id("transfer(address,uint256)"), | ||
| convert(msg.sender, bytes32), | ||
| convert(coin_amount, bytes32), | ||
| ), | ||
| max_outsize=32, | ||
| ) # dev: failed transfer | ||
| if len(_response) > 0: | ||
| assert convert(_response, bool) # dev: failed transfer | ||
| # end "safeTransfer" | ||
|
|
||
| return coin_amount | ||
|
|
||
|
|
||
| @external | ||
| def remove_liquidity_imbalance(_amounts: uint256[N_ALL_COINS], _max_burn_amount: uint256) -> uint256: | ||
| """ | ||
| @notice Withdraw coins from the pool in an imbalanced amount | ||
| @param _amounts List of amounts of underlying coins to withdraw | ||
| @param _max_burn_amount Maximum amount of LP token to burn in the withdrawal | ||
| @return Actual amount of the LP token burned in the withdrawal | ||
| """ | ||
| base_pool: address = self.base_pool | ||
| meta_pool: address = self.pool | ||
| base_coins: address[BASE_N_COINS] = self.base_coins | ||
| meta_coins: address[N_COINS] = self.coins | ||
| lp_token: address = self.token | ||
|
|
||
| fee: uint256 = CurveBase(base_pool).fee() * BASE_N_COINS / (4 * (BASE_N_COINS - 1)) | ||
| fee += fee * FEE_IMPRECISION / FEE_DENOMINATOR # Overcharge to account for imprecision | ||
|
|
||
| # Transfer the LP token in | ||
| assert ERC20(lp_token).transferFrom(msg.sender, self, _max_burn_amount) | ||
|
|
||
| withdraw_base: bool = False | ||
| amounts_base: uint256[BASE_N_COINS] = empty(uint256[BASE_N_COINS]) | ||
| amounts_meta: uint256[N_COINS] = empty(uint256[N_COINS]) | ||
| leftover_amounts: uint256[N_COINS] = empty(uint256[N_COINS]) | ||
|
|
||
| # Prepare quantities | ||
| for i in range(MAX_COIN): | ||
| amounts_meta[i] = _amounts[i] | ||
|
|
||
| for i in range(BASE_N_COINS): | ||
| amount: uint256 = _amounts[MAX_COIN + i] | ||
| if amount != 0: | ||
| amounts_base[i] = amount | ||
| withdraw_base = True | ||
|
|
||
| if withdraw_base: | ||
| amounts_meta[MAX_COIN] = CurveBase(self.base_pool).calc_token_amount(amounts_base, False) | ||
| amounts_meta[MAX_COIN] += amounts_meta[MAX_COIN] * fee / FEE_DENOMINATOR + 1 | ||
|
|
||
| # Remove liquidity and deposit leftovers back | ||
| CurveMeta(meta_pool).remove_liquidity_imbalance(amounts_meta, _max_burn_amount) | ||
| if withdraw_base: | ||
| CurveBase(base_pool).remove_liquidity_imbalance(amounts_base, amounts_meta[MAX_COIN]) | ||
| leftover_amounts[MAX_COIN] = ERC20(meta_coins[MAX_COIN]).balanceOf(self) | ||
| if leftover_amounts[MAX_COIN] > 0: | ||
| CurveMeta(meta_pool).add_liquidity(leftover_amounts, 0) | ||
|
|
||
| # Transfer all coins out | ||
| for i in range(N_ALL_COINS): | ||
| coin: address = ZERO_ADDRESS | ||
| amount: uint256 = 0 | ||
| if i < MAX_COIN: | ||
| coin = meta_coins[i] | ||
| amount = amounts_meta[i] | ||
| else: | ||
| coin = base_coins[i - MAX_COIN] | ||
| amount = amounts_base[i - MAX_COIN] | ||
| # "safeTransfer" which works for ERC20s which return bool or not | ||
| if amount > 0: | ||
| _response: Bytes[32] = raw_call( | ||
| coin, | ||
| concat( | ||
| method_id("transfer(address,uint256)"), | ||
| convert(msg.sender, bytes32), | ||
| convert(amount, bytes32), | ||
| ), | ||
| max_outsize=32, | ||
| ) # dev: failed transfer | ||
| if len(_response) > 0: | ||
| assert convert(_response, bool) # dev: failed transfer | ||
| # end "safeTransfer" | ||
|
|
||
| # Transfer the leftover LP token out | ||
| leftover: uint256 = ERC20(lp_token).balanceOf(self) | ||
| if leftover > 0: | ||
| assert ERC20(lp_token).transfer(msg.sender, leftover) | ||
|
|
||
| return _max_burn_amount - leftover | ||
|
|
||
|
|
||
| @view | ||
| @external | ||
| def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256: | ||
| """ | ||
| @notice Calculate the amount received when withdrawing and unwrapping a single coin | ||
| @param _token_amount Amount of LP tokens to burn in the withdrawal | ||
| @param i Index value of the underlying coin to withdraw | ||
| @return Amount of coin received | ||
| """ | ||
| if i < MAX_COIN: | ||
| return CurveMeta(self.pool).calc_withdraw_one_coin(_token_amount, i) | ||
| else: | ||
| base_tokens: uint256 = CurveMeta(self.pool).calc_withdraw_one_coin(_token_amount, MAX_COIN) | ||
| return CurveBase(self.base_pool).calc_withdraw_one_coin(base_tokens, i-MAX_COIN) | ||
|
|
||
|
|
||
| @view | ||
| @external | ||
| def calc_token_amount(_amounts: uint256[N_ALL_COINS], _is_deposit: bool) -> uint256: | ||
| """ | ||
| @notice Calculate addition or reduction in token supply from a deposit or withdrawal | ||
| @dev This calculation accounts for slippage, but not fees. | ||
| Needed to prevent front-running, not for precise calculations! | ||
| @param _amounts Amount of each underlying coin being deposited | ||
| @param _is_deposit set True for deposits, False for withdrawals | ||
| @return Expected amount of LP tokens received | ||
| """ | ||
| meta_amounts: uint256[N_COINS] = empty(uint256[N_COINS]) | ||
| base_amounts: uint256[BASE_N_COINS] = empty(uint256[BASE_N_COINS]) | ||
|
|
||
| for i in range(MAX_COIN): | ||
| meta_amounts[i] = _amounts[i] | ||
|
|
||
| for i in range(BASE_N_COINS): | ||
| base_amounts[i] = _amounts[i + MAX_COIN] | ||
|
|
||
| base_tokens: uint256 = CurveBase(self.base_pool).calc_token_amount(base_amounts, _is_deposit) | ||
| meta_amounts[MAX_COIN] = base_tokens | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thank you |
||
| return CurveMeta(self.pool).calc_token_amount(meta_amounts, _is_deposit) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank you