From 1ad7bdde909721e995638239dc315c3aa1291f48 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Tue, 27 Aug 2024 19:21:30 +0300 Subject: [PATCH 01/20] feat: update contract with arbitrary reward duration --- contracts/implementations/ChildGauge.vy | 212 +++++++++++++++++------- tests/child_gauge/test_rewards.py | 96 ++++++++--- tests/fixtures/deployments.py | 5 + 3 files changed, 227 insertions(+), 86 deletions(-) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index 5ddc6fa..5ae311b 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -16,6 +16,7 @@ implements: ERC20 interface ERC20Extended: def symbol() -> String[26]: view + def decimals() -> uint256: view interface Factory: def owner() -> address: view @@ -52,13 +53,19 @@ event UpdateLiquidityLimit: _working_balance: uint256 _working_supply: uint256 +event NewReward: + id: indexed(uint256) + token: indexed(ERC20) + struct Reward: + token: ERC20 distributor: address period_finish: uint256 rate: uint256 last_update: uint256 integral: uint256 + precision: uint256 DOMAIN_TYPE_HASH: constant(bytes32) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") @@ -99,15 +106,13 @@ integrate_inv_supply: public(HashMap[uint256, uint256]) integrate_inv_supply_of: public(HashMap[address, uint256]) # For tracking external rewards -reward_count: public(uint256) -reward_tokens: public(address[MAX_REWARDS]) -reward_data: public(HashMap[address, Reward]) +reward_data: public(DynArray[Reward, MAX_REWARDS]) # claimant -> default reward receiver rewards_receiver: public(HashMap[address, address]) -# reward token -> claiming address -> integral -reward_integral_for: public(HashMap[address, HashMap[address, uint256]]) -# user -> token -> [uint128 claimable amount][uint128 claimed amount] -claim_data: HashMap[address, HashMap[address, uint256]] +# reward id -> claiming address -> integral +reward_integral_for: public(HashMap[uint256, HashMap[address, uint256]]) +# user -> reward id -> [uint128 claimable amount][uint128 claimed amount] +claim_data: HashMap[address, uint256[MAX_REWARDS]] is_killed: public(bool) inflation_rate: public(HashMap[uint256, uint256]) @@ -122,6 +127,13 @@ def __init__(_factory: Factory): FACTORY = _factory +@external +@view +def reward_count() -> uint256: + # Backward-compatability + return len(self.reward_data) + + @internal def _checkpoint(_user: address): """ @@ -216,38 +228,37 @@ def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _r # if no default receiver is set, direct claims to the user receiver = _user - reward_count: uint256 = self.reward_count for i in range(MAX_REWARDS): - if i == reward_count: + if i >= len(self.reward_data): break - token: address = self.reward_tokens[i] + data: Reward = self.reward_data[i] - integral: uint256 = self.reward_data[token].integral - last_update: uint256 = min(block.timestamp, self.reward_data[token].period_finish) - duration: uint256 = last_update - self.reward_data[token].last_update + integral: uint256 = data.integral + last_update: uint256 = min(block.timestamp, data.period_finish) + duration: uint256 = last_update - data.last_update if duration != 0: - self.reward_data[token].last_update = last_update + self.reward_data[i].last_update = last_update if _total_supply != 0: - integral += duration * self.reward_data[token].rate * 10**18 / _total_supply - self.reward_data[token].integral = integral + integral += duration * data.rate * 10**18 / _total_supply + self.reward_data[i].integral = integral if _user != empty(address): - integral_for: uint256 = self.reward_integral_for[token][_user] + integral_for: uint256 = self.reward_integral_for[i][_user] new_claimable: uint256 = 0 if integral_for < integral: - self.reward_integral_for[token][_user] = integral + self.reward_integral_for[i][_user] = integral new_claimable = user_balance * (integral - integral_for) / 10**18 - claim_data: uint256 = self.claim_data[_user][token] + claim_data: uint256 = self.claim_data[_user][i] total_claimable: uint256 = shift(claim_data, -128) + new_claimable if total_claimable > 0: total_claimed: uint256 = claim_data % 2**128 if _claim: - assert ERC20(token).transfer(receiver, total_claimable, default_return_value=True) - self.claim_data[_user][token] = total_claimed + total_claimable + assert data.token.transfer(receiver, total_claimable / data.precision, default_return_value=True) + self.claim_data[_user][i] = total_claimed + total_claimable elif new_claimable > 0: - self.claim_data[_user][token] = total_claimed + shift(total_claimable, 128) + self.claim_data[_user][i] = total_claimed + shift(total_claimable, 128) @internal @@ -256,7 +267,7 @@ def _transfer(_from: address, _to: address, _value: uint256): return total_supply: uint256 = self.totalSupply - has_rewards: bool = self.reward_count != 0 + has_rewards: bool = len(self.reward_data) > 0 for addr in [_from, _to]: self._checkpoint(addr) self._checkpoint_rewards(addr, total_supply, False, empty(address)) @@ -287,7 +298,7 @@ def deposit(_value: uint256, _user: address = msg.sender, _claim_rewards: bool = total_supply: uint256 = self.totalSupply new_balance: uint256 = self.balanceOf[_user] + _value - if self.reward_count != 0: + if len(self.reward_data) > 0: self._checkpoint_rewards(_user, total_supply, _claim_rewards, empty(address)) total_supply += _value @@ -318,7 +329,7 @@ def withdraw(_value: uint256, _user: address = msg.sender, _claim_rewards: bool total_supply: uint256 = self.totalSupply new_balance: uint256 = self.balanceOf[msg.sender] - _value - if self.reward_count != 0: + if len(self.reward_data) > 0: self._checkpoint_rewards(_user, total_supply, _claim_rewards, empty(address)) total_supply -= _value @@ -495,36 +506,79 @@ def claimable_tokens(addr: address) -> uint256: @view @external -def claimed_reward(_addr: address, _token: address) -> uint256: +def claimed_reward_by_id(_addr: address, _reward_id: uint256) -> uint256: + """ + @notice Get the number of already-claimed reward tokens for a user + @param _addr Account to get reward amount for + @param _reward_id ID of reward (index in `.reward_data`) + @return uint256 Total amount of reward already claimed by `_addr` + """ + return self.claim_data[_addr][_reward_id] % 2**128 / self.reward_data[_reward_id].precision + + +@view +@external +def claimed_reward(_addr: address, _token: ERC20) -> uint256: """ @notice Get the number of already-claimed reward tokens for a user @param _addr Account to get reward amount for @param _token Token to get reward amount for @return uint256 Total amount of `_token` already claimed by `_addr` """ - return self.claim_data[_addr][_token] % 2**128 + claimed: uint256 = 0 + for i in range(MAX_REWARDS): + if i >= len(self.reward_data): + break + reward_data: Reward = self.reward_data[i] + if reward_data.token == _token: + claimed += self.claim_data[_addr][i] % 2**128 / reward_data.precision + return claimed + + +@view +@internal +def _claimable_reward(user: address, reward_id: uint256, reward_data: Reward) -> uint256: + integral: uint256 = reward_data.integral + total_supply: uint256 = self.totalSupply + if total_supply != 0: + duration: uint256 = min(block.timestamp, reward_data.period_finish) - reward_data.last_update + integral += (duration * reward_data.rate * 10**18 / total_supply) + + integral_for: uint256 = self.reward_integral_for[reward_id][user] + new_claimable: uint256 = self.balanceOf[user] * (integral - integral_for) / 10**18 + + return (shift(self.claim_data[user][reward_id], -128) + new_claimable) / reward_data.precision @view @external -def claimable_reward(_user: address, _reward_token: address) -> uint256: +def claimable_reward_by_id(_user: address, _reward_id: uint256) -> uint256: """ @notice Get the number of claimable reward tokens for a user @param _user Account to get reward amount for - @param _reward_token Token to get reward amount for + @param _reward_id ID of reward (index in `.reward_data`) @return uint256 Claimable reward token amount """ - integral: uint256 = self.reward_data[_reward_token].integral - total_supply: uint256 = self.totalSupply - if total_supply != 0: - last_update: uint256 = min(block.timestamp, self.reward_data[_reward_token].period_finish) - duration: uint256 = last_update - self.reward_data[_reward_token].last_update - integral += (duration * self.reward_data[_reward_token].rate * 10**18 / total_supply) + return self._claimable_reward(_user, _reward_id, self.reward_data[_reward_id]) - integral_for: uint256 = self.reward_integral_for[_reward_token][_user] - new_claimable: uint256 = self.balanceOf[_user] * (integral - integral_for) / 10**18 - return shift(self.claim_data[_user][_reward_token], -128) + new_claimable +@view +@external +def claimable_reward(_user: address, _reward_token: ERC20) -> uint256: + """ + @notice Get the number of claimable reward tokens for a user + @param _user Account to get reward amount for + @param _reward_token Token to get reward amount for + @return uint256 Claimable reward token amount + """ + total: uint256 = 0 + for i in range(MAX_REWARDS): + if i >= len(self.reward_data): + break + reward_data: Reward = self.reward_data[i] + if reward_data.token == _reward_token: + total += self._claimable_reward(_user, i, reward_data) + return total @external @@ -553,51 +607,85 @@ def claim_rewards(_addr: address = msg.sender, _receiver: address = empty(addres @external -def add_reward(_reward_token: address, _distributor: address): +def add_reward(_reward_token: ERC20, _distributor: address, _precision: uint256=0) -> uint256: """ @notice Set the active reward contract + @param _reward_token Address of reward token + @param _distributor Address that will deposit rewards + @param _precision Precision for rate calculation, optional. Will adjust to 18-decimal amounts by default + @return ID of added reward """ assert msg.sender == self.manager or msg.sender == FACTORY.owner() - reward_count: uint256 = self.reward_count - assert reward_count < MAX_REWARDS - assert self.reward_data[_reward_token].distributor == empty(address) + precision: uint256 = _precision + if precision == 0: + precision = 10 ** (18 - ERC20Extended(_reward_token.address).decimals()) + self.reward_data.append( + Reward({ + token: _reward_token, + distributor: _distributor, + period_finish: 0, + rate: 0, + last_update: 0, + integral: 0, + precision: precision, + }) + ) - self.reward_data[_reward_token].distributor = _distributor - self.reward_tokens[reward_count] = _reward_token - self.reward_count = reward_count + 1 + reward_id: uint256 = len(self.reward_data) - 1 + log NewReward(reward_id, _reward_token) + return reward_id @external -def set_reward_distributor(_reward_token: address, _distributor: address): - current_distributor: address = self.reward_data[_reward_token].distributor +def set_reward_distributor(_reward_id: uint256, _distributor: address): + current_distributor: address = self.reward_data[_reward_id].distributor assert msg.sender == current_distributor or msg.sender == self.manager or msg.sender == FACTORY.owner() assert current_distributor != empty(address) assert _distributor != empty(address) - self.reward_data[_reward_token].distributor = _distributor + self.reward_data[_reward_id].distributor = _distributor @external @nonreentrant("lock") -def deposit_reward_token(_reward_token: address, _amount: uint256): - assert msg.sender == self.reward_data[_reward_token].distributor +def deposit_reward_token(_reward_id: uint256, _amount: uint256, _new_duration: uint256=0, _new_period_finish: uint256=0): + """ + @notice Deposit tokens for rewards. + @param _reward_id ID of reward to deposit to + @param _amount Amount of reward token to deposit + @param _new_duration Optional. Minimum reward duration period + @param _new_period_finish Optional. Timestamp for new period finish + """ + reward_data: Reward = self.reward_data[_reward_id] + assert msg.sender == reward_data.distributor self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) - assert ERC20(_reward_token).transferFrom(msg.sender, self, _amount, default_return_value=True) - - period_finish: uint256 = self.reward_data[_reward_token].period_finish - if block.timestamp >= period_finish: - self.reward_data[_reward_token].rate = _amount / WEEK - else: - remaining: uint256 = period_finish - block.timestamp - leftover: uint256 = remaining * self.reward_data[_reward_token].rate - self.reward_data[_reward_token].rate = (_amount + leftover) / WEEK - - self.reward_data[_reward_token].last_update = block.timestamp - self.reward_data[_reward_token].period_finish = block.timestamp + WEEK + assert reward_data.token.transferFrom(msg.sender, self, _amount, default_return_value=True) + + new_period_finish: uint256 = block.timestamp + WEEK # default + if _new_period_finish != 0: + new_period_finish = _new_period_finish + elif _new_duration != 0: + new_period_finish = block.timestamp + _new_duration + elif reward_data.period_finish >= block.timestamp + WEEK: # Backward-compatible behaviour + new_period_finish = reward_data.period_finish + duration: uint256 = new_period_finish - block.timestamp + + amount: uint256 = _amount * reward_data.precision + if block.timestamp < reward_data.period_finish: # add leftover + amount += (reward_data.period_finish - block.timestamp) * reward_data.rate + + new_rate: uint256 = amount / duration + if block.timestamp + WEEK < reward_data.period_finish: # allow radical changes only last week + assert new_period_finish >= reward_data.period_finish, "Period rug too early" + assert new_rate >= reward_data.rate, "Rate rug too early" + + self.reward_data[_reward_id].rate = new_rate + self.reward_data[_reward_id].last_update = block.timestamp # in case last_update < block.timestamp + self.reward_data[_reward_id].period_finish = new_period_finish @external diff --git a/tests/child_gauge/test_rewards.py b/tests/child_gauge/test_rewards.py index 8aba953..df9f745 100644 --- a/tests/child_gauge/test_rewards.py +++ b/tests/child_gauge/test_rewards.py @@ -14,42 +14,90 @@ def test_only_manager_or_factory_owner(alice, bob, charlie, chain, child_gauge, child_gauge.add_reward(reward_token, charlie, {"from": charlie}) -def test_reward_data_updated(alice, charlie, child_gauge, reward_token): - - child_gauge.add_reward(reward_token, charlie, {"from": alice}) - expected_data = (charlie, 0, 0, 0, 0) - +def test_add_reward(alice, charlie, child_gauge, reward_token, reward_token_8): + reward_id = child_gauge.add_reward(reward_token, charlie, {"from": alice}).return_value assert child_gauge.reward_count() == 1 - assert child_gauge.reward_tokens(0) == reward_token - assert child_gauge.reward_data(reward_token) == expected_data - - -def test_reverts_for_double_adding(alice, child_gauge, reward_token): - child_gauge.add_reward(reward_token, alice, {"from": alice}) - - with brownie.reverts(): - child_gauge.add_reward(reward_token, alice, {"from": alice}) + assert child_gauge.reward_data(reward_id) == ( + reward_token, # token: ERC20 + charlie, # distributor: address + 0, # period_finish: uint256 + 0, # rate: uint256 + 0, # last_update: uint256 + 0, # integral: uint256 + 1, # precision: uint256 + ) + + reward_id = child_gauge.add_reward(reward_token_8, charlie, {"from": alice}).return_value + assert child_gauge.reward_count() == 2 + assert child_gauge.reward_data(reward_id) == ( + reward_token_8, # token: ERC20 + charlie, # distributor: address + 0, # period_finish: uint256 + 0, # rate: uint256 + 0, # last_update: uint256 + 0, # integral: uint256 + 10 ** 10, # precision: uint256 + ) + + reward_id = child_gauge.add_reward(reward_token_8, charlie, 10 ** 4, {"from": alice}).return_value + assert child_gauge.reward_count() == 3 + assert child_gauge.reward_data(reward_id) == ( + reward_token_8, # token: ERC20 + charlie, # distributor: address + 0, # period_finish: uint256 + 0, # rate: uint256 + 0, # last_update: uint256 + 0, # integral: uint256 + 10 ** 4, # precision: uint256 + ) def test_set_reward_distributor_admin_only(accounts, chain, reward_token, child_gauge): child_gauge.set_manager(accounts[1], {"from": accounts[0]}) - child_gauge.add_reward(reward_token, accounts[2], {"from": accounts[0]}) + reward_id = child_gauge.add_reward(reward_token, accounts[2], {"from": accounts[0]}).return_value for i in range(3): - child_gauge.set_reward_distributor(reward_token, accounts[-1], {"from": accounts[i]}) - assert child_gauge.reward_data(reward_token)["distributor"] == accounts[-1] + child_gauge.set_reward_distributor(reward_id, accounts[-1], {"from": accounts[i]}) + assert child_gauge.reward_data(reward_id)["distributor"] == accounts[-1] chain.undo() with brownie.reverts(): - child_gauge.set_reward_distributor(reward_token, accounts[-1], {"from": accounts[3]}) + child_gauge.set_reward_distributor(reward_id, accounts[-1], {"from": accounts[3]}) def test_deposit_reward_token(alice, child_gauge, reward_token): - reward_token._mint_for_testing(alice, 10**26, {"from": alice}) + amount = 10 ** 26 + reward_token._mint_for_testing(alice, amount, {"from": alice}) reward_token.approve(child_gauge, 2**256 - 1, {"from": alice}) - child_gauge.add_reward(reward_token, alice, {"from": alice}) - tx = child_gauge.deposit_reward_token(reward_token, 10**26, {"from": alice}) - - expected = (alice, tx.timestamp + WEEK, 10**26 // WEEK, tx.timestamp, 0) - assert child_gauge.reward_data(reward_token) == expected + reward_id = child_gauge.add_reward(reward_token, alice, {"from": alice}).return_value + tx = child_gauge.deposit_reward_token(reward_id, amount, {"from": alice}) + + reward_data = [ + reward_token, # token: ERC20 + alice, # distributor: address + tx.timestamp + WEEK, # period_finish: uint256 + amount // WEEK, # rate: uint256 + tx.timestamp, # last_update: uint256 + 0, # integral: uint256 + 1, # precision: uint256 + ] + assert child_gauge.reward_data(reward_id) == reward_data + + # Increase rate + amount += 10 ** 18 + reward_token._mint_for_testing(alice, 10 ** 18, {"from": alice}) + tx = child_gauge.deposit_reward_token(reward_id, 10 ** 18, {"from": alice}) + + reward_data = reward_data[:2] + [ + tx.timestamp + WEEK, # period_finish: uint256 + amount // WEEK, # rate: uint256, totalSupply == 0 not counted hence fail + tx.timestamp, # last_update: uint256 + ] + reward_data[5:] + assert child_gauge.reward_data(reward_id) == reward_data + + # Increase period + # _new_duration + # _new_period_finish + # Week period rekt allowed + # longer period rekt forbidden diff --git a/tests/fixtures/deployments.py b/tests/fixtures/deployments.py index 332a196..94f1fd4 100644 --- a/tests/fixtures/deployments.py +++ b/tests/fixtures/deployments.py @@ -39,6 +39,11 @@ def reward_token(alice): return ERC20("Dummy Reward Token", "dRT", 18, deployer=alice) +@pytest.fixture(scope="module") +def reward_token_8(alice): + return ERC20("Dummy Reward Token", "dRT", 8, deployer=alice) + + @pytest.fixture(scope="module") def unauthorised_token(alice): """This is for testing unauthorised token""" From 98243f94a48615a0ae1e28e8f111a244dfe2121c Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Wed, 28 Aug 2024 17:50:52 +0300 Subject: [PATCH 02/20] chore: forbid CRV as reward --- contracts/implementations/ChildGauge.vy | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index 5ae311b..e509de3 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -616,6 +616,7 @@ def add_reward(_reward_token: ERC20, _distributor: address, _precision: uint256= @return ID of added reward """ assert msg.sender == self.manager or msg.sender == FACTORY.owner() + assert _reward_token != FACTORY.crv() precision: uint256 = _precision if precision == 0: From 7a212f3393fc4c61a6f2074ab04afdc03d83b7f5 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Wed, 28 Aug 2024 19:27:17 +0300 Subject: [PATCH 03/20] feat: update implementation with remaining time and amount --- contracts/implementations/ChildGauge.vy | 107 +++++++++++++++--------- 1 file changed, 67 insertions(+), 40 deletions(-) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index e509de3..bd6dbda 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -61,8 +61,8 @@ event NewReward: struct Reward: token: ERC20 distributor: address - period_finish: uint256 - rate: uint256 + remaining_time: uint256 + remaining_amount: uint256 last_update: uint256 integral: uint256 precision: uint256 @@ -234,12 +234,14 @@ def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _r data: Reward = self.reward_data[i] integral: uint256 = data.integral - last_update: uint256 = min(block.timestamp, data.period_finish) - duration: uint256 = last_update - data.last_update + duration: uint256 = min(block.timestamp - data.last_update, data.remaining_time) if duration != 0: - self.reward_data[i].last_update = last_update + self.reward_data[i].last_update = data.last_update + duration if _total_supply != 0: - integral += duration * data.rate * 10**18 / _total_supply + amount_for_duration: uint256 = (data.remaining_amount * data.precision) * duration / data.remaining_time + integral += amount_for_duration * 10**18 / _total_supply + self.reward_data[i].remaining_time = data.remaining_time - duration + self.reward_data[i].remaining_amount = data.remaining_amount - amount_for_duration self.reward_data[i].integral = integral if _user != empty(address): @@ -256,7 +258,8 @@ def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _r total_claimed: uint256 = claim_data % 2**128 if _claim: assert data.token.transfer(receiver, total_claimable / data.precision, default_return_value=True) - self.claim_data[_user][i] = total_claimed + total_claimable + self.claim_data[_user][i] = total_claimed + total_claimable / data.precision +\ + shift(total_claimable % data.precision, 128) elif new_claimable > 0: self.claim_data[_user][i] = total_claimed + shift(total_claimable, 128) @@ -541,8 +544,10 @@ def _claimable_reward(user: address, reward_id: uint256, reward_data: Reward) -> integral: uint256 = reward_data.integral total_supply: uint256 = self.totalSupply if total_supply != 0: - duration: uint256 = min(block.timestamp, reward_data.period_finish) - reward_data.last_update - integral += (duration * reward_data.rate * 10**18 / total_supply) + duration: uint256 = min(block.timestamp - reward_data.last_update, reward_data.remaining_time) + amount_for_duration: uint256 = (reward_data.remaining_amount * reward_data.precision) *\ + duration / reward_data.remaining_time + integral += (amount_for_duration * 10**18 / total_supply) integral_for: uint256 = self.reward_integral_for[reward_id][user] new_claimable: uint256 = self.balanceOf[user] * (integral - integral_for) / 10**18 @@ -621,12 +626,13 @@ def add_reward(_reward_token: ERC20, _distributor: address, _precision: uint256= precision: uint256 = _precision if precision == 0: precision = 10 ** (18 - ERC20Extended(_reward_token.address).decimals()) + assert precision <= 10 ** 18, "Precision too big" self.reward_data.append( Reward({ token: _reward_token, distributor: _distributor, - period_finish: 0, - rate: 0, + remaining_time: 0, + remaining_amount: 0, last_update: 0, integral: 0, precision: precision, @@ -649,44 +655,65 @@ def set_reward_distributor(_reward_id: uint256, _distributor: address): self.reward_data[_reward_id].distributor = _distributor +@internal +@pure +def _check_reward_boundaries(reward_data: Reward): + """ + @notice Check that reward parameters will not overflow + """ + assert reward_data.remaining_time * reward_data.remaining_amount * reward_data.precision <= max_value(uint256) + assert reward_data.remaining_amount * reward_data.precision * 10 ** 18 <= max_value(uint256) + + @external -@nonreentrant("lock") -def deposit_reward_token(_reward_id: uint256, _amount: uint256, _new_duration: uint256=0, _new_period_finish: uint256=0): +def deposit_reward(_reward_id: uint256, _amount: uint256): """ - @notice Deposit tokens for rewards. - @param _reward_id ID of reward to deposit to - @param _amount Amount of reward token to deposit - @param _new_duration Optional. Minimum reward duration period - @param _new_period_finish Optional. Timestamp for new period finish + @notice Deposit reward tokens for distribution + @param _reward_id ID of reward (index in reward_data) + @param _amount Amount to deposit for rewards """ reward_data: Reward = self.reward_data[_reward_id] - assert msg.sender == reward_data.distributor self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) assert reward_data.token.transferFrom(msg.sender, self, _amount, default_return_value=True) + reward_data.remaining_amount += _amount + self._check_reward_boundaries(reward_data) + self.reward_data[_reward_id].remaining_time = reward_data.remaining_amount + + +@external +def recover_remaining_reward(_reward_id: uint256, _receiver: address=msg.sender): + """ + @notice Recover reward tokens. Only when remaining time = 0 + @param _reward_id ID of reward (index in reward_data) + @param _receiver Receiver of recovered tokens (distributor by default) + """ + reward_data: Reward = self.reward_data[_reward_id] + assert msg.sender == reward_data.distributor + assert reward_data.remaining_time == 0, "Distribution in progress" + + self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) + + self.reward_data[_reward_id].remaining_amount = 0 + assert reward_data.token.transfer(_receiver, reward_data.remaining_amount, default_return_value=True) + + +@external +def set_reward_duration(_reward_id: uint256, _duration: uint256): + """ + @notice Set duration for reward distribution. Function works as a trigger to start reward distribution + @param _reward_id ID of reward (index in reward_data) + @param _duration Time for reward distribution in seconds + """ + reward_data: Reward = self.reward_data[_reward_id] + assert msg.sender == reward_data.distributor + + self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) - new_period_finish: uint256 = block.timestamp + WEEK # default - if _new_period_finish != 0: - new_period_finish = _new_period_finish - elif _new_duration != 0: - new_period_finish = block.timestamp + _new_duration - elif reward_data.period_finish >= block.timestamp + WEEK: # Backward-compatible behaviour - new_period_finish = reward_data.period_finish - duration: uint256 = new_period_finish - block.timestamp - - amount: uint256 = _amount * reward_data.precision - if block.timestamp < reward_data.period_finish: # add leftover - amount += (reward_data.period_finish - block.timestamp) * reward_data.rate - - new_rate: uint256 = amount / duration - if block.timestamp + WEEK < reward_data.period_finish: # allow radical changes only last week - assert new_period_finish >= reward_data.period_finish, "Period rug too early" - assert new_rate >= reward_data.rate, "Rate rug too early" - - self.reward_data[_reward_id].rate = new_rate - self.reward_data[_reward_id].last_update = block.timestamp # in case last_update < block.timestamp - self.reward_data[_reward_id].period_finish = new_period_finish + reward_data.remaining_time = _duration + self._check_reward_boundaries(reward_data) + self.reward_data[_reward_id].remaining_time = _duration @external From 2b162fd789789d8395b313c3d22e98f57ae361fa Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Wed, 28 Aug 2024 20:01:03 +0300 Subject: [PATCH 04/20] refactor: naming and file delimiters --- contracts/implementations/ChildGauge.vy | 88 +++++++++++++++++++------ 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index bd6dbda..2b63448 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -6,7 +6,7 @@ @notice Layer2/Cross-Chain Gauge """ -version: public(constant(String[8])) = "0.2.1" +version: public(constant(String[8])) = "1.0.0" from vyper.interfaces import ERC20 @@ -58,7 +58,7 @@ event NewReward: token: indexed(ERC20) -struct Reward: +struct RewardData: token: ERC20 distributor: address remaining_time: uint256 @@ -106,7 +106,7 @@ integrate_inv_supply: public(HashMap[uint256, uint256]) integrate_inv_supply_of: public(HashMap[address, uint256]) # For tracking external rewards -reward_data: public(DynArray[Reward, MAX_REWARDS]) +reward_data: public(DynArray[RewardData, MAX_REWARDS]) # claimant -> default reward receiver rewards_receiver: public(HashMap[address, address]) # reward id -> claiming address -> integral @@ -127,13 +127,6 @@ def __init__(_factory: Factory): FACTORY = _factory -@external -@view -def reward_count() -> uint256: - # Backward-compatability - return len(self.reward_data) - - @internal def _checkpoint(_user: address): """ @@ -231,7 +224,7 @@ def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _r for i in range(MAX_REWARDS): if i >= len(self.reward_data): break - data: Reward = self.reward_data[i] + data: RewardData = self.reward_data[i] integral: uint256 = data.integral duration: uint256 = min(block.timestamp - data.last_update, data.remaining_time) @@ -286,6 +279,9 @@ def _transfer(_from: address, _to: address, _value: uint256): log Transfer(_from, _to, _value) +# User methods + + @external @nonreentrant("lock") def deposit(_value: uint256, _user: address = msg.sender, _claim_rewards: bool = False): @@ -532,7 +528,7 @@ def claimed_reward(_addr: address, _token: ERC20) -> uint256: for i in range(MAX_REWARDS): if i >= len(self.reward_data): break - reward_data: Reward = self.reward_data[i] + reward_data: RewardData = self.reward_data[i] if reward_data.token == _token: claimed += self.claim_data[_addr][i] % 2**128 / reward_data.precision return claimed @@ -540,7 +536,7 @@ def claimed_reward(_addr: address, _token: ERC20) -> uint256: @view @internal -def _claimable_reward(user: address, reward_id: uint256, reward_data: Reward) -> uint256: +def _claimable_reward(user: address, reward_id: uint256, reward_data: RewardData) -> uint256: integral: uint256 = reward_data.integral total_supply: uint256 = self.totalSupply if total_supply != 0: @@ -580,7 +576,7 @@ def claimable_reward(_user: address, _reward_token: ERC20) -> uint256: for i in range(MAX_REWARDS): if i >= len(self.reward_data): break - reward_data: Reward = self.reward_data[i] + reward_data: RewardData = self.reward_data[i] if reward_data.token == _reward_token: total += self._claimable_reward(_user, i, reward_data) return total @@ -611,6 +607,9 @@ def claim_rewards(_addr: address = msg.sender, _receiver: address = empty(addres self._checkpoint_rewards(_addr, self.totalSupply, True, _receiver) +# Rewarder methods + + @external def add_reward(_reward_token: ERC20, _distributor: address, _precision: uint256=0) -> uint256: """ @@ -628,7 +627,7 @@ def add_reward(_reward_token: ERC20, _distributor: address, _precision: uint256= precision = 10 ** (18 - ERC20Extended(_reward_token.address).decimals()) assert precision <= 10 ** 18, "Precision too big" self.reward_data.append( - Reward({ + RewardData({ token: _reward_token, distributor: _distributor, remaining_time: 0, @@ -657,7 +656,7 @@ def set_reward_distributor(_reward_id: uint256, _distributor: address): @internal @pure -def _check_reward_boundaries(reward_data: Reward): +def _check_reward_boundaries(reward_data: RewardData): """ @notice Check that reward parameters will not overflow """ @@ -672,7 +671,7 @@ def deposit_reward(_reward_id: uint256, _amount: uint256): @param _reward_id ID of reward (index in reward_data) @param _amount Amount to deposit for rewards """ - reward_data: Reward = self.reward_data[_reward_id] + reward_data: RewardData = self.reward_data[_reward_id] self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) @@ -689,7 +688,7 @@ def recover_remaining_reward(_reward_id: uint256, _receiver: address=msg.sender) @param _reward_id ID of reward (index in reward_data) @param _receiver Receiver of recovered tokens (distributor by default) """ - reward_data: Reward = self.reward_data[_reward_id] + reward_data: RewardData = self.reward_data[_reward_id] assert msg.sender == reward_data.distributor assert reward_data.remaining_time == 0, "Distribution in progress" @@ -706,7 +705,7 @@ def set_reward_duration(_reward_id: uint256, _duration: uint256): @param _reward_id ID of reward (index in reward_data) @param _duration Time for reward distribution in seconds """ - reward_data: Reward = self.reward_data[_reward_id] + reward_data: RewardData = self.reward_data[_reward_id] assert msg.sender == reward_data.distributor self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) @@ -716,6 +715,9 @@ def set_reward_duration(_reward_id: uint256, _duration: uint256): self.reward_data[_reward_id].remaining_time = _duration +# Owner(DAO) methods + + @external def set_manager(_manager: address): assert msg.sender == FACTORY.owner() @@ -754,6 +756,51 @@ def set_killed(_is_killed: bool): self.is_killed = _is_killed +# Helpers + + +@external +@view +def reward_count() -> uint256: + # Backward-compatability + return len(self.reward_data) + + +@internal +@view +def _rate(_reward_id: uint256) -> (uint256, uint256): + reward_data: RewardData = self.reward_data[_reward_id] + if reward_data.remaining_time == 0: + return 0, reward_data.precision + + return reward_data.remaining_amount * reward_data.precision / reward_data.remaining_time, reward_data.precision + + +@external +@view +def rate(_reward_id: uint256, _with_precision: bool=False) -> uint256: + rate: uint256 = 0 + precision: uint256 = 0 + rate, precision = self._rate(_reward_id) + + if _with_precision: + return rate + return rate / precision + + +@external +@view +def rate_per_lp_token(_reward_id: uint256, _with_precision: bool=False) -> uint256: + rate: uint256 = 0 + precision: uint256 = 0 + rate, precision = self._rate(_reward_id) + + rate = rate * 10 ** 18 / self.totalSupply + if _with_precision: + return rate + return rate / precision + + @view @external def decimals() -> uint256: @@ -781,6 +828,9 @@ def VERSION() -> String[8]: return version +# Initialization + + @external def initialize(_lp_token: address, _root: address, _manager: address): assert self.lp_token == empty(address) # dev: already initialzed From 4a0153ae04ab95674735d6b33549e571ce93e09f Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Thu, 29 Aug 2024 15:28:32 +0300 Subject: [PATCH 05/20] fix: rate per lp corner case --- contracts/implementations/ChildGauge.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index 2b63448..b4614b9 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -795,7 +795,7 @@ def rate_per_lp_token(_reward_id: uint256, _with_precision: bool=False) -> uint2 precision: uint256 = 0 rate, precision = self._rate(_reward_id) - rate = rate * 10 ** 18 / self.totalSupply + rate = rate * 10 ** 18 / max(self.totalSupply, 10 ** 18) if _with_precision: return rate return rate / precision From bdfc3f9933a87675c9ed413191cc0e156e473484 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Thu, 29 Aug 2024 17:11:36 +0300 Subject: [PATCH 06/20] chore: add claiming for multiple users and nonreentrant check --- contracts/implementations/ChildGauge.vy | 57 +++++++++++++++++++------ 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index b4614b9..c899a5e 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -59,13 +59,14 @@ event NewReward: struct RewardData: - token: ERC20 - distributor: address - remaining_time: uint256 - remaining_amount: uint256 - last_update: uint256 - integral: uint256 - precision: uint256 + token: ERC20 # Reward token + distributor: address # address responsible for period set and recovering funds + remaining_time: uint256 # + remaining_amount: uint256 # + last_update: uint256 # last update of integral + integral: uint256 # Integral for amount of reward distributed to 1 lp token + precision: uint256 # Calculations multiplier on amount + locked: bool # You can lock reward, so noone is able to change parameters before end of distribution DOMAIN_TYPE_HASH: constant(bytes32) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") @@ -607,6 +608,16 @@ def claim_rewards(_addr: address = msg.sender, _receiver: address = empty(addres self._checkpoint_rewards(_addr, self.totalSupply, True, _receiver) +@external +@nonreentrant('lock') +def claim_rewards_for(_users: DynArray[address, 64]): + """ + @notice Claim rewards for multiple users + """ + for addr in _users: + self._checkpoint_rewards(addr, self.totalSupply, True, empty(address)) + + # Rewarder methods @@ -635,6 +646,7 @@ def add_reward(_reward_token: ERC20, _distributor: address, _precision: uint256= last_update: 0, integral: 0, precision: precision, + locked: False, }) ) @@ -665,16 +677,16 @@ def _check_reward_boundaries(reward_data: RewardData): @external +@nonreentrant('lock') def deposit_reward(_reward_id: uint256, _amount: uint256): """ @notice Deposit reward tokens for distribution @param _reward_id ID of reward (index in reward_data) @param _amount Amount to deposit for rewards """ - reward_data: RewardData = self.reward_data[_reward_id] - self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) + reward_data: RewardData = self.reward_data[_reward_id] assert reward_data.token.transferFrom(msg.sender, self, _amount, default_return_value=True) reward_data.remaining_amount += _amount self._check_reward_boundaries(reward_data) @@ -682,39 +694,58 @@ def deposit_reward(_reward_id: uint256, _amount: uint256): @external +@nonreentrant('lock') def recover_remaining_reward(_reward_id: uint256, _receiver: address=msg.sender): """ @notice Recover reward tokens. Only when remaining time = 0 @param _reward_id ID of reward (index in reward_data) @param _receiver Receiver of recovered tokens (distributor by default) """ + self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) + reward_data: RewardData = self.reward_data[_reward_id] assert msg.sender == reward_data.distributor assert reward_data.remaining_time == 0, "Distribution in progress" - self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) - self.reward_data[_reward_id].remaining_amount = 0 assert reward_data.token.transfer(_receiver, reward_data.remaining_amount, default_return_value=True) @external +@nonreentrant('lock') def set_reward_duration(_reward_id: uint256, _duration: uint256): """ @notice Set duration for reward distribution. Function works as a trigger to start reward distribution @param _reward_id ID of reward (index in reward_data) @param _duration Time for reward distribution in seconds """ + self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) + reward_data: RewardData = self.reward_data[_reward_id] assert msg.sender == reward_data.distributor - - self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) + assert WEEK / 7 <= _duration and _duration <= WEEK * 4 * 12, "Duration out of range" + assert reward_data.remaining_time == 0 or not reward_data.locked, "Reward is locked" reward_data.remaining_time = _duration self._check_reward_boundaries(reward_data) self.reward_data[_reward_id].remaining_time = _duration +@external +@nonreentrant('lock') +def lock_reward(_reward_id: uint256): + """ + @notice Lock reward so noone(distributor/manager/DAO) can rug setting duration + recovering + """ + self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) + + reward_data: RewardData = self.reward_data[_reward_id] + assert msg.sender == reward_data.distributor + assert reward_data.remaining_time > 0, "Nothing to lock" + + self.reward_data[_reward_id].locked = True + + # Owner(DAO) methods From c9a08fa00a564cfdf2b7f4f146bedcd4dfea184c Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Fri, 30 Aug 2024 13:58:18 +0300 Subject: [PATCH 07/20] chore: gas optimizations --- contracts/implementations/ChildGauge.vy | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index c899a5e..6e80532 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -703,12 +703,12 @@ def recover_remaining_reward(_reward_id: uint256, _receiver: address=msg.sender) """ self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) - reward_data: RewardData = self.reward_data[_reward_id] - assert msg.sender == reward_data.distributor - assert reward_data.remaining_time == 0, "Distribution in progress" + assert msg.sender == self.reward_data[_reward_id].distributor + assert self.reward_data[_reward_id].remaining_time == 0, "Distribution in progress" + remaining_amount: uint256 = self.reward_data[_reward_id].remaining_amount self.reward_data[_reward_id].remaining_amount = 0 - assert reward_data.token.transfer(_receiver, reward_data.remaining_amount, default_return_value=True) + assert self.reward_data[_reward_id].token.transfer(_receiver, remaining_amount, default_return_value=True) @external @@ -739,9 +739,8 @@ def lock_reward(_reward_id: uint256): """ self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) - reward_data: RewardData = self.reward_data[_reward_id] - assert msg.sender == reward_data.distributor - assert reward_data.remaining_time > 0, "Nothing to lock" + assert msg.sender == self.reward_data[_reward_id].distributor + assert self.reward_data[_reward_id].remaining_time > 0, "Nothing to lock" self.reward_data[_reward_id].locked = True From 3d0ee3cfaf2075dbd470b8c96e8a4c443a60e86c Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Fri, 30 Aug 2024 14:13:19 +0300 Subject: [PATCH 08/20] chore: rewards per token getter --- contracts/implementations/ChildGauge.vy | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index 6e80532..db93a0a 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -831,6 +831,18 @@ def rate_per_lp_token(_reward_id: uint256, _with_precision: bool=False) -> uint2 return rate / precision +@external +@view +def get_rewards_of(_token: ERC20) -> DynArray[RewardData, MAX_REWARDS]: + rewards: DynArray[RewardData, MAX_REWARDS] = [] + for i in range(MAX_REWARDS): + if i >= len(self.reward_data): + break + if self.reward_data[i].token == _token: + rewards.append(self.reward_data[i]) + return rewards + + @view @external def decimals() -> uint256: From baf47452b13423668c20cc6449fc2a9bc42da0a5 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Thu, 12 Sep 2024 14:32:41 +0300 Subject: [PATCH 09/20] feat: rollback changes, was decided not to change abi --- contracts/implementations/ChildGauge.vy | 346 +++++------------------- 1 file changed, 69 insertions(+), 277 deletions(-) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index db93a0a..5ddc6fa 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -6,7 +6,7 @@ @notice Layer2/Cross-Chain Gauge """ -version: public(constant(String[8])) = "1.0.0" +version: public(constant(String[8])) = "0.2.1" from vyper.interfaces import ERC20 @@ -16,7 +16,6 @@ implements: ERC20 interface ERC20Extended: def symbol() -> String[26]: view - def decimals() -> uint256: view interface Factory: def owner() -> address: view @@ -53,20 +52,13 @@ event UpdateLiquidityLimit: _working_balance: uint256 _working_supply: uint256 -event NewReward: - id: indexed(uint256) - token: indexed(ERC20) - -struct RewardData: - token: ERC20 # Reward token - distributor: address # address responsible for period set and recovering funds - remaining_time: uint256 # - remaining_amount: uint256 # - last_update: uint256 # last update of integral - integral: uint256 # Integral for amount of reward distributed to 1 lp token - precision: uint256 # Calculations multiplier on amount - locked: bool # You can lock reward, so noone is able to change parameters before end of distribution +struct Reward: + distributor: address + period_finish: uint256 + rate: uint256 + last_update: uint256 + integral: uint256 DOMAIN_TYPE_HASH: constant(bytes32) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") @@ -107,13 +99,15 @@ integrate_inv_supply: public(HashMap[uint256, uint256]) integrate_inv_supply_of: public(HashMap[address, uint256]) # For tracking external rewards -reward_data: public(DynArray[RewardData, MAX_REWARDS]) +reward_count: public(uint256) +reward_tokens: public(address[MAX_REWARDS]) +reward_data: public(HashMap[address, Reward]) # claimant -> default reward receiver rewards_receiver: public(HashMap[address, address]) -# reward id -> claiming address -> integral -reward_integral_for: public(HashMap[uint256, HashMap[address, uint256]]) -# user -> reward id -> [uint128 claimable amount][uint128 claimed amount] -claim_data: HashMap[address, uint256[MAX_REWARDS]] +# reward token -> claiming address -> integral +reward_integral_for: public(HashMap[address, HashMap[address, uint256]]) +# user -> token -> [uint128 claimable amount][uint128 claimed amount] +claim_data: HashMap[address, HashMap[address, uint256]] is_killed: public(bool) inflation_rate: public(HashMap[uint256, uint256]) @@ -222,40 +216,38 @@ def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _r # if no default receiver is set, direct claims to the user receiver = _user + reward_count: uint256 = self.reward_count for i in range(MAX_REWARDS): - if i >= len(self.reward_data): + if i == reward_count: break - data: RewardData = self.reward_data[i] + token: address = self.reward_tokens[i] - integral: uint256 = data.integral - duration: uint256 = min(block.timestamp - data.last_update, data.remaining_time) + integral: uint256 = self.reward_data[token].integral + last_update: uint256 = min(block.timestamp, self.reward_data[token].period_finish) + duration: uint256 = last_update - self.reward_data[token].last_update if duration != 0: - self.reward_data[i].last_update = data.last_update + duration + self.reward_data[token].last_update = last_update if _total_supply != 0: - amount_for_duration: uint256 = (data.remaining_amount * data.precision) * duration / data.remaining_time - integral += amount_for_duration * 10**18 / _total_supply - self.reward_data[i].remaining_time = data.remaining_time - duration - self.reward_data[i].remaining_amount = data.remaining_amount - amount_for_duration - self.reward_data[i].integral = integral + integral += duration * self.reward_data[token].rate * 10**18 / _total_supply + self.reward_data[token].integral = integral if _user != empty(address): - integral_for: uint256 = self.reward_integral_for[i][_user] + integral_for: uint256 = self.reward_integral_for[token][_user] new_claimable: uint256 = 0 if integral_for < integral: - self.reward_integral_for[i][_user] = integral + self.reward_integral_for[token][_user] = integral new_claimable = user_balance * (integral - integral_for) / 10**18 - claim_data: uint256 = self.claim_data[_user][i] + claim_data: uint256 = self.claim_data[_user][token] total_claimable: uint256 = shift(claim_data, -128) + new_claimable if total_claimable > 0: total_claimed: uint256 = claim_data % 2**128 if _claim: - assert data.token.transfer(receiver, total_claimable / data.precision, default_return_value=True) - self.claim_data[_user][i] = total_claimed + total_claimable / data.precision +\ - shift(total_claimable % data.precision, 128) + assert ERC20(token).transfer(receiver, total_claimable, default_return_value=True) + self.claim_data[_user][token] = total_claimed + total_claimable elif new_claimable > 0: - self.claim_data[_user][i] = total_claimed + shift(total_claimable, 128) + self.claim_data[_user][token] = total_claimed + shift(total_claimable, 128) @internal @@ -264,7 +256,7 @@ def _transfer(_from: address, _to: address, _value: uint256): return total_supply: uint256 = self.totalSupply - has_rewards: bool = len(self.reward_data) > 0 + has_rewards: bool = self.reward_count != 0 for addr in [_from, _to]: self._checkpoint(addr) self._checkpoint_rewards(addr, total_supply, False, empty(address)) @@ -280,9 +272,6 @@ def _transfer(_from: address, _to: address, _value: uint256): log Transfer(_from, _to, _value) -# User methods - - @external @nonreentrant("lock") def deposit(_value: uint256, _user: address = msg.sender, _claim_rewards: bool = False): @@ -298,7 +287,7 @@ def deposit(_value: uint256, _user: address = msg.sender, _claim_rewards: bool = total_supply: uint256 = self.totalSupply new_balance: uint256 = self.balanceOf[_user] + _value - if len(self.reward_data) > 0: + if self.reward_count != 0: self._checkpoint_rewards(_user, total_supply, _claim_rewards, empty(address)) total_supply += _value @@ -329,7 +318,7 @@ def withdraw(_value: uint256, _user: address = msg.sender, _claim_rewards: bool total_supply: uint256 = self.totalSupply new_balance: uint256 = self.balanceOf[msg.sender] - _value - if len(self.reward_data) > 0: + if self.reward_count != 0: self._checkpoint_rewards(_user, total_supply, _claim_rewards, empty(address)) total_supply -= _value @@ -506,81 +495,36 @@ def claimable_tokens(addr: address) -> uint256: @view @external -def claimed_reward_by_id(_addr: address, _reward_id: uint256) -> uint256: - """ - @notice Get the number of already-claimed reward tokens for a user - @param _addr Account to get reward amount for - @param _reward_id ID of reward (index in `.reward_data`) - @return uint256 Total amount of reward already claimed by `_addr` - """ - return self.claim_data[_addr][_reward_id] % 2**128 / self.reward_data[_reward_id].precision - - -@view -@external -def claimed_reward(_addr: address, _token: ERC20) -> uint256: +def claimed_reward(_addr: address, _token: address) -> uint256: """ @notice Get the number of already-claimed reward tokens for a user @param _addr Account to get reward amount for @param _token Token to get reward amount for @return uint256 Total amount of `_token` already claimed by `_addr` """ - claimed: uint256 = 0 - for i in range(MAX_REWARDS): - if i >= len(self.reward_data): - break - reward_data: RewardData = self.reward_data[i] - if reward_data.token == _token: - claimed += self.claim_data[_addr][i] % 2**128 / reward_data.precision - return claimed - - -@view -@internal -def _claimable_reward(user: address, reward_id: uint256, reward_data: RewardData) -> uint256: - integral: uint256 = reward_data.integral - total_supply: uint256 = self.totalSupply - if total_supply != 0: - duration: uint256 = min(block.timestamp - reward_data.last_update, reward_data.remaining_time) - amount_for_duration: uint256 = (reward_data.remaining_amount * reward_data.precision) *\ - duration / reward_data.remaining_time - integral += (amount_for_duration * 10**18 / total_supply) - - integral_for: uint256 = self.reward_integral_for[reward_id][user] - new_claimable: uint256 = self.balanceOf[user] * (integral - integral_for) / 10**18 - - return (shift(self.claim_data[user][reward_id], -128) + new_claimable) / reward_data.precision + return self.claim_data[_addr][_token] % 2**128 @view @external -def claimable_reward_by_id(_user: address, _reward_id: uint256) -> uint256: +def claimable_reward(_user: address, _reward_token: address) -> uint256: """ @notice Get the number of claimable reward tokens for a user @param _user Account to get reward amount for - @param _reward_id ID of reward (index in `.reward_data`) + @param _reward_token Token to get reward amount for @return uint256 Claimable reward token amount """ - return self._claimable_reward(_user, _reward_id, self.reward_data[_reward_id]) + integral: uint256 = self.reward_data[_reward_token].integral + total_supply: uint256 = self.totalSupply + if total_supply != 0: + last_update: uint256 = min(block.timestamp, self.reward_data[_reward_token].period_finish) + duration: uint256 = last_update - self.reward_data[_reward_token].last_update + integral += (duration * self.reward_data[_reward_token].rate * 10**18 / total_supply) + integral_for: uint256 = self.reward_integral_for[_reward_token][_user] + new_claimable: uint256 = self.balanceOf[_user] * (integral - integral_for) / 10**18 -@view -@external -def claimable_reward(_user: address, _reward_token: ERC20) -> uint256: - """ - @notice Get the number of claimable reward tokens for a user - @param _user Account to get reward amount for - @param _reward_token Token to get reward amount for - @return uint256 Claimable reward token amount - """ - total: uint256 = 0 - for i in range(MAX_REWARDS): - if i >= len(self.reward_data): - break - reward_data: RewardData = self.reward_data[i] - if reward_data.token == _reward_token: - total += self._claimable_reward(_user, i, reward_data) - return total + return shift(self.claim_data[_user][_reward_token], -128) + new_claimable @external @@ -609,143 +553,51 @@ def claim_rewards(_addr: address = msg.sender, _receiver: address = empty(addres @external -@nonreentrant('lock') -def claim_rewards_for(_users: DynArray[address, 64]): - """ - @notice Claim rewards for multiple users - """ - for addr in _users: - self._checkpoint_rewards(addr, self.totalSupply, True, empty(address)) - - -# Rewarder methods - - -@external -def add_reward(_reward_token: ERC20, _distributor: address, _precision: uint256=0) -> uint256: +def add_reward(_reward_token: address, _distributor: address): """ @notice Set the active reward contract - @param _reward_token Address of reward token - @param _distributor Address that will deposit rewards - @param _precision Precision for rate calculation, optional. Will adjust to 18-decimal amounts by default - @return ID of added reward """ assert msg.sender == self.manager or msg.sender == FACTORY.owner() - assert _reward_token != FACTORY.crv() - - precision: uint256 = _precision - if precision == 0: - precision = 10 ** (18 - ERC20Extended(_reward_token.address).decimals()) - assert precision <= 10 ** 18, "Precision too big" - self.reward_data.append( - RewardData({ - token: _reward_token, - distributor: _distributor, - remaining_time: 0, - remaining_amount: 0, - last_update: 0, - integral: 0, - precision: precision, - locked: False, - }) - ) - reward_id: uint256 = len(self.reward_data) - 1 - log NewReward(reward_id, _reward_token) - return reward_id + reward_count: uint256 = self.reward_count + assert reward_count < MAX_REWARDS + assert self.reward_data[_reward_token].distributor == empty(address) + + self.reward_data[_reward_token].distributor = _distributor + self.reward_tokens[reward_count] = _reward_token + self.reward_count = reward_count + 1 @external -def set_reward_distributor(_reward_id: uint256, _distributor: address): - current_distributor: address = self.reward_data[_reward_id].distributor +def set_reward_distributor(_reward_token: address, _distributor: address): + current_distributor: address = self.reward_data[_reward_token].distributor assert msg.sender == current_distributor or msg.sender == self.manager or msg.sender == FACTORY.owner() assert current_distributor != empty(address) assert _distributor != empty(address) - self.reward_data[_reward_id].distributor = _distributor - - -@internal -@pure -def _check_reward_boundaries(reward_data: RewardData): - """ - @notice Check that reward parameters will not overflow - """ - assert reward_data.remaining_time * reward_data.remaining_amount * reward_data.precision <= max_value(uint256) - assert reward_data.remaining_amount * reward_data.precision * 10 ** 18 <= max_value(uint256) + self.reward_data[_reward_token].distributor = _distributor @external -@nonreentrant('lock') -def deposit_reward(_reward_id: uint256, _amount: uint256): - """ - @notice Deposit reward tokens for distribution - @param _reward_id ID of reward (index in reward_data) - @param _amount Amount to deposit for rewards - """ - self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) - - reward_data: RewardData = self.reward_data[_reward_id] - assert reward_data.token.transferFrom(msg.sender, self, _amount, default_return_value=True) - reward_data.remaining_amount += _amount - self._check_reward_boundaries(reward_data) - self.reward_data[_reward_id].remaining_time = reward_data.remaining_amount - - -@external -@nonreentrant('lock') -def recover_remaining_reward(_reward_id: uint256, _receiver: address=msg.sender): - """ - @notice Recover reward tokens. Only when remaining time = 0 - @param _reward_id ID of reward (index in reward_data) - @param _receiver Receiver of recovered tokens (distributor by default) - """ - self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) - - assert msg.sender == self.reward_data[_reward_id].distributor - assert self.reward_data[_reward_id].remaining_time == 0, "Distribution in progress" - - remaining_amount: uint256 = self.reward_data[_reward_id].remaining_amount - self.reward_data[_reward_id].remaining_amount = 0 - assert self.reward_data[_reward_id].token.transfer(_receiver, remaining_amount, default_return_value=True) - - -@external -@nonreentrant('lock') -def set_reward_duration(_reward_id: uint256, _duration: uint256): - """ - @notice Set duration for reward distribution. Function works as a trigger to start reward distribution - @param _reward_id ID of reward (index in reward_data) - @param _duration Time for reward distribution in seconds - """ - self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) - - reward_data: RewardData = self.reward_data[_reward_id] - assert msg.sender == reward_data.distributor - assert WEEK / 7 <= _duration and _duration <= WEEK * 4 * 12, "Duration out of range" - assert reward_data.remaining_time == 0 or not reward_data.locked, "Reward is locked" - - reward_data.remaining_time = _duration - self._check_reward_boundaries(reward_data) - self.reward_data[_reward_id].remaining_time = _duration - +@nonreentrant("lock") +def deposit_reward_token(_reward_token: address, _amount: uint256): + assert msg.sender == self.reward_data[_reward_token].distributor -@external -@nonreentrant('lock') -def lock_reward(_reward_id: uint256): - """ - @notice Lock reward so noone(distributor/manager/DAO) can rug setting duration + recovering - """ self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) - assert msg.sender == self.reward_data[_reward_id].distributor - assert self.reward_data[_reward_id].remaining_time > 0, "Nothing to lock" - - self.reward_data[_reward_id].locked = True + assert ERC20(_reward_token).transferFrom(msg.sender, self, _amount, default_return_value=True) + period_finish: uint256 = self.reward_data[_reward_token].period_finish + if block.timestamp >= period_finish: + self.reward_data[_reward_token].rate = _amount / WEEK + else: + remaining: uint256 = period_finish - block.timestamp + leftover: uint256 = remaining * self.reward_data[_reward_token].rate + self.reward_data[_reward_token].rate = (_amount + leftover) / WEEK -# Owner(DAO) methods + self.reward_data[_reward_token].last_update = block.timestamp + self.reward_data[_reward_token].period_finish = block.timestamp + WEEK @external @@ -786,63 +638,6 @@ def set_killed(_is_killed: bool): self.is_killed = _is_killed -# Helpers - - -@external -@view -def reward_count() -> uint256: - # Backward-compatability - return len(self.reward_data) - - -@internal -@view -def _rate(_reward_id: uint256) -> (uint256, uint256): - reward_data: RewardData = self.reward_data[_reward_id] - if reward_data.remaining_time == 0: - return 0, reward_data.precision - - return reward_data.remaining_amount * reward_data.precision / reward_data.remaining_time, reward_data.precision - - -@external -@view -def rate(_reward_id: uint256, _with_precision: bool=False) -> uint256: - rate: uint256 = 0 - precision: uint256 = 0 - rate, precision = self._rate(_reward_id) - - if _with_precision: - return rate - return rate / precision - - -@external -@view -def rate_per_lp_token(_reward_id: uint256, _with_precision: bool=False) -> uint256: - rate: uint256 = 0 - precision: uint256 = 0 - rate, precision = self._rate(_reward_id) - - rate = rate * 10 ** 18 / max(self.totalSupply, 10 ** 18) - if _with_precision: - return rate - return rate / precision - - -@external -@view -def get_rewards_of(_token: ERC20) -> DynArray[RewardData, MAX_REWARDS]: - rewards: DynArray[RewardData, MAX_REWARDS] = [] - for i in range(MAX_REWARDS): - if i >= len(self.reward_data): - break - if self.reward_data[i].token == _token: - rewards.append(self.reward_data[i]) - return rewards - - @view @external def decimals() -> uint256: @@ -870,9 +665,6 @@ def VERSION() -> String[8]: return version -# Initialization - - @external def initialize(_lp_token: address, _root: address, _manager: address): assert self.lp_token == empty(address) # dev: already initialzed From f6ff2fbe339ba5df218b2617df8480f21f0b52e8 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Thu, 12 Sep 2024 14:33:44 +0300 Subject: [PATCH 10/20] fix: not distributed reward bug --- contracts/implementations/ChildGauge.vy | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index 5ddc6fa..1ade09a 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -230,6 +230,9 @@ def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _r if _total_supply != 0: integral += duration * self.reward_data[token].rate * 10**18 / _total_supply self.reward_data[token].integral = integral + else: + # Return not distributed back + self.claim_data[data.distributor][i] += shift(duration * self.reward_data[token].rate, 128) if _user != empty(address): integral_for: uint256 = self.reward_integral_for[token][_user] From 39860c8d083c6ea8a47f8505483d5140ffaa8e20 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Thu, 12 Sep 2024 19:45:31 +0300 Subject: [PATCH 11/20] feat: import arbitrary duration reward distribution --- contracts/implementations/ChildGauge.vy | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index 1ade09a..f591944 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -584,23 +584,33 @@ def set_reward_distributor(_reward_token: address, _distributor: address): @external @nonreentrant("lock") -def deposit_reward_token(_reward_token: address, _amount: uint256): +def deposit_reward_token(_reward_token: address, _amount: uint256, _epoch: uint256 = WEEK): assert msg.sender == self.reward_data[_reward_token].distributor self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) - assert ERC20(_reward_token).transferFrom(msg.sender, self, _amount, default_return_value=True) + # transferFrom reward token and use transferred amount henceforth: + amount_received: uint256 = ERC20(_reward_token).balanceOf(self) + assert ERC20(_reward_token).transferFrom( + msg.sender, + self, + _amount, + default_return_value=True + ) + amount_received = ERC20(_reward_token).balanceOf(self) - amount_received period_finish: uint256 = self.reward_data[_reward_token].period_finish + assert amount_received > _epoch # dev: rate will tend to zero! + if block.timestamp >= period_finish: - self.reward_data[_reward_token].rate = _amount / WEEK + self.reward_data[_reward_token].rate = amount_received / _epoch else: remaining: uint256 = period_finish - block.timestamp leftover: uint256 = remaining * self.reward_data[_reward_token].rate - self.reward_data[_reward_token].rate = (_amount + leftover) / WEEK + self.reward_data[_reward_token].rate = (amount_received + leftover) / _epoch self.reward_data[_reward_token].last_update = block.timestamp - self.reward_data[_reward_token].period_finish = block.timestamp + WEEK + self.reward_data[_reward_token].period_finish = block.timestamp + _epoch @external From a40a98f04b7cfe2853298540e282f94e486cbdc4 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Thu, 12 Sep 2024 19:48:30 +0300 Subject: [PATCH 12/20] chore: forbid CRV as reward --- contracts/implementations/ChildGauge.vy | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index f591944..df93d22 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -561,6 +561,7 @@ def add_reward(_reward_token: address, _distributor: address): @notice Set the active reward contract """ assert msg.sender == self.manager or msg.sender == FACTORY.owner() + assert _reward_token != FACTORY.crv() reward_count: uint256 = self.reward_count assert reward_count < MAX_REWARDS From b41007ff8400136c9230e5f6d1a2a36fed2faa6e Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Thu, 12 Sep 2024 20:10:25 +0300 Subject: [PATCH 13/20] fix: compilation errors --- contracts/implementations/ChildGauge.vy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index df93d22..6579972 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -232,7 +232,7 @@ def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _r self.reward_data[token].integral = integral else: # Return not distributed back - self.claim_data[data.distributor][i] += shift(duration * self.reward_data[token].rate, 128) + self.claim_data[self.reward_data[token].distributor][token] += shift(duration * self.reward_data[token].rate, 128) if _user != empty(address): integral_for: uint256 = self.reward_integral_for[token][_user] @@ -561,7 +561,7 @@ def add_reward(_reward_token: address, _distributor: address): @notice Set the active reward contract """ assert msg.sender == self.manager or msg.sender == FACTORY.owner() - assert _reward_token != FACTORY.crv() + assert _reward_token != FACTORY.crv().address reward_count: uint256 = self.reward_count assert reward_count < MAX_REWARDS From 37a731e9ce3e56e855a6b8dfd244eb677d9c2f88 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Thu, 12 Sep 2024 20:46:22 +0300 Subject: [PATCH 14/20] refactor: new line --- contracts/implementations/ChildGauge.vy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index 6579972..0f8ff61 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -232,7 +232,8 @@ def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _r self.reward_data[token].integral = integral else: # Return not distributed back - self.claim_data[self.reward_data[token].distributor][token] += shift(duration * self.reward_data[token].rate, 128) + self.claim_data[self.reward_data[token].distributor][token] +=\ + shift(duration * self.reward_data[token].rate, 128) if _user != empty(address): integral_for: uint256 = self.reward_integral_for[token][_user] From 864da356b06705e610890c3d49816492c1518860 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Thu, 12 Sep 2024 20:48:09 +0300 Subject: [PATCH 15/20] fix: return unused amount from deposit --- contracts/implementations/ChildGauge.vy | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index 0f8ff61..6bb7623 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -604,12 +604,18 @@ def deposit_reward_token(_reward_token: address, _amount: uint256, _epoch: uint2 period_finish: uint256 = self.reward_data[_reward_token].period_finish assert amount_received > _epoch # dev: rate will tend to zero! + unused: uint256 = 0 if block.timestamp >= period_finish: self.reward_data[_reward_token].rate = amount_received / _epoch + unused = amount_received % _epoch else: remaining: uint256 = period_finish - block.timestamp leftover: uint256 = remaining * self.reward_data[_reward_token].rate self.reward_data[_reward_token].rate = (amount_received + leftover) / _epoch + unused = (amount_received + leftover) % _epoch + + # Return tokens unused due to rate calculation error + self.claim_data[self.reward_data[_reward_token].distributor][_reward_token] += shift(unused, 128) self.reward_data[_reward_token].last_update = block.timestamp self.reward_data[_reward_token].period_finish = block.timestamp + _epoch From f991ddfaafbfbeff355c82272167b85e1e0903d9 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Fri, 13 Sep 2024 01:37:20 +0300 Subject: [PATCH 16/20] feat: bump latest V6 version where possible, so code looks similar and has all these cool descriptive comments --- contracts/implementations/ChildGauge.vy | 692 ++++++++++++++---------- 1 file changed, 395 insertions(+), 297 deletions(-) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index 6bb7623..a2cf8f6 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -1,4 +1,5 @@ # pragma version 0.3.7 +# pragma optimize gas """ @title CurveXChainLiquidityGauge @license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved @@ -6,8 +7,6 @@ @notice Layer2/Cross-Chain Gauge """ -version: public(constant(String[8])) = "0.2.1" - from vyper.interfaces import ERC20 @@ -15,7 +14,10 @@ implements: ERC20 interface ERC20Extended: - def symbol() -> String[26]: view + def symbol() -> String[32]: view + +interface ERC1271: + def isValidSignature(_hash: bytes32, _signature: Bytes[65]) -> bytes32: view interface Factory: def owner() -> address: view @@ -23,35 +25,36 @@ interface Factory: def minted(_user: address, _gauge: address) -> uint256: view def crv() -> ERC20: view -interface ERC1271: - def isValidSignature(_hash: bytes32, _signature: Bytes[65]) -> bytes32: view +event Deposit: + provider: indexed(address) + value: uint256 + +event Withdraw: + provider: indexed(address) + value: uint256 + +event UpdateLiquidityLimit: + user: indexed(address) + original_balance: uint256 + original_supply: uint256 + working_balance: uint256 + working_supply: uint256 + +event SetGaugeManager: + _gauge_manager: address -event Approval: - _owner: indexed(address) - _spender: indexed(address) - _value: uint256 event Transfer: _from: indexed(address) _to: indexed(address) _value: uint256 -event Deposit: - _user: indexed(address) - _value: uint256 - -event Withdraw: - _user: indexed(address) +event Approval: + _owner: indexed(address) + _spender: indexed(address) _value: uint256 -event UpdateLiquidityLimit: - _user: indexed(address) - _original_balance: uint256 - _original_supply: uint256 - _working_balance: uint256 - _working_supply: uint256 - struct Reward: distributor: address @@ -61,57 +64,77 @@ struct Reward: integral: uint256 -DOMAIN_TYPE_HASH: constant(bytes32) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") -PERMIT_TYPE_HASH: constant(bytes32) = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") -ERC1271_MAGIC_VAL: constant(bytes32) = 0x1626ba7e00000000000000000000000000000000000000000000000000000000 - MAX_REWARDS: constant(uint256) = 8 TOKENLESS_PRODUCTION: constant(uint256) = 40 -WEEK: constant(uint256) = 86400 * 7 +WEEK: constant(uint256) = 604800 +VERSION: constant(String[8]) = "1.0.0" -FACTORY: immutable(Factory) +EIP712_TYPEHASH: constant(bytes32) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") +EIP2612_TYPEHASH: constant(bytes32) = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") -DOMAIN_SEPARATOR: public(bytes32) -nonces: public(HashMap[address, uint256]) +voting_escrow: public(address) -name: public(String[64]) -symbol: public(String[32]) -allowance: public(HashMap[address, HashMap[address, uint256]]) +# ERC20 balanceOf: public(HashMap[address, uint256]) totalSupply: public(uint256) +allowance: public(HashMap[address, HashMap[address, uint256]]) -lp_token: public(address) -manager: public(address) +name: public(String[64]) +symbol: public(String[40]) -voting_escrow: public(address) -working_balances: public(HashMap[address, uint256]) -working_supply: public(uint256) +# ERC2612 +DOMAIN_SEPARATOR: public(bytes32) +nonces: public(HashMap[address, uint256]) -period: public(uint256) -period_timestamp: public(HashMap[uint256, uint256]) +# Gauge +FACTORY: immutable(Factory) +manager: public(address) +lp_token: public(address) -integrate_checkpoint_of: public(HashMap[address, uint256]) -integrate_fraction: public(HashMap[address, uint256]) -integrate_inv_supply: public(HashMap[uint256, uint256]) -integrate_inv_supply_of: public(HashMap[address, uint256]) +is_killed: public(bool) + +inflation_rate: public(HashMap[uint256, uint256]) # For tracking external rewards reward_count: public(uint256) -reward_tokens: public(address[MAX_REWARDS]) reward_data: public(HashMap[address, Reward]) +reward_remaining: public(HashMap[address, uint256]) # fixes bad precision + # claimant -> default reward receiver rewards_receiver: public(HashMap[address, address]) + # reward token -> claiming address -> integral reward_integral_for: public(HashMap[address, HashMap[address, uint256]]) -# user -> token -> [uint128 claimable amount][uint128 claimed amount] + +# user -> [uint128 claimable amount][uint128 claimed amount] claim_data: HashMap[address, HashMap[address, uint256]] -is_killed: public(bool) -inflation_rate: public(HashMap[uint256, uint256]) +working_balances: public(HashMap[address, uint256]) +working_supply: public(uint256) + +# 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint +integrate_inv_supply_of: public(HashMap[address, uint256]) +integrate_checkpoint_of: public(HashMap[address, uint256]) +# ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint +# Units: rate * t = already number of coins per address to issue +integrate_fraction: public(HashMap[address, 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(int128) + +# array of reward tokens +reward_tokens: public(address[MAX_REWARDS]) + +period_timestamp: public(HashMap[int128, uint256]) +# 1e18 * ∫(rate(t) / totalSupply(t) dt) from 0 till checkpoint +integrate_inv_supply: public(HashMap[int128, uint256]) + +# custom xchain parameters root_gauge: public(address) @@ -122,13 +145,44 @@ def __init__(_factory: Factory): FACTORY = _factory +@external +def initialize(_lp_token: address, _root: address, _manager: address): + assert self.lp_token == empty(address) # dev: already initialized + + self.lp_token = _lp_token + self.root_gauge = _root + self.manager = _manager + + self.voting_escrow = Factory(msg.sender).voting_escrow() + + symbol: String[32] = ERC20Extended(_lp_token).symbol() + name: String[64] = concat("Curve.fi ", symbol, " Gauge Deposit") + + self.name = name + self.symbol = concat(symbol, "-gauge") + + self.period_timestamp[0] = block.timestamp + self.DOMAIN_SEPARATOR = keccak256( + _abi_encode( + EIP712_TYPEHASH, + keccak256(name), + keccak256(VERSION), + chain.id, + self + ) + ) + + +# Internal Functions + + @internal def _checkpoint(_user: address): """ @notice Checkpoint a user calculating their CRV entitlement @param _user User address """ - period: uint256 = self.period + period: int128 = self.period period_time: uint256 = self.period_timestamp[period] integrate_inv_supply: uint256 = self.integrate_inv_supply[period] @@ -173,38 +227,12 @@ def _checkpoint(_user: address): self.integrate_checkpoint_of[_user] = block.timestamp -@internal -def _update_liquidity_limit(_user: address, _user_balance: uint256, _total_supply: uint256): - """ - @notice Calculate working balances to apply amplification of CRV production. - @dev https://resources.curve.fi/guides/boosting-your-crv-rewards#formula - @param _user The user address - @param _user_balance User's amount of liquidity (LP tokens) - @param _total_supply Total amount of liquidity (LP tokens) - """ - working_balance: uint256 = _user_balance * TOKENLESS_PRODUCTION / 100 - - ve: address = self.voting_escrow - if ve != empty(address): - ve_ts: uint256 = ERC20(ve).totalSupply() - if ve_ts != 0: - working_balance += _total_supply * ERC20(ve).balanceOf(_user) / ve_ts * (100 - TOKENLESS_PRODUCTION) / 100 - working_balance = min(_user_balance, working_balance) - - old_working_balance: uint256 = self.working_balances[_user] - self.working_balances[_user] = working_balance - - working_supply: uint256 = self.working_supply + working_balance - old_working_balance - self.working_supply = working_supply - - log UpdateLiquidityLimit(_user, _user_balance, _total_supply, working_balance, working_supply) - - @internal def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _receiver: address): """ @notice Claim pending rewards and checkpoint rewards for a user """ + user_balance: uint256 = 0 receiver: address = _receiver if _user != empty(address): @@ -223,17 +251,21 @@ def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _r token: address = self.reward_tokens[i] integral: uint256 = self.reward_data[token].integral - last_update: uint256 = min(block.timestamp, self.reward_data[token].period_finish) + period_finish: uint256 = self.reward_data[token].period_finish + last_update: uint256 = min(block.timestamp, period_finish) duration: uint256 = last_update - self.reward_data[token].last_update - if duration != 0: + + if duration != 0 and _total_supply != 0: self.reward_data[token].last_update = last_update - if _total_supply != 0: - integral += duration * self.reward_data[token].rate * 10**18 / _total_supply - self.reward_data[token].integral = integral - else: - # Return not distributed back - self.claim_data[self.reward_data[token].distributor][token] +=\ - shift(duration * self.reward_data[token].rate, 128) + + rate: uint256 = self.reward_data[token].rate + excess: uint256 = self.reward_remaining[token] - (period_finish - last_update + duration) * rate + integral_change: uint256 = (duration * rate + excess) * 10**18 / _total_supply + integral += integral_change + self.reward_data[token].integral = integral + # There is still calculation error in user's claimable amount, + # but it has 18-decimal precision through LP(_total_supply) – safe + self.reward_remaining[token] -= integral_change * _total_supply / 10**18 if _user != empty(address): integral_for: uint256 = self.reward_integral_for[token][_user] @@ -255,124 +287,186 @@ def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _r @internal -def _transfer(_from: address, _to: address, _value: uint256): - if _value == 0: - return - total_supply: uint256 = self.totalSupply +def _update_liquidity_limit(_user: address, _user_balance: uint256, _total_supply: uint256): + """ + @notice Calculate working balances to apply amplification of CRV production. + @dev https://resources.curve.fi/guides/boosting-your-crv-rewards#formula + @param _user The user address + @param _user_balance User's amount of liquidity (LP tokens) + @param _total_supply Total amount of liquidity (LP tokens) + """ + working_balance: uint256 = _user_balance * TOKENLESS_PRODUCTION / 100 - has_rewards: bool = self.reward_count != 0 - for addr in [_from, _to]: - self._checkpoint(addr) - self._checkpoint_rewards(addr, total_supply, False, empty(address)) + ve: address = self.voting_escrow + if ve != empty(address): + ve_ts: uint256 = ERC20(ve).totalSupply() + if ve_ts != 0: + working_balance += _total_supply * ERC20(ve).balanceOf(_user) / ve_ts * (100 - TOKENLESS_PRODUCTION) / 100 + working_balance = min(_user_balance, working_balance) - new_balance: uint256 = self.balanceOf[_from] - _value - self.balanceOf[_from] = new_balance - self._update_liquidity_limit(_from, new_balance, total_supply) + old_working_balance: uint256 = self.working_balances[_user] + self.working_balances[_user] = working_balance + + working_supply: uint256 = self.working_supply + working_balance - old_working_balance + self.working_supply = working_supply - new_balance = self.balanceOf[_to] + _value - self.balanceOf[_to] = new_balance - self._update_liquidity_limit(_to, new_balance, total_supply) + log UpdateLiquidityLimit(_user, _user_balance, _total_supply, working_balance, working_supply) + + +@internal +def _transfer(_from: address, _to: address, _value: uint256): + """ + @notice Transfer tokens as well as checkpoint users + """ + self._checkpoint(_from) + self._checkpoint(_to) + + if _value != 0: + total_supply: uint256 = self.totalSupply + is_rewards: bool = self.reward_count != 0 + if is_rewards: + self._checkpoint_rewards(_from, total_supply, False, empty(address)) + new_balance: uint256 = self.balanceOf[_from] - _value + self.balanceOf[_from] = new_balance + self._update_liquidity_limit(_from, new_balance, total_supply) + + if is_rewards: + self._checkpoint_rewards(_to, total_supply, False, empty(address)) + new_balance = self.balanceOf[_to] + _value + self.balanceOf[_to] = new_balance + self._update_liquidity_limit(_to, new_balance, total_supply) log Transfer(_from, _to, _value) +# External User Facing Functions + + @external -@nonreentrant("lock") -def deposit(_value: uint256, _user: address = msg.sender, _claim_rewards: bool = False): +@nonreentrant('lock') +def deposit(_value: uint256, _addr: address = msg.sender, _claim_rewards: bool = False): """ @notice Deposit `_value` LP tokens + @dev Depositting also claims pending reward tokens @param _value Number of tokens to deposit - @param _user The account to send gauge tokens to + @param _addr Address to deposit for """ - self._checkpoint(_user) - if _value == 0: - return + assert _addr != empty(address) # dev: cannot deposit for zero address + self._checkpoint(_addr) - total_supply: uint256 = self.totalSupply - new_balance: uint256 = self.balanceOf[_user] + _value - - if self.reward_count != 0: - self._checkpoint_rewards(_user, total_supply, _claim_rewards, empty(address)) - - total_supply += _value + if _value != 0: + is_rewards: bool = self.reward_count != 0 + total_supply: uint256 = self.totalSupply + if is_rewards: + self._checkpoint_rewards(_addr, total_supply, _claim_rewards, empty(address)) - self.balanceOf[_user] = new_balance - self.totalSupply = total_supply + total_supply += _value + new_balance: uint256 = self.balanceOf[_addr] + _value + self.balanceOf[_addr] = new_balance + self.totalSupply = total_supply - self._update_liquidity_limit(_user, new_balance, total_supply) + self._update_liquidity_limit(_addr, new_balance, total_supply) - ERC20(self.lp_token).transferFrom(msg.sender, self, _value) + ERC20(self.lp_token).transferFrom(msg.sender, self, _value) - log Deposit(_user, _value) - log Transfer(empty(address), _user, _value) + log Deposit(_addr, _value) + log Transfer(empty(address), _addr, _value) @external -@nonreentrant("lock") -def withdraw(_value: uint256, _user: address = msg.sender, _claim_rewards: bool = False): +@nonreentrant('lock') +def withdraw(_value: uint256, _claim_rewards: bool = False): """ @notice Withdraw `_value` LP tokens + @dev Withdrawing also claims pending reward tokens @param _value Number of tokens to withdraw - @param _user The account to send LP tokens to """ - self._checkpoint(_user) - if _value == 0: - return + self._checkpoint(msg.sender) - total_supply: uint256 = self.totalSupply - new_balance: uint256 = self.balanceOf[msg.sender] - _value + if _value != 0: + is_rewards: bool = self.reward_count != 0 + total_supply: uint256 = self.totalSupply + if is_rewards: + self._checkpoint_rewards(msg.sender, total_supply, _claim_rewards, empty(address)) - if self.reward_count != 0: - self._checkpoint_rewards(_user, total_supply, _claim_rewards, empty(address)) + total_supply -= _value + new_balance: uint256 = self.balanceOf[msg.sender] - _value + self.balanceOf[msg.sender] = new_balance + self.totalSupply = total_supply - total_supply -= _value + self._update_liquidity_limit(msg.sender, new_balance, total_supply) - self.balanceOf[msg.sender] = new_balance - self.totalSupply = total_supply + ERC20(self.lp_token).transfer(msg.sender, _value) - self._update_liquidity_limit(msg.sender, new_balance, total_supply) + log Withdraw(msg.sender, _value) + log Transfer(msg.sender, empty(address), _value) - ERC20(self.lp_token).transfer(_user, _value) - log Withdraw(_user, _value) - log Transfer(msg.sender, empty(address), _value) +@external +@nonreentrant('lock') +def claim_rewards(_addr: address = msg.sender, _receiver: address = empty(address)): + """ + @notice Claim available reward tokens for `_addr` + @param _addr Address to claim for + @param _receiver Address to transfer rewards to - if set to + empty(address), uses the default reward receiver + for the caller + """ + if _receiver != empty(address): + assert _addr == msg.sender # dev: cannot redirect when claiming for another user + self._checkpoint_rewards(_addr, self.totalSupply, True, _receiver) @external -@nonreentrant("lock") -def transferFrom(_from: address, _to: address, _value: uint256) -> bool: +@nonreentrant('lock') +def transferFrom(_from: address, _to :address, _value: uint256) -> bool: """ - @notice Transfer tokens from one address to another - @param _from The address which you want to send tokens from - @param _to The address which you want to transfer to - @param _value the amount of tokens to be transferred - @return bool success + @notice Transfer tokens from one address to another. + @dev Transferring claims pending reward tokens for the sender and receiver + @param _from address The address which you want to send tokens from + @param _to address The address which you want to transfer to + @param _value uint256 the amount of tokens to be transferred """ - allowance: uint256 = self.allowance[_from][msg.sender] - if allowance != max_value(uint256): - self.allowance[_from][msg.sender] = allowance - _value + _allowance: uint256 = self.allowance[_from][msg.sender] + if _allowance != max_value(uint256): + self.allowance[_from][msg.sender] = _allowance - _value self._transfer(_from, _to, _value) + return True @external -def approve(_spender: address, _value: uint256) -> bool: +@nonreentrant('lock') +def transfer(_to: address, _value: uint256) -> bool: + """ + @notice Transfer token for a specified address + @dev Transferring claims pending reward tokens for the sender and receiver + @param _to The address to transfer to. + @param _value The amount to be transferred. + """ + self._transfer(msg.sender, _to, _value) + + return True + + +@external +def approve(_spender : address, _value : uint256) -> bool: """ @notice Approve the passed address to transfer the specified amount of tokens on behalf of msg.sender @dev Beware that changing an allowance via this method brings the risk that someone may use both the old and new allowance by unfortunate transaction ordering. This may be mitigated with the use of - {increaseAllowance} and {decreaseAllowance}. + {incraseAllowance} and {decreaseAllowance}. https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 @param _spender The address which will transfer the funds @param _value The amount of tokens that may be transferred @return bool success """ self.allowance[msg.sender][_spender] = _value - log Approval(msg.sender, _spender, _value) + return True @@ -401,44 +495,30 @@ def permit( @param _s The bytes[32:64] of the valid secp256k1 signature of permit by owner @return True, if transaction completes successfully """ - assert _owner != empty(address) - assert block.timestamp <= _deadline + assert _owner != empty(address) # dev: invalid owner + assert block.timestamp <= _deadline # dev: permit expired nonce: uint256 = self.nonces[_owner] digest: bytes32 = keccak256( concat( b"\x19\x01", self.DOMAIN_SEPARATOR, - keccak256(_abi_encode(PERMIT_TYPE_HASH, _owner, _spender, _value, nonce, _deadline)) + keccak256( + _abi_encode( + EIP2612_TYPEHASH, _owner, _spender, _value, nonce, _deadline + ) + ), ) ) - - if _owner.is_contract: - sig: Bytes[65] = concat(_abi_encode(_r, _s), slice(convert(_v, bytes32), 31, 1)) - assert ERC1271(_owner).isValidSignature(digest, sig) == ERC1271_MAGIC_VAL - else: - assert ecrecover(digest, convert(_v, uint256), convert(_r, uint256), convert(_s, uint256)) == _owner + assert ecrecover(digest, _v, _r, _s) == _owner # dev: invalid signature self.allowance[_owner][_spender] = _value - self.nonces[_owner] = nonce + 1 + self.nonces[_owner] = unsafe_add(nonce, 1) log Approval(_owner, _spender, _value) return True -@external -@nonreentrant("lock") -def transfer(_to: address, _value: uint256) -> bool: - """ - @notice Transfer token to a specified address - @param _to The address to transfer to - @param _value The amount to be transferred - @return bool success - """ - self._transfer(msg.sender, _to, _value) - return True - - @external def increaseAllowance(_spender: address, _added_value: uint256) -> bool: """ @@ -453,6 +533,7 @@ def increaseAllowance(_spender: address, _added_value: uint256) -> bool: self.allowance[msg.sender][_spender] = allowance log Approval(msg.sender, _spender, allowance) + return True @@ -470,6 +551,7 @@ def decreaseAllowance(_spender: address, _subtracted_value: uint256) -> bool: self.allowance[msg.sender][_spender] = allowance log Approval(msg.sender, _spender, allowance) + return True @@ -487,82 +569,108 @@ def user_checkpoint(addr: address) -> bool: @external -def claimable_tokens(addr: address) -> uint256: +def set_rewards_receiver(_receiver: address): """ - @notice Get the number of claimable tokens per user - @dev This function should be manually changed to "view" in the ABI - @return uint256 number of claimable tokens per user + @notice Set the default reward receiver for the caller. + @dev When set to empty(address), rewards are sent to the caller + @param _receiver Receiver address for any rewards claimed via `claim_rewards` """ - self._checkpoint(addr) - return self.integrate_fraction[addr] - FACTORY.minted(addr, self) + self.rewards_receiver[msg.sender] = _receiver + + +# Administrative Functions -@view @external -def claimed_reward(_addr: address, _token: address) -> uint256: +def set_gauge_manager(_gauge_manager: address): """ - @notice Get the number of already-claimed reward tokens for a user - @param _addr Account to get reward amount for - @param _token Token to get reward amount for - @return uint256 Total amount of `_token` already claimed by `_addr` + @notice Change the gauge manager for a gauge + @dev The manager of this contract, or the ownership admin can outright modify gauge + managership. A gauge manager can also transfer managership to a new manager via this + method, but only for the gauge which they are the manager of. + @param _gauge_manager The account to set as the new manager of the gauge. """ - return self.claim_data[_addr][_token] % 2**128 + assert msg.sender in [self.manager, FACTORY.owner()] # dev: only manager or factory admin + + self.manager = _gauge_manager + log SetGaugeManager(_gauge_manager) -@view @external -def claimable_reward(_user: address, _reward_token: address) -> uint256: +def set_manager(_gauge_manager: address): """ - @notice Get the number of claimable reward tokens for a user - @param _user Account to get reward amount for - @param _reward_token Token to get reward amount for - @return uint256 Claimable reward token amount + @notice Change the gauge manager for a gauge + @dev Copy of `set_gauge_manager` for back-compatability + @dev The manager of this contract, or the ownership admin can outright modify gauge + managership. A gauge manager can also transfer managership to a new manager via this + method, but only for the gauge which they are the manager of. + @param _gauge_manager The account to set as the new manager of the gauge. """ - integral: uint256 = self.reward_data[_reward_token].integral - total_supply: uint256 = self.totalSupply - if total_supply != 0: - last_update: uint256 = min(block.timestamp, self.reward_data[_reward_token].period_finish) - duration: uint256 = last_update - self.reward_data[_reward_token].last_update - integral += (duration * self.reward_data[_reward_token].rate * 10**18 / total_supply) - - integral_for: uint256 = self.reward_integral_for[_reward_token][_user] - new_claimable: uint256 = self.balanceOf[_user] * (integral - integral_for) / 10**18 + assert msg.sender in [self.manager, FACTORY.owner()] # dev: only manager or factory admin - return shift(self.claim_data[_user][_reward_token], -128) + new_claimable + self.manager = _gauge_manager + log SetGaugeManager(_gauge_manager) @external -def set_rewards_receiver(_receiver: address): +@nonreentrant("lock") +def deposit_reward_token(_reward_token: address, _amount: uint256, _epoch: uint256 = WEEK): """ - @notice Set the default reward receiver for the caller. - @dev When set to ZERO_ADDRESS, rewards are sent to the caller - @param _receiver Receiver address for any rewards claimed via `claim_rewards` + @notice Deposit a reward token for distribution + @param _reward_token The reward token being deposited + @param _amount The amount of `_reward_token` being deposited + @param _epoch The duration the rewards are distributed across. Between 3 days and a year, week by default """ - self.rewards_receiver[msg.sender] = _receiver + assert msg.sender == self.reward_data[_reward_token].distributor + assert 3 * WEEK / 7 <= _epoch and _epoch <= WEEK * 4 * 12, "Epoch duration" + + self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) + + # transferFrom reward token and use transferred amount henceforth: + amount_received: uint256 = ERC20(_reward_token).balanceOf(self) + assert ERC20(_reward_token).transferFrom( + msg.sender, + self, + _amount, + default_return_value=True + ) + amount_received = ERC20(_reward_token).balanceOf(self) - amount_received + + total_amount: uint256 = amount_received + self.reward_remaining[_reward_token] + self.reward_data[_reward_token].rate = total_amount / _epoch + self.reward_remaining[_reward_token] = total_amount + + self.reward_data[_reward_token].last_update = block.timestamp + self.reward_data[_reward_token].period_finish = block.timestamp + _epoch @external -@nonreentrant('lock') -def claim_rewards(_addr: address = msg.sender, _receiver: address = empty(address)): +def recover_remaining(_reward_token: address): """ - @notice Claim available reward tokens for `_addr` - @param _addr Address to claim for - @param _receiver Address to transfer rewards to - if set to - ZERO_ADDRESS, uses the default reward receiver - for the caller + @notice Recover reward token remaining after calculation errors. Helpful for small decimal tokens. + Remaining tokens will be claimable in favor of distributor. Callable by anyone after reward distribution finished. + @param _reward_token The reward token being recovered """ - if _receiver != empty(address): - assert _addr == msg.sender # dev: cannot redirect when claiming for another user - self._checkpoint_rewards(_addr, self.totalSupply, True, _receiver) + self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) + + period_finish: uint256 = self.reward_data[_reward_token].period_finish + assert period_finish < block.timestamp + assert self.reward_data[_reward_token].last_update >= period_finish + + assert ERC20(_reward_token).transfer(self.reward_data[_reward_token].distributor, + self.reward_remaining[_reward_token], default_return_value=True) + self.reward_remaining[_reward_token] = 0 @external def add_reward(_reward_token: address, _distributor: address): """ - @notice Set the active reward contract + @notice Add additional rewards to be distributed to stakers + @param _reward_token The token to add as an additional reward + @param _distributor Address permitted to fund this contract with the reward token """ - assert msg.sender == self.manager or msg.sender == FACTORY.owner() - assert _reward_token != FACTORY.crv().address + assert msg.sender in [self.manager, FACTORY.owner()] # dev: only manager or factory admin + assert _distributor != empty(address) # dev: distributor cannot be zero address reward_count: uint256 = self.reward_count assert reward_count < MAX_REWARDS @@ -575,9 +683,14 @@ def add_reward(_reward_token: address, _distributor: address): @external def set_reward_distributor(_reward_token: address, _distributor: address): + """ + @notice Reassign the reward distributor for a reward token + @param _reward_token The reward token to reassign distribution rights to + @param _distributor The address of the new distributor + """ current_distributor: address = self.reward_data[_reward_token].distributor - assert msg.sender == current_distributor or msg.sender == self.manager or msg.sender == FACTORY.owner() + assert msg.sender in [current_distributor, FACTORY.owner(), self.manager] assert current_distributor != empty(address) assert _distributor != empty(address) @@ -585,47 +698,15 @@ def set_reward_distributor(_reward_token: address, _distributor: address): @external -@nonreentrant("lock") -def deposit_reward_token(_reward_token: address, _amount: uint256, _epoch: uint256 = WEEK): - assert msg.sender == self.reward_data[_reward_token].distributor - - self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) - - # transferFrom reward token and use transferred amount henceforth: - amount_received: uint256 = ERC20(_reward_token).balanceOf(self) - assert ERC20(_reward_token).transferFrom( - msg.sender, - self, - _amount, - default_return_value=True - ) - amount_received = ERC20(_reward_token).balanceOf(self) - amount_received - - period_finish: uint256 = self.reward_data[_reward_token].period_finish - assert amount_received > _epoch # dev: rate will tend to zero! - - unused: uint256 = 0 - if block.timestamp >= period_finish: - self.reward_data[_reward_token].rate = amount_received / _epoch - unused = amount_received % _epoch - else: - remaining: uint256 = period_finish - block.timestamp - leftover: uint256 = remaining * self.reward_data[_reward_token].rate - self.reward_data[_reward_token].rate = (amount_received + leftover) / _epoch - unused = (amount_received + leftover) % _epoch - - # Return tokens unused due to rate calculation error - self.claim_data[self.reward_data[_reward_token].distributor][_reward_token] += shift(unused, 128) - - self.reward_data[_reward_token].last_update = block.timestamp - self.reward_data[_reward_token].period_finish = block.timestamp + _epoch - - -@external -def set_manager(_manager: address): - assert msg.sender == FACTORY.owner() +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 == FACTORY.owner() # dev: only owner - self.manager = _manager + self.is_killed = _is_killed @external @@ -648,67 +729,84 @@ def update_voting_escrow(): self.voting_escrow = FACTORY.voting_escrow() +# View Methods + + +@view @external -def set_killed(_is_killed: bool): +def claimed_reward(_addr: address, _token: address) -> uint256: """ - @notice Set the kill status of the gauge - @param _is_killed Kill status to put the gauge into + @notice Get the number of already-claimed reward tokens for a user + @param _addr Account to get reward amount for + @param _token Token to get reward amount for + @return uint256 Total amount of `_token` already claimed by `_addr` """ - assert msg.sender == FACTORY.owner() - - self.is_killed = _is_killed + return self.claim_data[_addr][_token] % 2**128 @view @external -def decimals() -> uint256: +def claimable_reward(_user: address, _reward_token: address) -> uint256: """ - @notice Returns the number of decimals the token uses + @notice Get the number of claimable reward tokens for a user + @param _user Account to get reward amount for + @param _reward_token Token to get reward amount for + @return uint256 Claimable reward token amount """ - return 18 + integral: uint256 = self.reward_data[_reward_token].integral + total_supply: uint256 = self.totalSupply + if total_supply != 0: + last_update: uint256 = min(block.timestamp, self.reward_data[_reward_token].period_finish) + duration: uint256 = last_update - self.reward_data[_reward_token].last_update + integral += (duration * self.reward_data[_reward_token].rate * 10**18 / total_supply) + + integral_for: uint256 = self.reward_integral_for[_reward_token][_user] + new_claimable: uint256 = self.balanceOf[_user] * (integral - integral_for) / 10**18 + + return shift(self.claim_data[_user][_reward_token], -128) + new_claimable + + +@external +def claimable_tokens(addr: address) -> uint256: + """ + @notice Get the number of claimable tokens per user + @dev This function should be manually changed to "view" in the ABI + @return uint256 number of claimable tokens per user + """ + self._checkpoint(addr) + return self.integrate_fraction[addr] - FACTORY.minted(addr, self) @view @external def integrate_checkpoint() -> uint256: + """ + @notice Get the timestamp of the last checkpoint + """ return self.period_timestamp[self.period] @view @external -def factory() -> Factory: - return FACTORY +def decimals() -> uint256: + """ + @notice Get the number of decimals for this token + @dev Implemented as a view method to reduce gas costs + @return uint256 decimal places + """ + return 18 @view @external -def VERSION() -> String[8]: - return version +def version() -> String[8]: + """ + @notice Get the version of this gauge contract + """ + return VERSION +@view @external -def initialize(_lp_token: address, _root: address, _manager: address): - assert self.lp_token == empty(address) # dev: already initialzed - - self.lp_token = _lp_token - self.root_gauge = _root - self.manager = _manager - - self.voting_escrow = Factory(msg.sender).voting_escrow() - - symbol: String[26] = ERC20Extended(_lp_token).symbol() - name: String[64] = concat("Curve.fi ", symbol, " Gauge Deposit") - - self.name = name - self.symbol = concat(symbol, "-gauge") - - self.period_timestamp[0] = block.timestamp - self.DOMAIN_SEPARATOR = keccak256( - _abi_encode( - DOMAIN_TYPE_HASH, - keccak256(name), - keccak256(version), - chain.id, - self - ) - ) +def factory() -> Factory: + return FACTORY From 1d0d65ece5c1f1bc34cf3977a0a584aa7f8b35ab Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Fri, 13 Sep 2024 01:37:45 +0300 Subject: [PATCH 17/20] tests: fix tests and test reward_remaining --- tests/child_gauge/test_rewards.py | 128 +++++++++++++++++++----------- tests/fixtures/deployments.py | 5 -- 2 files changed, 81 insertions(+), 52 deletions(-) diff --git a/tests/child_gauge/test_rewards.py b/tests/child_gauge/test_rewards.py index df9f745..fc9ea68 100644 --- a/tests/child_gauge/test_rewards.py +++ b/tests/child_gauge/test_rewards.py @@ -1,4 +1,6 @@ import brownie +from brownie.test import given, strategy +from hypothesis import settings WEEK = 86400 * 7 @@ -14,55 +16,29 @@ def test_only_manager_or_factory_owner(alice, bob, charlie, chain, child_gauge, child_gauge.add_reward(reward_token, charlie, {"from": charlie}) -def test_add_reward(alice, charlie, child_gauge, reward_token, reward_token_8): - reward_id = child_gauge.add_reward(reward_token, charlie, {"from": alice}).return_value +def test_add_reward(alice, charlie, child_gauge, reward_token): + child_gauge.add_reward(reward_token, charlie, {"from": alice}) assert child_gauge.reward_count() == 1 - assert child_gauge.reward_data(reward_id) == ( - reward_token, # token: ERC20 + assert child_gauge.reward_data(reward_token) == ( charlie, # distributor: address 0, # period_finish: uint256 0, # rate: uint256 0, # last_update: uint256 0, # integral: uint256 - 1, # precision: uint256 - ) - - reward_id = child_gauge.add_reward(reward_token_8, charlie, {"from": alice}).return_value - assert child_gauge.reward_count() == 2 - assert child_gauge.reward_data(reward_id) == ( - reward_token_8, # token: ERC20 - charlie, # distributor: address - 0, # period_finish: uint256 - 0, # rate: uint256 - 0, # last_update: uint256 - 0, # integral: uint256 - 10 ** 10, # precision: uint256 - ) - - reward_id = child_gauge.add_reward(reward_token_8, charlie, 10 ** 4, {"from": alice}).return_value - assert child_gauge.reward_count() == 3 - assert child_gauge.reward_data(reward_id) == ( - reward_token_8, # token: ERC20 - charlie, # distributor: address - 0, # period_finish: uint256 - 0, # rate: uint256 - 0, # last_update: uint256 - 0, # integral: uint256 - 10 ** 4, # precision: uint256 ) def test_set_reward_distributor_admin_only(accounts, chain, reward_token, child_gauge): child_gauge.set_manager(accounts[1], {"from": accounts[0]}) - reward_id = child_gauge.add_reward(reward_token, accounts[2], {"from": accounts[0]}).return_value + child_gauge.add_reward(reward_token, accounts[2], {"from": accounts[0]}) for i in range(3): - child_gauge.set_reward_distributor(reward_id, accounts[-1], {"from": accounts[i]}) - assert child_gauge.reward_data(reward_id)["distributor"] == accounts[-1] + child_gauge.set_reward_distributor(reward_token, accounts[-1], {"from": accounts[i]}) + assert child_gauge.reward_data(reward_token)["distributor"] == accounts[-1] chain.undo() with brownie.reverts(): - child_gauge.set_reward_distributor(reward_id, accounts[-1], {"from": accounts[3]}) + child_gauge.set_reward_distributor(reward_token, accounts[-1], {"from": accounts[3]}) def test_deposit_reward_token(alice, child_gauge, reward_token): @@ -70,34 +46,92 @@ def test_deposit_reward_token(alice, child_gauge, reward_token): reward_token._mint_for_testing(alice, amount, {"from": alice}) reward_token.approve(child_gauge, 2**256 - 1, {"from": alice}) - reward_id = child_gauge.add_reward(reward_token, alice, {"from": alice}).return_value - tx = child_gauge.deposit_reward_token(reward_id, amount, {"from": alice}) + child_gauge.add_reward(reward_token, alice, {"from": alice}) + tx = child_gauge.deposit_reward_token(reward_token, amount, {"from": alice}) reward_data = [ - reward_token, # token: ERC20 alice, # distributor: address tx.timestamp + WEEK, # period_finish: uint256 amount // WEEK, # rate: uint256 tx.timestamp, # last_update: uint256 0, # integral: uint256 - 1, # precision: uint256 ] - assert child_gauge.reward_data(reward_id) == reward_data + assert child_gauge.reward_data(reward_token) == reward_data # Increase rate amount += 10 ** 18 reward_token._mint_for_testing(alice, 10 ** 18, {"from": alice}) - tx = child_gauge.deposit_reward_token(reward_id, 10 ** 18, {"from": alice}) + tx = child_gauge.deposit_reward_token(reward_token, 10 ** 18, {"from": alice}) - reward_data = reward_data[:2] + [ + reward_data = [ + alice, # distributor: address tx.timestamp + WEEK, # period_finish: uint256 - amount // WEEK, # rate: uint256, totalSupply == 0 not counted hence fail + amount // WEEK, # rate: uint256 tx.timestamp, # last_update: uint256 - ] + reward_data[5:] - assert child_gauge.reward_data(reward_id) == reward_data + 0, # integral: uint256 + ] + assert child_gauge.reward_data(reward_token) == reward_data # Increase period - # _new_duration - # _new_period_finish - # Week period rekt allowed - # longer period rekt forbidden + tx = child_gauge.deposit_reward_token(reward_token, 0, 2 * WEEK, {"from": alice}) + + reward_data = [ + alice, # distributor: address + tx.timestamp + 2 * WEEK, # period_finish: uint256 + amount // (2 * WEEK), # rate: uint256 + tx.timestamp, # last_update: uint256 + 0, # integral: uint256 + ] + assert child_gauge.reward_data(reward_token) == reward_data + + # Decrease period + tx = child_gauge.deposit_reward_token(reward_token, 0, WEEK // 2, {"from": alice}) + + reward_data = [ + alice, # distributor: address + tx.timestamp + WEEK // 2, # period_finish: uint256 + amount // (WEEK // 2), # rate: uint256 + tx.timestamp, # last_update: uint256 + 0, # integral: uint256 + ] + assert child_gauge.reward_data(reward_token) == reward_data + + +def test_deposit_reward_token(alice, child_gauge, reward_token): + child_gauge.add_reward(reward_token, alice, {"from": alice}) + with brownie.reverts(): + child_gauge.deposit_reward_token(reward_token, 0, WEEK * 3 // 7 - 1, {"from": alice}) + with brownie.reverts(): + child_gauge.deposit_reward_token(reward_token, 0, WEEK * 4 * 12 + 1, {"from": alice}) + + +@given( + lp_amount=strategy('uint256', min_value=1, max_value=10 ** 9), + delta=strategy('uint256', min_value=1, max_value=10 ** 6), +) +@settings(max_examples=20) +def test_reward_remaining(alice, bob, charlie, child_gauge, reward_token, lp_token, chain, lp_amount, delta): + lp_token._mint_for_testing(alice, lp_amount * 10 ** 18, {"from": alice}) + lp_token.approve(child_gauge, lp_amount * 10 ** 18, {"from": alice}) + child_gauge.deposit(lp_amount * 10 ** 18, {"from": alice}) + + reward_amount = lp_amount + delta + reward_period = WEEK * 3 // 7 # minimum period + child_gauge.add_reward(reward_token, bob, {"from": alice}) + reward_token._mint_for_testing(bob, reward_amount, {"from": bob}) + reward_token.approve(child_gauge, reward_amount, {"from": bob}) + child_gauge.deposit_reward_token(reward_token, reward_amount, reward_period, {"from": bob}) + + checkpoints = 10 + for _ in range(checkpoints): + chain.sleep(reward_period // checkpoints) + child_gauge.claim_rewards({"from": alice}) + + received = reward_token.balanceOf(alice) + assert received == reward_amount - reward_amount % lp_amount + + remaining = child_gauge.reward_remaining(reward_token) + assert remaining + received == reward_amount + + child_gauge.recover_remaining(reward_token, {"from": charlie}) + assert reward_token.balanceOf(bob) == remaining diff --git a/tests/fixtures/deployments.py b/tests/fixtures/deployments.py index 94f1fd4..332a196 100644 --- a/tests/fixtures/deployments.py +++ b/tests/fixtures/deployments.py @@ -39,11 +39,6 @@ def reward_token(alice): return ERC20("Dummy Reward Token", "dRT", 18, deployer=alice) -@pytest.fixture(scope="module") -def reward_token_8(alice): - return ERC20("Dummy Reward Token", "dRT", 8, deployer=alice) - - @pytest.fixture(scope="module") def unauthorised_token(alice): """This is for testing unauthorised token""" From b6e43c07be7e3cb37930dc42be144d385d20bf6d Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Fri, 13 Sep 2024 11:05:57 +0300 Subject: [PATCH 18/20] chore: forbid CRV as reward, add few comments and refactor --- contracts/implementations/ChildGauge.vy | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index a2cf8f6..dd75473 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -16,9 +16,6 @@ implements: ERC20 interface ERC20Extended: def symbol() -> String[32]: view -interface ERC1271: - def isValidSignature(_hash: bytes32, _signature: Bytes[65]) -> bytes32: view - interface Factory: def owner() -> address: view def voting_escrow() -> address: view @@ -134,7 +131,7 @@ period_timestamp: public(HashMap[int128, uint256]) # 1e18 * ∫(rate(t) / totalSupply(t) dt) from 0 till checkpoint integrate_inv_supply: public(HashMap[int128, uint256]) -# custom xchain parameters +# xchain specific root_gauge: public(address) @@ -207,14 +204,13 @@ def _checkpoint(_user: address): week_time = min(week_time + WEEK, block.timestamp) # check CRV balance and increase weekly inflation rate by delta for the rest of the week - crv_balance: uint256 = 0 crv: ERC20 = FACTORY.crv() if crv != empty(ERC20): - crv_balance = crv.balanceOf(self) - if crv_balance != 0: - current_week: uint256 = block.timestamp / WEEK - self.inflation_rate[current_week] += crv_balance / ((current_week + 1) * WEEK - block.timestamp) - crv.transfer(FACTORY.address, crv_balance) + crv_balance: uint256 = crv.balanceOf(self) + if crv_balance != 0: + current_week: uint256 = block.timestamp / WEEK + self.inflation_rate[current_week] += crv_balance / ((current_week + 1) * WEEK - block.timestamp) + crv.transfer(FACTORY.address, crv_balance) period += 1 self.period = period @@ -670,6 +666,7 @@ def add_reward(_reward_token: address, _distributor: address): @param _distributor Address permitted to fund this contract with the reward token """ assert msg.sender in [self.manager, FACTORY.owner()] # dev: only manager or factory admin + assert _reward_token != FACTORY.crv().address # dev: can not distinguish CRV reward from CRV emission assert _distributor != empty(address) # dev: distributor cannot be zero address reward_count: uint256 = self.reward_count @@ -809,4 +806,7 @@ def version() -> String[8]: @view @external def factory() -> Factory: + """ + @notice Get factory of this gauge + """ return FACTORY From 58ef8b9636d9797c3933484086c1d1cedc04da45 Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Fri, 13 Sep 2024 11:53:30 +0300 Subject: [PATCH 19/20] fix: permit for smart contracts, withdraw parameters order and comments --- contracts/implementations/ChildGauge.vy | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/contracts/implementations/ChildGauge.vy b/contracts/implementations/ChildGauge.vy index dd75473..0ed9273 100644 --- a/contracts/implementations/ChildGauge.vy +++ b/contracts/implementations/ChildGauge.vy @@ -16,6 +16,9 @@ implements: ERC20 interface ERC20Extended: def symbol() -> String[32]: view +interface ERC1271: + def isValidSignature(_hash: bytes32, _signature: Bytes[65]) -> bytes32: view + interface Factory: def owner() -> address: view def voting_escrow() -> address: view @@ -69,7 +72,7 @@ VERSION: constant(String[8]) = "1.0.0" EIP712_TYPEHASH: constant(bytes32) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") EIP2612_TYPEHASH: constant(bytes32) = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") - +ERC1271_MAGIC_VAL: constant(bytes32) = 0x1626ba7e00000000000000000000000000000000000000000000000000000000 voting_escrow: public(address) @@ -371,11 +374,13 @@ def deposit(_value: uint256, _addr: address = msg.sender, _claim_rewards: bool = @external @nonreentrant('lock') -def withdraw(_value: uint256, _claim_rewards: bool = False): +def withdraw(_value: uint256, _claim_rewards: bool = False, _receiver: address = msg.sender): """ @notice Withdraw `_value` LP tokens @dev Withdrawing also claims pending reward tokens @param _value Number of tokens to withdraw + @param _claim_rewards Whether to claim rewards + @param _receiver Receiver of withdrawn LP tokens """ self._checkpoint(msg.sender) @@ -392,7 +397,7 @@ def withdraw(_value: uint256, _claim_rewards: bool = False): self._update_liquidity_limit(msg.sender, new_balance, total_supply) - ERC20(self.lp_token).transfer(msg.sender, _value) + ERC20(self.lp_token).transfer(_receiver, _value) log Withdraw(msg.sender, _value) log Transfer(msg.sender, empty(address), _value) @@ -506,7 +511,11 @@ def permit( ), ) ) - assert ecrecover(digest, _v, _r, _s) == _owner # dev: invalid signature + if _owner.is_contract: + sig: Bytes[65] = concat(_abi_encode(_r, _s), slice(convert(_v, bytes32), 31, 1)) + assert ERC1271(_owner).isValidSignature(digest, sig) == ERC1271_MAGIC_VAL # dev: invalid signature + else: + assert ecrecover(digest, _v, _r, _s) == _owner # dev: invalid signature self.allowance[_owner][_spender] = _value self.nonces[_owner] = unsafe_add(nonce, 1) @@ -698,7 +707,7 @@ def set_reward_distributor(_reward_token: address, _distributor: address): 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 + @dev Nothing happens, just stop emissions and that's it @param _is_killed Killed status to set """ assert msg.sender == FACTORY.owner() # dev: only owner From cf0e22fee85767b889bb11b32c0264a603834a5e Mon Sep 17 00:00:00 2001 From: Roman Agureev Date: Fri, 13 Sep 2024 19:02:37 +0300 Subject: [PATCH 20/20] feat: remove msg.sender from salt --- contracts/ChildGaugeFactory.vy | 2 +- contracts/RootGaugeFactory.vy | 2 +- scripts/calculate_proxy.py | 4 ++-- .../test_deploy_child_gauge.py | 2 +- .../test_gauge_addresses.py | 20 +------------------ .../test_root_gauge_deploy.py | 2 +- 6 files changed, 7 insertions(+), 25 deletions(-) diff --git a/contracts/ChildGaugeFactory.vy b/contracts/ChildGaugeFactory.vy index 1324acc..820df80 100644 --- a/contracts/ChildGaugeFactory.vy +++ b/contracts/ChildGaugeFactory.vy @@ -166,7 +166,7 @@ def deploy_gauge(_lp_token: address, _salt: bytes32, _manager: address = msg.sen gauge_data: uint256 = 1 # set is_valid_gauge = True implementation: address = self.get_implementation - salt: bytes32 = keccak256(_abi_encode(chain.id, msg.sender, _salt)) + salt: bytes32 = keccak256(_abi_encode(chain.id, _salt)) gauge: address = create_minimal_proxy_to( implementation, salt=salt ) diff --git a/contracts/RootGaugeFactory.vy b/contracts/RootGaugeFactory.vy index 9978faf..48a70b9 100644 --- a/contracts/RootGaugeFactory.vy +++ b/contracts/RootGaugeFactory.vy @@ -114,7 +114,7 @@ def deploy_gauge(_chain_id: uint256, _salt: bytes32) -> RootGauge: assert bridger != empty(Bridger) # dev: chain id not supported implementation: address = self.get_implementation - salt: bytes32 = keccak256(_abi_encode(_chain_id, msg.sender, _salt)) + salt: bytes32 = keccak256(_abi_encode(_chain_id, _salt)) gauge: RootGauge = RootGauge(create_minimal_proxy_to( implementation, value=msg.value, diff --git a/scripts/calculate_proxy.py b/scripts/calculate_proxy.py index 05221de..acb8fda 100644 --- a/scripts/calculate_proxy.py +++ b/scripts/calculate_proxy.py @@ -41,12 +41,12 @@ def create2_address_of(_addr, _salt, _initcode): return to_address(keccak(prefix + addr + salt + keccak(initcode))[12:]) -def main(_chain_id: str, _deployer: str, _salt: str): +def main(_chain_id: str, _salt: str): factory = RootGaugeFactory.at("0xabC000d88f23Bb45525E447528DBF656A9D55bf5") implementation_addr = factory.get_implementation() init_code = vyper_proxy_init_code(implementation_addr) salt = keccak( - encode_single("(uint256,address,bytes32)", [int(_chain_id), _deployer, HexBytes(_salt)]) + encode_single("(uint256,bytes32)", [int(_chain_id), HexBytes(_salt)]) ) print(create2_address_of(factory.address, salt, init_code)) diff --git a/tests/child_gauge_factory/test_deploy_child_gauge.py b/tests/child_gauge_factory/test_deploy_child_gauge.py index 1ef6ff4..c344615 100644 --- a/tests/child_gauge_factory/test_deploy_child_gauge.py +++ b/tests/child_gauge_factory/test_deploy_child_gauge.py @@ -16,7 +16,7 @@ def test_deploy_child_gauge( ): proxy_init_code = vyper_proxy_init_code(child_gauge_impl.address) salt = encode( - ["(uint256,address,bytes32)"], [(chain.id, alice.address, (0).to_bytes(32, "big"))] + ["(uint256,bytes32)"], [(chain.id, (0).to_bytes(32, "big"))] ) expected = create2_address_of(child_gauge_factory.address, web3.keccak(salt), proxy_init_code) diff --git a/tests/root_gauge_factory/test_gauge_addresses.py b/tests/root_gauge_factory/test_gauge_addresses.py index c1ded09..a6ac134 100644 --- a/tests/root_gauge_factory/test_gauge_addresses.py +++ b/tests/root_gauge_factory/test_gauge_addresses.py @@ -1,26 +1,9 @@ from brownie import Contract, web3 -from brownie.convert import to_address from eth_abi import encode -from hexbytes import HexBytes SALT = b"5A170000000000000000000000000000" -def salt(chain_id, sender): - return web3.keccak(encode(["(uint256,address,bytes32)"], [(chain_id, sender, SALT)])) - - -def zksync_create2_address_of(_addr, _salt, _initcode): - prefix = web3.keccak(text="zksyncCreate2") - addr = HexBytes(_addr) - addr = HexBytes(0) * 12 + addr + HexBytes(0) * (20 - len(addr)) - salt = HexBytes(_salt) - initcode = HexBytes(_initcode) - return to_address( - web3.keccak(prefix + addr + salt + web3.keccak(initcode) + web3.keccak(b""))[12:] - ) - - def test_gauge_address( alice, chain, @@ -57,12 +40,11 @@ def test_gauge_address_chain_id( RootGauge, vyper_proxy_init_code, create2_address_of, - web3, ): chain_id = chain.id + 1 proxy_init_code = vyper_proxy_init_code(child_gauge_impl.address) expected = create2_address_of( - child_gauge_factory.address, salt(chain_id, alice.address), proxy_init_code + child_gauge_factory.address, web3.keccak(encode(["(uint256,bytes32)"], [(chain_id, SALT)])), proxy_init_code ) bridger = MockBridger.deploy({"from": alice}) root_gauge_factory.set_child( diff --git a/tests/root_gauge_factory/test_root_gauge_deploy.py b/tests/root_gauge_factory/test_root_gauge_deploy.py index f56bde7..b18f3fc 100644 --- a/tests/root_gauge_factory/test_root_gauge_deploy.py +++ b/tests/root_gauge_factory/test_root_gauge_deploy.py @@ -15,7 +15,7 @@ def test_deploy_root_gauge( ): proxy_init_code = vyper_proxy_init_code(root_gauge_impl.address) salt = encode( - ["(uint256,address,bytes32)"], [(chain.id, alice.address, (0).to_bytes(32, "big"))] + ["(uint256,bytes32)"], [(chain.id, (0).to_bytes(32, "big"))] ) expected = create2_address_of(root_gauge_factory.address, web3.keccak(salt), proxy_init_code)