Skip to content

Commit

Permalink
docs: rename vault to Savings crvUSD (#34)
Browse files Browse the repository at this point in the history
* refactor: staking -> saving

* ci: remove parallel pytest to avoid hitting rate limits

* feat: we can now cache in boa if we reject n=auto

* chore: comments cleanup
  • Loading branch information
heswithme authored Oct 15, 2024
1 parent 148b847 commit 494e3eb
Show file tree
Hide file tree
Showing 9 changed files with 57 additions and 47 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@ jobs:
env:
ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }}
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
run: uv run pytest -n auto ${{ matrix.folder }}
run: uv run pytest ${{ matrix.folder }}
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# st-crvUSD
# scrvUSD

Staking vault for crvUSD.
Savings vault for crvUSD.

## Vault Implementation Details

Expand All @@ -14,7 +14,7 @@ Although the vault is called "multi-strategy" it actually doesn't contain any st

Rewards in the vault will come from the fees generated by crvUSD controllers. The vault distributes a percentage of the fees through the fee splitter. What percentage of the fees is distributed is determined by Curve's DAO and the vault's [RewardsHandler](contracts/RewardsHandler.vy).

Anyone sending crvUSD to the rewards handler is also effectively donating those funds for distribution to stakers.
Anyone sending crvUSD to the rewards handler is also effectively donating those funds for distribution to scrvUSD vault depositors.


