Skip to content

Commit

Permalink
Merge pull request #3 from vefunder/dev
Browse files Browse the repository at this point in the history
fix: emissions calculation given a maximum emissions and deadline, custom fallback receiver
  • Loading branch information
bout3fiddy authored Mar 10, 2022
2 parents 47e1922 + 7336096 commit 74d51bc
Show file tree
Hide file tree
Showing 8 changed files with 454 additions and 62 deletions.
6 changes: 3 additions & 3 deletions brownie-config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
networks:
default: mainnet-fork

autofetch_sources: true

dependencies:
- curvefi/curve-dao-contracts@1.3.0
194 changes: 135 additions & 59 deletions contracts/CRVFunder.vy
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ interface CRV20:
def rate() -> uint256: view
def future_epoch_time_write() -> uint256: nonpayable

interface Factory:
def owner() -> address: view
def fallback_receiver() -> address: view

interface GaugeController:
def checkpoint_gauge(_gauge: address): nonpayable
def gauge_relative_weight(_gauge: address, _time: uint256) -> uint256: view
Expand All @@ -25,7 +29,6 @@ event TransferOwnership:

CRV: constant(address) = 0xD533a949740bb3306d119CC777fa900bA034cd52
GAUGE_CONTROLLER: constant(address) = 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB
TREASURY_ADDRESS: constant(address) = 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB # todo: add treasury adddress here

WEEK: constant(uint256) = 604800
YEAR: constant(uint256) = 86400 * 365
Expand All @@ -42,29 +45,18 @@ inflation_params: uint256
integrate_fraction: public(HashMap[address, uint256])
last_checkpoint: public(uint256)

owner: public(address)
future_owner: public(address)
fund_receipient: public(address)
receiver: public(address)
# [uint216 max_emissions][uint40 deadline]
receiver_data: uint256

funding_end_timestamp: public(uint256)
max_integrate_fraction: public(uint256)
factory: public(address)
cached_fallback_receiver: public(address)


@external
def __init__(
fund_receipient: address,
funding_end_timestamp: uint256,
max_integrate_fraction: uint256
):
self.fund_receipient = fund_receipient
self.funding_end_timestamp = funding_end_timestamp
self.max_integrate_fraction = max_integrate_fraction

self.inflation_params = shift(CRV20(CRV).rate(), 40) + CRV20(CRV).future_epoch_time_write()
self.last_checkpoint = block.timestamp

self.owner = msg.sender
log TransferOwnership(ZERO_ADDRESS, msg.sender)
def __init__():
# prevent initialization of the implementation contract
self.factory = 0x000000000000000000000000000000000000dEaD


