Vault.sol
Setup
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "lib/chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "lib/yield-utils-v2/contracts/token/IERC20.sol";
import "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
contract Vault is Ownable {
//vault records DEPOSITS & Debt
mapping (address => uint) public deposits; //deposits in WETH
mapping (address => uint) public debts; //debt in DAI
///@notice ERC20 interface specifying token contract functions
///@dev For constant variables, the value has to be fixed at compile-time, while for immutable, it can still be assigned at construction time.
IERC20 public immutable collateral;
IERC20 public immutable debt;
// DAi/WETH Pricefeed
AggregatorV3Interface public immutable priceFeed;
event Deposit(address indexed collateralAsset, address indexed user, uint collateralAmount);
event Borrow(address indexed debtAsset, address indexed user, uint debtAmount);
event Repay(address indexed debtAsset, address indexed user, uint debtAmount);
event Withdraw(address indexed collateralAsset, address indexed user, uint collateralAmount);
event Liquidation(address indexed collateralAsset, address indexed debtAsset, address indexed user, uint debtToCover, uint liquidatedCollateralAmount);
uint bufferDenominator;
uint bufferNumerator;
constructor(address dai_, address weth_, address priceFeedAddress, uint bufferNumerator_, uint bufferDenominator_) {
collateral = IERC20(weth_);
debt = IERC20(dai_);
priceFeed = AggregatorV3Interface(priceFeedAddress);
// collateralization level
bufferNumerator = bufferNumerator_;
bufferDenominator = bufferDenominator_;
}
mappings to track deposits & debt for each user address
IERC20 interfaces to interact with both the DAI and WETH contracts
AggregatorV3Interface for interacting with Chainlink Oracle
use of immutable vs constant
for constant variables, the value has to be fixed at compile-time, (initial and assign in one line)
for immutable, it can still be assigned at construction time (within constructor)
Margin level
buffer Num/Denominator for setting margin level: 8/10 -> 80%
80% collateralization level.
maxDebt is calculated against 80% of deposits.
Functions
deposit()
function deposit(uint collateralAmount) external {
deposits[msg.sender] += collateralAmount;
bool sent = collateral.transferFrom(msg.sender, address(this), collateralAmount);
require(sent, "Deposit failed!");
emit Deposit(address(collateral), msg.sender, collateralAmount);
}
borrow()
function borrow(uint debtAmount) external {
uint maxDebt = getMaxDebt(msg.sender);
require(debtAmount <= maxDebt, "Insufficient collateral!");
debts[msg.sender] += debtAmount;
bool sent = debt.transfer(msg.sender, debtAmount);
require(sent, "Borrow failed!");
emit Borrow(address(debt), msg.sender, debtAmount);
}
can borrow up till maxDebt as calculated below
getMaxDebt()
function getMaxDebt(address user) public view returns(uint) {
uint availableCollateral = (deposits[user]/bufferDenominator)*bufferNumerator;
uint maxDebt = (availableCollateral*10**getDecimals() / get_daiETH_Price());
return maxDebt;
}
based on user's deposits, calculate the available collateral, as per specified Margin level
convert availableCollateral to the quantity of corresponding debtAsset as per mkt price
repay()
function repay(uint debtAmount) external {
debts[msg.sender] -= debtAmount;
bool sent = debt.transferFrom(msg.sender, address(this), debtAmount);
require(sent, "Repayment failed!");
emit Repay(address(debt), msg.sender, debtAmount);
}
covers partial and full repayments
withdraw()
function withdraw(uint collateralAmount) external {
if (debts[msg.sender] > 0){
uint collateralRequired = getCollateralRequired(debts[msg.sender]);
uint spareDeposit = deposits[msg.sender] - collateralRequired;
require(collateralAmount < spareDeposit, "Collateral unavailable!");
}
deposits[msg.sender] -= collateralAmount;
bool sent = collateral.transfer(msg.sender, collateralAmount);
require(sent, "Withdraw failed!");
emit Withdraw(address(collateral), msg.sender, collateralAmount);
}
users can withdraw their deposited collateral
if user has debt, checks for spare capacity before initiating withdrawal
getCollateralRequired()
function getCollateralRequired(uint debtAmount) public view returns(uint) {
uint baseCollateralRequired = debtAmount*get_daiETH_Price() / (10**getDecimals());
uint bufferCollateralRequired = (baseCollateralRequired/bufferNumerator)*bufferDenominator;
return bufferCollateralRequired;
}
Calculates collateral required to support existing debt position at current market prices
it is important to do the multiplication before the division in the pricing calculation
because leading with a division may result in a decimal -> which solidity cannot handle
liquidation()
function liquidation(address user) public onlyOwner {
uint collateralRequired = getCollateralRequired(debts[user]);
if (collateralRequired > deposits[user]){
emit Liquidation(address(collateral), address(debt), user, debts[user], deposits[user]);
delete deposits[user];
delete debts[user];
}
}
Triggers liquidation check on supplied user address
Can only be called by Vault owner;
if collateralRequired > deposits[user] -> liquidation
get_daiETH_price()
function get_daiETH_Price() public view returns(uint) {
(,int price,,,) = priceFeed.latestRoundData();
return uint(price);
}
Get current market price of Collateral/Debt asset from Chainlink
Price is returned as integer extending over its decimal places
getDecimals()
function getDecimals() public view returns(uint) {
return priceFeed.decimals();
}
Returns number of decimal places in price returned
Using get_daiETH_price & getDecimals() together
getPrice returns: 386372840000000
getDecimals returns: 18
Used to rebase chainlink price into market price, to account for decimal places
price = 386372840000000, decimals =18, actual_price = 0.00038637284
actual price should match the market price as quotes by websites and frontends.
Full code
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "lib/chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "lib/yield-utils-v2/contracts/token/IERC20.sol";
import "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
contract Vault is Ownable {
//vault records WETH DEPOSITS
mapping (address => uint) public deposits; //deposits in WETH
mapping (address => uint) public debts; //debt in DAI
///@notice ERC20 interface specifying token contract functions
///@dev For constant variables, the value has to be fixed at compile-time, while for immutable, it can still be assigned at construction time.
IERC20 public immutable collateral;
IERC20 public immutable debt;
// DAi/WETH Pricefeed
AggregatorV3Interface public immutable priceFeed;
///@dev Emitted on deposit()
///@param collateralAsset The address of the collateral asset
///@param user The address of the user calling deposit()
///@param collateralAmount The amount of collateral asset deposited
event Deposit(address indexed collateralAsset, address indexed user, uint collateralAmount);
///@dev Emitted on borrow()
///@param debtAsset The address of the debt asset
///@param user The address of the user calling borrow()
///@param debtAmount The amount of debt asset borrowed
event Borrow(address indexed debtAsset, address indexed user, uint debtAmount);
///@dev Emitted on repay()
///@param debtAsset The address of the debt asset
///@param user The address of the user calling repay()
///@param debtAmount The amount of debt asset being repaid
event Repay(address indexed debtAsset, address indexed user, uint debtAmount);
///@dev Emitted on withdraw()
///@param collateralAsset The address of the debt asset
///@param user The address of the user calling withdraw()
///@param collateralAmount The amount of collateral asset withdrawn
event Withdraw(address indexed collateralAsset, address indexed user, uint collateralAmount);
///@dev Emitted on liquidation()
///@param collateralAsset The address of the debt asset
///@param debtAsset The address of the debt asset
///@param user The address of the user calling withdraw()
///@param debtToCover The amount of debt the liquidator wants to cover
///@param liquidatedCollateralAmount The amount of collateral received by the liquidator
event Liquidation(address indexed collateralAsset, address indexed debtAsset, address indexed user, uint debtToCover, uint liquidatedCollateralAmount);
///@dev To set collateralization level of debt engine
///@notice To allow for margin call at defined level, preventing Vault from bearing risk in fast moving markets
uint bufferDenominator;
uint bufferNumerator;
constructor(address dai_, address weth_, address priceFeedAddress, uint bufferNumerator_, uint bufferDenominator_) {
collateral = IERC20(weth_);
debt = IERC20(dai_);
priceFeed = AggregatorV3Interface(priceFeedAddress);
// collateralization level
bufferNumerator = bufferNumerator_;
bufferDenominator = bufferDenominator_;
}
///@dev Users deposit collateral asset into Vault
///@param collateralAmount Amount of collateral to deposit
function deposit(uint collateralAmount) external {
deposits[msg.sender] += collateralAmount;
bool sent = collateral.transferFrom(msg.sender, address(this), collateralAmount);
require(sent, "Deposit failed!");
emit Deposit(address(collateral), msg.sender, collateralAmount);
}
///@notice Users borrow debt asset calculated based on collateralization level and their deposits
///@dev See getMaxDebt() for colleteralization colculation
///@param debtAmount Amount of debt asset to borrow
function borrow(uint debtAmount) external {
uint maxDebt = getMaxDebt(msg.sender);
require(debtAmount <= maxDebt, "Insufficient collateral!");
debts[msg.sender] += debtAmount;
bool sent = debt.transfer(msg.sender, debtAmount);
require(sent, "Borrow failed!");
emit Borrow(address(debt), msg.sender, debtAmount);
}
///@notice Get maximum debt asset borrowable, calculated based on collateralization level
///@dev Collateralization level: bufferNumerator/bufferDenominator (e.g. 8/10 -> 80%)
///@param user User address to calculate maximum debt possible based on collateralization level
function getMaxDebt(address user) public view returns(uint) {
uint availableCollateral = (deposits[user]/bufferDenominator)*bufferNumerator;
uint maxDebt = (availableCollateral*10**getDecimals() / get_daiETH_Price());
return maxDebt;
}
///@notice Users repay their debt, in debt asset terms
///@dev This covers partial and full repayment
///@param debtAmount Amount of debt asset to repay
function repay(uint debtAmount) external {
debts[msg.sender] -= debtAmount;
bool sent = debt.transferFrom(msg.sender, address(this), debtAmount);
require(sent, "Repayment failed!");
emit Repay(address(debt), msg.sender, debtAmount);
}
///@notice Users withdraw their deposited collateral
///@dev This covers partial and full withdrawal; checks for spare capacity before initiating withdrawal
///@param collateralAmount Amount of collateral asset to withdraw
function withdraw(uint collateralAmount) external {
if (debts[msg.sender] > 0){
uint collateralRequired = getCollateralRequired(debts[msg.sender]);
uint spareDeposit = deposits[msg.sender] - collateralRequired;
require(collateralAmount < spareDeposit, "Collateral unavailable!");
}
deposits[msg.sender] -= collateralAmount;
bool sent = collateral.transfer(msg.sender, collateralAmount);
require(sent, "Withdraw failed!");
emit Withdraw(address(collateral), msg.sender, collateralAmount);
}
///@dev Calculates collateral required to support existing debt position at current market prices
///@param debtAmount Amount of debt asset to support
function getCollateralRequired(uint debtAmount) public view returns(uint) {
uint baseCollateralRequired = debtAmount*get_daiETH_Price() / (10**getDecimals());
uint bufferCollateralRequired = (baseCollateralRequired/bufferNumerator)*bufferDenominator;
return bufferCollateralRequired;
}
///@dev Can only be called by Vault owner; triggers liquidation check on supplied user address
///@param user Address of user to trigger liquidation check
function liquidation(address user) public onlyOwner {
uint collateralRequired = getCollateralRequired(debts[user]);
if (collateralRequired > deposits[user]){
emit Liquidation(address(collateral), address(debt), user, debts[user], deposits[user]);
delete deposits[user];
delete debts[user];
}
}
///@notice Price is returned as integer extending over its decimal places
///@dev Get current market price of Collateral/Debt asset from Chainlink
function get_daiETH_Price() public view returns(uint) {
(,int price,,,) = priceFeed.latestRoundData();
return uint(price);
}
///@notice Returns number of decimal places in price returned
///@dev Used to rebase price to account for decimal places (e.g. price = 386372840000000, decimals =18, actual_price = 0.00038637284)
function getDecimals() public view returns(uint) {
return priceFeed.decimals();
}
}
Last updated