From 3aa302fc0d7e708a3c7277f84449aeb962667ae6 Mon Sep 17 00:00:00 2001 From: samwerner Date: Thu, 21 Jan 2021 10:34:40 +0100 Subject: [PATCH 01/12] feat: add impl for wZEC/renZEC pool --- contracts/pools/wzec/README.md | 20 + contracts/pools/wzec/StableSwapWZEC.vy | 883 +++++++++++++++++++++++++ contracts/pools/wzec/pooldata.json | 29 + 3 files changed, 932 insertions(+) create mode 100644 contracts/pools/wzec/README.md create mode 100644 contracts/pools/wzec/StableSwapWZEC.vy create mode 100644 contracts/pools/wzec/pooldata.json diff --git a/contracts/pools/wzec/README.md b/contracts/pools/wzec/README.md new file mode 100644 index 00000000..e2820a6a --- /dev/null +++ b/contracts/pools/wzec/README.md @@ -0,0 +1,20 @@ +# curve-contract/contracts/pools/wzec + +[Curve wZEC pool](). This is a no-lending pool. + +## Contracts + +- [`StableSwapWZEC`](StableSwapWZEC.vy): Curve stablecoin AMM contract + +## Deployments + +- [`CurveContractV1`](../../tokens/CurveTokenV1.vy): +- [`LiquidityGauge`](../../gauges/LiquidityGauge.vy): +- [`StableSwapWZEC`](StableSwapWZEC.vy): + +## Stablecoins + +Curve wZEC pool supports swaps between the following wrapped ZEC coins: + +- `wZEC`: [0x4A64515E5E1d1073e83f30cB97BEd20400b66E10](https://etherscan.io/address/0x4A64515E5E1d1073e83f30cB97BEd20400b66E10) +- `renZEC`: [0x1c5db575e2ff833e46a2e9864c22f4b22e0b37c2](https://etherscan.io/address/0x1c5db575e2ff833e46a2e9864c22f4b22e0b37c2) diff --git a/contracts/pools/wzec/StableSwapWZEC.vy b/contracts/pools/wzec/StableSwapWZEC.vy new file mode 100644 index 00000000..3cedfdac --- /dev/null +++ b/contracts/pools/wzec/StableSwapWZEC.vy @@ -0,0 +1,883 @@ +# @version ^0.2.8 +""" +@title Curve wZEC/renZEC StableSwap +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020 - all rights reserved +""" + +from vyper.interfaces import ERC20 + +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 + token_supply: 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 + + +N_COINS: constant(int128) = 2 +PRECISION_MUL: constant(uint256[N_COINS]) = [1, 10000000000] +RATES: constant(uint256[N_COINS]) = [1000000000000000000, 10000000000000000000000000000] + +# 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 conracts of coins + @param _pool_token Address of the token representing LP share + @param _A Amplification coefficient multiplied by n * (n - 1) + @param _fee Fee to charge for exchanges + @param _admin_fee Admin fee + """ + for i in range(N_COINS): + assert _coins[i] != ZERO_ADDRESS + self.coins = _coins + self.initial_A = _A * A_PRECISION + self.future_A = _A * A_PRECISION + self.fee = _fee + self.admin_fee = _admin_fee + self.owner = _owner + self.kill_deadline = block.timestamp + KILL_DEADLINE_DT + self.lp_token = _pool_token + + +@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 _xp() -> uint256[N_COINS]: + result: uint256[N_COINS] = RATES + for i in range(N_COINS): + result[i] = result[i] * self.balances[i] / LENDING_PRECISION + return result + + +@pure +@internal +def _xp_mem(_balances: uint256[N_COINS]) -> uint256[N_COINS]: + result: uint256[N_COINS] = RATES + for i in range(N_COINS): + result[i] = result[i] * _balances[i] / PRECISION + return result + + +@pure +@internal +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(_balances: uint256[N_COINS], amp: uint256) -> uint256: + return self.get_D(self._xp_mem(_balances), 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._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() + _balances: uint256[N_COINS] = self.balances + D0: uint256 = self.get_D_mem(_balances, amp) + for i in range(N_COINS): + if is_deposit: + _balances[i] += amounts[i] + else: + _balances[i] -= amounts[i] + D1: uint256 = self.get_D_mem(_balances, amp) + token_amount: uint256 = ERC20(self.lp_token).totalSupply() + diff: uint256 = 0 + if is_deposit: + diff = D1 - D0 + else: + diff = D0 - D1 + return diff * token_amount / D0 + + +@external +@nonreentrant('lock') +def add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256) -> uint256: + """ + @notice Deposit coins into the pool + @param amounts List of amounts of coins to deposit + @param min_mint_amount Minimum amount of LP tokens to mint from the deposit + @return Amount of LP tokens received by depositing + """ + assert not self.is_killed # dev: is killed + + amp: uint256 = self._A() + + _lp_token: address = self.lp_token + token_supply: uint256 = ERC20(_lp_token).totalSupply() + # Initial invariant + D0: uint256 = 0 + old_balances: uint256[N_COINS] = self.balances + if token_supply > 0: + D0 = self.get_D_mem(old_balances, amp) + new_balances: uint256[N_COINS] = old_balances + + for i in range(N_COINS): + if token_supply == 0: + assert amounts[i] > 0 # dev: initial deposit requires all coins + # balances store amounts of c-tokens + new_balances[i] = old_balances[i] + amounts[i] + + # Invariant after change + D1: uint256 = self.get_D_mem(new_balances, amp) + assert D1 > D0 + + # We need to recalculate the invariant accounting for fees + # to calculate fair user's share + D2: uint256 = D1 + fees: uint256[N_COINS] = empty(uint256[N_COINS]) + + if token_supply > 0: + # Only account for fees if we are not the first to deposit + _fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + _admin_fee: uint256 = self.admin_fee + for i in range(N_COINS): + ideal_balance: uint256 = D1 * old_balances[i] / D0 + difference: uint256 = 0 + if ideal_balance > new_balances[i]: + difference = ideal_balance - new_balances[i] + else: + difference = new_balances[i] - ideal_balance + fees[i] = _fee * difference / FEE_DENOMINATOR + self.balances[i] = new_balances[i] - (fees[i] * _admin_fee / FEE_DENOMINATOR) + new_balances[i] -= fees[i] + D2 = self.get_D_mem(new_balances, amp) + else: + self.balances = new_balances + + # Calculate, how much pool tokens to mint + mint_amount: uint256 = 0 + if token_supply == 0: + mint_amount = D1 # Take the dust if there was any + else: + mint_amount = token_supply * (D2 - D0) / D0 + + assert mint_amount >= min_mint_amount, "Slippage screwed you" + + # Take coins from the sender + for i in range(N_COINS): + if amounts[i] > 0: + # "safeTransferFrom" which works for ERC20s which return bool or not + _response: Bytes[32] = raw_call( + self.coins[i], + concat( + method_id("transferFrom(address,address,uint256)"), + convert(msg.sender, bytes32), + convert(self, bytes32), + convert(amounts[i], bytes32), + ), + max_outsize=32, + ) # dev: failed transfer + if len(_response) > 0: + assert convert(_response, bool) + + # Mint pool tokens + CurveToken(_lp_token).mint(msg.sender, mint_amount) + + log AddLiquidity(msg.sender, amounts, fees, D1, token_supply + mint_amount) + + return mint_amount + + +@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 + + amp: uint256 = self._A() + D: uint256 = self.get_D(xp_, amp) + Ann: uint256 = amp * 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: + xp: uint256[N_COINS] = self._xp() + rates: uint256[N_COINS] = RATES + + x: uint256 = xp[i] + (dx * rates[i] / PRECISION) + y: uint256 = self.get_y(i, j, x, xp) + dy: uint256 = (xp[j] - y - 1) + _fee: uint256 = self.fee * dy / FEE_DENOMINATOR + return (dy - _fee) * PRECISION / rates[j] + + +@external +@nonreentrant('lock') +def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256: + """ + @notice Perform an exchange between two coins + @dev Index values can be found via the `coins` public getter method + @param i Index value for the coin to send + @param j Index valie of the coin to recieve + @param dx Amount of `i` being exchanged + @param min_dy Minimum amount of `j` to receive + @return Actual amount of `j` received + """ + assert not self.is_killed # dev: is killed + + old_balances: uint256[N_COINS] = self.balances + xp: uint256[N_COINS] = self._xp_mem(old_balances) + + rates: uint256[N_COINS] = RATES + x: uint256 = xp[i] + dx * rates[i] / PRECISION + y: uint256 = self.get_y(i, j, x, xp) + + dy: uint256 = xp[j] - y - 1 # -1 just in case there were some rounding errors + dy_fee: uint256 = dy * self.fee / FEE_DENOMINATOR + + # Convert all to real units + dy = (dy - dy_fee) * PRECISION / rates[j] + assert dy >= min_dy, "Exchange resulted in fewer coins than expected" + + dy_admin_fee: uint256 = dy_fee * self.admin_fee / FEE_DENOMINATOR + dy_admin_fee = dy_admin_fee * PRECISION / rates[j] + + # Change balances exactly in same way as we change actual ERC20 coin amounts + self.balances[i] = old_balances[i] + dx + # When rounding errors happen, we undercharge admin fee in favor of LP + self.balances[j] = old_balances[j] - dy - dy_admin_fee + + # "safeTransferFrom" which works for ERC20s which return bool or not + _response: Bytes[32] = raw_call( + self.coins[i], + concat( + method_id("transferFrom(address,address,uint256)"), + convert(msg.sender, bytes32), + convert(self, bytes32), + convert(dx, bytes32), + ), + max_outsize=32, + ) # dev: failed transfer + if len(_response) > 0: + assert convert(_response, bool) + + _response = raw_call( + self.coins[j], + concat( + method_id("transfer(address,uint256)"), + convert(msg.sender, bytes32), + convert(dy, bytes32), + ), + max_outsize=32, + ) # dev: failed transfer + if len(_response) > 0: + assert convert(_response, bool) + + log TokenExchange(msg.sender, i, dx, j, dy) + + return dy + + +@external +@nonreentrant('lock') +def remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS]) -> uint256[N_COINS]: + """ + @notice Withdraw coins from the pool + @dev Withdrawal amounts are based on current deposit ratios + @param _amount Quantity of LP tokens to burn in the withdrawal + @param min_amounts Minimum amounts of underlying coins to receive + @return List of amounts of coins that were withdrawn + """ + _lp_token: address = self.lp_token + total_supply: uint256 = ERC20(_lp_token).totalSupply() + amounts: uint256[N_COINS] = empty(uint256[N_COINS]) + fees: uint256[N_COINS] = empty(uint256[N_COINS]) # Fees are unused but we've got them historically in event + + for i in range(N_COINS): + value: uint256 = self.balances[i] * _amount / total_supply + assert value >= min_amounts[i], "Withdrawal resulted in fewer coins than expected" + self.balances[i] -= value + amounts[i] = value + _response: Bytes[32] = raw_call( + self.coins[i], + concat( + method_id("transfer(address,uint256)"), + convert(msg.sender, bytes32), + convert(value, bytes32), + ), + max_outsize=32, + ) # dev: failed transfer + if len(_response) > 0: + assert convert(_response, bool) + + CurveToken(_lp_token).burnFrom(msg.sender, _amount) # dev: insufficient funds + + log RemoveLiquidity(msg.sender, amounts, fees, total_supply - _amount) + + return amounts + + +@external +@nonreentrant('lock') +def remove_liquidity_imbalance(amounts: uint256[N_COINS], max_burn_amount: uint256) -> uint256: + """ + @notice Withdraw coins from the pool in an imbalanced amount + @param amounts List of amounts of underlying coins to withdraw + @param max_burn_amount Maximum amount of LP token to burn in the withdrawal + @return Actual amount of the LP token burned in the withdrawal + """ + assert not self.is_killed # dev: is killed + + amp: uint256 = self._A() + + old_balances: uint256[N_COINS] = self.balances + new_balances: uint256[N_COINS] = old_balances + D0: uint256 = self.get_D_mem(old_balances, amp) + for i in range(N_COINS): + new_balances[i] -= amounts[i] + D1: uint256 = self.get_D_mem(new_balances, amp) + + _lp_token: address = self.lp_token + token_supply: uint256 = ERC20(_lp_token).totalSupply() + assert token_supply != 0 # dev: zero total supply + + _fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + _admin_fee: uint256 = self.admin_fee + fees: uint256[N_COINS] = empty(uint256[N_COINS]) + for i in range(N_COINS): + ideal_balance: uint256 = D1 * old_balances[i] / D0 + difference: uint256 = 0 + if ideal_balance > new_balances[i]: + difference = ideal_balance - new_balances[i] + else: + difference = new_balances[i] - ideal_balance + fees[i] = _fee * difference / FEE_DENOMINATOR + self.balances[i] = new_balances[i] - (fees[i] * _admin_fee / FEE_DENOMINATOR) + new_balances[i] -= fees[i] + D2: uint256 = self.get_D_mem(new_balances, amp) + + token_amount: uint256 = (D0 - D2) * token_supply / D0 + assert token_amount != 0 # dev: zero tokens burned + token_amount += 1 # In case of rounding errors - make it unfavorable for the "attacker" + assert token_amount <= max_burn_amount, "Slippage screwed you" + + CurveToken(_lp_token).burnFrom(msg.sender, token_amount) # dev: insufficient funds + for i in range(N_COINS): + if amounts[i] != 0: + _response: Bytes[32] = raw_call( + self.coins[i], + concat( + method_id("transfer(address,uint256)"), + convert(msg.sender, bytes32), + convert(amounts[i], bytes32), + ), + max_outsize=32, + ) # dev: failed transfer + if len(_response) > 0: + assert convert(_response, bool) + + + log RemoveLiquidityImbalance(msg.sender, amounts, fees, D1, token_supply - token_amount) + + return token_amount + + +@view +@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 + x1 * (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, uint256): + # First, need to calculate + # * Get current D + # * Solve Eqn against y_i for D - _token_amount + amp: uint256 = self._A() + xp: uint256[N_COINS] = self._xp() + D0: uint256 = self.get_D(xp, amp) + + total_supply: uint256 = ERC20(self.lp_token).totalSupply() + D1: uint256 = D0 - _token_amount * D0 / total_supply + new_y: uint256 = self.get_y_D(amp, i, xp, D1) + xp_reduced: uint256[N_COINS] = xp + + _fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + for j in range(N_COINS): + dx_expected: uint256 = 0 + if j == i: + dx_expected = xp[j] * D1 / D0 - new_y + else: + dx_expected = xp[j] - xp[j] * D1 / D0 + xp_reduced[j] -= _fee * dx_expected / FEE_DENOMINATOR + + dy: uint256 = xp_reduced[i] - self.get_y_D(amp, i, xp_reduced, D1) + precisions: uint256[N_COINS] = PRECISION_MUL + dy = (dy - 1) / precisions[i] # Withdraw less to account for rounding errors + dy_0: uint256 = (xp[i] - new_y) / precisions[i] # w/o fees + + return dy, dy_0 - dy, total_supply + + +@view +@external +def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256: + """ + @notice Calculate the amount received when withdrawing a single coin + @param _token_amount Amount of LP tokens to burn in the withdrawal + @param i Index value of the coin to withdraw + @return Amount of coin received + """ + return self._calc_withdraw_one_coin(_token_amount, i)[0] + + +@external +@nonreentrant('lock') +def remove_liquidity_one_coin(_token_amount: uint256, i: int128, _min_amount: uint256) -> uint256: + """ + @notice Withdraw a single coin from the pool + @param _token_amount Amount of LP tokens to burn in the withdrawal + @param i Index value of the coin to withdraw + @param _min_amount Minimum amount of coin to receive + @return Amount of coin received + """ + assert not self.is_killed # dev: is killed + + dy: uint256 = 0 + dy_fee: uint256 = 0 + total_supply: uint256 = 0 + dy, dy_fee, total_supply = self._calc_withdraw_one_coin(_token_amount, i) + assert dy >= _min_amount, "Not enough coins removed" + + self.balances[i] -= (dy + dy_fee * self.admin_fee / FEE_DENOMINATOR) + CurveToken(self.lp_token).burnFrom(msg.sender, _token_amount) # dev: insufficient funds + + + _response: Bytes[32] = raw_call( + self.coins[i], + concat( + method_id("transfer(address,uint256)"), + convert(msg.sender, bytes32), + convert(dy, bytes32), + ), + max_outsize=32, + ) # dev: failed transfer + if len(_response) > 0: + assert convert(_response, bool) + + log RemoveLiquidityOne(msg.sender, _token_amount, dy, total_supply - _token_amount) + + return dy + + +### 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 +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 +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: + return ERC20(self.coins[i]).balanceOf(self) - self.balances[i] + + +@external +def withdraw_admin_fees(): + assert msg.sender == self.owner # dev: only owner + + for i in range(N_COINS): + coin: address = self.coins[i] + value: uint256 = ERC20(coin).balanceOf(self) - self.balances[i] + if value > 0: + _response: Bytes[32] = raw_call( + coin, + concat( + method_id("transfer(address,uint256)"), + convert(msg.sender, bytes32), + convert(value, bytes32), + ), + max_outsize=32, + ) # dev: failed transfer + if len(_response) > 0: + assert convert(_response, bool) + + + +@external +def donate_admin_fees(): + assert msg.sender == self.owner # dev: only owner + for i in range(N_COINS): + self.balances[i] = ERC20(self.coins[i]).balanceOf(self) + + +@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 diff --git a/contracts/pools/wzec/pooldata.json b/contracts/pools/wzec/pooldata.json new file mode 100644 index 00000000..c7bc1f8d --- /dev/null +++ b/contracts/pools/wzec/pooldata.json @@ -0,0 +1,29 @@ +{ + "lp_contract": "CurveTokenV3", + "lp_constructor": { + "symbol": "zecCRV", + "name": "Curve.fi wZEC/renZEC" + }, + "swap_constructor": { + "_A": 100, + "_fee": 4000000, + "_admin_fee": 0 + }, + "coins": [ + { + "decimals": 18, + "tethered": false, + "name": "wZEC", + "underlying_address": "0x4A64515E5E1d1073e83f30cB97BEd20400b66E10" + }, + { + "decimals": 8, + "tethered": false, + "name": "renZEC", + "underlying_address": "0x1c5db575e2ff833e46a2e9864c22f4b22e0b37c2" + } + ], + "testing": { + "initial_amount": 100 + } +} From 1e778faa334d68dead84cd4d24abfdbac4f9b17e Mon Sep 17 00:00:00 2001 From: samwerner Date: Thu, 21 Jan 2021 16:07:30 +0100 Subject: [PATCH 02/12] fix: update admin fee --- contracts/pools/wzec/pooldata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/pools/wzec/pooldata.json b/contracts/pools/wzec/pooldata.json index c7bc1f8d..ac4568be 100644 --- a/contracts/pools/wzec/pooldata.json +++ b/contracts/pools/wzec/pooldata.json @@ -7,7 +7,7 @@ "swap_constructor": { "_A": 100, "_fee": 4000000, - "_admin_fee": 0 + "_admin_fee": 5000000000 }, "coins": [ { From f3a08ea5b387e0b404a39862c5e42832628240a7 Mon Sep 17 00:00:00 2001 From: samwerner Date: Thu, 21 Jan 2021 21:37:55 +0100 Subject: [PATCH 03/12] fix: checksum address in pooldata.json --- contracts/pools/wzec/pooldata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/pools/wzec/pooldata.json b/contracts/pools/wzec/pooldata.json index ac4568be..d01d0253 100644 --- a/contracts/pools/wzec/pooldata.json +++ b/contracts/pools/wzec/pooldata.json @@ -20,7 +20,7 @@ "decimals": 8, "tethered": false, "name": "renZEC", - "underlying_address": "0x1c5db575e2ff833e46a2e9864c22f4b22e0b37c2" + "underlying_address": "0x1C5db575E2Ff833E46a2E9864C22F4B22E0B37C2" } ], "testing": { From f229f78b5ed5aff6c622d6923306b9a9f14e6ab0 Mon Sep 17 00:00:00 2001 From: samwerner Date: Thu, 21 Jan 2021 21:43:04 +0100 Subject: [PATCH 04/12] tests: add mint for testing for w/renZEC --- tests/fixtures/coins.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/coins.py b/tests/fixtures/coins.py index 452b497f..2af54b63 100644 --- a/tests/fixtures/coins.py +++ b/tests/fixtures/coins.py @@ -90,17 +90,23 @@ def _mint_for_testing(self, target, amount, tx=None): if self.name().startswith("Aave"): underlying = _MintableTestToken(self.UNDERLYING_ASSET_ADDRESS()) lending_pool = Contract("0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9") - underlying._mint_for_testing(target, amount) underlying.approve(lending_pool, amount, {"from": target}) lending_pool.deposit(underlying, amount, target, 0, {"from": target}) return + if self.address == "0x4A64515E5E1d1073e83f30cB97BEd20400b66E10": + # wZEC + self.mint(target, amount, {"from": "0x5Ca1262e25A5Fb6CA8d74850Da2753f0c896e16c"}) + return + if self.address == "0x1C5db575E2Ff833E46a2E9864C22F4B22E0B37C2": + # renZEC + self.mint(target, amount, {"from": "0xc3BbD5aDb611dd74eCa6123F05B18acc886e122D"}) + return for address in _holders[self.address].copy(): if address == self.address: # don't claim from the treasury - that could cause wierdness continue - balance = self.balanceOf(address) try: if amount > balance: From 906a1cc1af7729ff18b898cea9c74a4c2157f268 Mon Sep 17 00:00:00 2001 From: samwerner Date: Mon, 8 Feb 2021 23:21:42 +0100 Subject: [PATCH 05/12] fix: Resolve conflicts in tests --- tests/fixtures/coins.py | 72 ++--------------------------------------- 1 file changed, 3 insertions(+), 69 deletions(-) diff --git a/tests/fixtures/coins.py b/tests/fixtures/coins.py index 2af54b63..679e43a0 100644 --- a/tests/fixtures/coins.py +++ b/tests/fixtures/coins.py @@ -1,13 +1,8 @@ import pytest -import requests -from brownie import ETH_ADDRESS, ZERO_ADDRESS, Contract, ERC20Mock, ERC20MockNoReturn -from brownie.convert import to_address - +from brownie import ETH_ADDRESS, ZERO_ADDRESS, ERC20Mock, ERC20MockNoReturn +from brownie_tokens import MintableForkToken from conftest import WRAPPED_COIN_METHODS -_holders = {} - - # public fixtures - these can be used when testing @@ -49,7 +44,7 @@ def _mint_for_testing(target, amount, tx=None): # private API below -class _MintableTestToken(Contract): +class _MintableTestToken(MintableForkToken): def __init__(self, address, pool_data=None): super().__init__(address) @@ -60,67 +55,6 @@ def __init__(self, address, pool_data=None): if hasattr(self, attr) and target != attr: setattr(self, target, getattr(self, attr)) - # get top token holder addresses - address = self.address - if address not in _holders: - holders = requests.get( - f"https://api.ethplorer.io/getTopTokenHolders/{address}", - params={"apiKey": "freekey", "limit": 50}, - ).json() - _holders[address] = [to_address(i["address"]) for i in holders["holders"]] - - def _mint_for_testing(self, target, amount, tx=None): - if self.address == "0x674C6Ad92Fd080e4004b2312b45f796a192D27a0": - # USDN - self.deposit(target, amount, {"from": "0x90f85042533F11b362769ea9beE20334584Dcd7D"}) - return - if self.address == "0x0E2EC54fC0B509F445631Bf4b91AB8168230C752": - # LinkUSD - self.mint(target, amount, {"from": "0x62F31E08e279f3091d9755a09914DF97554eAe0b"}) - return - if self.address == "0x196f4727526eA7FB1e17b2071B3d8eAA38486988": - # RSV - self.changeMaxSupply(2 ** 128, {"from": self.owner()}) - self.mint(target, amount, {"from": self.minter()}) - return - if self.address == "0x5228a22e72ccC52d415EcFd199F99D0665E7733b": - # pBTC - self.mint(target, amount, {"from": self.pNetwork()}) - return - if self.name().startswith("Aave"): - underlying = _MintableTestToken(self.UNDERLYING_ASSET_ADDRESS()) - lending_pool = Contract("0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9") - underlying._mint_for_testing(target, amount) - underlying.approve(lending_pool, amount, {"from": target}) - lending_pool.deposit(underlying, amount, target, 0, {"from": target}) - return - if self.address == "0x4A64515E5E1d1073e83f30cB97BEd20400b66E10": - # wZEC - self.mint(target, amount, {"from": "0x5Ca1262e25A5Fb6CA8d74850Da2753f0c896e16c"}) - return - if self.address == "0x1C5db575E2Ff833E46a2E9864C22F4B22E0B37C2": - # renZEC - self.mint(target, amount, {"from": "0xc3BbD5aDb611dd74eCa6123F05B18acc886e122D"}) - return - - for address in _holders[self.address].copy(): - if address == self.address: - # don't claim from the treasury - that could cause wierdness - continue - balance = self.balanceOf(address) - try: - if amount > balance: - self.transfer(target, balance, {"from": address}) - amount -= balance - else: - self.transfer(target, amount, {"from": address}) - return - except Exception: - # sometimes tokens just don't want to be stolen - pass - - raise ValueError(f"Insufficient tokens available to mint {self.name()}") - def _deploy_wrapped(project, alice, pool_data, idx, underlying, aave_lending_pool): coin_data = pool_data["coins"][idx] From 086d7bf3b859ec60cb4da679077bc474d07e1365 Mon Sep 17 00:00:00 2001 From: samwerner Date: Sun, 24 Jan 2021 14:43:48 +0100 Subject: [PATCH 06/12] build: add brownie-token-tester --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index e36c0252..e00aa253 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ black==19.10b0 eth-brownie>=1.13.0,<2.0.0 flake8==3.7.9 isort==4.3.21 +brownie-token-tester>=0.1.0 \ No newline at end of file From 27cf0cd109bcd388b73de60d1b8b8c1b3fa6816a Mon Sep 17 00:00:00 2001 From: samwerner Date: Sun, 24 Jan 2021 16:40:02 +0100 Subject: [PATCH 07/12] docs: update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b9a07dac..9c6e3464 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Curve allows users to trade between correlated cryptocurrencies with a bespoke l * [python3](https://www.python.org/downloads/release/python-368/) from version 3.6 to 3.8, python3-dev * [brownie](https://github.com/iamdefinitelyahuman/brownie) - tested with version [1.12.0](https://github.com/eth-brownie/brownie/releases/tag/v1.12.0) * [ganache-cli](https://github.com/trufflesuite/ganache-cli) - tested with version [6.11.0](https://github.com/trufflesuite/ganache-cli/releases/tag/v6.11.0) +* [brownie-token-tester](https://github.com/iamdefinitelyahuman/brownie-token-tester) - tested with version [0.0.3](https://github.com/iamdefinitelyahuman/brownie-token-tester/releases/tag/v0.0.3) Curve contracts are compiled using [Vyper](https://github.com/vyperlang/vyper), however installation of the required Vyper versions is handled by Brownie. From d4cf1e99ca60b40de1302c88ddae3028949a07ec Mon Sep 17 00:00:00 2001 From: samwerner Date: Sun, 24 Jan 2021 17:04:16 +0100 Subject: [PATCH 08/12] build: add workflow --- .github/workflows/wzec.yaml | 119 ++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 .github/workflows/wzec.yaml diff --git a/.github/workflows/wzec.yaml b/.github/workflows/wzec.yaml new file mode 100644 index 00000000..60089058 --- /dev/null +++ b/.github/workflows/wzec.yaml @@ -0,0 +1,119 @@ +name: wzec + +on: + pull_request: + paths: + - 'tests/**/*.py' + - 'contracts/pools/wzec/**.vy' + push: + paths: + - 'tests/**/*.py' + - 'contracts/pools/wzec/**.vy' + + +env: + pool: 'wzec' + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_OPTIONS: --max_old_space_size=4096 + + +jobs: + + unitary-pool: + runs-on: ubuntu-latest + + 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/pools/common/unitary --pool ${{ env.pool }} + + integration: + runs-on: ubuntu-latest + + 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: pytest tests/pools/common/integration --pool ${{ env.pool }} + + unitary-zap: + runs-on: ubuntu-latest + + 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/zaps/common --pool ${{ env.pool }} From 0338613ef148b79f101eb7db2163deffc3e04c55 Mon Sep 17 00:00:00 2001 From: samwerner Date: Sun, 24 Jan 2021 17:13:01 +0100 Subject: [PATCH 09/12] style: fix indentation --- tests/fixtures/coins.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/fixtures/coins.py b/tests/fixtures/coins.py index 679e43a0..3b190c77 100644 --- a/tests/fixtures/coins.py +++ b/tests/fixtures/coins.py @@ -65,12 +65,10 @@ def _deploy_wrapped(project, alice, pool_data, idx, underlying, aave_lending_poo name = coin_data.get("name", f"Coin {idx}") symbol = coin_data.get("name", f"C{idx}") - if pool_data["wrapped_contract"] == "ATokenMock": - contract = deployer.deploy( - name, symbol, decimals, underlying, aave_lending_pool, {"from": alice} - ) + if pool_data['wrapped_contract'] == "ATokenMock": + contract = deployer.deploy(name, symbol, decimals, underlying, aave_lending_pool, {'from': alice}) else: - contract = deployer.deploy(name, symbol, decimals, underlying, {"from": alice}) + contract = deployer.deploy(name, symbol, decimals, underlying, {'from': alice}) for target, attr in fn_names.items(): if target != attr: From c1ee0d1732680004c5f1a1b24e0ad10e27a9cb13 Mon Sep 17 00:00:00 2001 From: samwerner Date: Mon, 8 Feb 2021 23:37:55 +0100 Subject: [PATCH 10/12] style: Correct linting --- tests/fixtures/coins.py | 8 +++++--- tests/pools/common/integration/test_curve.py | 1 - 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/fixtures/coins.py b/tests/fixtures/coins.py index 3b190c77..679e43a0 100644 --- a/tests/fixtures/coins.py +++ b/tests/fixtures/coins.py @@ -65,10 +65,12 @@ def _deploy_wrapped(project, alice, pool_data, idx, underlying, aave_lending_poo name = coin_data.get("name", f"Coin {idx}") symbol = coin_data.get("name", f"C{idx}") - if pool_data['wrapped_contract'] == "ATokenMock": - contract = deployer.deploy(name, symbol, decimals, underlying, aave_lending_pool, {'from': alice}) + if pool_data["wrapped_contract"] == "ATokenMock": + contract = deployer.deploy( + name, symbol, decimals, underlying, aave_lending_pool, {"from": alice} + ) else: - contract = deployer.deploy(name, symbol, decimals, underlying, {'from': alice}) + contract = deployer.deploy(name, symbol, decimals, underlying, {"from": alice}) for target, attr in fn_names.items(): if target != attr: diff --git a/tests/pools/common/integration/test_curve.py b/tests/pools/common/integration/test_curve.py index b42e8c2a..81d3812c 100644 --- a/tests/pools/common/integration/test_curve.py +++ b/tests/pools/common/integration/test_curve.py @@ -4,7 +4,6 @@ import pytest from brownie.test import given, strategy from hypothesis import settings - from simulation import Curve pytestmark = [ From 0cb87899639bacdcddeeced54ca4859bfb170dfa Mon Sep 17 00:00:00 2001 From: samwerner Date: Wed, 24 Feb 2021 10:27:34 +0100 Subject: [PATCH 11/12] chore: Bump isort and flake --- .pre-commit-config.yaml | 11 ++++---- README.md | 28 +++++++++---------- requirements.txt | 4 +-- .../pools/aave/integration/test_curve_aave.py | 1 - .../test_simulate_exchange_aave.py | 1 - .../integration/test_simulate_exchange.py | 1 - 6 files changed, 22 insertions(+), 24 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b4c2d335..65212923 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,14 +3,15 @@ repos: rev: 19.3b0 hooks: - id: black -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.4 hooks: - id: flake8 -- repo: https://github.com/pre-commit/mirrors-isort - rev: v4.3.21 +- repo: https://github.com/PyCQA/isort + rev: 5.7.0 hooks: - id: isort default_language_version: - python: python3.8 + python: python3.8 \ No newline at end of file diff --git a/README.md b/README.md index 9c6e3464..9eca6e55 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ Curve allows users to trade between correlated cryptocurrencies with a bespoke l ### Dependencies -* [python3](https://www.python.org/downloads/release/python-368/) from version 3.6 to 3.8, python3-dev -* [brownie](https://github.com/iamdefinitelyahuman/brownie) - tested with version [1.12.0](https://github.com/eth-brownie/brownie/releases/tag/v1.12.0) -* [ganache-cli](https://github.com/trufflesuite/ganache-cli) - tested with version [6.11.0](https://github.com/trufflesuite/ganache-cli/releases/tag/v6.11.0) -* [brownie-token-tester](https://github.com/iamdefinitelyahuman/brownie-token-tester) - tested with version [0.0.3](https://github.com/iamdefinitelyahuman/brownie-token-tester/releases/tag/v0.0.3) +- [python3](https://www.python.org/downloads/release/python-368/) from version 3.6 to 3.8, python3-dev +- [brownie](https://github.com/iamdefinitelyahuman/brownie) - tested with version [1.12.0](https://github.com/eth-brownie/brownie/releases/tag/v1.12.0) +- [ganache-cli](https://github.com/trufflesuite/ganache-cli) - tested with version [6.11.0](https://github.com/trufflesuite/ganache-cli/releases/tag/v6.11.0) +- [brownie-token-tester](https://github.com/iamdefinitelyahuman/brownie-token-tester) - tested with version [0.1.0](https://github.com/iamdefinitelyahuman/brownie-token-tester/releases/tag/v0.1.0) Curve contracts are compiled using [Vyper](https://github.com/vyperlang/vyper), however installation of the required Vyper versions is handled by Brownie. @@ -31,8 +31,8 @@ pip install -r requirements.txt ### Organization and Workflow -* New Curve pools are built from the contract templates at [`contracts/pool-templates`](contracts/pool-templates) -* Once deployed, the contracts for a pool are added to [`contracts/pools`](contracts/pools) +- New Curve pools are built from the contract templates at [`contracts/pool-templates`](contracts/pool-templates) +- Once deployed, the contracts for a pool are added to [`contracts/pools`](contracts/pools) See the documentation within [`contracts`](contracts) and it's subdirectories for more detailed information on how to get started developing on Curve. @@ -62,19 +62,19 @@ To deploy a new pool: 2. Edit the configuration settings within [`scripts/deploy.py`](scripts/deploy.py). 3. Test the deployment locally against a forked mainnet. - ```bash - brownie run deploy --network mainnet-fork -I - ``` + ```bash + brownie run deploy --network mainnet-fork -I + ``` - When the script completes it will open a console. You should call the various getter methods on the deployed contracts to ensure the pool has been configured correctly. + When the script completes it will open a console. You should call the various getter methods on the deployed contracts to ensure the pool has been configured correctly. 4. Deploy the pool to the mainnet. - ```bash - brownie run deploy --network mainnet - ``` + ```bash + brownie run deploy --network mainnet + ``` - Be sure to open a pull request that adds the deployment addresses to the pool `README.md`. + Be sure to open a pull request that adds the deployment addresses to the pool `README.md`. ## Audits and Security diff --git a/requirements.txt b/requirements.txt index e00aa253..8347bf91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ black==19.10b0 eth-brownie>=1.13.0,<2.0.0 -flake8==3.7.9 -isort==4.3.21 +flake8==3.8.4 +isort==5.7.0 brownie-token-tester>=0.1.0 \ No newline at end of file diff --git a/tests/pools/aave/integration/test_curve_aave.py b/tests/pools/aave/integration/test_curve_aave.py index 895d1a5a..3f2425f4 100644 --- a/tests/pools/aave/integration/test_curve_aave.py +++ b/tests/pools/aave/integration/test_curve_aave.py @@ -4,7 +4,6 @@ import pytest from brownie.test import given, strategy from hypothesis import settings - from simulation import Curve pytestmark = pytest.mark.skip_meta diff --git a/tests/pools/aave/integration/test_simulate_exchange_aave.py b/tests/pools/aave/integration/test_simulate_exchange_aave.py index 4e634361..f130de72 100644 --- a/tests/pools/aave/integration/test_simulate_exchange_aave.py +++ b/tests/pools/aave/integration/test_simulate_exchange_aave.py @@ -1,7 +1,6 @@ import pytest from brownie.test import given, strategy from hypothesis import settings - from simulation import Curve # do not run this test on pools without lending or meta pools diff --git a/tests/pools/common/integration/test_simulate_exchange.py b/tests/pools/common/integration/test_simulate_exchange.py index 51dfd671..757f6a50 100644 --- a/tests/pools/common/integration/test_simulate_exchange.py +++ b/tests/pools/common/integration/test_simulate_exchange.py @@ -1,7 +1,6 @@ import pytest from brownie.test import given, strategy from hypothesis import settings - from simulation import Curve # do not run this test on pools without lending or meta pools From 5d5cb900b4500cba5e3aed007e515144bf6a3a33 Mon Sep 17 00:00:00 2001 From: samwerner Date: Wed, 24 Feb 2021 11:42:23 +0100 Subject: [PATCH 12/12] refactor: Use updated base pool template --- contracts/pools/wzec/StableSwapWZEC.vy | 337 +++++++++++++------------ 1 file changed, 171 insertions(+), 166 deletions(-) diff --git a/contracts/pools/wzec/StableSwapWZEC.vy b/contracts/pools/wzec/StableSwapWZEC.vy index 3cedfdac..411420ef 100644 --- a/contracts/pools/wzec/StableSwapWZEC.vy +++ b/contracts/pools/wzec/StableSwapWZEC.vy @@ -8,6 +8,7 @@ from vyper.interfaces import ERC20 interface CurveToken: + def totalSupply() -> uint256: view def mint(_to: address, _value: uint256) -> bool: nonpayable def burnFrom(_to: address, _value: uint256) -> bool: nonpayable @@ -79,7 +80,6 @@ RATES: constant(uint256[N_COINS]) = [1000000000000000000, 1000000000000000000000 # 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 @@ -184,7 +184,7 @@ def A_precise() -> uint256: def _xp() -> uint256[N_COINS]: result: uint256[N_COINS] = RATES for i in range(N_COINS): - result[i] = result[i] * self.balances[i] / LENDING_PRECISION + result[i] = result[i] * self.balances[i] / PRECISION return result @@ -199,20 +199,29 @@ def _xp_mem(_balances: uint256[N_COINS]) -> uint256[N_COINS]: @pure @internal -def get_D(xp: uint256[N_COINS], amp: uint256) -> uint256: +def _get_D(_xp: uint256[N_COINS], _amp: uint256) -> uint256: + """ + D invariant calculation in non-overflowing integer operations + iteratively + + A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i)) + + Converging solution: + D[j+1] = (A * n**n * sum(x_i) - D[j]**(n+1) / (n**n prod(x_i))) / (A * n**n - 1) + """ S: uint256 = 0 Dprev: uint256 = 0 - for _x in xp: + for _x in _xp: S += _x if S == 0: return 0 D: uint256 = S - Ann: uint256 = amp * N_COINS + Ann: uint256 = _amp * N_COINS for _i in range(255): D_P: uint256 = D - for _x in xp: + 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) @@ -223,7 +232,6 @@ def get_D(xp: uint256[N_COINS], amp: uint256) -> uint256: 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 @@ -231,8 +239,8 @@ def get_D(xp: uint256[N_COINS], amp: uint256) -> uint256: @view @internal -def get_D_mem(_balances: uint256[N_COINS], amp: uint256) -> uint256: - return self.get_D(self._xp_mem(_balances), amp) +def _get_D_mem(_balances: uint256[N_COINS], _amp: uint256) -> uint256: + return self._get_D(self._xp_mem(_balances), _amp) @view @@ -243,7 +251,7 @@ def get_virtual_price() -> uint256: @dev Useful for calculating profits @return LP token virtual price normalized to 1e18 """ - D: uint256 = self.get_D(self._xp(), self._A()) + D: uint256 = self._get_D(self._xp(), self._A()) # D is in the units similar to DAI (e.g. converted to precision 1e18) # When balanced, D = n * x_u - total virtual value of the portfolio token_supply: uint256 = ERC20(self.lp_token).totalSupply() @@ -252,27 +260,27 @@ def get_virtual_price() -> uint256: @view @external -def calc_token_amount(amounts: uint256[N_COINS], is_deposit: bool) -> uint256: +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 + @param _amounts Amount of each coin being deposited + @param _is_deposit set True for deposits, False for withdrawals @return Expected amount of LP tokens received """ amp: uint256 = self._A() - _balances: uint256[N_COINS] = self.balances - D0: uint256 = self.get_D_mem(_balances, amp) + balances: uint256[N_COINS] = self.balances + D0: uint256 = self._get_D_mem(balances, amp) for i in range(N_COINS): - if is_deposit: - _balances[i] += amounts[i] + if _is_deposit: + balances[i] += _amounts[i] else: - _balances[i] -= amounts[i] - D1: uint256 = self.get_D_mem(_balances, amp) - token_amount: uint256 = ERC20(self.lp_token).totalSupply() + balances[i] -= _amounts[i] + D1: uint256 = self._get_D_mem(balances, amp) + token_amount: uint256 = CurveToken(self.lp_token).totalSupply() diff: uint256 = 0 - if is_deposit: + if _is_deposit: diff = D1 - D0 else: diff = D0 - D1 @@ -281,71 +289,64 @@ def calc_token_amount(amounts: uint256[N_COINS], is_deposit: bool) -> uint256: @external @nonreentrant('lock') -def add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256) -> uint256: +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 + @param _amounts List of amounts of coins to deposit + @param _min_mint_amount Minimum amount of LP tokens to mint from the deposit @return Amount of LP tokens received by depositing """ assert not self.is_killed # dev: is killed amp: uint256 = self._A() + old_balances: uint256[N_COINS] = self.balances - _lp_token: address = self.lp_token - token_supply: uint256 = ERC20(_lp_token).totalSupply() # Initial invariant - D0: uint256 = 0 - old_balances: uint256[N_COINS] = self.balances - if token_supply > 0: - D0 = self.get_D_mem(old_balances, amp) - new_balances: uint256[N_COINS] = old_balances + D0: uint256 = self._get_D_mem(old_balances, amp) + lp_token: address = self.lp_token + token_supply: uint256 = CurveToken(lp_token).totalSupply() + new_balances: uint256[N_COINS] = old_balances for i in range(N_COINS): if token_supply == 0: - assert amounts[i] > 0 # dev: initial deposit requires all coins + assert _amounts[i] > 0 # dev: initial deposit requires all coins # balances store amounts of c-tokens - new_balances[i] = old_balances[i] + amounts[i] + new_balances[i] += _amounts[i] # Invariant after change - D1: uint256 = self.get_D_mem(new_balances, amp) + D1: uint256 = self._get_D_mem(new_balances, amp) assert D1 > D0 # We need to recalculate the invariant accounting for fees # to calculate fair user's share D2: uint256 = D1 fees: uint256[N_COINS] = empty(uint256[N_COINS]) - + 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 + 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] + new_balance: uint256 = new_balances[i] + if ideal_balance > new_balance: + difference = ideal_balance - new_balance 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) + 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] -= fees[i] - D2 = self.get_D_mem(new_balances, amp) + D2 = self._get_D_mem(new_balances, amp) + mint_amount = token_supply * (D2 - D0) / D0 else: self.balances = new_balances - - # Calculate, how much pool tokens to mint - mint_amount: uint256 = 0 - if token_supply == 0: mint_amount = D1 # Take the dust if there was any - else: - mint_amount = token_supply * (D2 - D0) / D0 - - assert mint_amount >= min_mint_amount, "Slippage screwed you" + assert mint_amount >= _min_mint_amount, "Slippage screwed you" # Take coins from the sender for i in range(N_COINS): - if amounts[i] > 0: + if _amounts[i] > 0: # "safeTransferFrom" which works for ERC20s which return bool or not _response: Bytes[32] = raw_call( self.coins[i], @@ -353,24 +354,34 @@ def add_liquidity(amounts: uint256[N_COINS], min_mint_amount: uint256) -> uint25 method_id("transferFrom(address,address,uint256)"), convert(msg.sender, bytes32), convert(self, bytes32), - convert(amounts[i], bytes32), + convert(_amounts[i], bytes32), ), max_outsize=32, - ) # dev: failed transfer + ) if len(_response) > 0: - assert convert(_response, bool) + assert convert(_response, bool) # dev: failed transfer + # end "safeTransferFrom" # Mint pool tokens - CurveToken(_lp_token).mint(msg.sender, mint_amount) + CurveToken(lp_token).mint(msg.sender, mint_amount) - log AddLiquidity(msg.sender, amounts, fees, D1, token_supply + 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: +def _get_y(i: int128, j: int128, x: uint256, _xp: uint256[N_COINS]) -> uint256: + """ + Calculate x[j] if one makes x[i] = x + + Done by solving quadratic equation iteratively. + x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + b*x_1 = c + + x_1 = (x_1**2 + c) / (2*x_1 + b) + """ # x in the input is converted to the same price/precision assert i != j # dev: same coin @@ -381,11 +392,11 @@ def get_y(i: int128, j: int128, x: uint256, xp_: uint256[N_COINS]) -> uint256: assert i >= 0 assert i < N_COINS - amp: uint256 = self._A() - D: uint256 = self.get_D(xp_, amp) - Ann: uint256 = amp * N_COINS + A: uint256 = self._A() + D: uint256 = self._get_D(_xp, A) + Ann: uint256 = A * N_COINS c: uint256 = D - S_: uint256 = 0 + S: uint256 = 0 _x: uint256 = 0 y_prev: uint256 = 0 @@ -393,13 +404,13 @@ def get_y(i: int128, j: int128, x: uint256, xp_: uint256[N_COINS]) -> uint256: if _i == i: _x = x elif _i != j: - _x = xp_[_i] + _x = _xp[_i] else: continue - S_ += _x + S += _x c = c * D / (_x * N_COINS) c = c * D * A_PRECISION / (Ann * N_COINS) - b: uint256 = S_ + D * A_PRECISION / Ann # - D + b: uint256 = S + D * A_PRECISION / Ann # - D y: uint256 = D for _i in range(255): y_prev = y @@ -416,27 +427,27 @@ def get_y(i: int128, j: int128, x: uint256, xp_: uint256[N_COINS]) -> uint256: @view @external -def get_dy(i: int128, j: int128, dx: uint256) -> uint256: +def get_dy(i: int128, j: int128, _dx: uint256) -> uint256: xp: uint256[N_COINS] = self._xp() rates: uint256[N_COINS] = RATES - x: uint256 = xp[i] + (dx * rates[i] / PRECISION) - y: uint256 = self.get_y(i, j, x, xp) - dy: uint256 = (xp[j] - y - 1) - _fee: uint256 = self.fee * dy / FEE_DENOMINATOR - return (dy - _fee) * PRECISION / rates[j] + x: uint256 = xp[i] + (_dx * rates[i] / PRECISION) + y: uint256 = self._get_y(i, j, x, xp) + dy: uint256 = xp[j] - y - 1 + fee: uint256 = self.fee * dy / FEE_DENOMINATOR + return (dy - fee) * PRECISION / rates[j] @external @nonreentrant('lock') -def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256: +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 + @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 @@ -445,35 +456,34 @@ def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256: xp: uint256[N_COINS] = self._xp_mem(old_balances) rates: uint256[N_COINS] = RATES - x: uint256 = xp[i] + dx * rates[i] / PRECISION - y: uint256 = self.get_y(i, j, x, xp) + x: uint256 = xp[i] + _dx * rates[i] / PRECISION + y: uint256 = self._get_y(i, j, x, xp) dy: uint256 = xp[j] - y - 1 # -1 just in case there were some rounding errors dy_fee: uint256 = dy * self.fee / FEE_DENOMINATOR # Convert all to real units dy = (dy - dy_fee) * PRECISION / rates[j] - assert dy >= min_dy, "Exchange resulted in fewer coins than expected" + assert dy >= _min_dy, "Exchange resulted in fewer coins than expected" dy_admin_fee: uint256 = dy_fee * self.admin_fee / FEE_DENOMINATOR dy_admin_fee = dy_admin_fee * PRECISION / rates[j] # Change balances exactly in same way as we change actual ERC20 coin amounts - self.balances[i] = old_balances[i] + dx + self.balances[i] = old_balances[i] + _dx # When rounding errors happen, we undercharge admin fee in favor of LP self.balances[j] = old_balances[j] - dy - dy_admin_fee - # "safeTransferFrom" which works for ERC20s which return bool or not _response: Bytes[32] = raw_call( self.coins[i], concat( method_id("transferFrom(address,address,uint256)"), convert(msg.sender, bytes32), convert(self, bytes32), - convert(dx, bytes32), + convert(_dx, bytes32), ), max_outsize=32, - ) # dev: failed transfer + ) if len(_response) > 0: assert convert(_response, bool) @@ -485,34 +495,35 @@ def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256: convert(dy, bytes32), ), max_outsize=32, - ) # dev: failed transfer + ) if len(_response) > 0: assert convert(_response, bool) - log TokenExchange(msg.sender, i, dx, j, dy) + 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]: +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 + @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() + lp_token: address = self.lp_token + total_supply: uint256 = CurveToken(lp_token).totalSupply() amounts: uint256[N_COINS] = empty(uint256[N_COINS]) fees: uint256[N_COINS] = empty(uint256[N_COINS]) # Fees are unused but we've got them historically in event for i in range(N_COINS): - value: uint256 = self.balances[i] * _amount / total_supply - assert value >= min_amounts[i], "Withdrawal resulted in fewer coins than expected" - self.balances[i] -= value + old_balance: uint256 = self.balances[i] + value: uint256 = old_balance * _amount / total_supply + assert value >= _min_amounts[i], "Withdrawal resulted in fewer coins than expected" + self.balances[i] = old_balance - value amounts[i] = value _response: Bytes[32] = raw_call( self.coins[i], @@ -522,11 +533,11 @@ def remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS]) -> uint256 convert(value, bytes32), ), max_outsize=32, - ) # dev: failed transfer + ) if len(_response) > 0: assert convert(_response, bool) - CurveToken(_lp_token).burnFrom(msg.sender, _amount) # dev: insufficient funds + CurveToken(lp_token).burnFrom(msg.sender, _amount) # dev: insufficient funds log RemoveLiquidity(msg.sender, amounts, fees, total_supply - _amount) @@ -535,77 +546,74 @@ def remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS]) -> uint256 @external @nonreentrant('lock') -def remove_liquidity_imbalance(amounts: uint256[N_COINS], max_burn_amount: uint256) -> uint256: +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 + @param _amounts List of amounts of underlying coins to withdraw + @param _max_burn_amount Maximum amount of LP token to burn in the withdrawal @return Actual amount of the LP token burned in the withdrawal """ assert not self.is_killed # dev: is killed amp: uint256 = self._A() - old_balances: uint256[N_COINS] = self.balances + D0: uint256 = self._get_D_mem(old_balances, amp) new_balances: uint256[N_COINS] = old_balances - D0: uint256 = self.get_D_mem(old_balances, amp) for i in range(N_COINS): - new_balances[i] -= amounts[i] - D1: uint256 = self.get_D_mem(new_balances, amp) - - _lp_token: address = self.lp_token - token_supply: uint256 = ERC20(_lp_token).totalSupply() - assert token_supply != 0 # dev: zero total supply + new_balances[i] -= _amounts[i] + D1: uint256 = self._get_D_mem(new_balances, amp) - _fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) - _admin_fee: uint256 = self.admin_fee + fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + admin_fee: uint256 = self.admin_fee fees: uint256[N_COINS] = empty(uint256[N_COINS]) for i in range(N_COINS): + new_balance: uint256 = new_balances[i] ideal_balance: uint256 = D1 * old_balances[i] / D0 difference: uint256 = 0 - if ideal_balance > new_balances[i]: - difference = ideal_balance - new_balances[i] + if ideal_balance > new_balance: + difference = ideal_balance - new_balance else: - difference = new_balances[i] - ideal_balance - fees[i] = _fee * difference / FEE_DENOMINATOR - self.balances[i] = new_balances[i] - (fees[i] * _admin_fee / FEE_DENOMINATOR) - new_balances[i] -= fees[i] - D2: uint256 = self.get_D_mem(new_balances, amp) - + 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(new_balances, amp) + + lp_token: address = self.lp_token + token_supply: uint256 = CurveToken(lp_token).totalSupply() token_amount: uint256 = (D0 - D2) * token_supply / D0 assert token_amount != 0 # dev: zero tokens burned token_amount += 1 # In case of rounding errors - make it unfavorable for the "attacker" - assert token_amount <= max_burn_amount, "Slippage screwed you" + assert token_amount <= _max_burn_amount, "Slippage screwed you" - CurveToken(_lp_token).burnFrom(msg.sender, token_amount) # dev: insufficient funds + CurveToken(lp_token).burnFrom(msg.sender, token_amount) # dev: insufficient funds for i in range(N_COINS): - if amounts[i] != 0: + if _amounts[i] != 0: _response: Bytes[32] = raw_call( self.coins[i], concat( method_id("transfer(address,uint256)"), convert(msg.sender, bytes32), - convert(amounts[i], bytes32), + convert(_amounts[i], bytes32), ), max_outsize=32, - ) # dev: failed transfer + ) if len(_response) > 0: assert convert(_response, bool) - - log RemoveLiquidityImbalance(msg.sender, amounts, fees, D1, token_supply - token_amount) + log RemoveLiquidityImbalance(msg.sender, _amounts, fees, D1, token_supply - token_amount) return token_amount -@view +@pure @internal -def get_y_D(A_: uint256, i: int128, xp: uint256[N_COINS], D: uint256) -> uint256: +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 + x1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + 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) @@ -615,23 +623,23 @@ def get_y_D(A_: uint256, i: int128, xp: uint256[N_COINS], D: uint256) -> uint256 assert i >= 0 # dev: i below zero assert i < N_COINS # dev: i above N_COINS - Ann: uint256 = A_ * N_COINS + Ann: uint256 = A * N_COINS c: uint256 = D - S_: uint256 = 0 + S: uint256 = 0 _x: uint256 = 0 y_prev: uint256 = 0 for _i in range(N_COINS): if _i != i: - _x = xp[_i] + _x = _xp[_i] else: continue - S_ += _x + S += _x c = c * D / (_x * N_COINS) c = c * D * A_PRECISION / (Ann * N_COINS) - b: uint256 = S_ + D * A_PRECISION / Ann - + 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) @@ -653,23 +661,22 @@ def _calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> (uint256, uint # * Solve Eqn against y_i for D - _token_amount amp: uint256 = self._A() xp: uint256[N_COINS] = self._xp() - D0: uint256 = self.get_D(xp, amp) + D0: uint256 = self._get_D(xp, amp) - total_supply: uint256 = ERC20(self.lp_token).totalSupply() + total_supply: uint256 = CurveToken(self.lp_token).totalSupply() D1: uint256 = D0 - _token_amount * D0 / total_supply - new_y: uint256 = self.get_y_D(amp, i, xp, D1) + 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)) + fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) for j in range(N_COINS): dx_expected: uint256 = 0 if j == i: dx_expected = xp[j] * D1 / D0 - new_y else: dx_expected = xp[j] - xp[j] * D1 / D0 - xp_reduced[j] -= _fee * dx_expected / FEE_DENOMINATOR + xp_reduced[j] -= fee * dx_expected / FEE_DENOMINATOR - dy: uint256 = xp_reduced[i] - self.get_y_D(amp, i, xp_reduced, D1) + dy: uint256 = xp_reduced[i] - self._get_y_D(amp, i, xp_reduced, D1) precisions: uint256[N_COINS] = PRECISION_MUL dy = (dy - 1) / precisions[i] # Withdraw less to account for rounding errors dy_0: uint256 = (xp[i] - new_y) / precisions[i] # w/o fees @@ -710,7 +717,6 @@ def remove_liquidity_one_coin(_token_amount: uint256, i: int128, _min_amount: ui self.balances[i] -= (dy + dy_fee * self.admin_fee / FEE_DENOMINATOR) CurveToken(self.lp_token).burnFrom(msg.sender, _token_amount) # dev: insufficient funds - _response: Bytes[32] = raw_call( self.coins[i], concat( @@ -719,7 +725,7 @@ def remove_liquidity_one_coin(_token_amount: uint256, i: int128, _min_amount: ui convert(dy, bytes32), ), max_outsize=32, - ) # dev: failed transfer + ) if len(_response) > 0: assert convert(_response, bool) @@ -735,21 +741,21 @@ def ramp_A(_future_A: uint256, _future_time: uint256): 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 + 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 + 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 + assert future_A_p <= initial_A * MAX_A_CHANGE - self.initial_A = _initial_A - self.future_A = _future_A_p + 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) + log RampA(initial_A, future_A_p, block.timestamp, _future_time) @external @@ -767,18 +773,18 @@ def stop_ramp_A(): @external -def commit_new_fee(new_fee: uint256, new_admin_fee: uint256): +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 + 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 + 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) + log CommitNewFee(deadline, _new_fee, _new_admin_fee) @external @@ -788,12 +794,12 @@ def apply_new_fee(): 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 + 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) + log NewFee(fee, admin_fee) @external @@ -808,11 +814,11 @@ 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 + deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY + self.transfer_ownership_deadline = deadline self.future_owner = _owner - log CommitNewAdmin(_deadline, _owner) + log CommitNewAdmin(deadline, _owner) @external @@ -822,10 +828,10 @@ def apply_transfer_ownership(): assert self.transfer_ownership_deadline != 0 # dev: no active transfer self.transfer_ownership_deadline = 0 - _owner: address = self.future_owner - self.owner = _owner + owner: address = self.future_owner + self.owner = owner - log NewAdmin(_owner) + log NewAdmin(owner) @external @@ -862,7 +868,6 @@ def withdraw_admin_fees(): assert convert(_response, bool) - @external def donate_admin_fees(): assert msg.sender == self.owner # dev: only owner