@external
Expand All @@ -73,65 +65,95 @@ def user_checkpoint(_user: address) -> bool:
@notice Checkpoint the gauge updating total emissions
@param _user The user to checkpoint and update accumulated emissions for
"""
# timestamp of the last checkpoint
last_checkpoint: uint256 = self.last_checkpoint
# timestamp of the last checkpoint and start point for calculating new emissions
prev_week_time: uint256 = self.last_checkpoint

# if time has not advanced since the last checkpoint
if block.timestamp == last_checkpoint:
if block.timestamp == prev_week_time:
return True

# if funding duration has expired, direct to treasury:
fund_receipient: address = self.fund_receipient
if block.timestamp >= self.funding_end_timestamp:
fund_receipient = TREASURY_ADDRESS

# checkpoint the gauge filling in gauge data across weeks
GaugeController(GAUGE_CONTROLLER).checkpoint_gauge(self)

# load and unpack inflation params
inflation_params: uint256 = self.inflation_params
rate: uint256 = shift(inflation_params, -40)
future_epoch_time: uint256 = bitwise_and(inflation_params, 2 ** 40 - 1)

# initialize variables for tracking timedelta between weeks
prev_week_time: uint256 = last_checkpoint
# either the start of the next week or the current timestamp
week_time: uint256 = min((last_checkpoint + WEEK) / WEEK * WEEK, block.timestamp)
# load the receiver
receiver: address = self.receiver
# load and unpack receiver data
receiver_data: uint256 = self.receiver_data
deadline: uint256 = bitwise_and(receiver_data, 2 ** 40 - 1)
max_emissions: uint256 = shift(receiver_data, -40)

# track total new emissions while we loop
# initialize emission tracking variables
new_emissions: uint256 = 0
multisig_emissions: uint256 = 0
receiver_emissions: uint256 = self.integrate_fraction[receiver]

# checkpoint the gauge filling in any missing gauge data across weeks
GaugeController(GAUGE_CONTROLLER).checkpoint_gauge(self)

# iterate over at maximum 512 weeks
# either the start of the next week or the current timestamp
week_time: uint256 = min((prev_week_time + WEEK) / WEEK * WEEK, block.timestamp)

# if the deadline is between our previous checkpoint and the end of the week
# set the week_time var to our deadline, so we can calculate up to it only
if prev_week_time < deadline and deadline < week_time:
week_time = deadline

# iterate 512 times at maximum
for i in range(512):
dt: uint256 = week_time - prev_week_time
w: uint256 = GaugeController(GAUGE_CONTROLLER).gauge_relative_weight(self, prev_week_time / WEEK * WEEK)
emissions: uint256 = 0

# if we cross over an inflation epoch, calculate the emissions using old and new rate
if prev_week_time <= future_epoch_time and future_epoch_time < week_time:
# calculate up to the epoch using the old rate
new_emissions += rate * w * (future_epoch_time - prev_week_time) / 10 ** 18
emissions = rate * w * (future_epoch_time - prev_week_time) / 10 ** 18
# update the rate in memory
rate = rate * RATE_DENOMINATOR / RATE_REDUCTION_COEFFICIENT
# calculate past the epoch to the start of the next week
new_emissions += rate * w * (week_time - future_epoch_time) / 10 ** 18
# calculate using the new rate for the rest of the time period
emissions += rate * w * (week_time - future_epoch_time) / 10 ** 18
# update the new future epoch time
future_epoch_time += RATE_REDUCTION_TIME
# update storage
self.inflation_params = shift(rate, 40) + future_epoch_time
else:
new_emissions += rate * w * dt / 10 ** 18
emissions = rate * w * dt / 10 ** 18

new_emissions += emissions
# if the time period we are calculating for ends before or at the deadline
if week_time <= deadline:
# if the receiver emissions + emissions from this period is greater than max_emissions
if receiver_emissions + emissions > max_emissions:
# the emissions from this period - amount given to receiver goes to multisig
multisig_emissions += emissions - (max_emissions - receiver_emissions)
# how much does the receiver get from this period
receiver_emissions = max_emissions
else:
receiver_emissions += emissions
else:
multisig_emissions += emissions

if week_time == block.timestamp:
break

# update timestamps for tracking timedelta
prev_week_time = week_time
week_time = min(week_time + WEEK, block.timestamp)
week_time = min((week_time + WEEK) / WEEK * WEEK, block.timestamp)

if prev_week_time < deadline and deadline < week_time:
week_time = deadline

# cap accumulated emissions only for fund receipient
# todo: check with skelletor if this is the right approach
if fund_receipient == self.fund_receipient:
new_emissions = min(self.max_integrate_fraction, new_emissions)

self.integrate_fraction[fund_receipient] += new_emissions
# multisig has received emissions
if multisig_emissions != 0:
self.integrate_fraction[self.cached_fallback_receiver] += multisig_emissions

# this will only be the case if receiver got emissions
if multisig_emissions != new_emissions:
self.integrate_fraction[receiver] = receiver_emissions

self.last_checkpoint = block.timestamp

log Checkpoint(block.timestamp, new_emissions)
Expand All @@ -144,7 +166,7 @@ def set_killed(_is_killed: bool):
@notice Set the gauge status
@dev Inflation params are modified accordingly to disable/enable emissions
"""
assert msg.sender == self.owner
assert msg.sender == Factory(self.factory).owner()

if _is_killed:
self.inflation_params = 0
Expand All @@ -153,23 +175,77 @@ def set_killed(_is_killed: bool):


@external
def commit_transfer_ownership(_future_owner: address):
def update_cached_fallback_receiver():
"""
@notice Commit the transfer of ownership to `_future_owner`
@param _future_owner The account to commit as the future owner
@notice Update the cached fallback receiver
"""
assert msg.sender == self.owner # dev: only owner
self.cached_fallback_receiver = Factory(self.factory).fallback_receiver()

self.future_owner = _future_owner

@view
@external
def max_emissions() -> uint256:
"""
@notice Get the maximum amount of emissions distributed to the receiver, afterwards
emissions are diverted to the Grant Council Multisig
"""
return shift(self.receiver_data, -40)


@view
@external
def deadline() -> uint256:
"""
@notice Get the timestamp at which emissions are diverted to the Grant Council Multisig
"""
return bitwise_and(self.receiver_data, 2 ** 40 - 1)


