From 2b2ce517414b0b964291e5e788693687b405932f Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sat, 27 Mar 2021 20:47:43 -0400 Subject: [PATCH 01/15] feat: add RETH pool README --- contracts/pools/reth/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 contracts/pools/reth/README.md diff --git a/contracts/pools/reth/README.md b/contracts/pools/reth/README.md new file mode 100644 index 00000000..33d8237d --- /dev/null +++ b/contracts/pools/reth/README.md @@ -0,0 +1,20 @@ +# curve-contract/contracts/pools/reth + +[Curve rETH]() + +## Contracts + +- [`StableSwaprETH`](StableSwapRETH.vy): Curve stablecoin AMM contract + +## Deployments + +- [`CurveContractV3`](../../tokens/CurveTokenV3.vy): +- [`LiquidityGaugeV2`](https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/gauges/LiquidityGaugeV2.vy): +- [`StableSwapRETH`](StableSwapRETH.vy): + +## Stablecoins + +Curve rETH pool supports swaps between ETH and [`rETH`](https://github.com/stafiprotocol/) staked ETH (rETH): + +- `ETH`: represented in the pool as `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` +- `rETH`: [0x9559aaa82d9649c7a7b220e7c461d2e74c9a3593](https://etherscan.io/token/0x9559aaa82d9649c7a7b220e7c461d2e74c9a3593#code) From e8389e409a4e6ccb986eaddbde5f28bd867c8b2e Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 28 Mar 2021 00:06:07 -0400 Subject: [PATCH 02/15] fix: pytest ignore collect --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 91661990..d6ee558a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,7 @@ "renERC20": {"get_rate": "exchangeRateCurrent"}, "yERC20": {"get_rate": "getPricePerFullShare", "mint": "deposit"}, "aETH": {"get_rate": "ratio"}, + "rETH": {"get_rate": "getExchangeRate"}, } pytest_plugins = [ @@ -103,7 +104,7 @@ def pytest_ignore_collect(path, config): return None # always allow forked tests - if path_parts[0] == "forked": + if path_parts[:1] == "forked": return None # with the `--unitary` flag, skip any tests in an `integration` subdirectory From 5a78b66b8ecb338c2fdea8acbfa24703c2d9c2af Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 28 Mar 2021 00:08:09 -0400 Subject: [PATCH 03/15] test: add rETH mock contract --- contracts/testing/rETH.vy | 102 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 contracts/testing/rETH.vy diff --git a/contracts/testing/rETH.vy b/contracts/testing/rETH.vy new file mode 100644 index 00000000..95616162 --- /dev/null +++ b/contracts/testing/rETH.vy @@ -0,0 +1,102 @@ +# @version ^0.2.8 +""" +@notice Mock rETH token +@dev This is for testing only, it is NOT safe for use +""" + +from vyper.interfaces import ERC20 + +interface ERC20Mock: + def decimals() -> uint256: view + def _mint_for_testing(_target: address, _value: uint256) -> bool: nonpayable + + +event Transfer: + _from: indexed(address) + _to: indexed(address) + _value: uint256 + +event Approval: + _owner: indexed(address) + _spender: indexed(address) + _value: uint256 + + +name: public(String[64]) +symbol: public(String[32]) +decimals: public(uint256) +balanceOf: public(HashMap[address, uint256]) +allowances: HashMap[address, HashMap[address, uint256]] +total_supply: uint256 + +exchangeRateStored: public(uint256) + +@external +def __init__( + _name: String[64], + _symbol: String[32], + _decimals: uint256, + _underlying_token: address +): + self.name = _name + self.symbol = _symbol + self.decimals = _decimals + self.exchangeRateStored = 10**18 + + +@external +@view +def totalSupply() -> uint256: + return self.total_supply + + +@external +@view +def allowance(_owner : address, _spender : address) -> uint256: + return self.allowances[_owner][_spender] + + +@external +def transfer(_to : address, _value : uint256) -> bool: + self.balanceOf[msg.sender] -= _value + self.balanceOf[_to] += _value + log Transfer(msg.sender, _to, _value) + return True + + +@external +def transferFrom(_from : address, _to : address, _value : uint256) -> bool: + self.balanceOf[_from] -= _value + self.balanceOf[_to] += _value + self.allowances[_from][msg.sender] -= _value + log Transfer(_from, _to, _value) + return True + + +@external +def approve(_spender : address, _value : uint256) -> bool: + self.allowances[msg.sender][_spender] = _value + log Approval(msg.sender, _spender, _value) + return True + + +# rETH-specific function +@view +@external +def getExchangeRate() -> uint256: + rate: uint256 = self.exchangeRateStored + return rate + + +@external +def set_exchange_rate(rate: uint256): + self.exchangeRateStored = rate + + +@external +def _mint_for_testing(_target: address, _value: uint256) -> bool: + self.total_supply += _value + self.balanceOf[_target] += _value + log Transfer(ZERO_ADDRESS, _target, _value) + + return True From 27f4bb184a41769392921345c625ce948d89d81d Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 28 Mar 2021 00:47:28 -0400 Subject: [PATCH 04/15] feat: add rETH pooldata.json --- contracts/pools/reth/pooldata.json | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 contracts/pools/reth/pooldata.json diff --git a/contracts/pools/reth/pooldata.json b/contracts/pools/reth/pooldata.json new file mode 100644 index 00000000..85ba25db --- /dev/null +++ b/contracts/pools/reth/pooldata.json @@ -0,0 +1,35 @@ +{ + "lp_contract": "CurveTokenV3", + "pool_types": ["eth", "crate"], + "wrapped_contract": "rETH", + "swap_address": "", + "lp_token_address": "", + "gauge_addresses": [""], + "lp_constructor": { + "symbol": "rCRV", + "name": "Curve.fi ETH/rETH" + }, + "swap_constructor": { + "_A": 10, + "_fee": 4000000, + "_admin_fee": 5000000000 + }, + "coins": [ + { + "name": "ETH", + "decimals": 18, + "tethered": false, + "underlying_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + }, + { + "name": "rETH", + "decimals": 18, + "wrapped_decimals": 18, + "tethered": false, + "wrapped_address": "0x9559aaa82d9649c7a7b220e7c461d2e74c9a3593" + } + ], + "testing": { + "initial_amount": 10000 + } +} From 2b6b1f859d6c58982c2042fa90a47ae1961b992a Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 28 Mar 2021 03:31:48 -0400 Subject: [PATCH 05/15] fix(test): modify forked/insufficient_balances For ETH pools charlie doesn't have enough funds which is ofcourse what we are testing. Modified the test to first assert that charlie doesn't have enough funds to cover the transaction. Second verified that if charlie sends no ETH the transaction will still revert. --- tests/forked/test_insufficient_balances.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/forked/test_insufficient_balances.py b/tests/forked/test_insufficient_balances.py index 7a49e38f..dcea9b62 100644 --- a/tests/forked/test_insufficient_balances.py +++ b/tests/forked/test_insufficient_balances.py @@ -46,8 +46,17 @@ def test_add_liquidity_insufficient_balance( value = 0 if wrapped_coins[send] != ETH_ADDRESS else amount if underlying_coins[send] == ETH_ADDRESS: value = amount - 1 - with brownie.reverts(): - swap.exchange(send, recv, amount, 0, {"from": charlie, "value": value}) + # charlie doesn't have enough ETH + if wrapped_coins[send] == ETH_ADDRESS: + with pytest.raises(ValueError) as e: + swap.exchange(send, recv, amount, 0, {"from": charlie, "value": value}) + assert "sender doesn't have enough funds to send tx" in str(e.value) + with brownie.reverts(): + # didn't send any ETH this time soooo tx fails + swap.exchange(send, recv, amount, 0, {"from": charlie}) + else: + with brownie.reverts(): + swap.exchange(send, recv, amount, 0, {"from": charlie, "value": value}) # attempt to perform swaps between coins with insufficient funds if hasattr(swap, "exchange_underlying"): From 961ebc3488485acfa9eba01075a83576e8d743db Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 28 Mar 2021 03:33:24 -0400 Subject: [PATCH 06/15] feat: add rETH stable swap contract --- contracts/pools/reth/StableSwapRETH.vy | 843 +++++++++++++++++++++++++ 1 file changed, 843 insertions(+) create mode 100644 contracts/pools/reth/StableSwapRETH.vy diff --git a/contracts/pools/reth/StableSwapRETH.vy b/contracts/pools/reth/StableSwapRETH.vy new file mode 100644 index 00000000..4d389e49 --- /dev/null +++ b/contracts/pools/reth/StableSwapRETH.vy @@ -0,0 +1,843 @@ + +# @version 0.2.8 +""" +@title ETH/rETH StableSwap +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2021 - all rights reserved +""" + +from vyper.interfaces import ERC20 + +# External Contracts +interface rETH: + def getExchangeRate() -> uint256: view + + +interface CurveToken: + def mint(_to: address, _value: uint256) -> bool: nonpayable + def burnFrom(_to: address, _value: uint256) -> bool: nonpayable + + +# Events +event TokenExchange: + buyer: indexed(address) + sold_id: int128 + tokens_sold: uint256 + bought_id: int128 + tokens_bought: uint256 + +event AddLiquidity: + provider: indexed(address) + token_amounts: uint256[N_COINS] + fees: uint256[N_COINS] + invariant: uint256 + token_supply: uint256 + +event RemoveLiquidity: + provider: indexed(address) + token_amounts: uint256[N_COINS] + fees: uint256[N_COINS] + token_supply: uint256 + +event RemoveLiquidityOne: + provider: indexed(address) + token_amount: uint256 + coin_amount: uint256 + +event RemoveLiquidityImbalance: + provider: indexed(address) + token_amounts: uint256[N_COINS] + fees: uint256[N_COINS] + invariant: uint256 + token_supply: uint256 + +event CommitNewAdmin: + deadline: indexed(uint256) + admin: indexed(address) + +event NewAdmin: + admin: indexed(address) + +event CommitNewFee: + deadline: indexed(uint256) + fee: uint256 + admin_fee: uint256 + +event NewFee: + fee: uint256 + admin_fee: uint256 + +event RampA: + old_A: uint256 + new_A: uint256 + initial_time: uint256 + future_time: uint256 + +event StopRampA: + A: uint256 + t: uint256 + + +# These constants must be set prior to compiling +N_COINS: constant(int128) = 2 + +# fixed constants +FEE_DENOMINATOR: constant(uint256) = 10 ** 10 +LENDING_PRECISION: constant(uint256) = 10 ** 18 +PRECISION: constant(uint256) = 10 ** 18 # The precision to convert to + +MAX_ADMIN_FEE: constant(uint256) = 10 * 10 ** 9 +MAX_FEE: constant(uint256) = 5 * 10 ** 9 +MAX_A: constant(uint256) = 10 ** 6 +MAX_A_CHANGE: constant(uint256) = 10 + +ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400 +MIN_RAMP_TIME: constant(uint256) = 86400 + +coins: public(address[N_COINS]) +balances: public(uint256[N_COINS]) + +fee: public(uint256) # fee * 1e10 +admin_fee: public(uint256) # admin_fee * 1e10 + +owner: public(address) +lp_token: public(address) + +A_PRECISION: constant(uint256) = 100 +initial_A: public(uint256) +future_A: public(uint256) +initial_A_time: public(uint256) +future_A_time: public(uint256) + +admin_actions_deadline: public(uint256) +transfer_ownership_deadline: public(uint256) +future_fee: public(uint256) +future_admin_fee: public(uint256) +future_owner: public(address) + +is_killed: bool +kill_deadline: uint256 +KILL_DEADLINE_DT: constant(uint256) = 2 * 30 * 86400 + + +@external +def __init__( + _owner: address, + _coins: address[N_COINS], + _pool_token: address, + _A: uint256, + _fee: uint256, + _admin_fee: uint256, +): + """ + @notice Contract constructor + @param _owner Contract owner address + @param _coins Addresses of ERC20 contracts of wrapped coins + @param _pool_token Address of the token representing LP share + @param _A Amplification coefficient multiplied by n * (n - 1) + @param _fee Fee to charge for exchanges + @param _admin_fee Admin fee + """ + + assert _coins[0] == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE + assert _coins[1] != ZERO_ADDRESS + + self.coins = _coins + self.initial_A = _A * A_PRECISION + self.future_A = _A * A_PRECISION + self.fee = _fee + self.admin_fee = _admin_fee + self.owner = _owner + self.kill_deadline = block.timestamp + KILL_DEADLINE_DT + self.lp_token = _pool_token + + +@view +@internal +def _A() -> uint256: + """ + Handle ramping A up or down + """ + t1: uint256 = self.future_A_time + A1: uint256 = self.future_A + + if block.timestamp < t1: + A0: uint256 = self.initial_A + t0: uint256 = self.initial_A_time + # Expressions in uint256 cannot have negative numbers, thus "if" + if A1 > A0: + return A0 + (A1 - A0) * (block.timestamp - t0) / (t1 - t0) + else: + return A0 - (A0 - A1) * (block.timestamp - t0) / (t1 - t0) + + else: # when t1 == 0 or block.timestamp >= t1 + return A1 + + +@view +@external +def A() -> uint256: + return self._A() / A_PRECISION + + +@view +@external +def A_precise() -> uint256: + return self._A() + + +@view +@internal +def _stored_rates() -> uint256[N_COINS]: + return [ + convert(PRECISION, uint256), + rETH(self.coins[1]).getExchangeRate() + ] + + +@view +@internal +def _xp(rates: uint256[N_COINS]) -> uint256[N_COINS]: + result: uint256[N_COINS] = rates + for i in range(N_COINS): + result[i] = result[i] * self.balances[i] / PRECISION + return result + + +@internal +@view +def get_D(xp: uint256[N_COINS], amp: uint256) -> uint256: + S: uint256 = 0 + Dprev: uint256 = 0 + + for _x in xp: + S += _x + if S == 0: + return 0 + + D: uint256 = S + Ann: uint256 = amp * N_COINS + for _i in range(255): + D_P: uint256 = D + for _x in xp: + D_P = D_P * D / (_x * N_COINS) # If division by 0, this will be borked: only withdrawal will work. And that is good + Dprev = D + D = (Ann * S / A_PRECISION + D_P * N_COINS) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS + 1) * D_P) + # Equality with the precision of 1 + if D > Dprev: + if D - Dprev <= 1: + return D + else: + if Dprev - D <= 1: + return D + # convergence typically occurs in 4 rounds or less, this should be unreachable! + # if it does happen the pool is borked and LPs can withdraw via `remove_liquidity` + raise + + +@view +@internal +def get_D_mem(rates: uint256[N_COINS], _balances: uint256[N_COINS], amp: uint256) -> uint256: + result: uint256[N_COINS] = rates + for i in range(N_COINS): + result[i] = result[i] * _balances[i] / PRECISION + return self.get_D(result, amp) + + +@view +@external +def get_virtual_price() -> uint256: + """ + @notice The current virtual price of the pool LP token + @dev Useful for calculating profits + @return LP token virtual price normalized to 1e18 + """ + D: uint256 = self.get_D(self._xp(self._stored_rates()), self._A()) + # D is in the units similar to DAI (e.g. converted to precision 1e18) + # When balanced, D = n * x_u - total virtual value of the portfolio + token_supply: uint256 = ERC20(self.lp_token).totalSupply() + return D * PRECISION / token_supply + + +@view +@external +def calc_token_amount(amounts: uint256[N_COINS], is_deposit: bool) -> uint256: + """ + @notice Calculate addition or reduction in token supply from a deposit or withdrawal + @dev This calculation accounts for slippage, but not fees. + Needed to prevent front-running, not for precise calculations! + @param amounts Amount of each coin being deposited + @param is_deposit set True for deposits, False for withdrawals + @return Expected amount of LP tokens received + """ + amp: uint256 = self._A() + rates: uint256[N_COINS] = self._stored_rates() + _balances: uint256[N_COINS] = self.balances + D0: uint256 = self.get_D_mem(rates, _balances, amp) + for i in range(N_COINS): + _amount: uint256 = amounts[i] + if is_deposit: + _balances[i] += _amount + else: + _balances[i] -= _amount + D1: uint256 = self.get_D_mem(rates, _balances, amp) + token_amount: uint256 = ERC20(self.lp_token).totalSupply() + diff: uint256 = 0 + if is_deposit: + diff = D1 - D0 + else: + diff = D0 - D1 + return diff * token_amount / D0 + +@payable +@external +@nonreentrant('lock') +def add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256) -> uint256: + """ + @notice Deposit coins into the pool + @param amounts List of amounts of coins to deposit + @param min_mint_amount Minimum amount of LP tokens to mint from the deposit + @return Amount of LP tokens received by depositing + """ + assert not self.is_killed + amp: uint256 = self._A() + rates: uint256[N_COINS] = self._stored_rates() + _lp_token: address = self.lp_token + token_supply: uint256 = ERC20(_lp_token).totalSupply() + + # Initial invariant + D0: uint256 = 0 + old_balances: uint256[N_COINS] = self.balances + if token_supply != 0: + D0 = self.get_D_mem(rates, old_balances, amp) + + new_balances: uint256[N_COINS] = old_balances + for i in range(N_COINS): + if token_supply == 0: + assert amounts[i] > 0 + new_balances[i] += amounts[i] + + # Invariant after change + D1: uint256 = self.get_D_mem(rates, new_balances, amp) + assert D1 > D0 + + # We need to recalculate the invariant accounting for fees + # to calculate fair user's share + D2: uint256 = D1 + fees: uint256[N_COINS] = empty(uint256[N_COINS]) + mint_amount: uint256 = 0 + if token_supply != 0: + # Only account for fees if we are not the first to deposit + _fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + _admin_fee: uint256 = self.admin_fee + for i in range(N_COINS): + ideal_balance: uint256 = D1 * old_balances[i] / D0 + difference: uint256 = 0 + if ideal_balance > new_balances[i]: + difference = ideal_balance - new_balances[i] + else: + difference = new_balances[i] - ideal_balance + fees[i] = _fee * difference / FEE_DENOMINATOR + self.balances[i] = new_balances[i] - (fees[i] * _admin_fee / FEE_DENOMINATOR) + new_balances[i] -= fees[i] + D2 = self.get_D_mem(rates, new_balances, amp) + mint_amount = token_supply * (D2 - D0) / D0 + else: + self.balances = new_balances + mint_amount = D1 # Take the dust if there was any + + assert mint_amount >= min_mint_amount, "Slippage screwed you" + + # Take coins from the sender + assert msg.value == amounts[0] + if amounts[1] > 0: + assert ERC20(self.coins[1]).transferFrom(msg.sender, self, amounts[1]) + + # Mint pool tokens + CurveToken(_lp_token).mint(msg.sender, mint_amount) + + log AddLiquidity(msg.sender, amounts, fees, D1, token_supply + mint_amount) + + return mint_amount + + +@view +@internal +def get_y(i: int128, j: int128, x: uint256, xp_: uint256[N_COINS]) -> uint256: + # x in the input is converted to the same price/precision + + assert i != j # dev: same coin + assert j >= 0 # dev: j below zero + assert j < N_COINS # dev: j above N_COINS + + # should be unreachable, but good for safety + assert i >= 0 + assert i < N_COINS + + A_: uint256 = self._A() + D: uint256 = self.get_D(xp_, A_) + Ann: uint256 = A_ * N_COINS + c: uint256 = D + S_: uint256 = 0 + _x: uint256 = 0 + y_prev: uint256 = 0 + + for _i in range(N_COINS): + if _i == i: + _x = x + elif _i != j: + _x = xp_[_i] + else: + continue + S_ += _x + c = c * D / (_x * N_COINS) + c = c * D * A_PRECISION / (Ann * N_COINS) + b: uint256 = S_ + D * A_PRECISION / Ann # - D + y: uint256 = D + for _i in range(255): + y_prev = y + y = (y*y + c) / (2 * y + b - D) + # Equality with the precision of 1 + if y > y_prev: + if y - y_prev <= 1: + return y + else: + if y_prev - y <= 1: + return y + raise + + +@view +@external +def get_dy(i: int128, j: int128, dx: uint256) -> uint256: + # dx and dy in c-units + rates: uint256[N_COINS] = self._stored_rates() + xp: uint256[N_COINS] = self._xp(rates) + + x: uint256 = xp[i] + dx * rates[i] / PRECISION + y: uint256 = self.get_y(i, j, x, xp) + dy: uint256 = xp[j] - y + _fee: uint256 = self.fee * dy / FEE_DENOMINATOR + return (dy - _fee) * PRECISION / rates[j] + + +@view +@external +def get_dx(i: int128, j: int128, dy: uint256) -> uint256: + # dx and dy in c-units + rates: uint256[N_COINS] = self._stored_rates() + xp: uint256[N_COINS] = self._xp(rates) + + y: uint256 = xp[j] - (dy * FEE_DENOMINATOR / (FEE_DENOMINATOR - self.fee)) * rates[j] / PRECISION + x: uint256 = self.get_y(j, i, y, xp) + dx: uint256 = (x - xp[i]) * PRECISION / rates[i] + return dx + + +@payable +@external +@nonreentrant('lock') +def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256: + """ + @notice Perform an exchange between two coins + @dev Index values can be found via the `coins` public getter method + @param i Index value for the coin to send + @param j Index valie of the coin to recieve + @param dx Amount of `i` being exchanged + @param min_dy Minimum amount of `j` to receive + @return Actual amount of `j` received + """ + assert not self.is_killed # dev: is killed + + rates: uint256[N_COINS] = self._stored_rates() + + xp: uint256[N_COINS] = self._xp(rates) + x: uint256 = xp[i] + dx * rates[i] / PRECISION + y: uint256 = self.get_y(i, j, x, xp) + dy: uint256 = xp[j] - y - 1 # -1 just in case there were some rounding errors + dy_fee: uint256 = dy * self.fee / FEE_DENOMINATOR + dy_admin_fee: uint256 = dy_fee * self.admin_fee / FEE_DENOMINATOR + + self.balances[i] = x * PRECISION / rates[i] + self.balances[j] = (y + (dy_fee - dy_admin_fee)) * PRECISION / rates[j] + + dy = (dy - dy_fee) * PRECISION / rates[j] + assert dy >= min_dy, "Exchange resulted in fewer coins than expected" + + coin: address = self.coins[1] + if i == 0: + assert msg.value == dx + assert ERC20(coin).transfer(msg.sender, dy) + else: + assert msg.value == 0 + assert ERC20(coin).transferFrom(msg.sender, self, dx) + raw_call(msg.sender, b"", value=dy) + + log TokenExchange(msg.sender, i, dx, j, dy) + + return dy + + +@external +@nonreentrant('lock') +def remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS]) -> uint256[N_COINS]: + """ + @notice Withdraw coins from the pool + @dev Withdrawal amounts are based on current deposit ratios + @param _amount Quantity of LP tokens to burn in the withdrawal + @param min_amounts Minimum amounts of underlying coins to receive + @return List of amounts of coins that were withdrawn + """ + _lp_token: address = self.lp_token + total_supply: uint256 = ERC20(_lp_token).totalSupply() + amounts: uint256[N_COINS] = empty(uint256[N_COINS]) + + for i in range(N_COINS): + _balance: uint256 = self.balances[i] + value: uint256 = _balance * _amount / total_supply + assert value >= min_amounts[i], "Withdrawal resulted in fewer coins than expected" + self.balances[i] = _balance - value + amounts[i] = value + if i == 0: + raw_call(msg.sender, b"", value=value) + else: + assert ERC20(self.coins[1]).transfer(msg.sender, value) + + CurveToken(_lp_token).burnFrom(msg.sender, _amount) # Will raise if not enough + + log RemoveLiquidity(msg.sender, amounts, empty(uint256[N_COINS]), total_supply - _amount) + + return amounts + + +@external +@nonreentrant('lock') +def remove_liquidity_imbalance(amounts: uint256[N_COINS], max_burn_amount: uint256) -> uint256: + """ + @notice Withdraw coins from the pool in an imbalanced amount + @param amounts List of amounts of underlying coins to withdraw + @param max_burn_amount Maximum amount of LP token to burn in the withdrawal + @return Actual amount of the LP token burned in the withdrawal + """ + assert not self.is_killed + amp: uint256 = self._A() + rates: uint256[N_COINS] = self._stored_rates() + old_balances: uint256[N_COINS] = self.balances + D0: uint256 = self.get_D_mem(rates, old_balances, amp) + + new_balances: uint256[N_COINS] = old_balances + for i in range(N_COINS): + new_balances[i] -= amounts[i] + D1: uint256 = self.get_D_mem(rates, new_balances, amp) + + fees: uint256[N_COINS] = empty(uint256[N_COINS]) + _fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + _admin_fee: uint256 = self.admin_fee + for i in range(N_COINS): + ideal_balance: uint256 = D1 * old_balances[i] / D0 + new_balance: uint256 = new_balances[i] + difference: uint256 = 0 + if ideal_balance > new_balance: + difference = ideal_balance - new_balance + else: + difference = new_balance - ideal_balance + fees[i] = _fee * difference / FEE_DENOMINATOR + self.balances[i] = new_balance - (fees[i] * _admin_fee / FEE_DENOMINATOR) + new_balances[i] = new_balance - fees[i] + D2: uint256 = self.get_D_mem(rates, new_balances, amp) + + lp_token: address = self.lp_token + token_supply: uint256 = ERC20(lp_token).totalSupply() + token_amount: uint256 = (D0 - D2) * token_supply / D0 + assert token_amount != 0 + assert token_amount <= max_burn_amount, "Slippage screwed you" + + CurveToken(lp_token).burnFrom(msg.sender, token_amount) # dev: insufficient funds + + if amounts[0] != 0: + raw_call(msg.sender, b"", value=amounts[0]) + if amounts[1] != 0: + assert ERC20(self.coins[1]).transfer(msg.sender, amounts[1]) + + log RemoveLiquidityImbalance(msg.sender, amounts, fees, D1, token_supply - token_amount) + + return token_amount + + +@pure +@internal +def get_y_D(A_: uint256, i: int128, xp: uint256[N_COINS], D: uint256) -> uint256: + """ + Calculate x[i] if one reduces D from being calculated for xp to D + + Done by solving quadratic equation iteratively. + x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + b*x_1 = c + + x_1 = (x_1**2 + c) / (2*x_1 + b) + """ + # x in the input is converted to the same price/precision + + assert i >= 0 # dev: i below zero + assert i < N_COINS # dev: i above N_COINS + + Ann: uint256 = A_ * N_COINS + c: uint256 = D + S_: uint256 = 0 + _x: uint256 = 0 + y_prev: uint256 = 0 + + for _i in range(N_COINS): + if _i != i: + _x = xp[_i] + else: + continue + S_ += _x + c = c * D / (_x * N_COINS) + c = c * D * A_PRECISION / (Ann * N_COINS) + b: uint256 = S_ + D * A_PRECISION / Ann + y: uint256 = D + + for _i in range(255): + y_prev = y + y = (y*y + c) / (2 * y + b - D) + # Equality with the precision of 1 + if y > y_prev: + if y - y_prev <= 1: + return y + else: + if y_prev - y <= 1: + return y + raise + + +@view +@internal +def _calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> (uint256, uint256): + # First, need to calculate + # * Get current D + # * Solve Eqn against y_i for D - _token_amount + amp: uint256 = self._A() + rates: uint256[N_COINS] = self._stored_rates() + xp: uint256[N_COINS] = self._xp(rates) + D0: uint256 = self.get_D(xp, amp) + + total_supply: uint256 = ERC20(self.lp_token).totalSupply() + + D1: uint256 = D0 - _token_amount * D0 / total_supply + new_y: uint256 = self.get_y_D(amp, i, xp, D1) + + xp_reduced: uint256[N_COINS] = xp + _fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + + for j in range(N_COINS): + dx_expected: uint256 = 0 + xp_j: uint256 = xp[j] + if j == i: + dx_expected = xp_j * D1 / D0 - new_y + else: + dx_expected = xp_j - xp_j * D1 / D0 + xp_reduced[j] -= _fee * dx_expected / FEE_DENOMINATOR + + dy: uint256 = xp_reduced[i] - self.get_y_D(amp, i, xp_reduced, D1) + rate: uint256 = rates[i] + dy = (dy - 1) * PRECISION / rate # Withdraw less to account for rounding errors + dy_0: uint256 = (xp[i] - new_y) * PRECISION / rate # w/o fees + + return dy, dy_0 - dy + + +@view +@external +def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256: + """ + @notice Calculate the amount received when withdrawing a single coin + @param _token_amount Amount of LP tokens to burn in the withdrawal + @param i Index value of the coin to withdraw + @return Amount of coin received + """ + return self._calc_withdraw_one_coin(_token_amount, i)[0] + + +@external +@nonreentrant('lock') +def remove_liquidity_one_coin(_token_amount: uint256, i: int128, _min_amount: uint256) -> uint256: + """ + @notice Withdraw a single coin from the pool + @param _token_amount Amount of LP tokens to burn in the withdrawal + @param i Index value of the coin to withdraw + @param _min_amount Minimum amount of coin to receive + @return Amount of coin received + """ + assert not self.is_killed # dev: is killed + + dy: uint256 = 0 + dy_fee: uint256 = 0 + dy, dy_fee = self._calc_withdraw_one_coin(_token_amount, i) + assert dy >= _min_amount, "Not enough coins removed" + + self.balances[i] -= (dy + dy_fee * self.admin_fee / FEE_DENOMINATOR) + CurveToken(self.lp_token).burnFrom(msg.sender, _token_amount) # dev: insufficient funds + + if i == 0: + raw_call(msg.sender, b"", value=dy) + else: + assert ERC20(self.coins[1]).transfer(msg.sender, dy) + + log RemoveLiquidityOne(msg.sender, _token_amount, dy) + + return dy + + +### Admin functions ### +@external +def ramp_A(_future_A: uint256, _future_time: uint256): + assert msg.sender == self.owner # dev: only owner + assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME + assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time + + _initial_A: uint256 = self._A() + _future_A_p: uint256 = _future_A * A_PRECISION + + assert _future_A > 0 and _future_A < MAX_A + if _future_A_p < _initial_A: + assert _future_A_p * MAX_A_CHANGE >= _initial_A + else: + assert _future_A_p <= _initial_A * MAX_A_CHANGE + + self.initial_A = _initial_A + self.future_A = _future_A_p + self.initial_A_time = block.timestamp + self.future_A_time = _future_time + + log RampA(_initial_A, _future_A_p, block.timestamp, _future_time) + + +@external +def stop_ramp_A(): + assert msg.sender == self.owner # dev: only owner + + current_A: uint256 = self._A() + self.initial_A = current_A + self.future_A = current_A + self.initial_A_time = block.timestamp + self.future_A_time = block.timestamp + # now (block.timestamp < t1) is always False, so we return saved A + + log StopRampA(current_A, block.timestamp) + + +@external +def commit_new_fee(new_fee: uint256, new_admin_fee: uint256): + assert msg.sender == self.owner # dev: only owner + assert self.admin_actions_deadline == 0 # dev: active action + assert new_fee <= MAX_FEE # dev: fee exceeds maximum + assert new_admin_fee <= MAX_ADMIN_FEE # dev: admin fee exceeds maximum + + _deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY + self.admin_actions_deadline = _deadline + self.future_fee = new_fee + self.future_admin_fee = new_admin_fee + + log CommitNewFee(_deadline, new_fee, new_admin_fee) + + +@external +@nonreentrant('lock') +def apply_new_fee(): + assert msg.sender == self.owner # dev: only owner + assert block.timestamp >= self.admin_actions_deadline # dev: insufficient time + assert self.admin_actions_deadline != 0 # dev: no active action + + self.admin_actions_deadline = 0 + _fee: uint256 = self.future_fee + _admin_fee: uint256 = self.future_admin_fee + self.fee = _fee + self.admin_fee = _admin_fee + + log NewFee(_fee, _admin_fee) + + +@external +def revert_new_parameters(): + assert msg.sender == self.owner # dev: only owner + + self.admin_actions_deadline = 0 + + +@external +def commit_transfer_ownership(_owner: address): + assert msg.sender == self.owner # dev: only owner + assert self.transfer_ownership_deadline == 0 # dev: active transfer + + _deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY + self.transfer_ownership_deadline = _deadline + self.future_owner = _owner + + log CommitNewAdmin(_deadline, _owner) + + +@external +@nonreentrant('lock') +def apply_transfer_ownership(): + assert msg.sender == self.owner # dev: only owner + assert block.timestamp >= self.transfer_ownership_deadline # dev: insufficient time + assert self.transfer_ownership_deadline != 0 # dev: no active transfer + + self.transfer_ownership_deadline = 0 + _owner: address = self.future_owner + self.owner = _owner + + log NewAdmin(_owner) + + +@external +def revert_transfer_ownership(): + assert msg.sender == self.owner # dev: only owner + self.transfer_ownership_deadline = 0 + + +@view +@external +def admin_balances(i: uint256) -> uint256: + if i == 0: + return self.balance - self.balances[0] + return ERC20(self.coins[i]).balanceOf(self) - self.balances[i] + + +@external +@nonreentrant('lock') +def withdraw_admin_fees(): + assert msg.sender == self.owner # dev: only owner + + amount: uint256 = self.balance - self.balances[0] + if amount != 0: + raw_call(msg.sender, b"", value=amount) + + amount = ERC20(self.coins[1]).balanceOf(self) - self.balances[1] + if amount != 0: + assert ERC20(self.coins[1]).transfer(msg.sender, amount) + + +@external +@nonreentrant('lock') +def donate_admin_fees(): + assert msg.sender == self.owner # dev: only owner + for i in range(N_COINS): + if i == 0: + self.balances[0] = self.balance + else: + self.balances[i] = ERC20(self.coins[i]).balanceOf(self) + + +@external +def kill_me(): + assert msg.sender == self.owner # dev: only owner + assert self.kill_deadline > block.timestamp # dev: deadline has passed + self.is_killed = True + + +@external +def unkill_me(): + assert msg.sender == self.owner # dev: only owner + self.is_killed = False From 5517e665aff5664fdd546e7c20703ec6acba21e7 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 28 Mar 2021 03:48:55 -0400 Subject: [PATCH 07/15] feat: add rETH github workflow --- .github/workflows/reth.yaml | 56 +++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/reth.yaml diff --git a/.github/workflows/reth.yaml b/.github/workflows/reth.yaml new file mode 100644 index 00000000..13952db1 --- /dev/null +++ b/.github/workflows/reth.yaml @@ -0,0 +1,56 @@ +name: reth + +on: + pull_request: + paths: + - "tests/**/*.py" + - "contracts/pools/reth/**.vy" + push: + paths: + - "tests/**/*.py" + - "contracts/pools/reth/**.vy" + +env: + pool: "reth" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_OPTIONS: --max_old_space_size=4096 + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + target: [pools, zaps] + type: [unitary, integration] + + steps: + - uses: actions/checkout@v2 + + - name: Cache Compiler Installations + uses: actions/cache@v2 + with: + path: | + ~/.solcx + ~/.vvm + key: compiler-cache + + - name: Setup Node.js + uses: actions/setup-node@v1 + + - name: Install Ganache + run: npm install + + - name: Setup Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install Requirements + run: | + pip install wheel + pip install -r requirements.txt + + - name: Run Tests + run: brownie test tests/${{ matrix.target }} --pool ${{ env.pool }} --${{ matrix.type }} From fc98ed22eec06aefb2fbe41523940e3a1ed8291e Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 28 Mar 2021 07:22:34 -0400 Subject: [PATCH 08/15] fix: use the correct rETH checksummed addr --- contracts/pools/reth/README.md | 2 +- contracts/pools/reth/pooldata.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/pools/reth/README.md b/contracts/pools/reth/README.md index 33d8237d..f274b6e1 100644 --- a/contracts/pools/reth/README.md +++ b/contracts/pools/reth/README.md @@ -17,4 +17,4 @@ Curve rETH pool supports swaps between ETH and [`rETH`](https://github.com/stafiprotocol/) staked ETH (rETH): - `ETH`: represented in the pool as `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` -- `rETH`: [0x9559aaa82d9649c7a7b220e7c461d2e74c9a3593](https://etherscan.io/token/0x9559aaa82d9649c7a7b220e7c461d2e74c9a3593#code) +- `rETH`: [0x9559Aaa82d9649C7A7b220E7c461d2E74c9a3593](https://etherscan.io/token/0x9559aaa82d9649c7a7b220e7c461d2e74c9a3593#code) diff --git a/contracts/pools/reth/pooldata.json b/contracts/pools/reth/pooldata.json index 85ba25db..7b713ac7 100644 --- a/contracts/pools/reth/pooldata.json +++ b/contracts/pools/reth/pooldata.json @@ -26,7 +26,7 @@ "decimals": 18, "wrapped_decimals": 18, "tethered": false, - "wrapped_address": "0x9559aaa82d9649c7a7b220e7c461d2e74c9a3593" + "wrapped_address": "0x9559Aaa82d9649C7A7b220E7c461d2E74c9a3593" } ], "testing": { From 355059cf0f9366278c56580b3d3e72770e699d9c Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 30 Mar 2021 11:06:15 -0400 Subject: [PATCH 09/15] Update tests/conftest.py Co-authored-by: Ben Hauser <35276322+iamdefinitelyahuman@users.noreply.github.com> --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index d6ee558a..c91a790a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -104,7 +104,7 @@ def pytest_ignore_collect(path, config): return None # always allow forked tests - if path_parts[:1] == "forked": + if path_parts[:1] == ("forked",): return None # with the `--unitary` flag, skip any tests in an `integration` subdirectory From 4fc6f82accb34eb174bb859c1240d2bc2f981b0a Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Mon, 12 Apr 2021 18:49:23 -0400 Subject: [PATCH 10/15] fix: remove conversion of PRECISION -> uint256 --- contracts/pools/reth/StableSwapRETH.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/pools/reth/StableSwapRETH.vy b/contracts/pools/reth/StableSwapRETH.vy index 4d389e49..ee2bc757 100644 --- a/contracts/pools/reth/StableSwapRETH.vy +++ b/contracts/pools/reth/StableSwapRETH.vy @@ -190,7 +190,7 @@ def A_precise() -> uint256: @internal def _stored_rates() -> uint256[N_COINS]: return [ - convert(PRECISION, uint256), + PRECISION, rETH(self.coins[1]).getExchangeRate() ] From 5ff8c05eee9f8d76b9245c44a60d798986b60a30 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Mon, 12 Apr 2021 18:49:53 -0400 Subject: [PATCH 11/15] fix: change get_D fn from view to pure --- contracts/pools/reth/StableSwapRETH.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/pools/reth/StableSwapRETH.vy b/contracts/pools/reth/StableSwapRETH.vy index ee2bc757..61db7718 100644 --- a/contracts/pools/reth/StableSwapRETH.vy +++ b/contracts/pools/reth/StableSwapRETH.vy @@ -205,7 +205,7 @@ def _xp(rates: uint256[N_COINS]) -> uint256[N_COINS]: @internal -@view +@pure def get_D(xp: uint256[N_COINS], amp: uint256) -> uint256: S: uint256 = 0 Dprev: uint256 = 0 From 1d88c8855b62846a3d2d61c5f671044b8d5d67e0 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 13 Apr 2021 02:12:59 -0400 Subject: [PATCH 12/15] Revert "fix: remove conversion of PRECISION -> uint256" This reverts commit c5460057edbb2f0fa24bf22dd921a20e5e3616aa. --- contracts/pools/reth/StableSwapRETH.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/pools/reth/StableSwapRETH.vy b/contracts/pools/reth/StableSwapRETH.vy index 61db7718..b1ad55ad 100644 --- a/contracts/pools/reth/StableSwapRETH.vy +++ b/contracts/pools/reth/StableSwapRETH.vy @@ -190,7 +190,7 @@ def A_precise() -> uint256: @internal def _stored_rates() -> uint256[N_COINS]: return [ - PRECISION, + convert(PRECISION, uint256), rETH(self.coins[1]).getExchangeRate() ] From f092aba1672353885cc04b617bafd31683610fdd Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Mon, 19 Apr 2021 14:37:38 -0400 Subject: [PATCH 13/15] feat: upgrade pragma version to 0.2.12 Additional improvements to mitigate memory corruption bug in vyper. Additional optimizations. vyperlang/vyper#2345 vyperlang/vyper#2350 vyperlang/vyper#2352 --- contracts/pools/reth/StableSwapRETH.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/pools/reth/StableSwapRETH.vy b/contracts/pools/reth/StableSwapRETH.vy index b1ad55ad..3d8911f1 100644 --- a/contracts/pools/reth/StableSwapRETH.vy +++ b/contracts/pools/reth/StableSwapRETH.vy @@ -1,5 +1,5 @@ -# @version 0.2.8 +# @version 0.2.12 """ @title ETH/rETH StableSwap @author Curve.Fi From b4a59122741a9c80c9367154e6ec6fa528f6b190 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Wed, 19 May 2021 04:02:08 -0400 Subject: [PATCH 14/15] feat: add swap/lp token deployment addrs to pooldata --- contracts/pools/reth/pooldata.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/pools/reth/pooldata.json b/contracts/pools/reth/pooldata.json index 7b713ac7..85922874 100644 --- a/contracts/pools/reth/pooldata.json +++ b/contracts/pools/reth/pooldata.json @@ -2,8 +2,8 @@ "lp_contract": "CurveTokenV3", "pool_types": ["eth", "crate"], "wrapped_contract": "rETH", - "swap_address": "", - "lp_token_address": "", + "swap_address": "0xF9440930043eb3997fc70e1339dBb11F341de7A8", + "lp_token_address": "0x53a901d48795C58f485cBB38df08FA96a24669D5", "gauge_addresses": [""], "lp_constructor": { "symbol": "rCRV", From 149dbe163896d281ef39147e1178920cf72971d0 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Wed, 19 May 2021 16:41:59 -0400 Subject: [PATCH 15/15] fix: add rETH gauge deployment address to pooldata --- contracts/pools/reth/README.md | 6 +++--- contracts/pools/reth/pooldata.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/pools/reth/README.md b/contracts/pools/reth/README.md index f274b6e1..0bede453 100644 --- a/contracts/pools/reth/README.md +++ b/contracts/pools/reth/README.md @@ -8,9 +8,9 @@ ## Deployments -- [`CurveContractV3`](../../tokens/CurveTokenV3.vy): -- [`LiquidityGaugeV2`](https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/gauges/LiquidityGaugeV2.vy): -- [`StableSwapRETH`](StableSwapRETH.vy): +- [`CurveContractV3`](../../tokens/CurveTokenV3.vy): [0x53a901d48795C58f485cBB38df08FA96a24669D5](https://etherscan.io/address/0x53a901d48795C58f485cBB38df08FA96a24669D5) +- [`LiquidityGaugeV3`](https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/gauges/LiquidityGaugeV3.vy): [0x824F13f1a2F29cFEEa81154b46C0fc820677A637](https://etherscan.io/address/0x824F13f1a2F29cFEEa81154b46C0fc820677A637) +- [`StableSwapRETH`](StableSwapRETH.vy): [0xF9440930043eb3997fc70e1339dBb11F341de7A8](https://etherscan.io/address/0xF9440930043eb3997fc70e1339dBb11F341de7A8) ## Stablecoins diff --git a/contracts/pools/reth/pooldata.json b/contracts/pools/reth/pooldata.json index 85922874..31d5673f 100644 --- a/contracts/pools/reth/pooldata.json +++ b/contracts/pools/reth/pooldata.json @@ -4,7 +4,7 @@ "wrapped_contract": "rETH", "swap_address": "0xF9440930043eb3997fc70e1339dBb11F341de7A8", "lp_token_address": "0x53a901d48795C58f485cBB38df08FA96a24669D5", - "gauge_addresses": [""], + "gauge_addresses": ["0x824F13f1a2F29cFEEa81154b46C0fc820677A637"], "lp_constructor": { "symbol": "rCRV", "name": "Curve.fi ETH/rETH"