Skip to content
This repository has been archived by the owner on Mar 2, 2023. It is now read-only.

WIP - review architecture #192

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
13 changes: 9 additions & 4 deletions contracts/Vault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ contract Vault is IVault, Ownable {
vaultState.netLoans -
vaultState.insuranceReserveBalance -
vaultState.boostedAmount -
VaultMath.calculateLockedProfit(vaultState.currentProfits, block.timestamp, vaultState.latestRepay);
VaultMath.calculateLockedProfits(vaultState.currentProfits, block.timestamp, vaultState.latestRepay);
}

function claimable(address token) external view override returns (uint256) {
Expand Down Expand Up @@ -260,9 +260,10 @@ contract Vault is IVault, Ownable {
uint256 baseInterestRate = 0;
uint256 fees = vaultData.fixedFee;
if (amount > 0) {
uint256 freeLiquidity = vaultData.takeLoan(IERC20(token), amount, riskFactor);
uint256 freeLiquidity = vaultData.takeLoan(token, amount, riskFactor);
IERC20(token).safeTransfer(msg.sender, amount);

baseInterestRate = VaultMath.computeInterestRateNoLeverage(
baseInterestRate = VaultMath.getLoanBaseFee(
vaultData.netLoans - amount,
freeLiquidity,
vaultData.insuranceReserveBalance,
Expand All @@ -288,8 +289,12 @@ contract Vault is IVault, Ownable {

VaultState.VaultData storage vaultData = vaults[token];

IERC20 tkn = IERC20(token);
uint256 amountToTransfer = vaultData.repayLoan(tkn, debt, fees, amount, riskFactor);
tkn.safeTransfer(borrower, amountToTransfer);

emit LoanRepaid(borrower, token, amount);

return vaultData.repayLoan(IERC20(token), borrower, debt, fees, amount, riskFactor);
return amountToTransfer;
}
}
22 changes: 22 additions & 0 deletions contracts/interfaces/IInterestRateModel.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.12;

Check warning

Code scanning / Slither

Incorrect versions of Solidity

Pragma version>=0.8.12 (contracts/interfaces/IInterestRateModel.sol#2) necessitates a version too recent to be trusted. Consider deploying with 0.6.12/0.7.6/0.8.7

/// @title Interface of the InterestRateModel contract
/// @author Ithil
interface IInterestRateModel {
function computePairRiskFactor(uint256 rf0, uint256 rf1) external pure returns (uint256);

function computeIR(
uint256 baseIR,
uint256 toBorrow,
uint256 amountIn,
uint256 initialExposure,
uint256 collateral
) external view returns (uint256);

function computeTimeFees(
uint256 principal,
uint256 interestRate,
uint256 time
) external pure returns (uint256 dueFees);
}
16 changes: 16 additions & 0 deletions contracts/interfaces/ISVGImageGenerator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.12;

Check warning

Code scanning / Slither

Incorrect versions of Solidity

Pragma version>=0.8.12 (contracts/interfaces/ISVGImageGenerator.sol#2) necessitates a version too recent to be trusted. Consider deploying with 0.6.12/0.7.6/0.8.7

/// @title Interface of the SVGImageGenerator contract
/// @author Ithil
interface ISVGImageGenerator {
function generateMetadata(
string memory name,
string memory symbol,
uint256 id,
address token,
uint256 amount,
uint256 createdAt,
int256 score
) external pure returns (string memory);
}
35 changes: 15 additions & 20 deletions contracts/interfaces/IStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface IStrategy {
uint256 deadline;
}

/// @param owner the account who opened the position
/// @param lender the account who lent the tokens (defaults to the Vault)
/// @param owedToken the token which must be repayed to the vault
/// @param heldToken the token held in the strategy as investment effect
/// @param collateralToken the token used as collateral
Expand All @@ -36,6 +36,7 @@ interface IStrategy {
/// @param fees the fees generated by the position so far
/// @param createdAt the date and time in unix epoch when the position was opened
struct Position {
address lender;
address owedToken;
address heldToken;
address collateralToken;
Expand Down Expand Up @@ -88,11 +89,6 @@ interface IStrategy {
uint256 amount
) external view returns (uint256, uint256);

/// @notice computes the risk factor of the token pair, from the individual risk factors
/// @param token0 first token of the pair
/// @param token1 second token of the pair
function computePairRiskFactor(address token0, address token1) external view returns (uint256);

function deleteAndBurn(uint256 positionId) external;

function approveAllowance(Position memory position) external;
Expand Down Expand Up @@ -144,21 +140,20 @@ interface IStrategy {

/// ==== ERRORS ==== ///

error Strategy__Order_Expired(uint256 timestamp, uint256 deadline);
error Strategy__Source_Eq_Dest(address token);
error Strategy__Insufficient_Collateral(uint256 collateral);
error Strategy__Restricted_Access(address owner, address sender);
error Strategy__Order_Expired();
error Strategy__Source_Eq_Dest();
error Strategy__Insufficient_Collateral();
error Strategy__Restricted_Access();
error Strategy__Action_Throttled();
error Strategy__Maximum_Leverage_Exceeded(uint256 interestRate);
error Strategy__Insufficient_Amount_Out(uint256 amountIn, uint256 minAmountOut);
error Strategy__Loan_Not_Repaid(uint256 repaid, uint256 debt);
error Strategy__Only_Liquidator(address sender, address liquidator);
error Strategy__Position_Not_Liquidable(uint256 id, int256 score);
error Strategy__Margin_Below_Minimum(uint256 marginProvider, uint256 minimumMargin);
error Strategy__Insufficient_Margin_Provided(int256 newScore);
error Strategy__Not_Enough_Liquidity(uint256 balance, uint256 amount);
error Strategy__Unsupported_Token(address token0, address token1);
error Strategy__Too_High_Risk(uint256 riskFactor);
error Strategy__Insufficient_Amount_Out();
error Strategy__Loan_Not_Repaid();
error Strategy__Only_Liquidator();
error Strategy__Position_Not_Liquidable();
error Strategy__Margin_Below_Minimum();
error Strategy__Insufficient_Margin_Provided();
error Strategy__Not_Enough_Liquidity();
error Strategy__Unsupported_Token();
error Strategy__Too_High_Risk();
error Strategy__Locked();
error Strategy__Only_Guardian();
error Strategy__Incorrect_Obtained_Token();
Expand Down
2 changes: 2 additions & 0 deletions contracts/libraries/GeneralMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { VaultState } from "./VaultState.sol";
/// @author Ithil
/// @notice A library to perform the most common math operations
library GeneralMath {
uint24 public constant RESOLUTION = 10000;

function positiveSub(uint256 a, uint256 b) internal pure returns (uint256) {
if (a > b) {
return a - b;
Expand Down
27 changes: 7 additions & 20 deletions contracts/libraries/VaultMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ import { GeneralMath } from "./GeneralMath.sol";
library VaultMath {
using GeneralMath for uint256;

uint24 internal constant RESOLUTION = 10000;
uint24 internal constant TIME_FEE_PERIOD = 86400;
uint24 internal constant MAX_RATE = 500;

uint256 internal constant DEGRADATION_COEFFICIENT = 21600; // six hours
uint24 internal constant TIME_FEE_WINDOW = 84600;

Check warning

Code scanning / Slither

Unused state variable

VaultMath.TIME_FEE_WINDOW (contracts/libraries/VaultMath.sol#14) is never used in VaultMath (contracts/libraries/VaultMath.sol#10-62)

/// @notice Computes the amount of native tokens to exchange for wrapped
/// @param amount the number of native tokens to stake
Expand All @@ -40,32 +37,22 @@ library VaultMath {
return (totalBalance != 0 && totalSupply != 0) ? (totalSupply * amount) / totalBalance : amount;
}

/// @notice Computes the due time fees of a certain position
function computeTimeFees(
uint256 principal,
uint256 interestRate,
uint256 time
) internal pure returns (uint256 dueFees) {
return (principal * interestRate * (time + 1)).ceilingDiv(uint32(TIME_FEE_PERIOD) * RESOLUTION);
}

/// @notice Computes the interest rate to apply to a position at its opening
function computeInterestRateNoLeverage(
function getLoanBaseFee(
uint256 netLoans,
uint256 freeLiquidity,
uint256 insuranceReserveBalance,
uint256 riskFactor,
uint256 baseFee
) internal pure returns (uint256) {
uint256 uncovered = netLoans.positiveSub(insuranceReserveBalance);
uint256 interestRate = (netLoans + uncovered) * riskFactor;
interestRate /= (netLoans + freeLiquidity);
interestRate += baseFee;
uint256 fee = (netLoans + uncovered) * riskFactor;
fee /= (netLoans + freeLiquidity);
fee += baseFee;

return interestRate;
return fee;
}

function calculateLockedProfit(
function calculateLockedProfits(
uint256 profits,
uint256 time,
uint256 latestRepay
Expand Down
50 changes: 16 additions & 34 deletions contracts/libraries/VaultState.sol
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.12;

import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { GeneralMath } from "./GeneralMath.sol";
import { VaultMath } from "./VaultMath.sol";

/// @title VaultState library
/// @author Ithil
/// @notice A library to store the vault status
library VaultState {
using SafeERC20 for IERC20;
using GeneralMath for uint256;

error Vault__Insufficient_Free_Liquidity(address token, uint256 requested, uint256 freeLiquidity);
Expand Down Expand Up @@ -45,22 +44,9 @@ library VaultState {
uint256 currentProfits;
}

function addInsuranceReserve(
VaultState.VaultData storage self,
uint256 totalBalance,
uint256 fees
) internal returns (uint256) {
uint256 availableInsuranceBalance = self.insuranceReserveBalance.positiveSub(self.netLoans);
uint256 insurancePortion = (fees * self.optimalRatio * (totalBalance - availableInsuranceBalance)) /
(totalBalance * VaultMath.RESOLUTION);
self.insuranceReserveBalance += insurancePortion;

return insurancePortion;
}

function takeLoan(
VaultState.VaultData storage self,
IERC20 token,
address token,
uint256 amount,
uint256 riskFactor
) internal returns (uint256) {
Expand All @@ -70,43 +56,40 @@ library VaultState {

// If the following fails drainage has occurred so we want failure
uint256 freeLiquidity = IERC20(token).balanceOf(address(this)) - self.insuranceReserveBalance;

if (amount > freeLiquidity) revert Vault__Insufficient_Free_Liquidity(address(token), amount, freeLiquidity);

token.safeTransfer(msg.sender, amount);
// We have transferred an amount <= freeLiquidity, therefore we now have
// We are transferring an amount <= freeLiquidity, therefore we now have
// IERC20(token).balanceOf(address(this)) >= self.insuranceReserveBalance
return freeLiquidity;
}

function subtractLoan(VaultState.VaultData storage self, uint256 b) private {
if (self.netLoans > b) self.netLoans -= b;
else self.netLoans = 0;
}

function subtractInsuranceReserve(VaultState.VaultData storage self, uint256 b) private {
self.insuranceReserveBalance = self.insuranceReserveBalance.positiveSub(b);
}

function repayLoan(
VaultState.VaultData storage self,
IERC20 token,
address borrower,
uint256 debt,
uint256 fees,
uint256 amount,
uint256 riskFactor
) internal returns (uint256) {
uint256 amountToTransfer = 0;
uint256 totalRisk = self.optimalRatio * self.netLoans;
subtractLoan(self, debt);

if (self.netLoans > debt) self.netLoans -= debt;
else self.netLoans = 0;

self.optimalRatio = self.netLoans != 0 ? totalRisk.positiveSub(riskFactor * debt) / self.netLoans : 0;
if (amount >= debt + fees) {
// At this point amount has been transferred here
// Insurance reserve increases by a portion of fees
uint256 insurancePortion = addInsuranceReserve(self, token.balanceOf(address(this)), fees);
uint256 totalBalance = token.balanceOf(address(this));
uint256 insurancePortion = (fees *
self.optimalRatio *
(totalBalance - self.insuranceReserveBalance.positiveSub(self.netLoans))) /
(totalBalance * GeneralMath.RESOLUTION);
self.insuranceReserveBalance += insurancePortion;

self.currentProfits =
VaultMath.calculateLockedProfit(self.currentProfits, block.timestamp, self.latestRepay) +
VaultMath.calculateLockedProfits(self.currentProfits, block.timestamp, self.latestRepay) +
fees -
insurancePortion;
self.latestRepay = block.timestamp;
Expand All @@ -116,11 +99,10 @@ library VaultState {
} else {
// Bad liquidation: rewards the liquidator with 5% of the amountIn
// amount is already adjusted in BaseStrategy
if (amount < debt) subtractInsuranceReserve(self, debt - amount);
if (amount < debt) self.insuranceReserveBalance.positiveSub(debt - amount);
amountToTransfer = amount / 19;
}

token.safeTransfer(borrower, amountToTransfer);
return amountToTransfer;
}
}
18 changes: 9 additions & 9 deletions contracts/liquidation/Liquidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ contract Liquidator is Ownable {
// rewardPercentage is computed as of the tokens staked
function rewardPercentage() public view returns (uint256) {
if (maximumStake > 0) {
uint256 stakePercentage = (stakes[address(token)][msg.sender] * VaultMath.RESOLUTION) / maximumStake;
if (stakePercentage > VaultMath.RESOLUTION) return VaultMath.RESOLUTION;
uint256 stakePercentage = (stakes[address(token)][msg.sender] * GeneralMath.RESOLUTION) / maximumStake;
if (stakePercentage > GeneralMath.RESOLUTION) return GeneralMath.RESOLUTION;
else return stakePercentage;
} else {
return 0;
Expand Down Expand Up @@ -116,8 +116,8 @@ contract Liquidator is Ownable {

// Computation of reward is done by adding to the dueFees
dueFees +=
((amountIn.positiveSub(position.principal + dueFees)) * (VaultMath.RESOLUTION - reward)) /
VaultMath.RESOLUTION;
((amountIn.positiveSub(position.principal + dueFees)) * (GeneralMath.RESOLUTION - reward)) /
GeneralMath.RESOLUTION;

// In a bad liquidation event, 5% of the paid amount is transferred
// Linearly scales with reward (with 0 reward corresponding to 0 transfer)
Expand All @@ -128,7 +128,7 @@ contract Liquidator is Ownable {
// then amountIn / 20 > amountIn - principal - dueFees and the liquidator may be better off
// not liquidating the position and instead wait for it to become bad liquidation
if (amountIn < (20 * (position.principal + dueFees)) / 19)
amountIn -= (amountIn * reward) / (20 * VaultMath.RESOLUTION);
amountIn -= (amountIn * reward) / (20 * GeneralMath.RESOLUTION);

strategy.directRepay(
position.owedToken,
Expand Down Expand Up @@ -165,7 +165,7 @@ contract Liquidator is Ownable {
fairPrice += dueFees;
// Apply discount based on reward (max 5%)
// In this case there is no distinction between good or bad liquidation
fairPrice -= (fairPrice * reward) / (VaultMath.RESOLUTION * 20);
fairPrice -= (fairPrice * reward) / (GeneralMath.RESOLUTION * 20);
if (price < fairPrice) {
revert Liquidator__Below_Fair_Price(price, fairPrice);
} else {
Expand Down Expand Up @@ -208,7 +208,7 @@ contract Liquidator is Ownable {
if (score > 0) {
strategy.transferNFT(positionId, liquidatorUser);
// reduce due fees based on reward (max 50%)
position.fees += (dueFees * (2 * VaultMath.RESOLUTION - reward)) / (2 * VaultMath.RESOLUTION);
position.fees += (dueFees * (2 * GeneralMath.RESOLUTION - reward)) / (2 * GeneralMath.RESOLUTION);
position.createdAt = block.timestamp;
bool collateralInOwedToken = position.collateralToken != position.heldToken;
if (collateralInOwedToken) {
Expand Down Expand Up @@ -246,7 +246,7 @@ contract Liquidator is Ownable {

uint256 dueFees = position.fees +
(position.interestRate * (block.timestamp - position.createdAt) * position.principal) /
(uint32(VaultMath.TIME_FEE_PERIOD) * VaultMath.RESOLUTION);
(uint32(VaultMath.TIME_FEE_WINDOW) * GeneralMath.RESOLUTION);

if (collateralInOwedToken) {
(expectedTokensOwed, ) = strategy.quote(position.heldToken, position.owedToken, position.allowance);
Expand All @@ -262,7 +262,7 @@ contract Liquidator is Ownable {

int256 score = SafeCast.toInt256(position.collateral * position.riskFactor) -
profitAndLoss *
int24(VaultMath.RESOLUTION);
int24(GeneralMath.RESOLUTION);

return (score, dueFees, expectedTokensOwed, expectedTokensHeld);
}
Expand Down
4 changes: 2 additions & 2 deletions contracts/mock/MockKyberNetworkProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ pragma solidity >=0.8.12;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { GeneralMath } from "../libraries/GeneralMath.sol";
import { IKyberNetworkProxy } from "../interfaces/external/IKyberNetworkProxy.sol";
import { VaultMath } from "../libraries/VaultMath.sol";

/// @dev Used for testing, unaudited
contract MockKyberNetworkProxy is IKyberNetworkProxy, Ownable {
Expand Down Expand Up @@ -57,7 +57,7 @@ contract MockKyberNetworkProxy is IKyberNetworkProxy, Ownable {
if (rate2 == 0) return (0, 0);

uint256 res = (rate1 * srcDec) / rate2;
res = (res * (VaultMath.RESOLUTION - slippages[src])) / VaultMath.RESOLUTION;
res = (res * (GeneralMath.RESOLUTION - slippages[src])) / GeneralMath.RESOLUTION;

if (res * rate2 < rate1 * srcDec) res++;

Expand Down
Loading