Created
October 22, 2022 01:21
-
-
Save zed-wong/02e518c2d1e7615f45941c5742b062be to your computer and use it in GitHub Desktop.
curve-contract
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# @version 0.2.4 | |
# (c) Curve.Fi, 2020 | |
# Pool for DAI/USDC/USDT | |
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 | |
# 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 | |
# This can (and needs to) be changed at compile time | |
N_COINS: constant(int128) = 3 # <- change | |
FEE_DENOMINATOR: constant(uint256) = 10 ** 10 | |
LENDING_PRECISION: constant(uint256) = 10 ** 18 | |
PRECISION: constant(uint256) = 10 ** 18 # The precision to convert to | |
# PRECISION_MUL: constant(uint256[N_COINS]) = [1, 1, 1] | |
# RATES: constant(uint256[N_COINS]) = [10**26, 10**26, 10**26] | |
PRECISION_MUL: constant(uint256[N_COINS]) = [1, 1, 1] | |
RATES: constant(uint256[N_COINS]) = [10**18, 10**18, 10**18] | |
FEE_INDEX: constant(int128) = 2 # Which coin may potentially have fees (USDT) | |
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) | |
token: CurveToken | |
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 | |
self.future_A = _A | |
self.fee = _fee | |
self.admin_fee = _admin_fee | |
self.owner = _owner | |
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT | |
self.token = CurveToken(_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() | |
@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 | |
for _x in xp: | |
S += _x | |
if S == 0: | |
return 0 | |
Dprev: uint256 = 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 + D_P * N_COINS) * D / ((Ann - 1) * D + (N_COINS + 1) * D_P) | |
# Equality with the precision of 1 | |
if D > Dprev: | |
if D - Dprev <= 1: | |
break | |
else: | |
if Dprev - D <= 1: | |
break | |
return D | |
@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: | |
""" | |
Returns portfolio virtual price (for calculating profit) | |
scaled up by 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 = self.token.totalSupply() | |
return D * PRECISION / token_supply | |
@view | |
@external | |
def calc_token_amount(amounts: uint256[N_COINS], deposit: bool) -> uint256: | |
""" | |
Simplified method to calculate addition or reduction in token supply at | |
deposit or withdrawal without taking fees into account (but looking at | |
slippage). | |
Needed to prevent front-running, not for precise calculations! | |
""" | |
_balances: uint256[N_COINS] = self.balances | |
amp: uint256 = self._A() | |
D0: uint256 = self.get_D_mem(_balances, amp) | |
for i in range(N_COINS): | |
if deposit: | |
_balances[i] += amounts[i] | |
else: | |
_balances[i] -= amounts[i] | |
D1: uint256 = self.get_D_mem(_balances, amp) | |
token_amount: uint256 = self.token.totalSupply() | |
diff: uint256 = 0 | |
if 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): | |
assert not self.is_killed # dev: is killed | |
fees: uint256[N_COINS] = empty(uint256[N_COINS]) | |
_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) | |
_admin_fee: uint256 = self.admin_fee | |
amp: uint256 = self._A() | |
token_supply: uint256 = self.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): | |
in_amount: uint256 = amounts[i] | |
if token_supply == 0: | |
assert in_amount > 0 # dev: initial deposit requires all coins | |
in_coin: address = self.coins[i] | |
# Take coins from the sender | |
if in_amount > 0: | |
if i == FEE_INDEX: | |
in_amount = ERC20(in_coin).balanceOf(self) | |
# "safeTransferFrom" which works for ERC20s which return bool or not | |
_response: Bytes[32] = raw_call( | |
in_coin, | |
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) # dev: failed transfer | |
if i == FEE_INDEX: | |
in_amount = ERC20(in_coin).balanceOf(self) - in_amount | |
new_balances[i] = old_balances[i] + in_amount | |
# 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 | |
if token_supply > 0: | |
# Only account for fees if we are not the first to deposit | |
for i in range(N_COINS): | |
ideal_balance: uint256 = D1 * old_balances[i] / D0 | |
difference: uint256 = 0 | |
if ideal_balance > new_balances[i]: | |
difference = ideal_balance - new_balances[i] | |
else: | |
difference = new_balances[i] - ideal_balance | |
fees[i] = _fee * difference / FEE_DENOMINATOR | |
self.balances[i] = new_balances[i] - (fees[i] * _admin_fee / FEE_DENOMINATOR) | |
new_balances[i] -= fees[i] | |
D2 = self.get_D_mem(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" | |
# Mint pool tokens | |
self.token.mint(msg.sender, mint_amount) | |
log AddLiquidity(msg.sender, amounts, fees, D1, token_supply + 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) | |
c: uint256 = D | |
S_: uint256 = 0 | |
Ann: uint256 = amp * N_COINS | |
_x: 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 / (Ann * N_COINS) | |
b: uint256 = S_ + D / Ann # - D | |
y_prev: uint256 = 0 | |
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: | |
break | |
else: | |
if y_prev - y <= 1: | |
break | |
return y | |
@view | |
@external | |
def get_dy(i: int128, j: int128, dx: uint256) -> uint256: | |
# dx and dy in c-units | |
rates: uint256[N_COINS] = RATES | |
xp: uint256[N_COINS] = self._xp() | |
x: uint256 = xp[i] + (dx * rates[i] / PRECISION) | |
y: uint256 = self.get_y(i, j, x, xp) | |
dy: uint256 = (xp[j] - y - 1) * PRECISION / rates[j] | |
_fee: uint256 = self.fee * dy / FEE_DENOMINATOR | |
return dy - _fee | |
@view | |
@external | |
def get_dy_underlying(i: int128, j: int128, dx: uint256) -> uint256: | |
# dx and dy in underlying units | |
xp: uint256[N_COINS] = self._xp() | |
precisions: uint256[N_COINS] = PRECISION_MUL | |
x: uint256 = xp[i] + dx * precisions[i] | |
y: uint256 = self.get_y(i, j, x, xp) | |
dy: uint256 = (xp[j] - y - 1) / precisions[j] | |
_fee: uint256 = self.fee * dy / FEE_DENOMINATOR | |
return dy - _fee | |
@external | |
@nonreentrant('lock') | |
def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256): | |
assert not self.is_killed # dev: is killed | |
rates: uint256[N_COINS] = RATES | |
old_balances: uint256[N_COINS] = self.balances | |
xp: uint256[N_COINS] = self._xp_mem(old_balances) | |
# Handling an unexpected charge of a fee on transfer (USDT, PAXG) | |
dx_w_fee: uint256 = dx | |
input_coin: address = self.coins[i] | |
if i == FEE_INDEX: | |
dx_w_fee = ERC20(input_coin).balanceOf(self) | |
# "safeTransferFrom" which works for ERC20s which return bool or not | |
_response: Bytes[32] = raw_call( | |
input_coin, | |
concat( | |
method_id("transferFrom(address,address,uint256)"), | |
convert(msg.sender, bytes32), | |
convert(self, bytes32), | |
convert(dx, bytes32), | |
), | |
max_outsize=32, | |
) # dev: failed transfer | |
if len(_response) > 0: | |
assert convert(_response, bool) # dev: failed transfer | |
if i == FEE_INDEX: | |
dx_w_fee = ERC20(input_coin).balanceOf(self) - dx_w_fee | |
x: uint256 = xp[i] + dx_w_fee * 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_w_fee | |
# When rounding errors happen, we undercharge admin fee in favor of LP | |
self.balances[j] = old_balances[j] - dy - dy_admin_fee | |
# "safeTransfer" which works for ERC20s which return bool or not | |
_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) # dev: failed transfer | |
log TokenExchange(msg.sender, i, dx, j, dy) | |
@external | |
@nonreentrant('lock') | |
def remove_liquidity(_amount: uint256, min_amounts: uint256[N_COINS]): | |
total_supply: uint256 = self.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 | |
# "safeTransfer" which works for ERC20s which return bool or not | |
_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) # dev: failed transfer | |
self.token.burnFrom(msg.sender, _amount) # dev: insufficient funds | |
log RemoveLiquidity(msg.sender, amounts, fees, total_supply - _amount) | |
@external | |
@nonreentrant('lock') | |
def remove_liquidity_imbalance(amounts: uint256[N_COINS], max_burn_amount: uint256): | |
assert not self.is_killed # dev: is killed | |
token_supply: uint256 = self.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 | |
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) | |
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" | |
self.token.burnFrom(msg.sender, token_amount) # dev: insufficient funds | |
for i in range(N_COINS): | |
if amounts[i] != 0: | |
# "safeTransfer" which works for ERC20s which return bool or not | |
_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) # dev: failed transfer | |
log RemoveLiquidityImbalance(msg.sender, amounts, fees, D1, token_supply - 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 | |
c: uint256 = D | |
S_: uint256 = 0 | |
Ann: uint256 = A_ * N_COINS | |
_x: 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 / (Ann * N_COINS) | |
b: uint256 = S_ + D / Ann | |
y_prev: uint256 = 0 | |
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: | |
break | |
else: | |
if y_prev - y <= 1: | |
break | |
return y | |
@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() | |
_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) | |
precisions: uint256[N_COINS] = PRECISION_MUL | |
total_supply: uint256 = self.token.totalSupply() | |
xp: uint256[N_COINS] = self._xp() | |
D0: uint256 = self.get_D(xp, amp) | |
D1: uint256 = D0 - _token_amount * D0 / total_supply | |
xp_reduced: uint256[N_COINS] = xp | |
new_y: uint256 = self.get_y_D(amp, i, xp, D1) | |
dy_0: uint256 = (xp[i] - new_y) / precisions[i] # w/o fees | |
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) | |
dy = (dy - 1) / precisions[i] # Withdraw less to account for rounding errors | |
return dy, dy_0 - dy | |
@view | |
@external | |
def calc_withdraw_one_coin(_token_amount: uint256, i: int128) -> uint256: | |
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): | |
""" | |
Remove _amount of liquidity all in a form of coin i | |
""" | |
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) | |
self.token.burnFrom(msg.sender, _token_amount) # dev: insufficient funds | |
# "safeTransfer" which works for ERC20s which return bool or not | |
_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) # dev: failed transfer | |
log RemoveLiquidityOne(msg.sender, _token_amount, 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() | |
assert (_future_A > 0) and (_future_A < MAX_A) | |
assert ((_future_A >= _initial_A) and (_future_A <= _initial_A * MAX_A_CHANGE)) or\ | |
((_future_A < _initial_A) and (_future_A * MAX_A_CHANGE >= _initial_A)) | |
self.initial_A = _initial_A | |
self.future_A = _future_A | |
self.initial_A_time = block.timestamp | |
self.future_A_time = _future_time | |
log RampA(_initial_A, _future_A, 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): | |
c: address = self.coins[i] | |
value: uint256 = ERC20(c).balanceOf(self) - self.balances[i] | |
if value > 0: | |
# "safeTransfer" which works for ERC20s which return bool or not | |
_response: Bytes[32] = raw_call( | |
c, | |
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) # dev: failed transfer | |
@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 |
deploy.py:
import json
import os
from brownie import accounts
from brownie import network
from brownie.network.gas.strategies import GasNowScalingStrategy
from brownie.project import load as load_project
from brownie.project.main import get_loaded_projects
# set a throwaway admin account here
DEPLOYER = accounts.add(os.environ.get('PRIVATE_KEY'))
REQUIRED_CONFIRMATIONS = 1
# deployment settings
# most settings are taken from `contracts/pools/{POOL_NAME}/pooldata.json`
POOL_NAME = "3pool"
# temporary owner address
POOL_OWNER = "0xaaC335113BC3e4391b7cbE7809b1A609476ee469"
GAUGE_OWNER = "0xaaC335113BC3e4391b7cbE7809b1A609476ee469"
MINTER = "0xd061D61a4d941c39E5453435B6345Dc261C2fcE0"
# POOL_OWNER = "0xeCb456EA5365865EbAb8a2661B0c503410e9B347" # PoolProxy
# GAUGE_OWNER = "0x519AFB566c05E00cfB9af73496D00217A630e4D5" # GaugeProxy
def _tx_params():
return {
"from": DEPLOYER,
"required_confs": REQUIRED_CONFIRMATIONS,
}
def main():
project = get_loaded_projects()[0]
balance = DEPLOYER.balance()
print("params:",_tx_params())
# load data about the deployment from `pooldata.json`
contracts_path = project._path.joinpath("contracts/pools")
with contracts_path.joinpath(f"{POOL_NAME}/pooldata.json").open() as fp:
pool_data = json.load(fp)
swap_name = next(i.stem for i in contracts_path.glob(f"{POOL_NAME}/StableSwap*"))
swap_deployer = getattr(project, swap_name)
token_deployer = getattr(project, pool_data.get("lp_contract"))
underlying_coins = [i["underlying_address"] for i in pool_data["coins"]]
wrapped_coins = [i.get("wrapped_address", i["underlying_address"]) for i in pool_data["coins"]]
base_pool = None
if "base_pool" in pool_data:
with contracts_path.joinpath(f"{pool_data['base_pool']}/pooldata.json").open() as fp:
base_pool_data = json.load(fp)
base_pool = base_pool_data["swap_address"]
# deploy the token
token_args = pool_data["lp_constructor"]
token = token_deployer.deploy(token_args["name"], token_args["symbol"], 18, 10**18, _tx_params())
# deploy the pool
abi = next(i["inputs"] for i in swap_deployer.abi if i["type"] == "constructor")
args = pool_data["swap_constructor"]
args.update(
_coins=wrapped_coins,
_underlying_coins=underlying_coins,
_pool_token=token,
_base_pool=base_pool,
_owner=POOL_OWNER,
)
deployment_args = [args[i["name"]] for i in abi] + [_tx_params()]
swap = swap_deployer.deploy(*deployment_args)
# set the minter
token.set_minter(swap, _tx_params())
# deploy the liquidity gauge
"""
LiquidityGaugeV3 = load_project("curvefi/[email protected]").LiquidityGaugeV3
LiquidityGaugeV3.deploy(token, MINTER, GAUGE_OWNER, _tx_params())
# deploy the zap
zap_name = next((i.stem for i in contracts_path.glob(f"{POOL_NAME}/Deposit*")), None)
if zap_name is not None:
zap_deployer = getattr(project, zap_name)
abi = next(i["inputs"] for i in zap_deployer.abi if i["type"] == "constructor")
args = {
"_coins": wrapped_coins,
"_underlying_coins": underlying_coins,
"_token": token,
"_pool": swap,
"_curve": swap,
}
deployment_args = [args[i["name"]] for i in abi] + [_tx_params()]
zap_deployer.deploy(*deployment_args)
# deploy the rate calculator
rate_calc_name = next(
(i.stem for i in contracts_path.glob(f"{POOL_NAME}/RateCalculator*")), None
)
if rate_calc_name is not None:
rate_calc_deployer = getattr(project, rate_calc_name)
rate_calc_deployer.deploy(_tx_params())
"""
print(f"Gas used in deployment: {(balance - DEPLOYER.balance()) / 1e18:.4f} ETH")
deploy.py return:
Transaction sent: 0xd15bbd484cb3282c5d1eff925d4605de4d829547b4207918c33f03c2c138a32c
Gas price: 0.002905991 gwei Gas limit: 714632 Nonce: 187
CurveTokenV2.constructor confirmed Block: 7811415 Gas used: 649666 (90.91%)
CurveTokenV2 deployed at: 0xf4A1B12C4fca20f0735F80Cc326937347a08DD81
Transaction sent: 0x0d758ed14ba6734ee152e53444f264625ba235c8b641b224520200845a2b28b8
Gas price: 0.002905989 gwei Gas limit: 5324650 Nonce: 188
StableSwap3Pool.constructor confirmed Block: 7811417 Gas used: 4840591 (90.91%)
StableSwap3Pool deployed at: 0x887b2e8fdc5A7AE62E3a962AB174862C8c5e752b
Transaction sent: 0x0bec8230ded2511f4f12aef207023ca663edd55309fcd884b3379f057dfa745c
Gas price: 0.00290599 gwei Gas limit: 29236 Nonce: 189
CurveTokenV2.set_minter confirmed Block: 7811419 Gas used: 26579 (90.91%)
Gas used in deployment: 0.0000 ETH
Interact contract with brownie:
>>> ad = '0x887b2e8fdc5A7AE62E3a962AB174862C8c5e752b'
am = 10**18
accounts.add('my account')
WETH=Contract.from_abi("WETH","0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",ERC20Mock.abi)
UNI=Contract.from_abi("UNI","0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",ERC20Mock.abi)
DAI=Contract.from_abi("DAI","0x11fE4B6AE13d2a6055C8D9cF65c55bac32B5d844",ERC20Mock.abi)
WETH.approve(ad,am,{'from':accounts[0]})
DAI.approve(ad,am,{'from':accounts[0]})
UNI.approve(ad,am,{'from':accounts[0]})
c=StableSwap3Pool.at(ad)
c.add_liquidity([am,am,am], 1, {'from':accounts[0]})
Transaction sent: 0xfccd1d9797a5d3e57ece3bcee8e25b85c148fc6fc28b557b48156484b828a9df
Gas price: 0.002905988 gwei Gas limit: 50695 Nonce: 190
ERC20Mock.approve confirmed Block: 7811431 Gas used: 46087 (90.91%)
Transaction sent: 0x792dd94117862c87e130725cf74ac292b55b1c232c3699e08cb7bdc99f6e7292
Gas price: 0.002905989 gwei Gas limit: 50760 Nonce: 191
DAI.approve confirmed Block: 7811432 Gas used: 46146 (90.91%)
Transaction sent: 0xe46baec11c7d9ecbd81c782ba60ad4003daeaf303fec25c8611aa73e9630d596
Gas price: 0.002905989 gwei Gas limit: 51548 Nonce: 192
ERC20Mock.approve confirmed Block: 7811433 Gas used: 46862 (90.91%)
File "<console>", line 15, in <module>
File "brownie/network/contract.py", line 1861, in __call__
return self.transact(*args)
File "brownie/network/contract.py", line 1734, in transact
return tx["from"].transfer(
File "brownie/network/account.py", line 644, in transfer
receipt, exc = self._make_transaction(
File "brownie/network/account.py", line 727, in _make_transaction
raise VirtualMachineError(e) from None
File "brownie/exceptions.py", line 93, in __init__
raise ValueError(str(exc)) from None
ValueError: Gas estimation failed: 'execution reverted'. This transaction will likely revert. If you wish to broadcast, you must set the gas limit manually.
>>> c.add_liquidity([am,am,am], 1, {'from':accounts[0],'gas_limit':300000,'allow_revert':True})
Transaction sent: 0x5ee5b5a2b33275856954c08ca8535761b271de790ce4d8d044fce408f13739f1
Gas price: 0.00290599 gwei Gas limit: 300000 Nonce: 193
StableSwap3Pool.add_liquidity confirmed (reverted) Block: 7811434 Gas used: 232423 (77.47%)
File "<console>", line 1, in <module>
File "brownie/network/contract.py", line 1861, in __call__
return self.transact(*args)
File "brownie/network/contract.py", line 1734, in transact
return tx["from"].transfer(
File "brownie/network/account.py", line 644, in transfer
receipt, exc = self._make_transaction(
File "brownie/network/account.py", line 780, in _make_transaction
f"VM Exception while processing transaction: revert {receipt.revert_msg}"
File "brownie/network/transaction.py", line 54, in wrapper
raise RPCRequestError(
RPCRequestError: Accessing `TransactionReceipt.revert_msg` on a reverted transaction requires the `debug_traceTransaction` RPC endpoint, but the node client does not support it or has not made it available.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
pooldata.json: