Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ __pycache__
build/
reports/
.env
env

# Node/npm
node_modules/
4 changes: 2 additions & 2 deletions brownie-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ autofetch_sources: True

# require OpenZepplin Contracts
dependencies:
- yearn/yearn-vaults@0.4.3
- yearn/yearn-vaults@0.3.0-2
- OpenZeppelin/openzeppelin-contracts@3.1.0

# path remapping to support imports from GitHub/NPM
compiler:
solc:
version: 0.6.12
remappings:
- "@yearnvaults=yearn/yearn-vaults@0.4.3"
- "@yearnvaults=yearn/yearn-vaults@0.3.0-2"
- "@openzeppelin=OpenZeppelin/openzeppelin-contracts@3.1.0"

reports:
Expand Down
190 changes: 190 additions & 0 deletions contracts/LossOnFeeChecker.vy
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# @version 0.3.7
"""
@title Loss On Fee Checker
@author Yearn Finance
@license MIT
@dev
Blocks harvests on strategies that would experiece a loss due to fees.
Standard healthcheck in unable to catch such losses, thus this special
purpose contract is useful.
"""

from vyper.interfaces import ERC20
from vyper.interfaces import ERC20Detailed

interface IVault:
def totalAssets() -> uint256: view
def managementFee() -> uint256: view
def performanceFee() -> uint256: view
def strategies(strategy: address) -> StrategyParams: view
def lastReport() -> uint256: view
def totalDebt() -> uint256: view
def apiVersion() -> String[28]: view

interface IVaultNew:
def strategies(strategy: address) -> StrategyParamsNew: view

interface IStrategy:
def vault() -> address: view
def want() -> address: view

event Sweep:
sweeper: indexed(address)
token: indexed(address)
amount: uint256

struct StrategyParams:
performanceFee: uint256
activation: uint256
debtRatio: uint256
rateLimit: uint256
lastReport: uint256
totalDebt: uint256
totalGain: uint256
totalLoss: uint256

struct StrategyParamsNew:
performanceFee: uint256
activation: uint256
debtRatio: uint256
minDebtPerHarvest: uint256
maxDebtPerHarvest: uint256
lastReport: uint256
totalDebt: uint256
totalGain: uint256
totalLoss: uint256

MAX_BPS: constant(uint256) = 10_000
SECS_PER_YEAR: constant(uint256) = 31_557_600

@external
@view
def check_loss(gain: uint256, loss: uint256, strategy: address = msg.sender) -> uint256:
return self._check(gain, loss, strategy)

@internal
@view
def _check(gain: uint256, loss: uint256, strategy: address) -> uint256:
"""
@notice
Check for losses generated by fees and return a precise amount.
@param gain - amount of reported gain
@param loss - amount of reported loss
@param strategy - strategy to compute for. defaults to msg.sender for use on-chain.
@return amount of projected loss
@dev
Fee calculations replicate logic from vault versions
0.3.0 - mgmt fee calculated based on total assets in vault
0.3.1 - mgmt calculated based on total debt of vault
0.3.2 - fee calculations are same as 0.3.1, but StrategyParams are different
"""

vault: IVault = IVault(IStrategy(strategy).vault())

management_fee: uint256 = vault.managementFee()
if management_fee == 0:
return 0

total_assets: uint256 = vault.totalAssets()
if total_assets == 0:
return 0

last_report: uint256 = vault.lastReport()
if last_report == 0:
return 0

time_since: uint256 = block.timestamp - last_report
if time_since == 0:
return 0

api: String[28] = vault.apiVersion()
if api == "0.3.0":
params: StrategyParams = vault.strategies(strategy)
return self._calc030(vault, gain, loss, management_fee, params.performanceFee, total_assets, time_since)
if api == "0.3.1":
params: StrategyParams = vault.strategies(strategy)
return self._calc031(vault, gain, loss, management_fee, params.performanceFee, time_since)
if api == "0.3.2":
params: StrategyParamsNew = IVaultNew(vault.address).strategies(strategy)
return self._calc031(vault, gain, loss, management_fee, params.performanceFee, time_since)
else:
raise # @dev: Vault api version not supported

return 0

@internal
@view
def _calc030(
vault: IVault,
gain: uint256,
loss: uint256,
management_fee: uint256,
strat_perf_fee: uint256,
total_assets: uint256,
time_since: uint256
) -> uint256:
total_fee: uint256 = (
total_assets * time_since * management_fee
/ MAX_BPS
/ SECS_PER_YEAR
)

if gain > 0:
strategist_fee: uint256 = gain * strat_perf_fee / MAX_BPS
performance_fee: uint256 = gain * vault.performanceFee() / MAX_BPS
total_fee = total_fee + strategist_fee + performance_fee

if gain >= loss:
gross_profit: uint256 = gain - loss
if gross_profit >= total_fee:
return 0
else:
return total_fee - gross_profit
else:
gross_loss: uint256 = loss - gain
return gross_loss + total_fee

return 0

@internal
@view
def _calc031(
vault: IVault,
gain: uint256,
loss: uint256,
management_fee: uint256,
strat_perf_fee: uint256,
time_since: uint256
) -> uint256:
vault_debt: uint256 = vault.totalDebt()
total_fee: uint256 = (
vault_debt * time_since * management_fee
/ MAX_BPS
/ SECS_PER_YEAR
)

