diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8b0bced..f1b2f1b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -55,4 +55,9 @@ jobs: env: ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }} ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} - run: uv run pytest ${{ matrix.folder }} + run: | + if [ "${{ matrix.folder }}" == "tests/unitary" ]; then + uv run pytest ${{ matrix.folder }} -n=auto + else + uv run pytest ${{ matrix.folder }} + fi diff --git a/contracts/RewardsHandler.vy b/contracts/RewardsHandler.vy index e7c5b31..a0563e1 100644 --- a/contracts/RewardsHandler.vy +++ b/contracts/RewardsHandler.vy @@ -160,7 +160,7 @@ def __init__( twa.__init__( WEEK, # twa_window = 1 week - 1, # min_snapshot_dt_seconds = 1 second + 600, # min_snapshot_dt_seconds = 600 seconds ) self._set_minimum_weight(minimum_weight) @@ -188,15 +188,22 @@ def take_snapshot(): or the minimum amount of rewards can always be increased (if a malicious actor deflates the value of the snapshot). """ + self._take_snapshot() + +@internal +def _take_snapshot(): + """ + @notice Internal function to take a snapshot of the current deposited supply + ratio in the vault. + """ # get the circulating supply from a helper contract. # supply in circulation = controllers' debt + peg keppers' debt circulating_supply: uint256 = staticcall self.stablecoin_lens.circulating_supply() - # obtain the supply of crvUSD contained in the vault by simply checking its - # balance since it's an ERC4626 vault. This will also take into account - # rewards that are not yet distributed. - supply_in_vault: uint256 = staticcall stablecoin.balanceOf(vault.address) + # obtain the supply of crvUSD contained in the vault by checking its totalAssets. + # This will not take into account rewards that are not yet distributed. + supply_in_vault: uint256 = staticcall vault.totalAssets() # here we intentionally reduce the precision of the ratio because the # dynamic weight interface expects a percentage in BPS. @@ -206,11 +213,14 @@ def take_snapshot(): @external -def process_rewards(): +def process_rewards(take_snapshot: bool = True): """ @notice Permissionless function that let anyone distribute rewards (if any) to the crvUSD vault. """ + # optional (advised) snapshot before distributing the rewards + if take_snapshot: + self._take_snapshot() # prevent the rewards from being distributed untill the distribution rate # has been set diff --git a/contracts/TWA.vy b/contracts/TWA.vy index f6571be..4bd37d4 100644 --- a/contracts/TWA.vy +++ b/contracts/TWA.vy @@ -107,7 +107,9 @@ def _take_snapshot(_value: uint256): @notice Stores a snapshot of the tracked value. @param _value The value to store. """ - if self.last_snapshot_timestamp + self.min_snapshot_dt_seconds <= block.timestamp: + if (len(self.snapshots) == 0) or ( # First snapshot + self.last_snapshot_timestamp + self.min_snapshot_dt_seconds <= block.timestamp # after dt + ): self.last_snapshot_timestamp = block.timestamp self.snapshots.append( Snapshot(tracked_value=_value, timestamp=block.timestamp) diff --git a/tests/unitary/rewards_handler/test_process_rewards.py b/tests/unitary/rewards_handler/test_process_rewards.py index 4dedca5..af201da 100644 --- a/tests/unitary/rewards_handler/test_process_rewards.py +++ b/tests/unitary/rewards_handler/test_process_rewards.py @@ -27,3 +27,23 @@ def test_no_rewards(rewards_handler, rate_manager): with boa.reverts("no rewards to distribute"): rewards_handler.process_rewards() + + +def test_snapshots_taking(rewards_handler, rate_manager, crvusd): + rewards_handler.set_distribution_time(1234, sender=rate_manager) # to enable process_rewards + assert rewards_handler.get_len_snapshots() == 0 + boa.deal(crvusd, rewards_handler, 1) + rewards_handler.process_rewards() + assert crvusd.balanceOf(rewards_handler) == 0 # crvusd gone + assert rewards_handler.get_len_snapshots() == 1 # first snapshot taken + + boa.deal(crvusd, rewards_handler, 1) + rewards_handler.process_rewards() + assert crvusd.balanceOf(rewards_handler) == 0 # crvusd gone (again) + assert rewards_handler.get_len_snapshots() == 1 # not changed since dt has not passed + + boa.env.time_travel(seconds=rewards_handler.min_snapshot_dt_seconds()) + boa.deal(crvusd, rewards_handler, 1) + rewards_handler.process_rewards() + assert crvusd.balanceOf(rewards_handler) == 0 # crvusd gone (they always go) + assert rewards_handler.get_len_snapshots() == 2 # changed since dt has passed diff --git a/tests/unitary/rewards_handler/test_rh_constructor.py b/tests/unitary/rewards_handler/test_rh_constructor.py index ba3f900..82e6402 100644 --- a/tests/unitary/rewards_handler/test_rh_constructor.py +++ b/tests/unitary/rewards_handler/test_rh_constructor.py @@ -33,4 +33,4 @@ def test_default_behavior( # eoa would be the deployer from which we revoke the role assert not rewards_handler.hasRole(rewards_handler.DEFAULT_ADMIN_ROLE(), boa.env.eoa) assert rewards_handler.eval("twa.twa_window") == 86_400 * 7 - assert rewards_handler.eval("twa.min_snapshot_dt_seconds") == 1 + assert rewards_handler.eval("twa.min_snapshot_dt_seconds") == 600 # 10 minutes diff --git a/tests/unitary/rewards_handler/test_take_snapshot.py b/tests/unitary/rewards_handler/test_take_snapshot.py new file mode 100644 index 0000000..2bbcc66 --- /dev/null +++ b/tests/unitary/rewards_handler/test_take_snapshot.py @@ -0,0 +1,15 @@ +import pytest +import boa + + +@pytest.mark.gas_profile +def test_take_snapshot_compute(rewards_handler): + n_days = 7 + dt = 600 + snaps_per_day = 86_400 // dt + for i_day in range(n_days): + for i_snap in range(snaps_per_day): + rewards_handler.take_snapshot() + boa.env.time_travel(seconds=dt) + twa = rewards_handler.compute_twa() + assert twa >= 0, f"Computed TWA is negative: {twa}"