From 8bcf56bc33e06dc2cd02ea65ccb7dd2903772956 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Sat, 8 May 2021 18:34:02 +0600 Subject: [PATCH 01/19] feat: root chain gauge --- contracts/gauges/sidechain/RootChainGauge.vy | 190 +++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 contracts/gauges/sidechain/RootChainGauge.vy diff --git a/contracts/gauges/sidechain/RootChainGauge.vy b/contracts/gauges/sidechain/RootChainGauge.vy new file mode 100644 index 00000000..7d30c981 --- /dev/null +++ b/contracts/gauges/sidechain/RootChainGauge.vy @@ -0,0 +1,190 @@ +# @version 0.2.12 +""" +@title Root-Chain Gauge +@author Curve Finance +@license MIT +""" + +from vyper.interfaces import ERC20 + + +interface CRV20: + def future_epoch_time_write() -> uint256: nonpayable + def rate() -> uint256: view + +interface Controller: + def period() -> int128: view + def period_write() -> int128: nonpayable + def period_timestamp(p: int128) -> uint256: view + def gauge_relative_weight(addr: address, time: uint256) -> uint256: view + def voting_escrow() -> address: view + def checkpoint(): nonpayable + def checkpoint_gauge(addr: address): nonpayable + +interface Minter: + def token() -> address: view + def controller() -> address: view + def minted(user: address, gauge: address) -> uint256: view + def mint(gauge: address): nonpayable + + +event Deposit: + provider: indexed(address) + value: uint256 + +event Withdraw: + provider: indexed(address) + value: uint256 + +event UpdateLiquidityLimit: + user: address + original_balance: uint256 + original_supply: uint256 + working_balance: uint256 + working_supply: uint256 + +event CommitOwnership: + admin: address + +event ApplyOwnership: + admin: address + +event Transfer: + _from: indexed(address) + _to: indexed(address) + _value: uint256 + +event Approval: + _owner: indexed(address) + _spender: indexed(address) + _value: uint256 + + +WEEK: constant(uint256) = 604800 + +minter: public(address) +crv_token: public(address) +lp_token: public(address) +controller: public(address) +future_epoch_time: public(uint256) + + +# The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint +# All values are kept in units of being multiplied by 1e18 +period: public(uint256) +emissions: public(uint256) +inflation_rate: public(uint256) + +admin: public(address) +future_admin: public(address) # Can and will be a smart contract +is_killed: public(bool) + + +@external +def __init__(_lp_token: address, _minter: address, _admin: address): + """ + @notice Contract constructor + @param _lp_token Liquidity Pool contract address + @param _minter Minter contract address + @param _admin Admin who can kill the gauge + """ + + crv_token: address = Minter(_minter).token() + controller: address = Minter(_minter).controller() + + self.lp_token = _lp_token + self.minter = _minter + self.admin = _admin + self.crv_token = crv_token + self.controller = controller + + self.period = block.timestamp / WEEK + self.inflation_rate = CRV20(crv_token).rate() + self.future_epoch_time = CRV20(crv_token).future_epoch_time_write() + + +@external +def checkpoint(): + """ + @notice Checkpoint + """ + rate: uint256 = self.inflation_rate + new_rate: uint256 = rate + prev_future_epoch: uint256 = self.future_epoch_time + if prev_future_epoch < block.timestamp: + token: address = self.crv_token + self.future_epoch_time = CRV20(token).future_epoch_time_write() + new_rate = CRV20(token).rate() + self.inflation_rate = new_rate + + last_period: uint256 = self.period + current_period: uint256 = block.timestamp / WEEK + + if last_period < current_period: + controller: address = self.controller + Controller(controller).checkpoint_gauge(self) + + emissions: uint256 = 0 + for i in range(last_period, last_period+255): + if i > current_period: + break + week_time: uint256 = i * WEEK + gauge_weight: uint256 = Controller(controller).gauge_relative_weight(self, i * WEEK) + emissions += gauge_weight * rate * WEEK / 10**18 + + if prev_future_epoch < week_time: + # If we went across one or multiple epochs, apply the rate + # of the first epoch until it ends, and then the rate of + # the last epoch. + # If more than one epoch is crossed - the gauge gets less, + # but that'd meen it wasn't called for more than 1 year + rate = new_rate + prev_future_epoch = MAX_UINT256 + + self.period = current_period + self.emissions += emissions + if emissions > 0 and not self.is_killed: + Minter(self.minter).mint(self) + # TODO custom logic depending on which bridge we're using + + +@external +def integration_fraction(addr: address) -> uint256: + assert addr == self, "Gauge can only mint for itself" + return self.emissions + + +@external +def set_killed(_is_killed: bool): + """ + @notice Set the killed status for this contract + @dev When killed, the gauge always yields a rate of 0 and so cannot mint CRV + @param _is_killed Killed status to set + """ + assert msg.sender == self.admin + + self.is_killed = _is_killed + + +@external +def commit_transfer_ownership(addr: address): + """ + @notice Transfer ownership of GaugeController to `addr` + @param addr Address to have ownership transferred to + """ + assert msg.sender == self.admin # dev: admin only + + self.future_admin = addr + log CommitOwnership(addr) + + +@external +def accept_transfer_ownership(): + """ + @notice Accept a pending ownership transfer + """ + _admin: address = self.future_admin + assert msg.sender == _admin # dev: future admin only + + self.admin = _admin + log ApplyOwnership(_admin) From 922ff4e72d5d4df0c003aea5e4fb8a63d0340643 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Sun, 9 May 2021 02:04:14 +0600 Subject: [PATCH 02/19] feat: child chain streamer --- .../gauges/sidechain/ChildChainStreamer.vy | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 contracts/gauges/sidechain/ChildChainStreamer.vy diff --git a/contracts/gauges/sidechain/ChildChainStreamer.vy b/contracts/gauges/sidechain/ChildChainStreamer.vy new file mode 100644 index 00000000..54462fe2 --- /dev/null +++ b/contracts/gauges/sidechain/ChildChainStreamer.vy @@ -0,0 +1,214 @@ +# @version 0.2.12 +""" +@title Child-Chain Streamer +@author Curve.Fi +@license MIT +@notice Evenly streams one or more reward tokens to a single recipient +""" + +from vyper.interfaces import ERC20 + +struct RewardToken: + distributor: address + period_finish: uint256 + rate: uint256 + duration: uint256 + received: uint256 + paid: uint256 + + +owner: public(address) +future_owner: public(address) + +reward_receiver: public(address) +reward_tokens: public(address[8]) +reward_count: public(uint256) +reward_data: public(HashMap[address, RewardToken]) +last_update_time: public(uint256) + + +@external +def __init__(_owner: address): + self.owner = _owner + + +@external +def add_reward(_token: address, _distributor: address, _duration: uint256): + """ + @notice Add a reward token + @param _token Address of the reward token + @param _distributor Address permitted to call `notify_reward_amount` for this token + @param _duration Number of seconds that rewards of this token are streamed over + """ + assert msg.sender == self.owner + assert self.reward_data[_token].distributor == ZERO_ADDRESS, "Reward token already added" + + idx: uint256 = self.reward_count + self.reward_tokens[idx] = _token + self.reward_count = idx + 1 + self.reward_data[_token].distributor = _distributor + self.reward_data[_token].duration = _duration + + +@external +def remove_reward(_token: address): + """ + @notice Remove a reward token + @dev Any remaining balance of the reward token is transferred to the owner + @param _token Address of the reward token + """ + assert msg.sender == self.owner + assert self.reward_data[_token].distributor != ZERO_ADDRESS, "Reward token not added" + + self.reward_data[_token] = empty(RewardToken) + amount: uint256 = ERC20(_token).balanceOf(self) + response: Bytes[32] = raw_call( + _token, + concat( + method_id("transfer(address,uint256)"), + convert(msg.sender, bytes32), + convert(amount, bytes32), + ), + max_outsize=32, + ) + if len(response) != 0: + assert convert(response, bool) + + idx: uint256 = self.reward_count - 1 + for i in range(8): + if self.reward_tokens[i] == _token: + self.reward_tokens[i] = self.reward_tokens[idx] + self.reward_tokens[idx] = ZERO_ADDRESS + self.reward_count = idx + return + raise + + +@internal +def _update_reward(_token: address, _last_update: uint256): + # update data about a reward and distribute any pending tokens to the receiver + last_time: uint256 = min(block.timestamp, self.reward_data[_token].period_finish) + if last_time > _last_update: + amount: uint256 = (last_time - _last_update) * self.reward_data[_token].rate + if amount > 0: + self.reward_data[_token].paid += amount + response: Bytes[32] = raw_call( + _token, + concat( + method_id("transfer(address,uint256)"), + convert(self.reward_receiver, bytes32), + convert(amount, bytes32), + ), + max_outsize=32, + ) + if len(response) != 0: + assert convert(response, bool) + + +@external +def set_receiver(_receiver: address): + """ + @notice Set the reward receiver + @dev When the receiver is a smart contract, it must be capable of recognizing + rewards that are directly pushed to it (without a call to `get_reward`) + @param _receiver Address of the reward receiver + """ + assert msg.sender == self.owner # dev: only owner + self.reward_receiver = _receiver + + +@external +def get_reward(): + """ + @notice Claim pending rewards + @dev Only callable by the reward receiver + """ + assert msg.sender == self.reward_receiver, "Caller is not receiver" + last_update: uint256 = self.last_update_time + for token in self.reward_tokens: + if token == ZERO_ADDRESS: + break + self._update_reward(token, last_update) + self.last_update_time = block.timestamp + + +@external +def notify_reward_amount(_token: address): + """ + @notice Notify the contract of a newly received reward + @dev Only callable by the distributor. The reward tokens must be transferred + into the contract prior to calling this function. Rewards are distributed + over `reward_duration` seconds. Updating the reward amount while an existing + reward period is still active causes the remaining rewards to be evenly + distributed over the new reward period. + @param _token Address of the reward token + """ + assert msg.sender == self.reward_data[_token].distributor # dev: only distributor + last_update: uint256 = self.last_update_time + for token in self.reward_tokens: + if token == ZERO_ADDRESS: + break + self._update_reward(token, last_update) + if token == _token: + received: uint256 = self.reward_data[token].received + expected_balance: uint256 = received - self.reward_data[token].paid + actual_balance: uint256 = ERC20(token).balanceOf(self) + if actual_balance > expected_balance: + new_amount: uint256 = actual_balance - expected_balance + duration: uint256 = self.reward_data[token].duration + if block.timestamp >= self.reward_data[token].period_finish: + self.reward_data[token].rate = new_amount / duration + else: + remaining: uint256 = self.reward_data[token].period_finish - block.timestamp + leftover: uint256 = remaining * self.reward_data[token].rate + self.reward_data[token].rate = (new_amount + leftover) / duration + self.reward_data[token].period_finish = block.timestamp + duration + self.reward_data[token].received = received + new_amount + self.last_update_time = block.timestamp + + + +@external +def set_reward_duration(_token: address, _duration: uint256): + """ + @notice Modify the duration that rewards are distributed over + @dev Only callable when there is not an active reward period + @param _token Address of the reward token + @param _duration Number of seconds to distribute rewards over + """ + assert msg.sender == self.owner # dev: only owner + assert block.timestamp > self.reward_data[_token].period_finish, "Reward period still active" + self.reward_data[_token].duration = _duration + + +@external +def set_reward_distributor(_token: address, _distributor: address): + """ + @notice Modify the reward distributor + @param _token Address of the reward token + @param _distributor Reward distributor + """ + assert msg.sender == self.owner # dev: only owner + self.reward_data[_token].distributor = _distributor + + +@external +def commit_transfer_ownership(_owner: address): + """ + @notice Initiate ownership tansfer of the contract + @param _owner Address to have ownership transferred to + """ + assert msg.sender == self.owner # dev: only owner + + self.future_owner = _owner + + +@external +def accept_transfer_ownership(): + """ + @notice Accept a pending ownership transfer + """ + owner: address = self.future_owner + assert msg.sender == owner # dev: only new owner + + self.owner = owner From ffb5f5d505eb89a3e5238c27592c31dc219563dd Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Tue, 11 May 2021 14:20:18 +0600 Subject: [PATCH 03/19] feat: bridge-specific root gauges --- ...{RootChainGauge.vy => RootGaugeAnyswap.vy} | 36 ++- .../gauges/sidechain/RootGaugePolygon.vy | 219 ++++++++++++++++++ contracts/gauges/sidechain/RootGaugeXdai.vy | 213 +++++++++++++++++ 3 files changed, 460 insertions(+), 8 deletions(-) rename contracts/gauges/sidechain/{RootChainGauge.vy => RootGaugeAnyswap.vy} (84%) create mode 100644 contracts/gauges/sidechain/RootGaugePolygon.vy create mode 100644 contracts/gauges/sidechain/RootGaugeXdai.vy diff --git a/contracts/gauges/sidechain/RootChainGauge.vy b/contracts/gauges/sidechain/RootGaugeAnyswap.vy similarity index 84% rename from contracts/gauges/sidechain/RootChainGauge.vy rename to contracts/gauges/sidechain/RootGaugeAnyswap.vy index 7d30c981..a8e678cc 100644 --- a/contracts/gauges/sidechain/RootChainGauge.vy +++ b/contracts/gauges/sidechain/RootGaugeAnyswap.vy @@ -3,6 +3,8 @@ @title Root-Chain Gauge @author Curve Finance @license MIT +@notice Calculates total allocated weekly CRV emission + mints and sends across a sidechain bridge """ from vyper.interfaces import ERC20 @@ -68,9 +70,6 @@ lp_token: public(address) controller: public(address) future_epoch_time: public(uint256) - -# The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint -# All values are kept in units of being multiplied by 1e18 period: public(uint256) emissions: public(uint256) inflation_rate: public(uint256) @@ -79,9 +78,12 @@ admin: public(address) future_admin: public(address) # Can and will be a smart contract is_killed: public(bool) +checkpoint_admin: public(address) +anyswap_bridge: public(address) + @external -def __init__(_lp_token: address, _minter: address, _admin: address): +def __init__(_lp_token: address, _minter: address, _admin: address, _anyswap_bridge: address): """ @notice Contract constructor @param _lp_token Liquidity Pool contract address @@ -97,22 +99,26 @@ def __init__(_lp_token: address, _minter: address, _admin: address): self.admin = _admin self.crv_token = crv_token self.controller = controller + self.anyswap_bridge = _anyswap_bridge self.period = block.timestamp / WEEK self.inflation_rate = CRV20(crv_token).rate() self.future_epoch_time = CRV20(crv_token).future_epoch_time_write() + @external -def checkpoint(): +def checkpoint() -> bool: """ - @notice Checkpoint + @notice Mint all allocated CRV emissions and transfer across the bridge + @dev Should be called once per week, after the new epoch period has begun """ + assert self.checkpoint_admin in [ZERO_ADDRESS, msg.sender] rate: uint256 = self.inflation_rate new_rate: uint256 = rate prev_future_epoch: uint256 = self.future_epoch_time + token: address = self.crv_token if prev_future_epoch < block.timestamp: - token: address = self.crv_token self.future_epoch_time = CRV20(token).future_epoch_time_write() new_rate = CRV20(token).rate() self.inflation_rate = new_rate @@ -145,7 +151,9 @@ def checkpoint(): self.emissions += emissions if emissions > 0 and not self.is_killed: Minter(self.minter).mint(self) - # TODO custom logic depending on which bridge we're using + ERC20(token).transfer(self.anyswap_bridge, emissions) + + return True @external @@ -188,3 +196,15 @@ def accept_transfer_ownership(): self.admin = _admin log ApplyOwnership(_admin) + + +@external +def set_checkpoint_admin(_admin: address): + """ + @notice Set the checkpoint admin address + @dev Setting to ZERO_ADDRESS allows anyone to call `checkpoint` + @param _admin Address of the checkpoint admin + """ + assert msg.sender == self.admin # dev: admin only + + self.checkpoint_admin = _admin diff --git a/contracts/gauges/sidechain/RootGaugePolygon.vy b/contracts/gauges/sidechain/RootGaugePolygon.vy new file mode 100644 index 00000000..bf0df1e2 --- /dev/null +++ b/contracts/gauges/sidechain/RootGaugePolygon.vy @@ -0,0 +1,219 @@ +# @version 0.2.12 +""" +@title Root-Chain Gauge +@author Curve Finance +@license MIT +@notice Calculates total allocated weekly CRV emission + mints and sends across a sidechain bridge +""" + +from vyper.interfaces import ERC20 + + +interface CRV20: + def future_epoch_time_write() -> uint256: nonpayable + def rate() -> uint256: view + +interface Controller: + def period() -> int128: view + def period_write() -> int128: nonpayable + def period_timestamp(p: int128) -> uint256: view + def gauge_relative_weight(addr: address, time: uint256) -> uint256: view + def voting_escrow() -> address: view + def checkpoint(): nonpayable + def checkpoint_gauge(addr: address): nonpayable + +interface Minter: + def token() -> address: view + def controller() -> address: view + def minted(user: address, gauge: address) -> uint256: view + def mint(gauge: address): nonpayable + + +event Deposit: + provider: indexed(address) + value: uint256 + +event Withdraw: + provider: indexed(address) + value: uint256 + +event UpdateLiquidityLimit: + user: address + original_balance: uint256 + original_supply: uint256 + working_balance: uint256 + working_supply: uint256 + +event CommitOwnership: + admin: address + +event ApplyOwnership: + admin: address + +event Transfer: + _from: indexed(address) + _to: indexed(address) + _value: uint256 + +event Approval: + _owner: indexed(address) + _spender: indexed(address) + _value: uint256 + + +WEEK: constant(uint256) = 604800 +POLYGON_BRIDGE_MANAGER: constant(address) = 0xA0c68C638235ee32657e8f720a23ceC1bFc77C77 +POLYGON_BRIDGE_RECEIVER: constant(address) = 0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf + +minter: public(address) +crv_token: public(address) +lp_token: public(address) +controller: public(address) +future_epoch_time: public(uint256) + +period: public(uint256) +emissions: public(uint256) +inflation_rate: public(uint256) + +admin: public(address) +future_admin: public(address) # Can and will be a smart contract +checkpoint_admin: public(address) +is_killed: public(bool) + + +@external +def __init__(_lp_token: address, _minter: address, _admin: address): + """ + @notice Contract constructor + @param _lp_token Liquidity Pool contract address + @param _minter Minter contract address + @param _admin Admin who can kill the gauge + """ + + crv_token: address = Minter(_minter).token() + controller: address = Minter(_minter).controller() + + self.lp_token = _lp_token + self.minter = _minter + self.admin = _admin + self.crv_token = crv_token + self.controller = controller + + self.period = block.timestamp / WEEK + self.inflation_rate = CRV20(crv_token).rate() + self.future_epoch_time = CRV20(crv_token).future_epoch_time_write() + + ERC20(crv_token).approve(POLYGON_BRIDGE_RECEIVER, MAX_UINT256) + + +@external +def checkpoint() -> bool: + """ + @notice Mint all allocated CRV emissions and transfer across the bridge + @dev Should be called once per week, after the new epoch period has begun + """ + assert self.checkpoint_admin in [ZERO_ADDRESS, msg.sender] + rate: uint256 = self.inflation_rate + new_rate: uint256 = rate + prev_future_epoch: uint256 = self.future_epoch_time + token: address = self.crv_token + if prev_future_epoch < block.timestamp: + self.future_epoch_time = CRV20(token).future_epoch_time_write() + new_rate = CRV20(token).rate() + self.inflation_rate = new_rate + + last_period: uint256 = self.period + current_period: uint256 = block.timestamp / WEEK + + if last_period < current_period: + controller: address = self.controller + Controller(controller).checkpoint_gauge(self) + + emissions: uint256 = 0 + for i in range(last_period, last_period+255): + if i > current_period: + break + week_time: uint256 = i * WEEK + gauge_weight: uint256 = Controller(controller).gauge_relative_weight(self, i * WEEK) + emissions += gauge_weight * rate * WEEK / 10**18 + + if prev_future_epoch < week_time: + # If we went across one or multiple epochs, apply the rate + # of the first epoch until it ends, and then the rate of + # the last epoch. + # If more than one epoch is crossed - the gauge gets less, + # but that'd meen it wasn't called for more than 1 year + rate = new_rate + prev_future_epoch = MAX_UINT256 + + self.period = current_period + self.emissions += emissions + if emissions > 0 and not self.is_killed: + Minter(self.minter).mint(self) + raw_call( + POLYGON_BRIDGE_MANAGER, + concat( + method_id("depositFor(address,address,bytes)"), + convert(self, bytes32), + convert(token, bytes32), + convert(96, bytes32), + convert(32, bytes32), + convert(emissions, bytes32), + ) + ) + return True + + +@external +def integration_fraction(addr: address) -> uint256: + assert addr == self, "Gauge can only mint for itself" + return self.emissions + + +@external +def set_killed(_is_killed: bool): + """ + @notice Set the killed status for this contract + @dev When killed, the gauge always yields a rate of 0 and so cannot mint CRV + @param _is_killed Killed status to set + """ + assert msg.sender == self.admin + + self.is_killed = _is_killed + + +@external +def commit_transfer_ownership(addr: address): + """ + @notice Transfer ownership of GaugeController to `addr` + @param addr Address to have ownership transferred to + """ + assert msg.sender == self.admin # dev: admin only + + self.future_admin = addr + log CommitOwnership(addr) + + +@external +def accept_transfer_ownership(): + """ + @notice Accept a pending ownership transfer + """ + _admin: address = self.future_admin + assert msg.sender == _admin # dev: future admin only + + self.admin = _admin + log ApplyOwnership(_admin) + + +@external +def set_checkpoint_admin(_admin: address): + """ + @notice Set the checkpoint admin address + @dev Setting to ZERO_ADDRESS allows anyone to call `checkpoint` + @param _admin Address of the checkpoint admin + """ + assert msg.sender == self.admin # dev: admin only + + self.checkpoint_admin = _admin diff --git a/contracts/gauges/sidechain/RootGaugeXdai.vy b/contracts/gauges/sidechain/RootGaugeXdai.vy new file mode 100644 index 00000000..2ba9c95c --- /dev/null +++ b/contracts/gauges/sidechain/RootGaugeXdai.vy @@ -0,0 +1,213 @@ +# @version 0.2.12 +""" +@title Root-Chain Gauge +@author Curve Finance +@license MIT +@notice Calculates total allocated weekly CRV emission + mints and sends across a sidechain bridge +""" + +from vyper.interfaces import ERC20 + + +interface CRV20: + def future_epoch_time_write() -> uint256: nonpayable + def rate() -> uint256: view + +interface Controller: + def period() -> int128: view + def period_write() -> int128: nonpayable + def period_timestamp(p: int128) -> uint256: view + def gauge_relative_weight(addr: address, time: uint256) -> uint256: view + def voting_escrow() -> address: view + def checkpoint(): nonpayable + def checkpoint_gauge(addr: address): nonpayable + +interface Minter: + def token() -> address: view + def controller() -> address: view + def minted(user: address, gauge: address) -> uint256: view + def mint(gauge: address): nonpayable + +interface XDaiBridge: + def relayTokens(_token: address, _receiver: address, _amount: uint256): nonpayable + + +event Deposit: + provider: indexed(address) + value: uint256 + +event Withdraw: + provider: indexed(address) + value: uint256 + +event UpdateLiquidityLimit: + user: address + original_balance: uint256 + original_supply: uint256 + working_balance: uint256 + working_supply: uint256 + +event CommitOwnership: + admin: address + +event ApplyOwnership: + admin: address + +event Transfer: + _from: indexed(address) + _to: indexed(address) + _value: uint256 + +event Approval: + _owner: indexed(address) + _spender: indexed(address) + _value: uint256 + + +WEEK: constant(uint256) = 604800 +XDAI_BRIDGE: constant(address) = 0x88ad09518695c6c3712AC10a214bE5109a655671 + + +minter: public(address) +crv_token: public(address) +lp_token: public(address) +controller: public(address) +future_epoch_time: public(uint256) + +period: public(uint256) +emissions: public(uint256) +inflation_rate: public(uint256) + +admin: public(address) +future_admin: public(address) # Can and will be a smart contract +checkpoint_admin: public(address) +is_killed: public(bool) + + +@external +def __init__(_lp_token: address, _minter: address, _admin: address): + """ + @notice Contract constructor + @param _lp_token Liquidity Pool contract address + @param _minter Minter contract address + @param _admin Admin who can kill the gauge + """ + + crv_token: address = Minter(_minter).token() + controller: address = Minter(_minter).controller() + + self.lp_token = _lp_token + self.minter = _minter + self.admin = _admin + self.crv_token = crv_token + self.controller = controller + + self.period = block.timestamp / WEEK + self.inflation_rate = CRV20(crv_token).rate() + self.future_epoch_time = CRV20(crv_token).future_epoch_time_write() + + ERC20(crv_token).approve(XDAI_BRIDGE, MAX_UINT256) + + +@external +def checkpoint() -> bool: + """ + @notice Mint all allocated CRV emissions and transfer across the bridge + @dev Should be called once per week, after the new epoch period has begun + """ + assert self.checkpoint_admin in [ZERO_ADDRESS, msg.sender] + rate: uint256 = self.inflation_rate + new_rate: uint256 = rate + prev_future_epoch: uint256 = self.future_epoch_time + token: address = self.crv_token + if prev_future_epoch < block.timestamp: + self.future_epoch_time = CRV20(token).future_epoch_time_write() + new_rate = CRV20(token).rate() + self.inflation_rate = new_rate + + last_period: uint256 = self.period + current_period: uint256 = block.timestamp / WEEK + + if last_period < current_period: + controller: address = self.controller + Controller(controller).checkpoint_gauge(self) + + emissions: uint256 = 0 + for i in range(last_period, last_period+255): + if i > current_period: + break + week_time: uint256 = i * WEEK + gauge_weight: uint256 = Controller(controller).gauge_relative_weight(self, i * WEEK) + emissions += gauge_weight * rate * WEEK / 10**18 + + if prev_future_epoch < week_time: + # If we went across one or multiple epochs, apply the rate + # of the first epoch until it ends, and then the rate of + # the last epoch. + # If more than one epoch is crossed - the gauge gets less, + # but that'd meen it wasn't called for more than 1 year + rate = new_rate + prev_future_epoch = MAX_UINT256 + + self.period = current_period + self.emissions += emissions + if emissions > 0 and not self.is_killed: + Minter(self.minter).mint(self) + XDaiBridge(XDAI_BRIDGE).relayTokens(token, self, emissions) + + return True + + +@external +def integration_fraction(addr: address) -> uint256: + assert addr == self, "Gauge can only mint for itself" + return self.emissions + + +@external +def set_killed(_is_killed: bool): + """ + @notice Set the killed status for this contract + @dev When killed, the gauge always yields a rate of 0 and so cannot mint CRV + @param _is_killed Killed status to set + """ + assert msg.sender == self.admin + + self.is_killed = _is_killed + + +@external +def commit_transfer_ownership(addr: address): + """ + @notice Transfer ownership of GaugeController to `addr` + @param addr Address to have ownership transferred to + """ + assert msg.sender == self.admin # dev: admin only + + self.future_admin = addr + log CommitOwnership(addr) + + +@external +def accept_transfer_ownership(): + """ + @notice Accept a pending ownership transfer + """ + _admin: address = self.future_admin + assert msg.sender == _admin # dev: future admin only + + self.admin = _admin + log ApplyOwnership(_admin) + + +@external +def set_checkpoint_admin(_admin: address): + """ + @notice Set the checkpoint admin address + @dev Setting to ZERO_ADDRESS allows anyone to call `checkpoint` + @param _admin Address of the checkpoint admin + """ + assert msg.sender == self.admin # dev: admin only + + self.checkpoint_admin = _admin From 2e387ddddb47b515814d47f203f2815af88ae19f Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 11 May 2021 11:26:22 -0400 Subject: [PATCH 04/19] chore: add root chain gauge fixtures --- tests/unitary/Sidechain/conftest.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/unitary/Sidechain/conftest.py diff --git a/tests/unitary/Sidechain/conftest.py b/tests/unitary/Sidechain/conftest.py new file mode 100644 index 00000000..fbd6f3d8 --- /dev/null +++ b/tests/unitary/Sidechain/conftest.py @@ -0,0 +1,27 @@ +import pytest +from brownie import ETH_ADDRESS + + +@pytest.fixture(scope="module") +def anyswap_root_gauge(RootGaugeAnyswap, mock_lp_token, minter, alice): + return RootGaugeAnyswap.deploy(mock_lp_token, minter, alice, ETH_ADDRESS, {"from": alice}) + + +@pytest.fixture(scope="module") +def polygon_root_gauge(RootGaugePolygon, mock_lp_token, minter, alice): + return RootGaugePolygon.deploy(mock_lp_token, minter, alice, {"from": alice}) + + +@pytest.fixture(scope="module") +def xdai_root_gauge(RootGaugeXdai, mock_lp_token, minter, alice): + return RootGaugeXdai.deploy(mock_lp_token, minter, alice, {"from": alice}) + + +@pytest.fixture(scope="module", params=range(3), ids=["Anyswap", "Polygon", "XDAI"]) +def root_gauge(request, anyswap_root_gauge, polygon_root_gauge, xdai_root_gauge): + if request.param == 0: + return anyswap_root_gauge + elif request.param == 1: + return polygon_root_gauge + else: + return xdai_root_gauge From 982ca2fff2da604e43b9f6138073d814519b08eb Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 11 May 2021 11:34:03 -0400 Subject: [PATCH 05/19] test: set_checkpoint_admin for root gauges --- .../unitary/Sidechain/test_set_checkpoint_admin.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/unitary/Sidechain/test_set_checkpoint_admin.py diff --git a/tests/unitary/Sidechain/test_set_checkpoint_admin.py b/tests/unitary/Sidechain/test_set_checkpoint_admin.py new file mode 100644 index 00000000..98ae645f --- /dev/null +++ b/tests/unitary/Sidechain/test_set_checkpoint_admin.py @@ -0,0 +1,14 @@ +import pytest +import brownie + + +@pytest.mark.parametrize("idx", range(1, 6)) +def test_admin_only(root_gauge, accounts, idx): + with brownie.reverts("dev: admin only"): + root_gauge.set_checkpoint_admin(accounts[idx], {"from": accounts[idx]}) + + +def test_immediate_change_of_admin(alice, bob, root_gauge): + root_gauge.set_checkpoint_admin(bob, {"from": alice}) + + assert root_gauge.checkpoint_admin() == bob From ccc44224b0601de7ae4303f4cc807793f7072d22 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 11 May 2021 11:39:47 -0400 Subject: [PATCH 06/19] test: root gauges commit_transfer_ownership fn --- .../test_commit_transfer_ownership.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/unitary/Sidechain/test_commit_transfer_ownership.py diff --git a/tests/unitary/Sidechain/test_commit_transfer_ownership.py b/tests/unitary/Sidechain/test_commit_transfer_ownership.py new file mode 100644 index 00000000..f24c36b9 --- /dev/null +++ b/tests/unitary/Sidechain/test_commit_transfer_ownership.py @@ -0,0 +1,30 @@ +import pytest +import brownie + + +@pytest.mark.parametrize("idx", range(1, 6)) +def test_admin_only(root_gauge, accounts, idx): + with brownie.reverts("dev: admin only"): + root_gauge.commit_transfer_ownership(accounts[idx], {"from": accounts[idx]}) + + +def test_admin_remains_the_same_future_admin_changed(alice, bob, root_gauge): + before = root_gauge.future_admin() + root_gauge.commit_transfer_ownership(bob, {"from": alice}) + + assert root_gauge.admin() == alice + assert root_gauge.future_admin() == bob and before != bob + + +def test_future_admin_not_admin_until_accepted(alice, bob, charlie, root_gauge): + root_gauge.commit_transfer_ownership(bob, {"from": alice}) + + with brownie.reverts("dev: admin only"): + root_gauge.commit_transfer_ownership(charlie, {"from": bob}) + + +def test_event_emitted(alice, bob, root_gauge): + tx = root_gauge.commit_transfer_ownership(bob, {"from": alice}) + + assert "CommitOwnership" in tx.events + assert tx.events["CommitOwnership"]["admin"] == bob From 9f66cd43037d399cd5fc5a434fbcb02eb43843f5 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 11 May 2021 11:47:15 -0400 Subject: [PATCH 07/19] test: accept transfer ownership fn root gauges --- .../test_accept_transfer_ownership.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/unitary/Sidechain/test_accept_transfer_ownership.py diff --git a/tests/unitary/Sidechain/test_accept_transfer_ownership.py b/tests/unitary/Sidechain/test_accept_transfer_ownership.py new file mode 100644 index 00000000..1f21bbd9 --- /dev/null +++ b/tests/unitary/Sidechain/test_accept_transfer_ownership.py @@ -0,0 +1,28 @@ +import pytest +import brownie + + +@pytest.fixture(scope="module", autouse=True) +def local_setup(alice, bob, root_gauge): + root_gauge.commit_transfer_ownership(bob, {"from": alice}) + + +@pytest.mark.parametrize("idx", range(2, 6)) +def test_future_admin_only(root_gauge, accounts, idx): + with brownie.reverts("dev: future admin only"): + root_gauge.accept_transfer_ownership({"from": accounts[idx]}) + + +def test_admin_updated(root_gauge, bob): + before = root_gauge.admin() + root_gauge.accept_transfer_ownership({"from": bob}) + + assert before != bob + assert root_gauge.admin() == bob + + +def test_event_emitted(bob, root_gauge): + tx = root_gauge.accept_transfer_ownership({"from": bob}) + + assert "ApplyOwnership" in tx.events + assert tx.events["ApplyOwnership"]["admin"] == bob From a6c657d985bcf4de6086f9039a3425f405f607b8 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 11 May 2021 11:51:58 -0400 Subject: [PATCH 08/19] fix: add dev revert comment --- contracts/gauges/sidechain/RootGaugeAnyswap.vy | 2 +- contracts/gauges/sidechain/RootGaugePolygon.vy | 2 +- contracts/gauges/sidechain/RootGaugeXdai.vy | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/gauges/sidechain/RootGaugeAnyswap.vy b/contracts/gauges/sidechain/RootGaugeAnyswap.vy index a8e678cc..092c8003 100644 --- a/contracts/gauges/sidechain/RootGaugeAnyswap.vy +++ b/contracts/gauges/sidechain/RootGaugeAnyswap.vy @@ -169,7 +169,7 @@ def set_killed(_is_killed: bool): @dev When killed, the gauge always yields a rate of 0 and so cannot mint CRV @param _is_killed Killed status to set """ - assert msg.sender == self.admin + assert msg.sender == self.admin # dev: admin only self.is_killed = _is_killed diff --git a/contracts/gauges/sidechain/RootGaugePolygon.vy b/contracts/gauges/sidechain/RootGaugePolygon.vy index bf0df1e2..1f7191ad 100644 --- a/contracts/gauges/sidechain/RootGaugePolygon.vy +++ b/contracts/gauges/sidechain/RootGaugePolygon.vy @@ -178,7 +178,7 @@ def set_killed(_is_killed: bool): @dev When killed, the gauge always yields a rate of 0 and so cannot mint CRV @param _is_killed Killed status to set """ - assert msg.sender == self.admin + assert msg.sender == self.admin # dev: admin only self.is_killed = _is_killed diff --git a/contracts/gauges/sidechain/RootGaugeXdai.vy b/contracts/gauges/sidechain/RootGaugeXdai.vy index 2ba9c95c..2fe32c93 100644 --- a/contracts/gauges/sidechain/RootGaugeXdai.vy +++ b/contracts/gauges/sidechain/RootGaugeXdai.vy @@ -172,7 +172,7 @@ def set_killed(_is_killed: bool): @dev When killed, the gauge always yields a rate of 0 and so cannot mint CRV @param _is_killed Killed status to set """ - assert msg.sender == self.admin + assert msg.sender == self.admin # dev: admin only self.is_killed = _is_killed From 07ea51e4b1406864b123f1ed9af24b66b054f015 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 11 May 2021 11:52:17 -0400 Subject: [PATCH 09/19] test: set_killed fn root gauges --- tests/unitary/Sidechain/test_set_killed.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/unitary/Sidechain/test_set_killed.py diff --git a/tests/unitary/Sidechain/test_set_killed.py b/tests/unitary/Sidechain/test_set_killed.py new file mode 100644 index 00000000..b6aa60cd --- /dev/null +++ b/tests/unitary/Sidechain/test_set_killed.py @@ -0,0 +1,14 @@ +import pytest +import brownie + + +@pytest.mark.parametrize("idx", range(1, 6)) +def test_admin_only(root_gauge, accounts, idx): + with brownie.reverts("dev: admin only"): + root_gauge.set_killed(True, {"from": accounts[idx]}) + + +def test_set_killed_updates_state(alice, root_gauge): + root_gauge.set_killed(True, {"from": alice}) + + assert root_gauge.is_killed() == True From 84edf7f14e72cd54ec77300e474a5b5f3b1e4a3a Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Wed, 12 May 2021 01:23:22 +0600 Subject: [PATCH 10/19] feat: checkpoint proxy --- contracts/gauges/sidechain/CheckpointProxy.vy | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 contracts/gauges/sidechain/CheckpointProxy.vy diff --git a/contracts/gauges/sidechain/CheckpointProxy.vy b/contracts/gauges/sidechain/CheckpointProxy.vy new file mode 100644 index 00000000..e0781b06 --- /dev/null +++ b/contracts/gauges/sidechain/CheckpointProxy.vy @@ -0,0 +1,27 @@ +# @version 0.2.12 +""" +@title Checkpoint Proxy +@author Curve.Fi +@license MIT +@notice Calls `checkpoint` on Anyswap gauges to meet bridge whitelisting requirements +""" + +interface RootGauge: + def checkpoint() -> bool: nonpayable + + +@external +def checkpoint(_gauge: address) -> bool: + RootGauge(_gauge).checkpoint() + + return True + + +@external +def checkpoint_many(_gauges: address[10]) -> bool: + for gauge in _gauges: + if gauge == ZERO_ADDRESS: + break + RootGauge(gauge).checkpoint() + + return True From d3b6479541ae720c987cbeb400038f5920ce7143 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Wed, 12 May 2021 02:06:47 +0600 Subject: [PATCH 11/19] fix: required minter view methods --- contracts/gauges/sidechain/RootGaugeAnyswap.vy | 9 ++++++++- contracts/gauges/sidechain/RootGaugePolygon.vy | 9 ++++++++- contracts/gauges/sidechain/RootGaugeXdai.vy | 9 ++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/contracts/gauges/sidechain/RootGaugeAnyswap.vy b/contracts/gauges/sidechain/RootGaugeAnyswap.vy index 092c8003..146e5d9c 100644 --- a/contracts/gauges/sidechain/RootGaugeAnyswap.vy +++ b/contracts/gauges/sidechain/RootGaugeAnyswap.vy @@ -156,8 +156,15 @@ def checkpoint() -> bool: return True +@view @external -def integration_fraction(addr: address) -> uint256: +def user_checkpoint(addr: address) -> bool: + return True + + +@view +@external +def integrate_fraction(addr: address) -> uint256: assert addr == self, "Gauge can only mint for itself" return self.emissions diff --git a/contracts/gauges/sidechain/RootGaugePolygon.vy b/contracts/gauges/sidechain/RootGaugePolygon.vy index 1f7191ad..e4321a31 100644 --- a/contracts/gauges/sidechain/RootGaugePolygon.vy +++ b/contracts/gauges/sidechain/RootGaugePolygon.vy @@ -165,8 +165,15 @@ def checkpoint() -> bool: return True +@view @external -def integration_fraction(addr: address) -> uint256: +def user_checkpoint(addr: address) -> bool: + return True + + +@view +@external +def integrate_fraction(addr: address) -> uint256: assert addr == self, "Gauge can only mint for itself" return self.emissions diff --git a/contracts/gauges/sidechain/RootGaugeXdai.vy b/contracts/gauges/sidechain/RootGaugeXdai.vy index 2fe32c93..ce68487d 100644 --- a/contracts/gauges/sidechain/RootGaugeXdai.vy +++ b/contracts/gauges/sidechain/RootGaugeXdai.vy @@ -159,8 +159,15 @@ def checkpoint() -> bool: return True +@view @external -def integration_fraction(addr: address) -> uint256: +def user_checkpoint(addr: address) -> bool: + return True + + +@view +@external +def integrate_fraction(addr: address) -> uint256: assert addr == self, "Gauge can only mint for itself" return self.emissions From 1b6566b0bfee2c0d197ba1d1d33a7750755dc489 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Wed, 12 May 2021 02:19:14 +0600 Subject: [PATCH 12/19] fix: increment last_period before iteration --- contracts/gauges/sidechain/RootGaugeAnyswap.vy | 1 + contracts/gauges/sidechain/RootGaugePolygon.vy | 1 + contracts/gauges/sidechain/RootGaugeXdai.vy | 1 + 3 files changed, 3 insertions(+) diff --git a/contracts/gauges/sidechain/RootGaugeAnyswap.vy b/contracts/gauges/sidechain/RootGaugeAnyswap.vy index 146e5d9c..6ac66d5a 100644 --- a/contracts/gauges/sidechain/RootGaugeAnyswap.vy +++ b/contracts/gauges/sidechain/RootGaugeAnyswap.vy @@ -131,6 +131,7 @@ def checkpoint() -> bool: Controller(controller).checkpoint_gauge(self) emissions: uint256 = 0 + last_period += 1 for i in range(last_period, last_period+255): if i > current_period: break diff --git a/contracts/gauges/sidechain/RootGaugePolygon.vy b/contracts/gauges/sidechain/RootGaugePolygon.vy index e4321a31..951eff0a 100644 --- a/contracts/gauges/sidechain/RootGaugePolygon.vy +++ b/contracts/gauges/sidechain/RootGaugePolygon.vy @@ -131,6 +131,7 @@ def checkpoint() -> bool: Controller(controller).checkpoint_gauge(self) emissions: uint256 = 0 + last_period += 1 for i in range(last_period, last_period+255): if i > current_period: break diff --git a/contracts/gauges/sidechain/RootGaugeXdai.vy b/contracts/gauges/sidechain/RootGaugeXdai.vy index ce68487d..4d10ebcb 100644 --- a/contracts/gauges/sidechain/RootGaugeXdai.vy +++ b/contracts/gauges/sidechain/RootGaugeXdai.vy @@ -134,6 +134,7 @@ def checkpoint() -> bool: Controller(controller).checkpoint_gauge(self) emissions: uint256 = 0 + last_period += 1 for i in range(last_period, last_period+255): if i > current_period: break From 1771b52feb7a7c08ab6af6d572e938e8cdfec78f Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Wed, 12 May 2021 02:28:35 +0600 Subject: [PATCH 13/19] test: checkpoint math (bad test, sorry ed) --- .../unitary/Sidechain/test_root_checkpoint.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/unitary/Sidechain/test_root_checkpoint.py diff --git a/tests/unitary/Sidechain/test_root_checkpoint.py b/tests/unitary/Sidechain/test_root_checkpoint.py new file mode 100644 index 00000000..0e767619 --- /dev/null +++ b/tests/unitary/Sidechain/test_root_checkpoint.py @@ -0,0 +1,35 @@ +import pytest + +WEEK = 7 * 86400 + + +@pytest.mark.skip_coverage +def test_relative_weight_write(accounts, chain, gauge_controller, liquidity_gauge, root_gauge, token, minter): + token.set_minter(minter, {'from': accounts[0]}) + chain.mine(timedelta=WEEK) + token.update_mining_parameters({'from': accounts[0]}) + + gauge_controller.add_type("Test", 10**18, {'from': accounts[0]}) + gauge_controller.add_gauge(liquidity_gauge, 0, 0, {"from": accounts[0]}) + gauge_controller.add_gauge(root_gauge, 0, 1, {"from": accounts[0]}) + + chain.mine(timedelta=WEEK) + + rate = token.rate() + total_emissions = 0 + assert rate > 0 + + for i in range(1, 110): + # 110 weeks ensures we see 2 reductions in the rate + root_gauge.checkpoint() + new_emissions = root_gauge.emissions() - total_emissions + expected = rate * WEEK // i + assert abs(new_emissions - expected) / expected < 0.0001 + + total_emissions += new_emissions + rate = token.rate() + + # increaseing the gauge weight on `liquidity_gauge` each week reducees + # the expected emission for `root_gauge` in the following week + gauge_controller.change_gauge_weight(liquidity_gauge, i, {'from': accounts[0]}) + chain.mine(timedelta=WEEK) From fc243fe399958466a51a43be21a1304cbbfcdb43 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 11 May 2021 18:49:52 -0400 Subject: [PATCH 14/19] feat: replace XDAIBridge interface with a raw_call No need to mock the contract since we're using a raw_call now. --- contracts/gauges/sidechain/RootGaugeXdai.vy | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/contracts/gauges/sidechain/RootGaugeXdai.vy b/contracts/gauges/sidechain/RootGaugeXdai.vy index 4d10ebcb..48e8f7e4 100644 --- a/contracts/gauges/sidechain/RootGaugeXdai.vy +++ b/contracts/gauges/sidechain/RootGaugeXdai.vy @@ -29,9 +29,6 @@ interface Minter: def minted(user: address, gauge: address) -> uint256: view def mint(gauge: address): nonpayable -interface XDaiBridge: - def relayTokens(_token: address, _receiver: address, _amount: uint256): nonpayable - event Deposit: provider: indexed(address) @@ -155,7 +152,15 @@ def checkpoint() -> bool: self.emissions += emissions if emissions > 0 and not self.is_killed: Minter(self.minter).mint(self) - XDaiBridge(XDAI_BRIDGE).relayTokens(token, self, emissions) + raw_call( + XDAI_BRIDGE, + concat( + method_id("relayTokens(address,address,uint256)"), + convert(token, bytes32), + convert(self, bytes32), + convert(emissions, bytes32), + ) + ) return True From b7e0f5d85c990e1423fd452e20ba67b061f5d218 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 11 May 2021 19:28:08 -0400 Subject: [PATCH 15/19] fix: modify checkpoitn test setup --- .../unitary/Sidechain/test_root_checkpoint.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/unitary/Sidechain/test_root_checkpoint.py b/tests/unitary/Sidechain/test_root_checkpoint.py index 0e767619..7457c683 100644 --- a/tests/unitary/Sidechain/test_root_checkpoint.py +++ b/tests/unitary/Sidechain/test_root_checkpoint.py @@ -1,20 +1,24 @@ import pytest +import math WEEK = 7 * 86400 -@pytest.mark.skip_coverage -def test_relative_weight_write(accounts, chain, gauge_controller, liquidity_gauge, root_gauge, token, minter): - token.set_minter(minter, {'from': accounts[0]}) +@pytest.fixture(autouse=True) +def local_setup(alice, chain, gauge_controller, liquidity_gauge, root_gauge, token, minter): + token.set_minter(minter, {"from": alice}) chain.mine(timedelta=WEEK) - token.update_mining_parameters({'from': accounts[0]}) + token.update_mining_parameters({"from": alice}) - gauge_controller.add_type("Test", 10**18, {'from': accounts[0]}) - gauge_controller.add_gauge(liquidity_gauge, 0, 0, {"from": accounts[0]}) - gauge_controller.add_gauge(root_gauge, 0, 1, {"from": accounts[0]}) + gauge_controller.add_type("Test", 10 ** 18, {"from": alice}) + gauge_controller.add_gauge(liquidity_gauge, 0, 0, {"from": alice}) + gauge_controller.add_gauge(root_gauge, 0, 1, {"from": alice}) chain.mine(timedelta=WEEK) + +@pytest.mark.skip_coverage +def test_relative_weight_write(alice, chain, gauge_controller, liquidity_gauge, root_gauge, token): rate = token.rate() total_emissions = 0 assert rate > 0 @@ -24,12 +28,13 @@ def test_relative_weight_write(accounts, chain, gauge_controller, liquidity_gaug root_gauge.checkpoint() new_emissions = root_gauge.emissions() - total_emissions expected = rate * WEEK // i - assert abs(new_emissions - expected) / expected < 0.0001 + + assert math.isclose(new_emissions, expected) total_emissions += new_emissions rate = token.rate() # increaseing the gauge weight on `liquidity_gauge` each week reducees # the expected emission for `root_gauge` in the following week - gauge_controller.change_gauge_weight(liquidity_gauge, i, {'from': accounts[0]}) + gauge_controller.change_gauge_weight(liquidity_gauge, i, {"from": alice}) chain.mine(timedelta=WEEK) From 82e4123ff5985ea7ac52e017ffd427b0c044048e Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 11 May 2021 22:26:05 -0400 Subject: [PATCH 16/19] test: child chain streamer functions --- .../gauges/sidechain/ChildChainStreamer.vy | 6 +-- .../child_chain_streamer/test_add_reward.py | 24 +++++++++ .../test_admin_functions.py | 47 ++++++++++++++++ .../child_chain_streamer/test_get_reward.py | 37 +++++++++++++ .../test_notify_reward_amount.py | 54 +++++++++++++++++++ .../test_remove_reward.py | 42 +++++++++++++++ tests/unitary/Sidechain/conftest.py | 5 ++ 7 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 tests/unitary/Sidechain/child_chain_streamer/test_add_reward.py create mode 100644 tests/unitary/Sidechain/child_chain_streamer/test_admin_functions.py create mode 100644 tests/unitary/Sidechain/child_chain_streamer/test_get_reward.py create mode 100644 tests/unitary/Sidechain/child_chain_streamer/test_notify_reward_amount.py create mode 100644 tests/unitary/Sidechain/child_chain_streamer/test_remove_reward.py diff --git a/contracts/gauges/sidechain/ChildChainStreamer.vy b/contracts/gauges/sidechain/ChildChainStreamer.vy index 54462fe2..8db7d224 100644 --- a/contracts/gauges/sidechain/ChildChainStreamer.vy +++ b/contracts/gauges/sidechain/ChildChainStreamer.vy @@ -40,7 +40,7 @@ def add_reward(_token: address, _distributor: address, _duration: uint256): @param _distributor Address permitted to call `notify_reward_amount` for this token @param _duration Number of seconds that rewards of this token are streamed over """ - assert msg.sender == self.owner + assert msg.sender == self.owner # dev: owner only assert self.reward_data[_token].distributor == ZERO_ADDRESS, "Reward token already added" idx: uint256 = self.reward_count @@ -57,7 +57,7 @@ def remove_reward(_token: address): @dev Any remaining balance of the reward token is transferred to the owner @param _token Address of the reward token """ - assert msg.sender == self.owner + assert msg.sender == self.owner # dev: only owner assert self.reward_data[_token].distributor != ZERO_ADDRESS, "Reward token not added" self.reward_data[_token] = empty(RewardToken) @@ -81,7 +81,7 @@ def remove_reward(_token: address): self.reward_tokens[idx] = ZERO_ADDRESS self.reward_count = idx return - raise + raise # this should never be reached @internal diff --git a/tests/unitary/Sidechain/child_chain_streamer/test_add_reward.py b/tests/unitary/Sidechain/child_chain_streamer/test_add_reward.py new file mode 100644 index 00000000..c900cffa --- /dev/null +++ b/tests/unitary/Sidechain/child_chain_streamer/test_add_reward.py @@ -0,0 +1,24 @@ +import brownie + + +def test_only_owner(bob, child_chain_streamer, token): + with brownie.reverts("dev: owner only"): + child_chain_streamer.add_reward(token, bob, 86400 * 7, {"from": bob}) + + +def test_reward_data_updated(alice, charlie, child_chain_streamer, token): + + child_chain_streamer.add_reward(token, charlie, 86400 * 7, {"from": alice}) + expected_data = (charlie, 0, 0, 86400 * 7, 0, 0) + + assert child_chain_streamer.reward_count() == 1 + assert child_chain_streamer.reward_tokens(0) == token + assert child_chain_streamer.reward_data(token) == expected_data + + +def test_reverts_for_double_adding(alice, charlie, child_chain_streamer, token): + child_chain_streamer.add_reward(token, charlie, 86400 * 7, {"from": alice}) + + with brownie.reverts("Reward token already added"): + child_chain_streamer.add_reward(token, charlie, 86400 * 7, {"from": alice}) + diff --git a/tests/unitary/Sidechain/child_chain_streamer/test_admin_functions.py b/tests/unitary/Sidechain/child_chain_streamer/test_admin_functions.py new file mode 100644 index 00000000..8556c1d8 --- /dev/null +++ b/tests/unitary/Sidechain/child_chain_streamer/test_admin_functions.py @@ -0,0 +1,47 @@ +import brownie + + +def test_set_reward_duration_admin_only(alice, bob, token, child_chain_streamer): + with brownie.reverts("dev: only owner"): + child_chain_streamer.set_reward_duration(token, 86400 * 7, {"from": bob}) + + child_chain_streamer.set_reward_duration(token, 86400 * 7, {"from": alice}) + + assert child_chain_streamer.reward_data(token)["duration"] == 86400 * 7 + + +def test_set_reward_distributor_admin_only(alice, bob, charlie, token, child_chain_streamer): + with brownie.reverts("dev: only owner"): + child_chain_streamer.set_reward_distributor(token, bob, {"from": bob}) + + child_chain_streamer.set_reward_distributor(token, charlie, {"from": alice}) + + assert child_chain_streamer.reward_data(token)["distributor"] == charlie + + +def test_commit_transfer_ownership_admin_only(alice, bob, charlie, child_chain_streamer): + with brownie.reverts("dev: only owner"): + child_chain_streamer.commit_transfer_ownership(bob, {"from": bob}) + + child_chain_streamer.commit_transfer_ownership(charlie, {"from": alice}) + + assert child_chain_streamer.future_owner() == charlie + + +def test_accept_transfer_ownership_future_admin_only(alice, bob, charlie, child_chain_streamer): + child_chain_streamer.commit_transfer_ownership(charlie, {"from": alice}) + + with brownie.reverts("dev: only new owner"): + child_chain_streamer.accept_transfer_ownership({"from": bob}) + + child_chain_streamer.accept_transfer_ownership({"from": charlie}) + + assert child_chain_streamer.owner() == charlie + + +def test_set_receiver_admin_only(alice, bob, charlie, child_chain_streamer): + with brownie.reverts("dev: only owner"): + child_chain_streamer.set_receiver(bob, {"from": bob}) + + child_chain_streamer.set_receiver(charlie, {"from": alice}) + assert child_chain_streamer.reward_receiver == charlie diff --git a/tests/unitary/Sidechain/child_chain_streamer/test_get_reward.py b/tests/unitary/Sidechain/child_chain_streamer/test_get_reward.py new file mode 100644 index 00000000..ecfef38a --- /dev/null +++ b/tests/unitary/Sidechain/child_chain_streamer/test_get_reward.py @@ -0,0 +1,37 @@ +import brownie +import pytest +import math + + +DAY = 86400 + + +@pytest.fixture(scope="module", autouse=True) +def local_setup(alice, bob, charlie, coin_reward, child_chain_streamer): + coin_reward._mint_for_testing(100 * 10 ** 18, {"from": alice}) + coin_reward.transfer(child_chain_streamer, 100 * 10 ** 18, {"from": alice}) + child_chain_streamer.add_reward(coin_reward, charlie, DAY * 7, {"from": alice}) + child_chain_streamer.set_receiver(bob, {"from": alice}) + child_chain_streamer.notify_reward_amount(coin_reward, {"from": charlie}) + + +def test_receiver_only(alice, child_chain_streamer): + with brownie.reverts("Caller is not receiver"): + child_chain_streamer.get_reward({"from": alice}) + + +def test_last_update_time(bob, chain, child_chain_streamer): + chain.sleep(1000) + tx = child_chain_streamer.get_reward({"from": bob}) + + assert tx.timestamp == child_chain_streamer.last_update_time() + + +def test_update_reward(bob, coin_reward, chain, child_chain_streamer): + chain.mine(timedelta=DAY * 3) + tx = child_chain_streamer.get_reward({"from": bob}) + + assert tx.subcalls[-1]["to"] == coin_reward + assert tx.subcalls[-1]["function"] == "transfer(address,uint256)" + assert math.isclose(coin_reward.balanceOf(bob), 3 * 100 * 10 ** 18 // 7, rel_tol=0.0001) + diff --git a/tests/unitary/Sidechain/child_chain_streamer/test_notify_reward_amount.py b/tests/unitary/Sidechain/child_chain_streamer/test_notify_reward_amount.py new file mode 100644 index 00000000..8d9699cd --- /dev/null +++ b/tests/unitary/Sidechain/child_chain_streamer/test_notify_reward_amount.py @@ -0,0 +1,54 @@ +from tests.conftest import bob +from brownie.network.state import Chain +import pytest +import brownie +import math + + +DAY = 86400 + + +@pytest.fixture(scope="module", autouse=True) +def local_setup(alice, bob, charlie, chain, coin_reward, child_chain_streamer): + coin_reward._mint_for_testing(100 * 10 ** 18, {"from": alice}) + coin_reward.transfer(child_chain_streamer, 100 * 10 ** 18, {"from": alice}) + + child_chain_streamer.add_reward(coin_reward, charlie, DAY * 14, {"from": alice}) + child_chain_streamer.set_receiver(bob, {"from": alice}) + + +def test_only_distributor(alice, charlie, coin_reward, child_chain_streamer): + with brownie.reverts("dev: only distributor"): + child_chain_streamer.notify_reward_amount(coin_reward, {"from": alice}) + + child_chain_streamer.notify_reward_amount(coin_reward, {"from": charlie}) + + +def test_available_rewards_are_distributed(charlie, bob, chain, coin_reward, child_chain_streamer): + + child_chain_streamer.notify_reward_amount(coin_reward, {"from": charlie}) + + chain.mine(timedelta=DAY * 7) + + child_chain_streamer.notify_reward_amount(coin_reward, {"from": charlie}) + + assert math.isclose(coin_reward.balanceOf(bob), 50 * 10 ** 18, rel_tol=0.00001) + + +def test_increase_reward_amount_mid_distribution( + alice, chain, charlie, coin_reward, child_chain_streamer +): + child_chain_streamer.notify_reward_amount(coin_reward, {"from": charlie}) + chain.mine(timedelta=DAY * 7) # half the rewards have been streamed + coin_reward._mint_for_testing(100 * 10 ** 18, {"from": alice}) # give another 100 + # approx 50 will be distributed in this call leaving 150 behind over the course of 14 days + + coin_reward.transfer(child_chain_streamer, 100 * 10 ** 18, {"from": alice}) + tx = child_chain_streamer.notify_reward_amount(coin_reward, {"from": charlie}) + data = child_chain_streamer.reward_data(coin_reward) + + expected_rate = (50 + 100) * 10 ** 18 // (14 * DAY) + + assert data["period_finish"] == tx.timestamp + 14 * DAY + assert data["received"] == 200 * 10 ** 18 + assert math.isclose(data["rate"], expected_rate, rel_tol=0.00001) diff --git a/tests/unitary/Sidechain/child_chain_streamer/test_remove_reward.py b/tests/unitary/Sidechain/child_chain_streamer/test_remove_reward.py new file mode 100644 index 00000000..a54a0231 --- /dev/null +++ b/tests/unitary/Sidechain/child_chain_streamer/test_remove_reward.py @@ -0,0 +1,42 @@ +import brownie +from brownie import ZERO_ADDRESS +import pytest + + +DAY = 86400 + + +@pytest.fixture(scope="module", autouse=True) +def local_setup(alice, charlie, coin_reward, child_chain_streamer): + coin_reward._mint_for_testing(100 * 10 ** 18, {"from": alice}) + coin_reward.transfer(child_chain_streamer, 100 * 10 ** 18, {"from": alice}) + child_chain_streamer.add_reward(coin_reward, charlie, DAY * 7, {"from": alice}) + + +def test_admin_only(charlie, child_chain_streamer, coin_reward): + with brownie.reverts("dev: only owner"): + child_chain_streamer.remove_reward(coin_reward, {"from": charlie}) + + +def test_reward_not_active(alice, child_chain_streamer, token): + with brownie.reverts("Reward token not added"): + child_chain_streamer.remove_reward(token, {"from": alice}) + + +def test_returns_reward_to_owner(alice, child_chain_streamer, coin_reward): + child_chain_streamer.remove_reward(coin_reward, {"from": alice}) + + assert coin_reward.balanceOf(alice) == 100 * 10 ** 18 + + +def test_reward_data_cleared(alice, child_chain_streamer, coin_reward): + child_chain_streamer.remove_reward(coin_reward, {"from": alice}) + + assert child_chain_streamer.reward_data(coin_reward) == (ZERO_ADDRESS, 0, 0, 0, 0, 0) + + +def test_reward_indexes_corrected(alice, child_chain_streamer, token, coin_reward): + child_chain_streamer.add_reward(token, alice, DAY * 18, {"from": alice}) + child_chain_streamer.remove_reward(coin_reward, {"from": alice}) + + child_chain_streamer.reward_tokens(0) == token diff --git a/tests/unitary/Sidechain/conftest.py b/tests/unitary/Sidechain/conftest.py index fbd6f3d8..18611618 100644 --- a/tests/unitary/Sidechain/conftest.py +++ b/tests/unitary/Sidechain/conftest.py @@ -17,6 +17,11 @@ def xdai_root_gauge(RootGaugeXdai, mock_lp_token, minter, alice): return RootGaugeXdai.deploy(mock_lp_token, minter, alice, {"from": alice}) +@pytest.fixture(scope="module") +def child_chain_streamer(alice, ChildChainStreamer): + return ChildChainStreamer.deploy(alice, {"from": alice}) + + @pytest.fixture(scope="module", params=range(3), ids=["Anyswap", "Polygon", "XDAI"]) def root_gauge(request, anyswap_root_gauge, polygon_root_gauge, xdai_root_gauge): if request.param == 0: From ff8ebdbad41ea00f0d461e865f2a7cfcec9a5175 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Wed, 12 May 2021 00:31:02 -0400 Subject: [PATCH 17/19] test: checkpoint proxy single/many fns --- tests/unitary/Sidechain/conftest.py | 5 ++++ .../Sidechain/test_checkpoint_proxy.py | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/unitary/Sidechain/test_checkpoint_proxy.py diff --git a/tests/unitary/Sidechain/conftest.py b/tests/unitary/Sidechain/conftest.py index 18611618..0fd5b103 100644 --- a/tests/unitary/Sidechain/conftest.py +++ b/tests/unitary/Sidechain/conftest.py @@ -22,6 +22,11 @@ def child_chain_streamer(alice, ChildChainStreamer): return ChildChainStreamer.deploy(alice, {"from": alice}) +@pytest.fixture(scope="module") +def checkpoint_proxy(alice, CheckpointProxy): + return CheckpointProxy.deploy({"from": alice}) + + @pytest.fixture(scope="module", params=range(3), ids=["Anyswap", "Polygon", "XDAI"]) def root_gauge(request, anyswap_root_gauge, polygon_root_gauge, xdai_root_gauge): if request.param == 0: diff --git a/tests/unitary/Sidechain/test_checkpoint_proxy.py b/tests/unitary/Sidechain/test_checkpoint_proxy.py new file mode 100644 index 00000000..b489b146 --- /dev/null +++ b/tests/unitary/Sidechain/test_checkpoint_proxy.py @@ -0,0 +1,23 @@ +from brownie import ZERO_ADDRESS + + +def test_checkpoint_single(alice, root_gauge, checkpoint_proxy): + tx = checkpoint_proxy.checkpoint(root_gauge, {"from": alice}) + + assert tx.return_value is True + + assert tx.subcalls[0]["function"] == "checkpoint()" + assert tx.subcalls[0]["to"] == root_gauge + + +def test_checkpoint_many( + alice, checkpoint_proxy, anyswap_root_gauge, polygon_root_gauge, xdai_root_gauge +): + gauges = [anyswap_root_gauge, polygon_root_gauge, xdai_root_gauge] + [ZERO_ADDRESS] * 7 + tx = checkpoint_proxy.checkpoint_many(gauges, {"from": alice}) + + assert tx.return_value is True + + for i in range(3): + assert tx.subcalls[i]["function"] == "checkpoint()" + assert tx.subcalls[i]["to"] == gauges[i] From 01967a30dd957ec1c1829cece1203c0c9e08d722 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Wed, 12 May 2021 03:26:52 -0400 Subject: [PATCH 18/19] chore: lint --- .../Sidechain/child_chain_streamer/test_add_reward.py | 1 - .../Sidechain/child_chain_streamer/test_admin_functions.py | 2 +- .../Sidechain/child_chain_streamer/test_get_reward.py | 5 ++--- .../child_chain_streamer/test_notify_reward_amount.py | 6 ++---- .../Sidechain/child_chain_streamer/test_remove_reward.py | 3 +-- tests/unitary/Sidechain/test_accept_transfer_ownership.py | 2 +- tests/unitary/Sidechain/test_commit_transfer_ownership.py | 2 +- tests/unitary/Sidechain/test_root_checkpoint.py | 3 ++- tests/unitary/Sidechain/test_set_checkpoint_admin.py | 2 +- tests/unitary/Sidechain/test_set_killed.py | 4 ++-- 10 files changed, 13 insertions(+), 17 deletions(-) diff --git a/tests/unitary/Sidechain/child_chain_streamer/test_add_reward.py b/tests/unitary/Sidechain/child_chain_streamer/test_add_reward.py index c900cffa..1c945d72 100644 --- a/tests/unitary/Sidechain/child_chain_streamer/test_add_reward.py +++ b/tests/unitary/Sidechain/child_chain_streamer/test_add_reward.py @@ -21,4 +21,3 @@ def test_reverts_for_double_adding(alice, charlie, child_chain_streamer, token): with brownie.reverts("Reward token already added"): child_chain_streamer.add_reward(token, charlie, 86400 * 7, {"from": alice}) - diff --git a/tests/unitary/Sidechain/child_chain_streamer/test_admin_functions.py b/tests/unitary/Sidechain/child_chain_streamer/test_admin_functions.py index 8556c1d8..222b3dd1 100644 --- a/tests/unitary/Sidechain/child_chain_streamer/test_admin_functions.py +++ b/tests/unitary/Sidechain/child_chain_streamer/test_admin_functions.py @@ -44,4 +44,4 @@ def test_set_receiver_admin_only(alice, bob, charlie, child_chain_streamer): child_chain_streamer.set_receiver(bob, {"from": bob}) child_chain_streamer.set_receiver(charlie, {"from": alice}) - assert child_chain_streamer.reward_receiver == charlie + assert child_chain_streamer.reward_receiver() == charlie diff --git a/tests/unitary/Sidechain/child_chain_streamer/test_get_reward.py b/tests/unitary/Sidechain/child_chain_streamer/test_get_reward.py index ecfef38a..571b48cf 100644 --- a/tests/unitary/Sidechain/child_chain_streamer/test_get_reward.py +++ b/tests/unitary/Sidechain/child_chain_streamer/test_get_reward.py @@ -1,7 +1,7 @@ -import brownie -import pytest import math +import brownie +import pytest DAY = 86400 @@ -34,4 +34,3 @@ def test_update_reward(bob, coin_reward, chain, child_chain_streamer): assert tx.subcalls[-1]["to"] == coin_reward assert tx.subcalls[-1]["function"] == "transfer(address,uint256)" assert math.isclose(coin_reward.balanceOf(bob), 3 * 100 * 10 ** 18 // 7, rel_tol=0.0001) - diff --git a/tests/unitary/Sidechain/child_chain_streamer/test_notify_reward_amount.py b/tests/unitary/Sidechain/child_chain_streamer/test_notify_reward_amount.py index 8d9699cd..4b2e78cc 100644 --- a/tests/unitary/Sidechain/child_chain_streamer/test_notify_reward_amount.py +++ b/tests/unitary/Sidechain/child_chain_streamer/test_notify_reward_amount.py @@ -1,9 +1,7 @@ -from tests.conftest import bob -from brownie.network.state import Chain -import pytest -import brownie import math +import brownie +import pytest DAY = 86400 diff --git a/tests/unitary/Sidechain/child_chain_streamer/test_remove_reward.py b/tests/unitary/Sidechain/child_chain_streamer/test_remove_reward.py index a54a0231..d1023555 100644 --- a/tests/unitary/Sidechain/child_chain_streamer/test_remove_reward.py +++ b/tests/unitary/Sidechain/child_chain_streamer/test_remove_reward.py @@ -1,7 +1,6 @@ import brownie -from brownie import ZERO_ADDRESS import pytest - +from brownie import ZERO_ADDRESS DAY = 86400 diff --git a/tests/unitary/Sidechain/test_accept_transfer_ownership.py b/tests/unitary/Sidechain/test_accept_transfer_ownership.py index 1f21bbd9..d93c75f8 100644 --- a/tests/unitary/Sidechain/test_accept_transfer_ownership.py +++ b/tests/unitary/Sidechain/test_accept_transfer_ownership.py @@ -1,5 +1,5 @@ -import pytest import brownie +import pytest @pytest.fixture(scope="module", autouse=True) diff --git a/tests/unitary/Sidechain/test_commit_transfer_ownership.py b/tests/unitary/Sidechain/test_commit_transfer_ownership.py index f24c36b9..04d948c9 100644 --- a/tests/unitary/Sidechain/test_commit_transfer_ownership.py +++ b/tests/unitary/Sidechain/test_commit_transfer_ownership.py @@ -1,5 +1,5 @@ -import pytest import brownie +import pytest @pytest.mark.parametrize("idx", range(1, 6)) diff --git a/tests/unitary/Sidechain/test_root_checkpoint.py b/tests/unitary/Sidechain/test_root_checkpoint.py index 7457c683..8f2105a7 100644 --- a/tests/unitary/Sidechain/test_root_checkpoint.py +++ b/tests/unitary/Sidechain/test_root_checkpoint.py @@ -1,6 +1,7 @@ -import pytest import math +import pytest + WEEK = 7 * 86400 diff --git a/tests/unitary/Sidechain/test_set_checkpoint_admin.py b/tests/unitary/Sidechain/test_set_checkpoint_admin.py index 98ae645f..2fbcc994 100644 --- a/tests/unitary/Sidechain/test_set_checkpoint_admin.py +++ b/tests/unitary/Sidechain/test_set_checkpoint_admin.py @@ -1,5 +1,5 @@ -import pytest import brownie +import pytest @pytest.mark.parametrize("idx", range(1, 6)) diff --git a/tests/unitary/Sidechain/test_set_killed.py b/tests/unitary/Sidechain/test_set_killed.py index b6aa60cd..0f6eb248 100644 --- a/tests/unitary/Sidechain/test_set_killed.py +++ b/tests/unitary/Sidechain/test_set_killed.py @@ -1,5 +1,5 @@ -import pytest import brownie +import pytest @pytest.mark.parametrize("idx", range(1, 6)) @@ -11,4 +11,4 @@ def test_admin_only(root_gauge, accounts, idx): def test_set_killed_updates_state(alice, root_gauge): root_gauge.set_killed(True, {"from": alice}) - assert root_gauge.is_killed() == True + assert root_gauge.is_killed() is True From e8022339006b9efc677466467b3c828e9c9e7854 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Wed, 12 May 2021 23:53:09 -0400 Subject: [PATCH 19/19] fix: set default time in brownie config --- brownie-config.yaml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/brownie-config.yaml b/brownie-config.yaml index df68fb43..d459cfbc 100644 --- a/brownie-config.yaml +++ b/brownie-config.yaml @@ -3,11 +3,12 @@ reports: - contracts/testing/*.* networks: - development: - cmd_settings: - accounts: 100 - mainnet-fork: - cmd_settings: - unlock: 0xC447FcAF1dEf19A583F97b3620627BF69c05b5fB + development: + cmd_settings: + accounts: 100 + time: 2021-05-12T21:52:33.856164 + mainnet-fork: + cmd_settings: + unlock: 0xC447FcAF1dEf19A583F97b3620627BF69c05b5fB autofetch_sources: True