Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: v3.0.3 #201

Merged
merged 20 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 1 addition & 28 deletions .github/workflows/foundry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,43 +19,16 @@ jobs:
- ubuntu-latest
architecture:
- "x64"
python-version:
- "3.10"
node_version:
- 16

steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: recursive

- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }}

- name: Install Ape
uses: ApeWorX/[email protected]
with:
python-version: '3.10'

- name: install vyper
run: pip install git+https://github.com/vyperlang/vyper

- name: Compile contracts
# Compile Ape contracts to get dependencies
run: ape compile --force --size


- name: Install Vyper
run: pip install vyper==0.3.7

- name: Use Node.js ${{ matrix.node_version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node_version }}

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ jobs:
with:
fetch-depth: 1

- name: Set up python 3.8
- name: Set up python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.8
python-version: 3.9

- name: Set pip cache directory path
id: pip-cache-dir-path
Expand Down
13 changes: 8 additions & 5 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Test
name: Ape tests

on:
push:
Expand All @@ -20,12 +20,15 @@ jobs:

steps:
- uses: actions/checkout@v1
- uses: ApeWorX/[email protected]
with:
python-version: '3.10'

- name: install vyper
run: pip install git+https://github.com/vyperlang/vyper
- name: install requirements
run: python3 -m pip install -r requirements.txt

- name: install plugins
run: ape plugins install .

- name: Compile contracts
# TODO: Force recompiles until ape compile caching is fixed
run: ape compile --force --size
Expand All @@ -34,7 +37,7 @@ jobs:
- name: Setup node.js
uses: actions/setup-node@v1
with:
node-version: '16.x'
node-version: '18.x'

- name: Install hardhat
run: npm install hardhat
Expand Down
7 changes: 7 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@
[submodule "lib/erc4626-tests"]
path = lib/erc4626-tests
url = https://github.com/a16z/erc4626-tests
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
release = v4.9.5
[submodule "lib/tokenized-strategy"]
path = lib/tokenized-strategy
url = https://github.com/yearn/tokenized-strategy
15 changes: 10 additions & 5 deletions TECH_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ When deploying a new vault, it requires the following parameters:
- role_manager: account that can assign and revoke Roles
- profit_max_unlock_time: max amount of time profit will be locked before being distributed

All deployment variables besides the `asset` can be updated post deployment.

## Normal Operation

### Deposits / Mints
Expand Down Expand Up @@ -127,11 +129,7 @@ Every role can be filled by an EOA, multi-sig or other smart contracts. Each rol

The account that manages roles is a single account, set in `role_manager`.

This role_manager can be an EOA, a multi-sig or a Governance Module that relays calls.

The vault comes with the ability to "open" every role. Meaning that any function that requires the caller to hold that role would be come permsissionless.

The vault imposes no restrictions on the role managers ability to open or close any role. **But this should be done with extreme care as most of the roles are not meant to be opened and can lead to loss of funds if done incorrectly**.
This role_manager can be an EOA, a multi-sig or a Governance contract that relays calls.