@view
@external
def inflation_rate() -> uint256:
"""
@notice Get the locally stored inflation rate
"""
return shift(self.inflation_params, -40)


@view
@external
def accept_transfer_ownership():
def future_epoch_time() -> uint256:
"""
@notice Accept the transfer of ownership
@dev Only the committed future owner can call this function
@notice Get the locally stored timestamp of the inflation rate epoch end
"""
assert msg.sender == self.future_owner # dev: only future owner
return bitwise_and(self.inflation_params, 2 ** 40 - 1)

log TransferOwnership(self.owner, msg.sender)
self.owner = msg.sender

@external
def initialize(
_receiver: address,
_deadline: uint256,
_max_emissions: uint256
):
"""
@notice Proxy initializer method
@dev Placed last in the source file to save some gas, this fn is called only once.
Additional checks should be made by the DAO before voting in this gauge, specifically
to make sure that `_fund_recipient` is capable of collecting emissions.
@param _receiver The address which will receive CRV emissions
@param _deadline The timestamp at which emissions will redirect to
the Curve Grant Council Multisig
@param _max_emissions The maximum amount of emissions which `_receiver` will
receive
"""
assert self.factory == ZERO_ADDRESS # dev: already initialized

assert _deadline < 2 ** 40 # dev: invalid deadline
assert _max_emissions < 2 ** 216 # dev: invalid maximum emissions

self.factory = msg.sender

self.receiver = _receiver
self.receiver_data = shift(_max_emissions, 40) + _deadline
self.cached_fallback_receiver = Factory(msg.sender).fallback_receiver()

self.inflation_params = shift(CRV20(CRV).rate(), 40) + CRV20(CRV).future_epoch_time_write()
self.last_checkpoint = block.timestamp
92 changes: 92 additions & 0 deletions contracts/Factory.vy
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# @version 0.3.1
"""
@title veFunder Factory
@license MIT
"""


interface Funder:
def initialize(_receiver: address, _deadline: uint256, _max_emissions: uint256): nonpayable


event UpdateImplementation:
_old_implementation: address
_new_implementation: address

event UpdateFallbackReceiver:
_old_fallback: address
_new_fallback: address

event TransferOwnership:
_old_owner: address
_new_owner: address

event NewFunder:
_receiver: indexed(address)
_deadline: uint256
_max_emissions: uint256
_funder_instance: address


implementation: public(address)
fallback_receiver: public(address)

owner: public(address)
future_owner: public(address)

get_funders_count: public(uint256)
funders: public(address[1000000])


@external
def __init__(_fallback_receiver: address):
self.owner = msg.sender
self.fallback_receiver = _fallback_receiver

log TransferOwnership(ZERO_ADDRESS, msg.sender)
log UpdateFallbackReceiver(ZERO_ADDRESS, _fallback_receiver)


@external
def deploy(_receiver: address, _deadline: uint256, _max_emissions: uint256) -> address:
funder: address = create_forwarder_to(self.implementation)
Funder(funder).initialize(_receiver, _deadline, _max_emissions)

# update for easy enumeration
funders_count: uint256 = self.get_funders_count
self.funders[funders_count] = funder
self.get_funders_count = funders_count + 1

log NewFunder(_receiver, _deadline, _max_emissions, funder)
return funder


@external
def set_implementation(_implementation: address):
assert msg.sender == self.owner

log UpdateImplementation(self.implementation, _implementation)
self.implementation = _implementation


@external
def set_fallback_receiver(_fallback_receiver: address):
assert msg.sender == self.owner

log UpdateFallbackReceiver(self.fallback_receiver, _fallback_receiver)
self.fallback_receiver = _fallback_receiver


@external
def commit_transfer_ownership(_future_owner: address):
assert msg.sender == self.owner

self.future_owner = _future_owner


@external
def accept_transfer_ownership():
assert msg.sender == self.future_owner

log TransferOwnership(self.owner, msg.sender)
self.owner = msg.sender
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import pytest

pytest_plugins = ["fixtures.accounts", "fixtures.deployments"]


def pytest_sessionfinish(session, exitstatus):
if exitstatus == pytest.ExitCode.NO_TESTS_COLLECTED:
# we treat "no tests collected" as passing
session.exitstatus = pytest.ExitCode.OK


@pytest.fixture(autouse=True)
def isolation(module_isolation, fn_isolation):
pass
Empty file added tests/fixtures/__init__.py
Empty file.
Loading

0 comments on commit 74d51bc

Please sign in to comment.