Skip to content

Instantly share code, notes, and snippets.

@zed-wong
Created October 22, 2022 01:21
Show Gist options
  • Save zed-wong/02e518c2d1e7615f45941c5742b062be to your computer and use it in GitHub Desktop.
Save zed-wong/02e518c2d1e7615f45941c5742b062be to your computer and use it in GitHub Desktop.
curve-contract
# @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
@zed-wong
Copy link
Author

pooldata.json:

{
    "lp_contract": "CurveTokenV2",
    "swap_address": "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7",
    "lp_token_address": "0xA9DAe48612D679fCC727B885238a37551B6fa00b",
    "gauge_addresses": ["0xbFcF63294aD7105dEa65aA58F8AE5BE2D9d0952A"],
    "lp_constructor": {
        "name": "Satoshi1",
        "symbol": "imsatoshi1"
    },
    "swap_constructor": {
        "_A": 100,
        "_fee": 4000000,
        "_admin_fee": 5000000000
    },
    "coins": [
        {
            "name": "WETH",
            "decimals": 18,
            "tethered": false,
            "underlying_address": "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6"
        },
        {
            "name": "UNI",
            "decimals": 18,
            "tethered": false,
            "underlying_address": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"

        },
        {
            "name": "DAI",
            "decimals": 18,
            "tethered": false,
            "underlying_address": "0x11fE4B6AE13d2a6055C8D9cF65c55bac32B5d844"
        }
    ]
}

@zed-wong
Copy link
Author

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")

@zed-wong
Copy link
Author

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

@zed-wong
Copy link
Author

zed-wong commented Oct 22, 2022

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.

@zed-wong
Copy link
Author

zed-wong commented Oct 22, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment