Skip to content

Commit

Permalink
Merge pull request #100 from curvefi/feat/gauge-v2
Browse files Browse the repository at this point in the history
Gauge Improvements: LiquidityGaugeV3 and RewardsOnlyGaugeV1.1
  • Loading branch information
iamdefinitelyahuman authored May 22, 2021
2 parents b4e1390 + ce5af0c commit ce5487b
Show file tree
Hide file tree
Showing 35 changed files with 3,257 additions and 393 deletions.
803 changes: 803 additions & 0 deletions contracts/gauges/LiquidityGaugeV3.vy

Large diffs are not rendered by default.

255 changes: 145 additions & 110 deletions contracts/gauges/RewardsOnlyGauge.vy

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ def gauge_v2(LiquidityGaugeV2, alice, mock_lp_token, minter):
yield LiquidityGaugeV2.deploy(mock_lp_token, minter, alice, {"from": alice})


@pytest.fixture(scope="module")
def gauge_v3(LiquidityGaugeV3, alice, mock_lp_token, minter):
yield LiquidityGaugeV3.deploy(mock_lp_token, minter, alice, {"from": alice})


@pytest.fixture(scope="module")
def rewards_only_gauge(RewardsOnlyGauge, alice, mock_lp_token):
yield RewardsOnlyGauge.deploy(mock_lp_token, alice, {"from": alice})
Expand Down
111 changes: 111 additions & 0 deletions tests/integration/LiquidityGaugeV3/test_deposits_withdrawals_v3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import brownie
from brownie import chain
from brownie.test import strategy


class StateMachine:
"""
Validate that deposits and withdrawals work correctly over time.
Strategies
----------
st_account : Account
Account to perform deposit or withdrawal from
st_value : int
Amount to deposit or withdraw
st_time : int
Amount of time to advance the clock
"""

st_account = strategy("address", length=5)
st_value = strategy("uint64")
st_time = strategy("uint", max_value=86400 * 365)

def __init__(self, accounts, gauge_v3, mock_lp_token):
self.accounts = accounts
self.token = mock_lp_token
self.gauge_v3 = gauge_v3

def setup(self):
self.balances = {i: 0 for i in self.accounts}

def rule_deposit(self, st_account, st_value):
"""
Make a deposit into the `LiquidityGauge` contract.
Because of the upper bound of `st_value` relative to the initial account
balances, this rule should never fail.
"""
balance = self.token.balanceOf(st_account)

self.gauge_v3.deposit(st_value, {"from": st_account})
self.balances[st_account] += st_value

assert self.token.balanceOf(st_account) == balance - st_value

def rule_withdraw(self, st_account, st_value):
"""
Attempt to withdraw from the `LiquidityGauge` contract.
"""
if self.balances[st_account] < st_value:
# fail path - insufficient balance
with brownie.reverts():
self.gauge_v3.withdraw(st_value, {"from": st_account})
return

# success path
balance = self.token.balanceOf(st_account)
self.gauge_v3.withdraw(st_value, {"from": st_account})
self.balances[st_account] -= st_value

assert self.token.balanceOf(st_account) == balance + st_value

def rule_advance_time(self, st_time):
"""
Advance the clock.
"""
chain.sleep(st_time)

def rule_checkpoint(self, st_account):
"""
Create a new user checkpoint.
"""
self.gauge_v3.user_checkpoint(st_account, {"from": st_account})

def invariant_balances(self):
"""
Validate expected balances against actual balances.
"""
for account, balance in self.balances.items():
assert self.gauge_v3.balanceOf(account) == balance

def invariant_total_supply(self):
"""
Validate expected total supply against actual total supply.
"""
assert self.gauge_v3.totalSupply() == sum(self.balances.values())

def teardown(self):
"""
Final check to ensure that all balances may be withdrawn.
"""
for account, balance in ((k, v) for k, v in self.balances.items() if v):
initial = self.token.balanceOf(account)
self.gauge_v3.withdraw(balance, {"from": account})

assert self.token.balanceOf(account) == initial + balance


def test_state_machine(state_machine, accounts, gauge_v3, mock_lp_token, no_call_coverage):
# fund accounts to be used in the test
for acct in accounts[1:5]:
mock_lp_token.transfer(acct, 10 ** 21, {"from": accounts[0]})

# approve gauge_v3 from the funded accounts
for acct in accounts[:5]:
mock_lp_token.approve(gauge_v3, 2 ** 256 - 1, {"from": acct})

# because this is a simple state machine, we use more steps than normal
settings = {"stateful_step_count": 25, "max_examples": 30}

state_machine(StateMachine, accounts[:5], gauge_v3, mock_lp_token, settings=settings)
204 changes: 204 additions & 0 deletions tests/integration/LiquidityGaugeV3/test_liquidity_gauge_v3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
from random import random, randrange

from tests.conftest import YEAR, approx

MAX_UINT256 = 2 ** 256 - 1
WEEK = 7 * 86400


def test_gauge_integral(accounts, chain, mock_lp_token, token, gauge_v3, gauge_controller):
alice, bob = accounts[:2]

# Wire up Gauge to the controller to have proper rates and stuff
gauge_controller.add_type(b"Liquidity", {"from": alice})
gauge_controller.change_type_weight(0, 10 ** 18, {"from": alice})
gauge_controller.add_gauge(gauge_v3.address, 0, 10 ** 18, {"from": alice})

alice_staked = 0
bob_staked = 0
integral = 0 # ∫(balance * rate(t) / totalSupply(t) dt)
checkpoint = chain[-1].timestamp
checkpoint_rate = token.rate()
checkpoint_supply = 0
checkpoint_balance = 0

# Let Alice and Bob have about the same token amount
mock_lp_token.transfer(bob, mock_lp_token.balanceOf(alice) // 2, {"from": alice})

def update_integral():
nonlocal checkpoint, checkpoint_rate, integral, checkpoint_balance, checkpoint_supply

t1 = chain[-1].timestamp
rate1 = token.rate()
t_epoch = token.start_epoch_time()
if checkpoint >= t_epoch:
rate_x_time = (t1 - checkpoint) * rate1
else:
rate_x_time = (t_epoch - checkpoint) * checkpoint_rate + (t1 - t_epoch) * rate1
if checkpoint_supply > 0:
integral += rate_x_time * checkpoint_balance // checkpoint_supply
checkpoint_rate = rate1
checkpoint = t1
checkpoint_supply = gauge_v3.totalSupply()
checkpoint_balance = gauge_v3.balanceOf(alice)

# Now let's have a loop where Bob always deposit or withdraws,
# and Alice does so more rarely
for i in range(40):
is_alice = random() < 0.2
dt = randrange(1, YEAR // 5)
chain.sleep(dt)
chain.mine()

# For Bob
is_withdraw = (i > 0) * (random() < 0.5)
print("Bob", "withdraws" if is_withdraw else "deposits")
if is_withdraw:
amount = randrange(1, gauge_v3.balanceOf(bob) + 1)
gauge_v3.withdraw(amount, {"from": bob})
update_integral()
bob_staked -= amount
else:
amount = randrange(1, mock_lp_token.balanceOf(bob) // 10 + 1)
mock_lp_token.approve(gauge_v3.address, amount, {"from": bob})
gauge_v3.deposit(amount, {"from": bob})
update_integral()
bob_staked += amount

if is_alice:
# For Alice
is_withdraw_alice = (gauge_v3.balanceOf(alice) > 0) * (random() < 0.5)
print("Alice", "withdraws" if is_withdraw_alice else "deposits")

if is_withdraw_alice:
amount_alice = randrange(1, gauge_v3.balanceOf(alice) // 10 + 1)
gauge_v3.withdraw(amount_alice, {"from": alice})
update_integral()
alice_staked -= amount_alice
else:
amount_alice = randrange(1, mock_lp_token.balanceOf(alice) + 1)
mock_lp_token.approve(gauge_v3.address, amount_alice, {"from": alice})
gauge_v3.deposit(amount_alice, {"from": alice})
update_integral()
alice_staked += amount_alice

# Checking that updating the checkpoint in the same second does nothing
# Also everyone can update: that should make no difference, too
if random() < 0.5:
gauge_v3.user_checkpoint(alice, {"from": alice})
if random() < 0.5:
gauge_v3.user_checkpoint(bob, {"from": bob})

assert gauge_v3.balanceOf(alice) == alice_staked
assert gauge_v3.balanceOf(bob) == bob_staked
assert gauge_v3.totalSupply() == alice_staked + bob_staked

dt = randrange(1, YEAR // 20)
chain.sleep(dt)
chain.mine()

gauge_v3.user_checkpoint(alice, {"from": alice})
update_integral()
print(i, dt / 86400, integral, gauge_v3.integrate_fraction(alice))
assert approx(gauge_v3.integrate_fraction(alice), integral, 1e-15)


def test_mining_with_votelock(
accounts, chain, history, mock_lp_token, token, gauge_v3, gauge_controller, voting_escrow,
):
alice, bob = accounts[:2]
chain.sleep(2 * WEEK + 5)

# Wire up Gauge to the controller to have proper rates and stuff
gauge_controller.add_type(b"Liquidity", {"from": alice})
gauge_controller.change_type_weight(0, 10 ** 18, {"from": alice})
gauge_controller.add_gauge(gauge_v3.address, 0, 10 ** 18, {"from": alice})

# Prepare tokens
token.transfer(bob, 10 ** 20, {"from": alice})
token.approve(voting_escrow, MAX_UINT256, {"from": alice})
token.approve(voting_escrow, MAX_UINT256, {"from": bob})
mock_lp_token.transfer(bob, mock_lp_token.balanceOf(alice) // 2, {"from": alice})
mock_lp_token.approve(gauge_v3.address, MAX_UINT256, {"from": alice})
mock_lp_token.approve(gauge_v3.address, MAX_UINT256, {"from": bob})

# Alice deposits to escrow. She now has a BOOST
t = chain[-1].timestamp
voting_escrow.create_lock(10 ** 20, t + 2 * WEEK, {"from": alice})

# Alice and Bob deposit some liquidity
gauge_v3.deposit(10 ** 21, {"from": alice})
gauge_v3.deposit(10 ** 21, {"from": bob})

# Time travel and checkpoint
chain.sleep(4 * WEEK)
alice.transfer(alice, 1)
while True:
gauge_v3.user_checkpoint(alice, {"from": alice})
gauge_v3.user_checkpoint(bob, {"from": bob})
if chain[-1].timestamp != chain[-2].timestamp:
chain.undo(2)
else:
break

# 4 weeks down the road, balanceOf must be 0
assert voting_escrow.balanceOf(alice) == 0
assert voting_escrow.balanceOf(bob) == 0

# Alice earned 2.5 times more CRV because she vote-locked her CRV
rewards_alice = gauge_v3.integrate_fraction(alice)
rewards_bob = gauge_v3.integrate_fraction(bob)
assert approx(rewards_alice / rewards_bob, 2.5, 1e-5)

# Time travel / checkpoint: no one has CRV vote-locked
chain.sleep(4 * WEEK)
alice.transfer(alice, 1)
voting_escrow.withdraw({"from": alice})
while True:
gauge_v3.user_checkpoint(alice, {"from": alice})
gauge_v3.user_checkpoint(bob, {"from": bob})
if chain[-1].timestamp != chain[-2].timestamp:
chain.undo(2)
else:
break
old_rewards_alice = rewards_alice
old_rewards_bob = rewards_bob

# Alice earned the same as Bob now
rewards_alice = gauge_v3.integrate_fraction(alice)
rewards_bob = gauge_v3.integrate_fraction(bob)
d_alice = rewards_alice - old_rewards_alice
d_bob = rewards_bob - old_rewards_bob
assert d_alice == d_bob

# Both Alice and Bob votelock
while True:
t = chain[-1].timestamp
voting_escrow.create_lock(10 ** 20, t + 2 * WEEK, {"from": alice})
voting_escrow.create_lock(10 ** 20, t + 2 * WEEK, {"from": bob})
if chain[-1].timestamp != chain[-2].timestamp:
chain.undo(2)
else:
break

# Time travel / checkpoint: no one has CRV vote-locked
chain.sleep(4 * WEEK)
alice.transfer(alice, 1)
voting_escrow.withdraw({"from": alice})
voting_escrow.withdraw({"from": bob})
while True:
gauge_v3.user_checkpoint(alice, {"from": alice})
gauge_v3.user_checkpoint(bob, {"from": bob})
if chain[-1].timestamp != chain[-2].timestamp:
chain.undo(2)
else:
break
old_rewards_alice = rewards_alice
old_rewards_bob = rewards_bob

# Alice earned the same as Bob now
rewards_alice = gauge_v3.integrate_fraction(alice)
rewards_bob = gauge_v3.integrate_fraction(bob)
d_alice = rewards_alice - old_rewards_alice
d_bob = rewards_bob - old_rewards_bob
assert d_alice == d_bob
Loading

0 comments on commit ce5487b

Please sign in to comment.