From 308475c54a40bb61a36a5815c258a282792cb533 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 02:10:42 +0000 Subject: [PATCH 01/22] fix: add grant council multisig address Need to add this somewhere that end users can verify this is correct. Need to also provably verify who is on the multisig --- contracts/CRVFunder.vy | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/contracts/CRVFunder.vy b/contracts/CRVFunder.vy index 4cb1a02..a2498c1 100644 --- a/contracts/CRVFunder.vy +++ b/contracts/CRVFunder.vy @@ -25,7 +25,7 @@ event TransferOwnership: CRV: constant(address) = 0xD533a949740bb3306d119CC777fa900bA034cd52 GAUGE_CONTROLLER: constant(address) = 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB -TREASURY_ADDRESS: constant(address) = 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB # todo: add treasury adddress here +GRANT_COUNCIL_MULTISIG: constant(address) = 0xc420C9d507D0E038BD76383AaADCAd576ed0073c WEEK: constant(uint256) = 604800 YEAR: constant(uint256) = 86400 * 365 @@ -173,3 +173,21 @@ def accept_transfer_ownership(): log TransferOwnership(self.owner, msg.sender) self.owner = msg.sender + + +@view +@external +def inflation_rate() -> uint256: + """ + @notice Get the locally stored inflation rate + """ + return shift(self.inflation_params, -40) + + +@view +@external +def future_epoch_time() -> uint256: + """ + @notice Get the locally stored timestamp of the inflation rate epoch end + """ + return bitwise_and(self.inflation_params, 2 ** 40 - 1) From ea6ec591ff57612eec990255277649df8bdd89d6 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 02:19:55 +0000 Subject: [PATCH 02/22] fix: add proxy initializer and prevent implementation initialization --- contracts/CRVFunder.vy | 70 ++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/contracts/CRVFunder.vy b/contracts/CRVFunder.vy index a2498c1..68dad07 100644 --- a/contracts/CRVFunder.vy +++ b/contracts/CRVFunder.vy @@ -42,29 +42,15 @@ inflation_params: uint256 integrate_fraction: public(HashMap[address, uint256]) last_checkpoint: public(uint256) -owner: public(address) -future_owner: public(address) fund_receipient: public(address) - funding_end_timestamp: public(uint256) max_integrate_fraction: public(uint256) @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.fund_recipient = 0x000000000000000000000000000000000000dEaD @external @@ -152,29 +138,6 @@ def set_killed(_is_killed: bool): self.inflation_params = shift(CRV20(CRV).rate(), 40) + CRV20(CRV).future_epoch_time_write() -@external -def commit_transfer_ownership(_future_owner: address): - """ - @notice Commit the transfer of ownership to `_future_owner` - @param _future_owner The account to commit as the future owner - """ - assert msg.sender == self.owner # dev: only owner - - self.future_owner = _future_owner - - -@external -def accept_transfer_ownership(): - """ - @notice Accept the transfer of ownership - @dev Only the committed future owner can call this function - """ - assert msg.sender == self.future_owner # dev: only future owner - - log TransferOwnership(self.owner, msg.sender) - self.owner = msg.sender - - @view @external def inflation_rate() -> uint256: @@ -191,3 +154,30 @@ def future_epoch_time() -> uint256: @notice Get the locally stored timestamp of the inflation rate epoch end """ return bitwise_and(self.inflation_params, 2 ** 40 - 1) + + +@external +def initialize( + _fund_receipient: address, + _funding_end_timestamp: uint256, + _max_integrate_fraction: 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 _fund_recipient The address which will receive CRV emissions + @param _funding_end_timestamp The timestamp at which emissions will redirect to + the Curve Grant Council Multisig + @param _max_integrate_fraction The maximum amount of emissions which `_fund_recipient` will + receive + """ + assert self.fund_receipient == ZERO_ADDRESS # dev: already initialized + + 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 From 09dfe80d8e33c5395cc4a8b32b388f358a52900d Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 02:58:45 +0000 Subject: [PATCH 03/22] fix: typo + only allow factory owner to kill gauge --- contracts/CRVFunder.vy | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/contracts/CRVFunder.vy b/contracts/CRVFunder.vy index 68dad07..bb996f9 100644 --- a/contracts/CRVFunder.vy +++ b/contracts/CRVFunder.vy @@ -9,6 +9,9 @@ interface CRV20: def rate() -> uint256: view def future_epoch_time_write() -> uint256: nonpayable +interface Factory: + def owner() -> address: view + interface GaugeController: def checkpoint_gauge(_gauge: address): nonpayable def gauge_relative_weight(_gauge: address, _time: uint256) -> uint256: view @@ -42,15 +45,17 @@ inflation_params: uint256 integrate_fraction: public(HashMap[address, uint256]) last_checkpoint: public(uint256) -fund_receipient: public(address) +fund_recipient: public(address) funding_end_timestamp: public(uint256) max_integrate_fraction: public(uint256) +factory: public(address) + @external def __init__(): # prevent initialization of the implementation contract - self.fund_recipient = 0x000000000000000000000000000000000000dEaD + self.factory = 0x000000000000000000000000000000000000dEaD @external @@ -67,9 +72,9 @@ def user_checkpoint(_user: address) -> bool: return True # if funding duration has expired, direct to treasury: - fund_receipient: address = self.fund_receipient + fund_recipient: address = self.fund_recipient if block.timestamp >= self.funding_end_timestamp: - fund_receipient = TREASURY_ADDRESS + fund_recipient = GRANT_COUNCIL_MULTISIG # checkpoint the gauge filling in gauge data across weeks GaugeController(GAUGE_CONTROLLER).checkpoint_gauge(self) @@ -114,10 +119,10 @@ def user_checkpoint(_user: address) -> bool: # 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) + if fund_recipient == self.fund_recipient: + new_emissions = max(self.max_integrate_fraction, new_emissions) - self.integrate_fraction[fund_receipient] += new_emissions + self.integrate_fraction[fund_recipient] += new_emissions self.last_checkpoint = block.timestamp log Checkpoint(block.timestamp, new_emissions) @@ -130,7 +135,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 @@ -158,7 +163,7 @@ def future_epoch_time() -> uint256: @external def initialize( - _fund_receipient: address, + _fund_recipient: address, _funding_end_timestamp: uint256, _max_integrate_fraction: uint256 ): @@ -173,9 +178,11 @@ def initialize( @param _max_integrate_fraction The maximum amount of emissions which `_fund_recipient` will receive """ - assert self.fund_receipient == ZERO_ADDRESS # dev: already initialized + assert self.factory == ZERO_ADDRESS # dev: already initialized + + self.factory = msg.sender - self.fund_receipient = _fund_receipient + self.fund_recipient = _fund_recipient self.funding_end_timestamp = _funding_end_timestamp self.max_integrate_fraction = _max_integrate_fraction From 32d48ae52d7c96b6848ef130c099036b416d86f7 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 03:54:26 +0000 Subject: [PATCH 04/22] fix: remove duplicate var --- contracts/CRVFunder.vy | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/contracts/CRVFunder.vy b/contracts/CRVFunder.vy index bb996f9..8781c54 100644 --- a/contracts/CRVFunder.vy +++ b/contracts/CRVFunder.vy @@ -64,34 +64,32 @@ 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 + # either the start of the next week or the current timestamp + week_time: uint256 = min((prev_week_time + WEEK) / WEEK * WEEK, block.timestamp) + # if funding duration has expired, direct to treasury: fund_recipient: address = self.fund_recipient if block.timestamp >= self.funding_end_timestamp: fund_recipient = GRANT_COUNCIL_MULTISIG - # 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) - # track total new emissions while we loop new_emissions: uint256 = 0 + # checkpoint the gauge filling in any missing gauge data across weeks + GaugeController(GAUGE_CONTROLLER).checkpoint_gauge(self) + # iterate over at maximum 512 weeks for i in range(512): dt: uint256 = week_time - prev_week_time From ed1640eef111db6ba50c6d837cd23e4e014de024 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 04:13:40 +0000 Subject: [PATCH 05/22] fix: pack tightly receiver data in storage --- contracts/CRVFunder.vy | 57 +++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/contracts/CRVFunder.vy b/contracts/CRVFunder.vy index 8781c54..51b5776 100644 --- a/contracts/CRVFunder.vy +++ b/contracts/CRVFunder.vy @@ -45,9 +45,9 @@ inflation_params: uint256 integrate_fraction: public(HashMap[address, uint256]) last_checkpoint: public(uint256) -fund_recipient: public(address) -funding_end_timestamp: public(uint256) -max_integrate_fraction: public(uint256) +receiver: public(address) +# [uint216 max_emissions][uint40 deadline] +receiver_data: uint256 factory: public(address) @@ -74,11 +74,6 @@ def user_checkpoint(_user: address) -> bool: # either the start of the next week or the current timestamp week_time: uint256 = min((prev_week_time + WEEK) / WEEK * WEEK, block.timestamp) - # if funding duration has expired, direct to treasury: - fund_recipient: address = self.fund_recipient - if block.timestamp >= self.funding_end_timestamp: - fund_recipient = GRANT_COUNCIL_MULTISIG - # load and unpack inflation params inflation_params: uint256 = self.inflation_params rate: uint256 = shift(inflation_params, -40) @@ -115,12 +110,7 @@ def user_checkpoint(_user: address) -> bool: prev_week_time = week_time week_time = min(week_time + WEEK, block.timestamp) - # cap accumulated emissions only for fund receipient - # todo: check with skelletor if this is the right approach - if fund_recipient == self.fund_recipient: - new_emissions = max(self.max_integrate_fraction, new_emissions) - - self.integrate_fraction[fund_recipient] += new_emissions + self.integrate_fraction[GRANT_COUNCIL_MULTISIG] += new_emissions self.last_checkpoint = block.timestamp log Checkpoint(block.timestamp, new_emissions) @@ -141,6 +131,25 @@ def set_killed(_is_killed: bool): self.inflation_params = shift(CRV20(CRV).rate(), 40) + CRV20(CRV).future_epoch_time_write() +@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: @@ -161,28 +170,30 @@ def future_epoch_time() -> uint256: @external def initialize( - _fund_recipient: address, - _funding_end_timestamp: uint256, - _max_integrate_fraction: uint256 + _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 _fund_recipient The address which will receive CRV emissions - @param _funding_end_timestamp The timestamp at which emissions will redirect to + @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_integrate_fraction The maximum amount of emissions which `_fund_recipient` will + @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.fund_recipient = _fund_recipient - self.funding_end_timestamp = _funding_end_timestamp - self.max_integrate_fraction = _max_integrate_fraction + self.receiver = _receiver + self.receiver_data = shift(_max_emissions, 40) + _deadline self.inflation_params = shift(CRV20(CRV).rate(), 40) + CRV20(CRV).future_epoch_time_write() self.last_checkpoint = block.timestamp From d90991f8d227e9aa4ad7f722314439038e336122 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 05:31:13 +0000 Subject: [PATCH 06/22] fix: calculate emissions given a maximum and deadline --- contracts/CRVFunder.vy | 66 +++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/contracts/CRVFunder.vy b/contracts/CRVFunder.vy index 51b5776..1778e84 100644 --- a/contracts/CRVFunder.vy +++ b/contracts/CRVFunder.vy @@ -71,46 +71,90 @@ def user_checkpoint(_user: address) -> bool: if block.timestamp == prev_week_time: return True - # either the start of the next week or the current timestamp - week_time: uint256 = min((prev_week_time + WEEK) / WEEK * WEEK, block.timestamp) - # 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) - # track total new emissions while we loop + # 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) + + # 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 + + # TODO: make this part cleaner + # if the time period we are calculating for ends before or at the deadline + # deadline should never be between prev_week_time and week_time + if week_time <= deadline: + # if the receiver emissions + emissions from this period is greater than max_emissions + if receiver_emissions + emissions > max_emissions: + # how much does the receiver get from this period + amount: uint256 = max_emissions - receiver_emissions + receiver_emissions += amount + # the emissions from this period - amount given to receiver goes to multisig + multisig_emissions += emissions - amount + 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 + + + # multisig has received emissions + if multisig_emissions != 0: + self.integrate_fraction[GRANT_COUNCIL_MULTISIG] += multisig_emissions + + # this will only be the case if receiver got emissions + if multisig_emissions != new_emissions: + self.integrate_fraction[receiver] = receiver_emissions - self.integrate_fraction[GRANT_COUNCIL_MULTISIG] += new_emissions self.last_checkpoint = block.timestamp log Checkpoint(block.timestamp, new_emissions) From b8a78cf101dda018c2b901a5de38e492718300b4 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 06:14:12 +0000 Subject: [PATCH 07/22] feat: add very basic Factory contract --- contracts/Factory.vy | 76 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 contracts/Factory.vy diff --git a/contracts/Factory.vy b/contracts/Factory.vy new file mode 100644 index 0000000..b256d50 --- /dev/null +++ b/contracts/Factory.vy @@ -0,0 +1,76 @@ +# @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 TransferOwnership: + _old_owner: address + _new_owner: address + +event NewFunder: + _receiver: indexed(address) + _deadline: uint256 + _max_emissions: uint256 + _funder_instance: address + + +implementation: public(address) + +owner: public(address) +future_owner: public(address) + +get_funders_count: public(uint256) +funders: public(address[1000000]) + + +@external +def __init__(): + self.owner = msg.sender + + log TransferOwnership(ZERO_ADDRESS, msg.sender) + + +@external +def deploy(_receiver: address, _deadline: uint256, _max_emissions: uint256): + 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) + + +@external +def set_implementation(_implementation: address): + assert msg.sender == self.owner + + log UpdateImplementation(self.implementation, _implementation) + self.implementation = _implementation + + +@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 From 3f1017829920ab34695b33768fd6dfd71fd4984f Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 08:44:12 +0000 Subject: [PATCH 08/22] feat: add `fallback_receiver` fn to funder implementation --- contracts/CRVFunder.vy | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/contracts/CRVFunder.vy b/contracts/CRVFunder.vy index 1778e84..58fb5ba 100644 --- a/contracts/CRVFunder.vy +++ b/contracts/CRVFunder.vy @@ -120,9 +120,7 @@ def user_checkpoint(_user: address) -> bool: else: emissions = rate * w * dt / 10 ** 18 - # TODO: make this part cleaner # if the time period we are calculating for ends before or at the deadline - # deadline should never be between prev_week_time and week_time if week_time <= deadline: # if the receiver emissions + emissions from this period is greater than max_emissions if receiver_emissions + emissions > max_emissions: @@ -212,6 +210,17 @@ def future_epoch_time() -> uint256: return bitwise_and(self.inflation_params, 2 ** 40 - 1) +@pure +@external +def fallback_receiver() -> address: + """ + @notice Get the address of the fallback receiver. This address will + receiver emissions when either max_emissions has been reached for + the primary receiver or the deadline has passed. + """ + return GRANT_COUNCIL_MULTISIG + + @external def initialize( _receiver: address, From 0944ed010de856570a5a5e4513968c37e08db9db Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 09:02:38 +0000 Subject: [PATCH 09/22] chore: add curve dao fixtures --- tests/conftest.py | 7 +++++++ tests/fixtures/__init__.py | 0 tests/fixtures/accounts.py | 16 ++++++++++++++++ tests/fixtures/deployments.py | 31 +++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 tests/fixtures/__init__.py create mode 100644 tests/fixtures/accounts.py create mode 100644 tests/fixtures/deployments.py diff --git a/tests/conftest.py b/tests/conftest.py index 3378679..0296740 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/accounts.py b/tests/fixtures/accounts.py new file mode 100644 index 0000000..b334c83 --- /dev/null +++ b/tests/fixtures/accounts.py @@ -0,0 +1,16 @@ +import pytest + + +@pytest.fixture(scope="session") +def alice(accounts): + return accounts[0] + + +@pytest.fixture(scope="session") +def bob(accounts): + return accounts[1] + + +@pytest.fixture(scope="session") +def charlie(accounts): + return accounts[2] diff --git a/tests/fixtures/deployments.py b/tests/fixtures/deployments.py new file mode 100644 index 0000000..15c7a33 --- /dev/null +++ b/tests/fixtures/deployments.py @@ -0,0 +1,31 @@ +import pytest + + +@pytest.fixture(scope="module") +def curve_dao(pm): + return pm("curvefi/curve-dao-contracts@1.3.0") + + +@pytest.fixture(scope="module") +def crv20(alice, chain, curve_dao): + crv = curve_dao.ERC20CRV.deploy("Curve DAO Token", "CRV", 18, {"from": alice}) + chain.sleep(86400 * 14) # let emissions begin + crv.update_mining_parameters({"from": alice}) + return crv + + +@pytest.fixture(scope="module") +def voting_escrow(alice, crv20, curve_dao): + return curve_dao.VotingEscrow.deploy(crv20, "Dummy VECRV", "veCRV", "v1", {"from": alice}) + + +@pytest.fixture(scope="module") +def gauge_controller(alice, crv20, voting_escrow, curve_dao): + return curve_dao.GaugeController.deploy(crv20, voting_escrow, {"from": alice}) + + +@pytest.fixture(scope="module") +def minter(alice, crv20, gauge_controller, curve_dao): + minter = curve_dao.Minter.deploy(crv20, gauge_controller, {"from": alice}) + crv20.set_minter(minter, {"from": alice}) + return minter From 82a778da499080a62e7bb8bd167b262c231cac9f Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 09:03:01 +0000 Subject: [PATCH 10/22] fix: set default network to local devnet --- brownie-config.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/brownie-config.yaml b/brownie-config.yaml index 27c8540..142f2d9 100644 --- a/brownie-config.yaml +++ b/brownie-config.yaml @@ -1,4 +1 @@ -networks: - default: mainnet-fork - autofetch_sources: true From bd61be18c8e1eae9b894ab0c4f481f282f4eb4da Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 09:19:48 +0000 Subject: [PATCH 11/22] fix: return new funder address from deploy fn --- contracts/Factory.vy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/Factory.vy b/contracts/Factory.vy index b256d50..3aae0bb 100644 --- a/contracts/Factory.vy +++ b/contracts/Factory.vy @@ -41,7 +41,7 @@ def __init__(): @external -def deploy(_receiver: address, _deadline: uint256, _max_emissions: uint256): +def deploy(_receiver: address, _deadline: uint256, _max_emissions: uint256) -> address: funder: address = create_forwarder_to(self.implementation) Funder(funder).initialize(_receiver, _deadline, _max_emissions) @@ -51,6 +51,7 @@ def deploy(_receiver: address, _deadline: uint256, _max_emissions: uint256): self.get_funders_count = funders_count + 1 log NewFunder(_receiver, _deadline, _max_emissions, funder) + return funder @external From 6ddf58fb7c98d994df172763ec1c1eda3c2370ee Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 09:20:47 +0000 Subject: [PATCH 12/22] chore: add contract fixtures --- tests/fixtures/deployments.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/fixtures/deployments.py b/tests/fixtures/deployments.py index 15c7a33..1d3b202 100644 --- a/tests/fixtures/deployments.py +++ b/tests/fixtures/deployments.py @@ -1,4 +1,5 @@ import pytest +from brownie import compile_source @pytest.fixture(scope="module") @@ -29,3 +30,34 @@ def minter(alice, crv20, gauge_controller, curve_dao): minter = curve_dao.Minter.deploy(crv20, gauge_controller, {"from": alice}) crv20.set_minter(minter, {"from": alice}) return minter + + +@pytest.fixture(scope="module") +def factory(alice, Factory): + return Factory.deploy({"from": alice}) + + +@pytest.fixture(scope="module") +def CRVFunderLocal(alice, CRVFunder, crv20, gauge_controller): + src = CRVFunder._build["source"] + addrs = [ + "0xD533a949740bb3306d119CC777fa900bA034cd52", + "0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB", + ] + for old, new in zip(addrs, [crv20.address, gauge_controller.address]): + src = src.replace(old, new, 1) + + return compile_source(src, vyper_version="0.3.1").Vyper + + +@pytest.fixture(scope="module") +def implementation(alice, CRVFunderLocal): + return CRVFunderLocal.deploy({"from": alice}) + + +@pytest.fixture(scope="module") +def funder(alice, factory, implementation, CRVFunderLocal): + factory.set_implementation(implementation, {"from": alice}) + return CRVFunderLocal.at( + factory.deploy(alice, 2**40 - 1, 2**128 - 1, {"from": alice}).return_value + ) From 46fc93464e9879f31bc7a2dcb9717175b0f291c2 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 09:50:43 +0000 Subject: [PATCH 13/22] fix: update new_emissions var on each checkpoint loop --- contracts/CRVFunder.vy | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/CRVFunder.vy b/contracts/CRVFunder.vy index 58fb5ba..f7d30ad 100644 --- a/contracts/CRVFunder.vy +++ b/contracts/CRVFunder.vy @@ -120,6 +120,7 @@ def user_checkpoint(_user: address) -> bool: else: 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 From 2138fa0e712e3dac2758823284c928cbf0f22367 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 10:07:48 +0000 Subject: [PATCH 14/22] test: checkpoint logic simple --- tests/test_user_checkpoint.py | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/test_user_checkpoint.py diff --git a/tests/test_user_checkpoint.py b/tests/test_user_checkpoint.py new file mode 100644 index 0000000..1fb61fd --- /dev/null +++ b/tests/test_user_checkpoint.py @@ -0,0 +1,40 @@ +import pytest +from brownie import chain + +WEEK = 7 * 86400 +YEAR = 365 * 86400 + + +@pytest.mark.skip_coverage +def test_emissions_against_expected(alice, gauge_controller, funder, crv20): + gauge_controller.add_type("Test", 10**18, {"from": alice}) + gauge_controller.add_gauge(funder, 0, 10**18, {"from": alice}) + + rate = crv20.rate() + future_epoch_time = crv20.future_epoch_time_write({"from": alice}).return_value + total_emissions = 0 + + prev_week_time = funder.last_checkpoint() + chain.mine(timedelta=2 * YEAR) + tx = funder.user_checkpoint(alice, {"from": alice}) + + while True: + week_time = min((prev_week_time + WEEK) // WEEK * WEEK, tx.timestamp) + gauge_weight = gauge_controller.gauge_relative_weight(funder, prev_week_time) + + if prev_week_time <= future_epoch_time < week_time: + total_emissions += ( + gauge_weight * rate * (future_epoch_time - prev_week_time) // 10**18 + ) + rate = rate * 10**18 // 1189207115002721024 + total_emissions += gauge_weight * rate * (week_time - future_epoch_time) // 10**18 + future_epoch_time += YEAR + else: + total_emissions += gauge_weight * rate * (week_time - prev_week_time) // 10**18 + + if week_time == tx.timestamp: + break + + prev_week_time = week_time + + assert total_emissions == funder.integrate_fraction(alice) From 78b29a7a1f3442081c6465074fc8990dc1e5a95f Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 10:27:50 +0000 Subject: [PATCH 15/22] test: emissions after deadline go to fallback receiver --- tests/test_user_checkpoint.py | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_user_checkpoint.py b/tests/test_user_checkpoint.py index 1fb61fd..03e11b6 100644 --- a/tests/test_user_checkpoint.py +++ b/tests/test_user_checkpoint.py @@ -38,3 +38,53 @@ def test_emissions_against_expected(alice, gauge_controller, funder, crv20): prev_week_time = week_time assert total_emissions == funder.integrate_fraction(alice) + + +@pytest.mark.skip_coverage +def test_reach_deadline(alice, gauge_controller, factory, CRVFunderLocal, crv20): + deadline = chain.time() + WEEK * 5 + funder = CRVFunderLocal.at( + factory.deploy(alice, deadline, 2**216 - 1, {"from": alice}).return_value + ) + + gauge_controller.add_type("Test", 10**18, {"from": alice}) + gauge_controller.add_gauge(funder, 0, 10**18, {"from": alice}) + + rate = crv20.rate() + future_epoch_time = crv20.future_epoch_time_write({"from": alice}).return_value + + alice_emissions = 0 + fallback_emissions = 0 + + prev_week_time = funder.last_checkpoint() + chain.mine(timedelta=2 * YEAR) + tx = funder.user_checkpoint(alice, {"from": alice}) + + while True: + week_time = min((prev_week_time + WEEK) // WEEK * WEEK, tx.timestamp) + if prev_week_time < deadline < week_time: + week_time = deadline + + gauge_weight = gauge_controller.gauge_relative_weight(funder, prev_week_time) + emissions = 0 + + if prev_week_time <= future_epoch_time < week_time: + emissions += gauge_weight * rate * (future_epoch_time - prev_week_time) // 10**18 + rate = rate * 10**18 // 1189207115002721024 + emissions += gauge_weight * rate * (week_time - future_epoch_time) // 10**18 + future_epoch_time += YEAR + else: + emissions += gauge_weight * rate * (week_time - prev_week_time) // 10**18 + + if week_time <= deadline: + alice_emissions += emissions + else: + fallback_emissions += emissions + + if week_time == tx.timestamp: + break + + prev_week_time = week_time + + assert alice_emissions == funder.integrate_fraction(alice) + assert fallback_emissions == funder.integrate_fraction(funder.fallback_receiver()) From 897281bf59ed01f527e0ea506a6d0bb0aa3ee486 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 10:36:20 +0000 Subject: [PATCH 16/22] fix: eliminate redundant memory var --- contracts/CRVFunder.vy | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/CRVFunder.vy b/contracts/CRVFunder.vy index f7d30ad..13f2e26 100644 --- a/contracts/CRVFunder.vy +++ b/contracts/CRVFunder.vy @@ -125,11 +125,10 @@ def user_checkpoint(_user: address) -> bool: if week_time <= deadline: # if the receiver emissions + emissions from this period is greater than max_emissions if receiver_emissions + emissions > max_emissions: - # how much does the receiver get from this period - amount: uint256 = max_emissions - receiver_emissions - receiver_emissions += amount # the emissions from this period - amount given to receiver goes to multisig - multisig_emissions += emissions - amount + 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: From 2b9839d39f87cc284eb0d1f06d14bc4d06f241f6 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 10:38:06 +0000 Subject: [PATCH 17/22] test: emissions past maximum go to fallback --- tests/test_user_checkpoint.py | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/test_user_checkpoint.py b/tests/test_user_checkpoint.py index 03e11b6..385ee62 100644 --- a/tests/test_user_checkpoint.py +++ b/tests/test_user_checkpoint.py @@ -88,3 +88,51 @@ def test_reach_deadline(alice, gauge_controller, factory, CRVFunderLocal, crv20) assert alice_emissions == funder.integrate_fraction(alice) assert fallback_emissions == funder.integrate_fraction(funder.fallback_receiver()) + + +@pytest.mark.skip_coverage +def test_reach_emissions_max(alice, gauge_controller, factory, CRVFunderLocal, crv20): + max_emissions = 500_000 * 10**18 + funder = CRVFunderLocal.at( + factory.deploy(alice, 2**40 - 1, max_emissions, {"from": alice}).return_value + ) + + gauge_controller.add_type("Test", 10**18, {"from": alice}) + gauge_controller.add_gauge(funder, 0, 10**18, {"from": alice}) + + rate = crv20.rate() + future_epoch_time = crv20.future_epoch_time_write({"from": alice}).return_value + + alice_emissions = 0 + fallback_emissions = 0 + + prev_week_time = funder.last_checkpoint() + chain.mine(timedelta=2 * YEAR) + tx = funder.user_checkpoint(alice, {"from": alice}) + + while True: + week_time = min((prev_week_time + WEEK) // WEEK * WEEK, tx.timestamp) + gauge_weight = gauge_controller.gauge_relative_weight(funder, prev_week_time) + emissions = 0 + + if prev_week_time <= future_epoch_time < week_time: + emissions += gauge_weight * rate * (future_epoch_time - prev_week_time) // 10**18 + rate = rate * 10**18 // 1189207115002721024 + emissions += gauge_weight * rate * (week_time - future_epoch_time) // 10**18 + future_epoch_time += YEAR + else: + emissions += gauge_weight * rate * (week_time - prev_week_time) // 10**18 + + if alice_emissions + emissions > max_emissions: + fallback_emissions += emissions - (max_emissions - alice_emissions) + alice_emissions = max_emissions + else: + alice_emissions += emissions + + if week_time == tx.timestamp: + break + + prev_week_time = week_time + + assert alice_emissions == funder.integrate_fraction(alice) == max_emissions + assert fallback_emissions == funder.integrate_fraction(funder.fallback_receiver()) From cc4c6554a42e8f511e161e4a470f41cf8d331728 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 10:48:35 +0000 Subject: [PATCH 18/22] feat: store global fallback receiver in factory --- contracts/Factory.vy | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/contracts/Factory.vy b/contracts/Factory.vy index 3aae0bb..7356769 100644 --- a/contracts/Factory.vy +++ b/contracts/Factory.vy @@ -25,6 +25,7 @@ event NewFunder: implementation: public(address) +fallback_receiver: public(address) owner: public(address) future_owner: public(address) @@ -34,10 +35,12 @@ funders: public(address[1000000]) @external -def __init__(): +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 @@ -62,6 +65,14 @@ def set_implementation(_implementation: address): 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 From e459452101ff821d30264055d15e75bd6ca5def0 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 10:51:50 +0000 Subject: [PATCH 19/22] fix: cache the fallback receiver from the factory --- contracts/CRVFunder.vy | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/contracts/CRVFunder.vy b/contracts/CRVFunder.vy index 13f2e26..f82981f 100644 --- a/contracts/CRVFunder.vy +++ b/contracts/CRVFunder.vy @@ -11,6 +11,7 @@ interface CRV20: interface Factory: def owner() -> address: view + def fallback_receiver() -> address: view interface GaugeController: def checkpoint_gauge(_gauge: address): nonpayable @@ -28,7 +29,6 @@ event TransferOwnership: CRV: constant(address) = 0xD533a949740bb3306d119CC777fa900bA034cd52 GAUGE_CONTROLLER: constant(address) = 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB -GRANT_COUNCIL_MULTISIG: constant(address) = 0xc420C9d507D0E038BD76383AaADCAd576ed0073c WEEK: constant(uint256) = 604800 YEAR: constant(uint256) = 86400 * 365 @@ -50,6 +50,7 @@ receiver: public(address) receiver_data: uint256 factory: public(address) +cached_fallback_receiver: public(address) @external @@ -147,7 +148,7 @@ def user_checkpoint(_user: address) -> bool: # multisig has received emissions if multisig_emissions != 0: - self.integrate_fraction[GRANT_COUNCIL_MULTISIG] += multisig_emissions + self.integrate_fraction[self.cached_fallback_receiver] += multisig_emissions # this will only be the case if receiver got emissions if multisig_emissions != new_emissions: @@ -173,6 +174,14 @@ def set_killed(_is_killed: bool): self.inflation_params = shift(CRV20(CRV).rate(), 40) + CRV20(CRV).future_epoch_time_write() +@external +def update_cached_fallback_receiver(): + """ + @notice Update the cached fallback receiver + """ + self.cached_fallback_receiver = Factory(self.factory).fallback_receiver() + + @view @external def max_emissions() -> uint256: @@ -210,17 +219,6 @@ def future_epoch_time() -> uint256: return bitwise_and(self.inflation_params, 2 ** 40 - 1) -@pure -@external -def fallback_receiver() -> address: - """ - @notice Get the address of the fallback receiver. This address will - receiver emissions when either max_emissions has been reached for - the primary receiver or the deadline has passed. - """ - return GRANT_COUNCIL_MULTISIG - - @external def initialize( _receiver: address, @@ -247,6 +245,7 @@ def initialize( 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 From 0acc5e415f58e2fef24e070dc997326181b84c5b Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 10:54:13 +0000 Subject: [PATCH 20/22] fix: add curve dao contracts as dependency --- brownie-config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/brownie-config.yaml b/brownie-config.yaml index 142f2d9..9a3c15a 100644 --- a/brownie-config.yaml +++ b/brownie-config.yaml @@ -1 +1,4 @@ autofetch_sources: true + +dependencies: + - curvefi/curve-dao-contracts@1.3.0 From d4642e4d82fc867579c0d57e95087adc2501d483 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 11:28:04 +0000 Subject: [PATCH 21/22] fix: add missing event --- contracts/Factory.vy | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/Factory.vy b/contracts/Factory.vy index 7356769..b3a0195 100644 --- a/contracts/Factory.vy +++ b/contracts/Factory.vy @@ -13,6 +13,10 @@ event UpdateImplementation: _old_implementation: address _new_implementation: address +event UpdateFallbackReceiver: + _old_fallback: address + _new_fallback: address + event TransferOwnership: _old_owner: address _new_owner: address From 733609650da0c59eecbfcd000cf1a82bb6500ecb Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 10 Mar 2022 11:30:16 +0000 Subject: [PATCH 22/22] fix: correct method name in tests --- tests/fixtures/deployments.py | 4 ++-- tests/test_user_checkpoint.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/fixtures/deployments.py b/tests/fixtures/deployments.py index 1d3b202..61be88c 100644 --- a/tests/fixtures/deployments.py +++ b/tests/fixtures/deployments.py @@ -33,8 +33,8 @@ def minter(alice, crv20, gauge_controller, curve_dao): @pytest.fixture(scope="module") -def factory(alice, Factory): - return Factory.deploy({"from": alice}) +def factory(alice, bob, Factory): + return Factory.deploy(bob, {"from": alice}) @pytest.fixture(scope="module") diff --git a/tests/test_user_checkpoint.py b/tests/test_user_checkpoint.py index 385ee62..c36e2f5 100644 --- a/tests/test_user_checkpoint.py +++ b/tests/test_user_checkpoint.py @@ -87,7 +87,7 @@ def test_reach_deadline(alice, gauge_controller, factory, CRVFunderLocal, crv20) prev_week_time = week_time assert alice_emissions == funder.integrate_fraction(alice) - assert fallback_emissions == funder.integrate_fraction(funder.fallback_receiver()) + assert fallback_emissions == funder.integrate_fraction(funder.cached_fallback_receiver()) @pytest.mark.skip_coverage @@ -135,4 +135,4 @@ def test_reach_emissions_max(alice, gauge_controller, factory, CRVFunderLocal, c prev_week_time = week_time assert alice_emissions == funder.integrate_fraction(alice) == max_emissions - assert fallback_emissions == funder.integrate_fraction(funder.fallback_receiver()) + assert fallback_emissions == funder.integrate_fraction(funder.cached_fallback_receiver())