Skip to content

Commit

Permalink
feat: add management fee
Browse files Browse the repository at this point in the history
Co-authored-by: banteg <[email protected]>
  • Loading branch information
fubuloubu and banteg committed Oct 20, 2020
1 parent 3a7e60d commit 1228e46
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 64 deletions.
58 changes: 37 additions & 21 deletions contracts/Vault.vy
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,13 @@ emergencyShutdown: public(bool)
depositLimit: public(uint256) # Limit for totalAssets the Vault can hold
debtLimit: public(uint256) # Debt limit for the Vault across all strategies
totalDebt: public(uint256) # Amount of tokens that all strategies have borrowed
lastReport: public(uint256) # Number of blocks since last report

rewards: public(address)
performanceFee: public(uint256) # Fee for governance rewards
FEE_MAX: constant(uint256) = 10000 # 100%, or 10000 basis points
rewards: public(address) # Rewards contract where Governance fees are sent to
managementFee: public(uint256) # Governance Fee for management of Vault (given to `rewards`)
performanceFee: public(uint256) # Governance Fee for performance of Vault (given to `rewards`)
FEE_MAX: constant(uint256) = 10_000 # 100%, or 10k basis points
BLOCKS_PER_YEAR: constant(uint256) = 2_300_000

@external
def __init__(
Expand All @@ -113,7 +116,9 @@ def __init__(
self.rewards = _rewards
self.guardian = msg.sender
self.performanceFee = 450 # 4.5% of yield (per strategy)
self.managementFee = 200 # 2% per year
self.depositLimit = MAX_UINT256 # Start unlimited
self.lastReport = block.number


@pure
Expand Down Expand Up @@ -153,6 +158,12 @@ def setPerformanceFee(_fee: uint256):
self.performanceFee = _fee


@external
def setManagementFee(_fee: uint256):
assert msg.sender == self.governance
self.managementFee = _fee


@external
def setGuardian(_guardian: address):
assert msg.sender in [self.guardian, self.governance]
Expand Down Expand Up @@ -689,29 +700,34 @@ def report(_return: uint256) -> uint256:
debt: uint256 = self._debtOutstanding(msg.sender)

# Issue new shares to cover fees
# NOTE: Applies if strategy is not shutting down, or it is but all debt paid off
# NOTE: In effect, this reduces overall share price by the combined fee
governance_fee: uint256 = (
self._totalAssets() * (block.number - self.lastReport) * self.managementFee
) / FEE_MAX / BLOCKS_PER_YEAR
self.lastReport = block.number
strategist_fee: uint256 = 0 # Only applies in certain conditions

# NOTE: Applies if strategy is not shutting down, or it is but all debt paid off
# NOTE: No fee is taken when a strategy is unwinding it's position, until all debt is paid
if _return > debt:
strategist_fee: uint256 = (
strategist_fee = (
(_return - debt) * self.strategies[msg.sender].performanceFee
) / FEE_MAX
governance_fee: uint256 = ((_return - debt) * self.performanceFee) / FEE_MAX
total_fee: uint256 = governance_fee + strategist_fee

# NOTE: In certain cases, the calculated fee might be too small
if total_fee > 0:
# NOTE: This must be called prior to taking new collateral,
# or the calculation will be wrong!
# NOTE: This must be done at the same time, to ensure the relative
# ratio of governance_fee : strategist_fee is kept intact
reward: uint256 = self._issueSharesForAmount(self, total_fee)

# Send the rewards out as new shares in this Vault
strategist_reward: uint256 = (strategist_fee * reward) / total_fee
self._transfer(self, Strategy(msg.sender).strategist(), strategist_reward)
# NOTE: Governance earns the dust
self._transfer(self, self.rewards, self.balanceOf[self])
governance_fee += (_return - debt) * self.performanceFee / FEE_MAX

# NOTE: This must be called prior to taking new collateral,
# or the calculation will be wrong!
# NOTE: This must be done at the same time, to ensure the relative
# ratio of governance_fee : strategist_fee is kept intact
total_fee: uint256 = governance_fee + strategist_fee
reward: uint256 = self._issueSharesForAmount(self, total_fee)

# Send the rewards out as new shares in this Vault
if strategist_fee > 0:
strategist_reward: uint256 = (strategist_fee * reward) / total_fee
self._transfer(self, Strategy(msg.sender).strategist(), strategist_reward)
# NOTE: Governance earns any dust leftover from flooring math above
self._transfer(self, self.rewards, self.balanceOf[self])

# Compute the line of credit the Vault is able to offer the Strategy (if any)
credit: uint256 = self._creditAvailable(msg.sender)
Expand Down
1 change: 1 addition & 0 deletions tests/functional/vault/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def test_vault_deployment_with_overrides(guardian, gov, rewards, token, Vault):
("guardian", "setGuardian", None),
("rewards", "setRewards", None),
("performanceFee", "setPerformanceFee", 1000),
("managementFee", "setManagementFee", 1000),
("depositLimit", "setDepositLimit", 1000),
("guardian", "setGuardian", None),
],
Expand Down
1 change: 1 addition & 0 deletions tests/functional/vault/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def test_reject_ether(gov, vault):
("setRewards", ["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"]),
("setGuardian", ["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"]),
("setPerformanceFee", [0]),
("setManagementFee", [0]),
("setEmergencyShutdown", [True]),
("approve", ["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 1]),
("transfer", ["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 1]),
Expand Down
61 changes: 42 additions & 19 deletions tests/functional/vault/test_withdrawal.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,59 @@
import brownie


def test_multiple_withdrawals(token, gov, vault, TestStrategy):
def test_multiple_withdrawals(token, gov, Vault, TestStrategy):
# Need a fresh vault to do this math right
vault = Vault.deploy(token, gov, gov, "", "", {"from": gov})
starting_balance = token.balanceOf(gov)
strategies = [gov.deploy(TestStrategy, vault) for _ in range(5)]
[vault.addStrategy(s, 1000, 10, 50, {"from": gov}) for s in strategies]

before_balance = token.balanceOf(gov)
[
vault.addStrategy(
s,
token.balanceOf(gov) // 10, # 10% of all tokens
2 ** 256 - 1, # No rate limit
0, # No fee
{"from": gov},
)
for s in strategies
]

token.approve(vault, 2 ** 256 - 1, {"from": gov})
vault.deposit(token.balanceOf(gov), {"from": gov})

assert token.balanceOf(gov) < before_balance
before_balance = token.balanceOf(gov)
assert token.balanceOf(gov) == 0
assert token.balanceOf(vault) == starting_balance

[s.harvest({"from": gov}) for s in strategies] # Seed all the strategies with debt

assert token.balanceOf(vault) == starting_balance // 2
for s in strategies: # All of them have debt
print(s, vault.balanceSheetOfStrategy(s))
assert vault.balanceSheetOfStrategy(s) > 0
assert vault.balanceSheetOfStrategy(s) == token.balanceOf(s)

# We withdraw from all the strategies
vault.withdraw(vault.balanceOf(gov) // 2, {"from": gov})
assert token.balanceOf(gov) > before_balance
before_balance = token.balanceOf(gov)

assert vault.balanceOf(gov) > 0
assert (
vault.balanceSheetOfStrategy(s)
== token.balanceOf(s)
== starting_balance // 10
)

# Withdraw only from Vault
before = token.balanceOf(vault)
vault.withdraw(vault.balanceOf(gov) // 2 + 1, {"from": gov})
assert token.balanceOf(vault) == 0
assert token.balanceOf(gov) == before
for s in strategies:
assert (
vault.balanceSheetOfStrategy(s)
== token.balanceOf(s)
== starting_balance // 10
)

# We've drained all the debt
vault.withdraw(vault.balanceOf(gov), {"from": gov})
assert token.balanceOf(gov) > before_balance

for s in strategies: # Should have pulled everything from each strategy
for s in strategies:
assert vault.balanceSheetOfStrategy(s) == 0
assert token.balanceOf(s) == 0

assert vault.totalDebt() == 0
for s in strategies:
assert vault.balanceSheetOfStrategy(s) == token.balanceOf(s) == 0


def test_forced_withdrawal(token, gov, vault, TestStrategy, rando, chain):
Expand Down
44 changes: 20 additions & 24 deletions tests/integration/test_operation.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,44 @@
class NormalOperation:
def __init__(self, token, vault, strategy, user, farm, keeper):
def __init__(self, web3, token, vault, strategy, user, farm, keeper):
self.web3 = web3
self.token = token
self.vault = vault
self.strategy = strategy
self.farm = farm
self.keeper = keeper
self.user = user

def setup(self):
self.last_price = 1.0

def rule_deposit(self):
print(" NormalOperation.deposit()")
print(" Vault.deposit()")

# Deposit 50% of what they have left
self.vault.deposit(self.token.balanceOf(self.user) // 2, {"from": self.user})
amt = self.token.balanceOf(self.user) // 2
self.vault.deposit(amt, {"from": self.user})

def rule_withdraw(self):
print(" NormalOperation.withdraw()")
print(" Vault.withdraw()")

# Withdraw 50% of what they have in the Vault
self.vault.withdraw(self.vault.balanceOf(self.user) // 2, {"from": self.user})
amt = self.vault.balanceOf(self.user) // 2
self.vault.withdraw(amt, {"from": self.user})

def rule_harvest(self):
print(" Strategy.harvest()")

def rule_yield(self):
print(" NormalOperation.yield()")
# Earn 1% yield on deposits in some farming protocol
self.token.transfer(
self.strategy,
self.token.balanceOf(self.strategy) // 100,
{"from": self.farm},
)
amt = self.token.balanceOf(self.strategy) // 100
self.token.transfer(self.strategy, amt, {"from": self.farm})

def rule_harvest(self):
print(" NormalOperation.harvest()")
# Keeper decides to harvest the yield
self.strategy.harvest({"from": self.keeper})

def invariant_numbergoup(self):
# Positive-return Strategy should never reduce the price of a share
price = self.vault.pricePerShare() / 10 ** self.vault.decimals()
assert price >= self.last_price
self.last_price = price
# TODO: Invariant that user did not get > they should have
# TODO: Invariant that fees/accounting is all perfect
# TODO: Invariant that all economic assumptions are maintained


def test_normal_operation(
gov, strategy, vault, token, chad, andre, keeper, state_machine
web3, gov, strategy, vault, token, chad, andre, keeper, state_machine
):
vault.addStrategy(
strategy,
Expand All @@ -53,4 +49,4 @@ def test_normal_operation(
)
strategy.harvest({"from": keeper})
assert token.balanceOf(vault) == 0
state_machine(NormalOperation, token, vault, strategy, chad, andre, keeper)
state_machine(NormalOperation, web3, token, vault, strategy, chad, andre, keeper)

0 comments on commit 1228e46

Please sign in to comment.