### Strategy Management
This responsibility is taken by callers with ADD_STRATEGY_MANAGER, REVOKE_STRATEGY_MANAGER and FORCE_REVOKE_MANAGER roles
Expand Down Expand Up @@ -171,6 +169,13 @@ The vault checks that the `minimumTotalIdle` parameter is respected (i.e. there'

If the strategy has more debt than the max_debt, the vault will request the funds back. These funds may be locked in the strategy, which will result in the strategy returning less funds than requested by the vault.

#### Auto Allocations
The DEBT_MANAGER can set the vaults `auto_allocate` flag to `True`.

This will cause every deposit or mint call to end by the vault pushing as much debt as possible to the first strategy in the queue.

NOTE: Not having at least 1 strategy in the `default_queue` with the `auto_allocate` flag will cause all deposits to revert.

#### Setting maximum debt for a specific strategy
The MAX_DEBT_MANAGER can set the maximum amount of tokens the vault will allow a strategy to owe at any moment in time.

Expand Down
6 changes: 2 additions & 4 deletions ape-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ dependencies:
- name: tokenized-strategy
github: yearn/tokenized-strategy
ref: v3.0.2
contracts_folder: src
config_override:
contracts_folder: src

solidity:
version: 0.8.18
import_remapping:
- "@openzeppelin/contracts=openzeppelin/v4.9.5"
- "@tokenized-strategy=tokenized-strategy/v3.0.2"

ethereum:
local:
Expand Down
Binary file added audits/Yearn V3 report Statemind.pdf
Binary file not shown.
131 changes: 93 additions & 38 deletions contracts/VaultFactory.vy
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,16 @@ event UpdateGovernance:
event NewPendingGovernance:
pending_governance: indexed(address)

struct PFConfig:
# Percent of protocol's split of fees in Basis Points.
fee_bps: uint16
# Address the protocol fees get paid to.
fee_recipient: address

# Identifier for this version of the vault.
API_VERSION: constant(String[28]) = "3.0.2"
API_VERSION: constant(String[28]) = "3.0.3"

# The max amount the protocol fee can be set to.
MAX_FEE_BPS: constant(uint16) = 5_000 # 50%

# Mask used to unpack the protocol fee bps.
FEE_BPS_MASK: constant(uint256) = 2**16-1

# The address that all newly deployed vaults are based from.
VAULT_ORIGINAL: immutable(address)

Expand All @@ -92,12 +90,13 @@ pending_governance: public(address)
# Name for identification.
name: public(String[64])

# Protocol Fee Data is packed into a uint256 slot
# 72 Bits Empty | 160 Bits fee recipient | 16 bits fee bps | 8 bits custom flag

# The default config for assessing protocol fees.
default_protocol_fee_config: public(PFConfig)
default_protocol_fee_data: uint256
# Custom fee to charge for a specific vault or strategy.
custom_protocol_fee: public(HashMap[address, uint16])
# Represents if a custom protocol fee should be used.
use_custom_protocol_fee: public(HashMap[address, bool])
custom_protocol_fee_data: HashMap[address, uint256]

@external
def __init__(name: String[64], vault_original: address, governance: address):
Expand Down Expand Up @@ -163,24 +162,74 @@ def apiVersion() -> String[28]:

@view
@external
def protocol_fee_config(vault: address = msg.sender) -> PFConfig:
def protocol_fee_config(vault: address = msg.sender) -> (uint16, address):
"""
@notice Called during vault and strategy reports
to retrieve the protocol fee to charge and address
to receive the fees.
@param vault Address of the vault that would be reporting.
@return The protocol fee config for the msg sender.
@return Fee in bps
@return Address of fee recipient
"""
# If there is a custom protocol fee set we return it.
if self.use_custom_protocol_fee[vault]:
config_data: uint256 = self.custom_protocol_fee_data[vault]
if self._unpack_custom_flag(config_data):
# Always use the default fee recipient even with custom fees.
return PFConfig({
fee_bps: self.custom_protocol_fee[vault],
fee_recipient: self.default_protocol_fee_config.fee_recipient
})
return (
self._unpack_protocol_fee(config_data),
self._unpack_fee_recipient(self.default_protocol_fee_data)
)
else:
# Otherwise return the default config.
return self.default_protocol_fee_config
config_data = self.default_protocol_fee_data
return (
self._unpack_protocol_fee(config_data),
self._unpack_fee_recipient(config_data)
)

@view
@external
def use_custom_protocol_fee(vault: address) -> bool:
"""
@notice If a custom protocol fee is used for a vault.
@param vault Address of the vault to check.
@return If a custom protocol fee is used.
"""
return self._unpack_custom_flag(self.custom_protocol_fee_data[vault])

@view
@internal
def _unpack_protocol_fee(config_data: uint256) -> uint16:
"""
Unpacks the protocol fee from the packed data uint.
"""
return convert(shift(config_data, -8) & FEE_BPS_MASK, uint16)

@view
@internal
def _unpack_fee_recipient(config_data: uint256) -> address:
"""
Unpacks the fee recipient from the packed data uint.
"""
return convert(shift(config_data, -24), address)

@view
@internal
def _unpack_custom_flag(config_data: uint256) -> bool:
"""
Unpacks the custom fee flag from the packed data uint.
"""
return config_data & 1 == 1

@view
@internal
def _pack_protocol_fee_data(recipient: address, fee: uint16, custom: bool) -> uint256:
"""
Packs the full protocol fee data into a single uint256 slot.
This is used for both the default fee storage as well as for custom fees.
72 Bits Empty | 160 Bits fee recipient | 16 bits fee bps | 8 bits custom flag
"""
return shift(convert(recipient, uint256), 24) | shift(convert(fee, uint256), 8) | convert(custom, uint256)

@external
def set_protocol_fee_bps(new_protocol_fee_bps: uint16):
Expand All @@ -194,14 +243,20 @@ def set_protocol_fee_bps(new_protocol_fee_bps: uint16):
assert new_protocol_fee_bps <= MAX_FEE_BPS, "fee too high"

# Cache the current default protocol fee.
default_config: PFConfig = self.default_protocol_fee_config
assert default_config.fee_recipient != empty(address), "no recipient"
default_fee_data: uint256 = self.default_protocol_fee_data
recipient: address = self._unpack_fee_recipient(default_fee_data)

assert recipient != empty(address), "no recipient"

# Set the new fee
self.default_protocol_fee_config.fee_bps = new_protocol_fee_bps
self.default_protocol_fee_data = self._pack_protocol_fee_data(
recipient,
new_protocol_fee_bps,
False
)

log UpdateProtocolFeeBps(
default_config.fee_bps,
self._unpack_protocol_fee(default_fee_data),
new_protocol_fee_bps
)

Expand All @@ -216,10 +271,15 @@ def set_protocol_fee_recipient(new_protocol_fee_recipient: address):
assert msg.sender == self.governance, "not governance"
assert new_protocol_fee_recipient != empty(address), "zero address"

old_recipient: address = self.default_protocol_fee_config.fee_recipient

self.default_protocol_fee_config.fee_recipient = new_protocol_fee_recipient
default_fee_data: uint256 = self.default_protocol_fee_data
old_recipient: address = self._unpack_fee_recipient(default_fee_data)

self.default_protocol_fee_data = self._pack_protocol_fee_data(
new_protocol_fee_recipient,
self._unpack_protocol_fee(default_fee_data),
False
)

log UpdateProtocolFeeRecipient(
old_recipient,
new_protocol_fee_recipient
Expand All @@ -238,14 +298,13 @@ def set_custom_protocol_fee_bps(vault: address, new_custom_protocol_fee: uint16)
"""
assert msg.sender == self.governance, "not governance"
assert new_custom_protocol_fee <= MAX_FEE_BPS, "fee too high"
assert self.default_protocol_fee_config.fee_recipient != empty(address), "no recipient"
assert self._unpack_fee_recipient(self.default_protocol_fee_data) != empty(address), "no recipient"

self.custom_protocol_fee[vault] = new_custom_protocol_fee

# If this is the first time a custom fee is set for this vault
# set the bool indicator so it returns the correct fee.
if not self.use_custom_protocol_fee[vault]:
self.use_custom_protocol_fee[vault] = True
self.custom_protocol_fee_data[vault] = self._pack_protocol_fee_data(
empty(address),
new_custom_protocol_fee,
True
)

log UpdateCustomProtocolFee(vault, new_custom_protocol_fee)

Expand All @@ -259,11 +318,8 @@ def remove_custom_protocol_fee(vault: address):
"""
assert msg.sender == self.governance, "not governance"

# Reset the custom fee to 0.
self.custom_protocol_fee[vault] = 0

# Set custom fee bool back to false.
self.use_custom_protocol_fee[vault] = False
# Reset the custom fee to 0 and flag to False.
self.custom_protocol_fee_data[vault] = self._pack_protocol_fee_data(empty(address), 0, False)

log RemovedCustomProtocolFee(vault)

Expand Down Expand Up @@ -304,4 +360,3 @@ def accept_governance():
self.pending_governance = empty(address)

log UpdateGovernance(msg.sender)

Loading
Loading