```mermaid
Expand All @@ -31,7 +31,7 @@ For the fee splitter to send funds to the rewards handler, the rewards handler m

The `weight` function allows anyone to take snapshots of the ratio of crvUSD in the vault compared to the circulating supply of crvUSD. This ratio is used to determine the percentage of the fees that can be requested by the fee splitter.

For instance if the time-weighed average of the ratio is 0.1 (10% of the circulating supply is staked), the fee splitter will request 10% of the fees generated by the crvUSD controllers.
For instance if the time-weighed average of the ratio is 0.1 (10% of the circulating supply is deposited into the vault), the fee splitter will request 10% of the fees generated by the crvUSD controllers.

---

Expand Down
3 changes: 2 additions & 1 deletion contracts/DepositLimitModule.vy
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ def available_deposit_limit(receiver: address) -> uint256:
"""
@notice Checks the available deposit limit for a given receiver.
@param receiver The address querying deposit limit.
@return uint256 Returns the maximum deposit limit if deposits are not paused, otherwise returns 0.
@return uint256 Returns the maximum deposit limit if deposits are not paused,
otherwise returns (self.max_deposit_limit - vault_balance).
"""
if self.deposits_paused:
return 0
Expand Down
44 changes: 26 additions & 18 deletions contracts/RewardsHandler.vy
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@

"""
@title Rewards Handler
@notice A contract that helps distributing rewards for st-crvUSD, an ERC4626
vault for crvUSD (yearn's vault v3 multi-vault implementaiton is used). Any
crvUSD token sent to this contract is considered donated as rewards for staker
and will not be recoverable. This contract can receive funds to be distributed
from the FeeSplitter (crvUSD borrow rates revenues) and potentially other
sources as well. The amount of funds that this contract should receive from the
fee splitter is determined by computing the time-weighted average of the vault
@notice A contract that helps distributing rewards for scrvUSD, an ERC4626 vault
for crvUSD (yearn's vault v3 multi-vault implementaiton is used). Any crvUSD
token sent to this contract is considered donated as rewards for depositors and
will not be recoverable. This contract can receive funds to be distributed from
the FeeSplitter (crvUSD borrow rates revenues) and potentially other sources as
well. The amount of funds that this contract should receive from the fee
splitter is determined by computing the time-weighted average of the vault
balance over crvUSD circulating supply ratio. The contract handles the rewards
in a permissionless manner, anyone can take snapshots of the TVL and distribute
rewards. In case of manipulation of the time-weighted average, the contract
allows trusted contracts given the role of `RATE_MANGER` to correct the
distribution rate of the rewards.
@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved
@author curve.fi
@custom:security [email protected]
"""

Expand Down Expand Up @@ -113,7 +117,7 @@ _SUPPORTED_INTERFACES: constant(bytes4[3]) = [
stablecoin: immutable(IERC20)
vault: public(immutable(IVault))

# scaling factor for the staked token / circulating supply ratio.
# scaling factor for the deposited token / circulating supply ratio.
scaling_factor: public(uint256)

# the minimum amount of rewards requested to the FeeSplitter.
Expand Down Expand Up @@ -152,7 +156,7 @@ def __init__(

twa.__init__(
WEEK, # twa_window = 1 week
1, # min_snapshot_dt_seconds = 1 second (if 0, then spam is possible)
1, # min_snapshot_dt_seconds = 1 second
)

self._set_minimum_weight(minimum_weight)
Expand All @@ -166,12 +170,14 @@ def __init__(
# PERMISSIONLESS FUNCTIONS #
################################################################


@external
def take_snapshot():
"""
@notice Function that anyone can call to take a snapshot of the current staked
supply ratio in the vault. This is used to compute the time-weighted average of
the TVL to decide on the amount of rewards to ask for (weight).
@notice Function that anyone can call to take a snapshot of the current
deposited supply ratio in the vault. This is used to compute the time-weighted
average of the TVL to decide on the amount of rewards to ask for (weight).
@dev There's no point in MEVing this snapshot as the rewards distribution rate
can always be reduced (if a malicious actor inflates the value of the snapshot)
or the minimum amount of rewards can always be increased (if a malicious actor
Expand Down Expand Up @@ -208,8 +214,8 @@ def process_rewards():
), "rewards should be distributed over time"

# any crvUSD sent to this contract (usually through the fee splitter, but
# could also come from other sources) will be used as a reward for crvUSD
# stakers in the vault.
# could also come from other sources) will be used as a reward for scrvUSD
# vault depositors.
available_balance: uint256 = staticcall stablecoin.balanceOf(self)

assert available_balance > 0, "no rewards to distribute"
Expand Down Expand Up @@ -245,9 +251,9 @@ def weight() -> uint256:
"""
@notice this function is part of the dynamic weight interface expected by the
FeeSplitter to know what percentage of funds should be sent for rewards
distribution to crvUSD stakers.
distribution to scrvUSD vault depositors.
@dev `minimum_weight` acts as a lower bound for the percentage of rewards that
should be distributed to stakers. This is useful to bootstrapping TVL by asking
should be distributed to depositors. This is useful to bootstrapping TVL by asking
for more at the beginning and can also be increased in the future if someone
tries to manipulate the time-weighted average of the tvl ratio.
"""
Expand Down Expand Up @@ -289,6 +295,7 @@ def set_distribution_time(new_distribution_time: uint256):
this value lower will reduce the time it takes to stream the rewards, making it
longer will do the opposite. Setting it to 0 will immediately distribute all the
rewards.
@dev This function can be used to prevent the rewards distribution from being
manipulated (i.e. MEV twa snapshots to obtain higher APR for the vault). Setting
this value to zero can be used to pause `process_rewards`.
Expand All @@ -310,10 +317,11 @@ def set_distribution_time(new_distribution_time: uint256):
def set_minimum_weight(new_minimum_weight: uint256):
"""
@notice Update the minimum weight that the the vault will ask for.
@dev This function can be used to prevent the rewards requested from being
manipulated (i.e. MEV twa snapshots to obtain lower APR for the vault). Setting
this value to zero makes the amount of rewards requested fully determined by the
twa of the staked supply ratio.
twa of the deposited supply ratio.
"""
access_control._check_role(RATE_MANAGER, msg.sender)
self._set_minimum_weight(new_minimum_weight)
Expand Down Expand Up @@ -354,7 +362,7 @@ def recover_erc20(token: IERC20, receiver: address):
access_control._check_role(RECOVERY_MANAGER, msg.sender)

# if crvUSD was sent by accident to the contract the funds are lost and will
# be distributed as staking rewards on the next `process_rewards` call.
# be distributed as rewards on the next `process_rewards` call.
assert token != stablecoin, "can't recover crvusd"
# when funds are recovered the whole balanced is sent to a trusted address.
Expand Down
9 changes: 5 additions & 4 deletions contracts/TWA.vy
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ MAX_SNAPSHOTS: constant(uint256) = 10**18 # 31.7 billion years if snapshot ever
snapshots: public(DynArray[Snapshot, MAX_SNAPSHOTS])
min_snapshot_dt_seconds: public(uint256) # Minimum time between snapshots in seconds
twa_window: public(uint256) # Time window in seconds for TWA calculation
last_snapshot_timestamp: public(uint256) # Timestamp of the last snapshot (assigned in RewardsHandler)
last_snapshot_timestamp: public(uint256) # Timestamp of the last snapshot


struct Snapshot:
tracked_value: uint256 # In 1e18 precision
tracked_value: uint256
timestamp: uint256


Expand All @@ -83,7 +83,6 @@ def __init__(_twa_window: uint256, _min_snapshot_dt_seconds: uint256):
def get_len_snapshots() -> uint256:
"""
@notice Returns the number of snapshots stored.
@return Number of snapshots.
"""
return len(self.snapshots)

Expand Down Expand Up @@ -115,6 +114,7 @@ def _take_snapshot(_value: uint256):
) # store the snapshot into the DynArray
log SnapshotTaken(_value, block.timestamp)


@internal
def _set_twa_window(_new_window: uint256):
"""
Expand All @@ -136,12 +136,13 @@ def _set_snapshot_dt(_new_dt_seconds: uint256):
self.min_snapshot_dt_seconds = _new_dt_seconds
log SnapshotIntervalUpdated(_new_dt_seconds)


@internal
@view
def _compute() -> uint256:
"""
@notice Computes the TWA over the specified time window by iterating backwards over the snapshots.
@return The TWA for tracked value over the self.twa_window (10**18 decimals precision).
@return The TWA for tracked value over the self.twa_window.
"""
num_snapshots: uint256 = len(self.snapshots)
if num_snapshots == 0:
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest

boa.set_etherscan(api_key=os.getenv("ETHERSCAN_API_KEY"))
BOA_CACHE = False
BOA_CACHE = True


@pytest.fixture(autouse=True)
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/test_deploy_vault.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
def test_deploy_vault(vault_factory, crvusd):
vault = vault_factory.deploy_new_vault(
crvusd,
"Staked crvUSD",
"st-crvUSD",
"Savings crvUSD",
"scrvUSD",
# TODO figure out who's going to be the role manager
boa.env.generate_address(),
1234,
)
vault = boa.load_partial("contracts/yearn/VaultV3.vy").at(vault)
assert vault.name() == "Staked crvUSD"
assert vault.name() == "Savings crvUSD"
2 changes: 1 addition & 1 deletion tests/unitary/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def vault(vault_factory, crvusd, role_manager, dev_address):

with boa.env.prank(dev_address):
address = vault_factory.deploy_new_vault(
crvusd, "Staked crvUSD", "st-crvUSD", role_manager, 0
crvusd, "Savings crvUSD", "scrvUSD", role_manager, 0
)

return vault_deployer.at(address)
Expand Down
28 changes: 14 additions & 14 deletions tests/unitary/twa/test_internal_compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,15 @@ def test_default_behavior_twa_multiple_deposits(
N_ITER = 5
time_between_deposits = twa_window // N_ITER

staked_supply_rates = []
deposited_supply_rates = []
timestamps = []

for i in range(N_ITER):
vault.deposit(amt_deposit, alice, sender=alice)
rewards_handler.take_snapshot()

rate, ts = rewards_handler.snapshots(i)
staked_supply_rates.append(rate)
deposited_supply_rates.append(rate)
timestamps.append(ts)

boa.env.time_travel(seconds=time_between_deposits)
Expand All @@ -105,11 +105,11 @@ def test_default_behavior_twa_multiple_deposits(
if remaining_time > 0:
boa.env.time_travel(seconds=remaining_time)

total_weighted_staked_supply_rate = 0
total_weighted_deposited_supply_rate = 0
total_time = 0
for i in range(len(staked_supply_rates) - 1):
current_rate = staked_supply_rates[i]
next_rate = staked_supply_rates[i + 1]
for i in range(len(deposited_supply_rates) - 1):
current_rate = deposited_supply_rates[i]
next_rate = deposited_supply_rates[i + 1]

current_timestamp = timestamps[i]
next_timestamp = timestamps[i + 1]
Expand All @@ -118,24 +118,24 @@ def test_default_behavior_twa_multiple_deposits(

trapezoidal_rate = (current_rate + next_rate) // 2

total_weighted_staked_supply_rate += trapezoidal_rate * time_delta
total_weighted_deposited_supply_rate += trapezoidal_rate * time_delta
total_time += time_delta

if len(staked_supply_rates) > 0:
last_rate = staked_supply_rates[-1]
if len(deposited_supply_rates) > 0:
last_rate = deposited_supply_rates[-1]
last_timestamp = timestamps[-1]
time_delta = boa.env.evm.patch.timestamp - last_timestamp

total_weighted_staked_supply_rate += last_rate * time_delta
total_weighted_deposited_supply_rate += last_rate * time_delta
total_time += time_delta

expected_twa = total_weighted_staked_supply_rate // total_time
expected_twa = total_weighted_deposited_supply_rate // total_time

twa = rewards_handler.compute_twa()

total_staked_amount = amt_deposit * N_ITER
total_deposited_amount = amt_deposit * N_ITER
circulating_supply = rewards_handler.eval("lens._circulating_supply()")
staked_rate = total_staked_amount * 10**4 // circulating_supply
deposited_rate = total_deposited_amount * 10**4 // circulating_supply

assert twa <= staked_rate, "TWA is unexpectedly higher than the staked rate"
assert twa <= deposited_rate, "TWA is unexpectedly higher than the staked rate"
assert twa == expected_twa, f"TWA {twa} does not match expected {expected_twa}"

0 comments on commit 494e3eb

Please sign in to comment.