if gain > 0:
strategist_fee: uint256 = gain * strat_perf_fee / MAX_BPS
performance_fee: uint256 = gain * vault.performanceFee() / MAX_BPS
total_fee = total_fee + strategist_fee + performance_fee

if gain >= loss:
gross_profit: uint256 = gain - loss
if gross_profit >= total_fee:
return 0
else:
return total_fee - gross_profit
else:
gross_loss: uint256 = loss - gain
return gross_loss + total_fee

return 0

@external
def sweep(token: address):
"""
@notice Allow governance ms to sweep
"""
assert msg.sender == 0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52 # @dev: !approved
bal: uint256 = ERC20(token).balanceOf(self)
ERC20(token).transfer(msg.sender, bal, default_return_value=True)
73 changes: 23 additions & 50 deletions contracts/RouterStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@ import {
} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
import "@openzeppelin/contracts/math/Math.sol";

interface ILossChecker {
function check_loss(uint, uint) external view returns (uint);
}

interface ISharesHelper {
function sharesToAmount(address, uint) external view returns (uint);
function amountToShares(address, uint) external view returns (uint);
}

interface IVault is IERC20 {
function token() external view returns (address);

function decimals() external view returns (uint256);

function deposit() external;

function pricePerShare() external view returns (uint256);

function withdraw(
uint256 amount,
address account,
Expand All @@ -35,8 +39,12 @@ contract RouterStrategy is BaseStrategy {

string internal strategyName;
IVault public yVault;
ILossChecker public constant lossChecker = ILossChecker(0x6b6003d4Bc320Ed25E8E2be49600EC1006676239);
uint256 public feeLossTolerance;
uint256 public maxLoss;
bool internal isOriginal = true;
ISharesHelper public constant sharesHelper =
ISharesHelper(0x444443bae5bB8640677A8cdF94CB8879Fec948Ec); // CREATE2 generated address, deployable to all chains

constructor(
address _vault,
Expand Down Expand Up @@ -104,6 +112,7 @@ contract RouterStrategy is BaseStrategy {
{
yVault = IVault(_yVault);
strategyName = _strategyName;
IERC20(address(want)).approve(_yVault, uint256(-1));
}

function name() external view override returns (string memory) {
Expand All @@ -120,10 +129,6 @@ contract RouterStrategy is BaseStrategy {
return balanceOfWant().add(valueOfInvestment());
}

function delegatedAssets() external view override returns (uint256) {
return vault.strategies(address(this)).totalDebt;
}

function prepareReturn(uint256 _debtOutstanding)
internal
virtual
Expand All @@ -138,7 +143,7 @@ contract RouterStrategy is BaseStrategy {
uint256 _totalAsset = estimatedTotalAssets();

// Estimate the profit we have so far
if (_totalDebt <= _totalAsset) {
if (_totalDebt < _totalAsset) {
_profit = _totalAsset.sub(_totalDebt);
}

Expand All @@ -164,6 +169,9 @@ contract RouterStrategy is BaseStrategy {
_profit = _profit.sub(_loss);
_loss = 0;
}

uint expectedLoss = lossChecker.check_loss(_profit, _loss);
require(feeLossTolerance >= expectedLoss, "LossyWithFees");
}

function adjustPosition(uint256 _debtOutstanding)
Expand All @@ -177,7 +185,6 @@ contract RouterStrategy is BaseStrategy {

uint256 balance = balanceOfWant();
if (balance > 0) {
_checkAllowance(address(yVault), address(want), balance);
yVault.deposit();
}
}
Expand Down Expand Up @@ -221,20 +228,6 @@ contract RouterStrategy is BaseStrategy {
yVault.withdraw(sharesToWithdraw, address(this), maxLoss);
}

function liquidateAllPositions()
internal
virtual
override
returns (uint256 _amountFreed)
{
return
yVault.withdraw(
yVault.balanceOf(address(this)),
address(this),
maxLoss
);
}

function prepareMigration(address _newStrategy) internal virtual override {
IERC20(yVault).safeTransfer(
_newStrategy,
Expand All @@ -252,29 +245,12 @@ contract RouterStrategy is BaseStrategy {
ret[0] = address(yVault);
}

function ethToWant(uint256 _amtInWei)
public
view
virtual
override
returns (uint256)
{
return _amtInWei;
}

function setMaxLoss(uint256 _maxLoss) public onlyVaultManagers {
function setMaxLoss(uint256 _maxLoss) public onlyAuthorized {
maxLoss = _maxLoss;
}

function _checkAllowance(
address _contract,
address _token,
uint256 _amount
) internal {
if (IERC20(_token).allowance(address(this), _contract) < _amount) {
IERC20(_token).safeApprove(_contract, 0);
IERC20(_token).safeApprove(_contract, type(uint256).max);
}
function setFeeLossTolerance(uint256 _tolerance) public onlyAuthorized {
feeLossTolerance = _tolerance;
}

function balanceOfWant() public view returns (uint256) {
Expand All @@ -286,13 +262,10 @@ contract RouterStrategy is BaseStrategy {
view
returns (uint256)
{
return amount.mul(10**yVault.decimals()).div(yVault.pricePerShare());
return sharesHelper.amountToShares(address(yVault), amount);
}

function valueOfInvestment() public view virtual returns (uint256) {
return
yVault.balanceOf(address(this)).mul(yVault.pricePerShare()).div(
10**yVault.decimals()
);
return sharesHelper.sharesToAmount(address(yVault), yVault.balanceOf(address(this)));
}
}
Loading