Skip to content

Instantly share code, notes, and snippets.

@OxMarco
Created March 4, 2024 10:38
Show Gist options
  • Save OxMarco/d9c6984ec96e60073d7d76808d0ff321 to your computer and use it in GitHub Desktop.
Save OxMarco/d9c6984ec96e60073d7d76808d0ff321 to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: BUSL-1.1
pragma solidity =0.8.24;
import {ERC4626, IERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
contract CollateralCoin is ERC20 {
constructor() ERC20("CollateralCoin", "CTRL") {}
function mint(uint256 amount) external {
_mint(msg.sender, amount);
}
}
contract StableCoin is ERC20, ERC20Permit {
address public immutable owner;
error Unauthorised();
constructor() ERC20("StableCoin", "STBL") ERC20Permit("STBL") {
owner = msg.sender;
}
modifier onlyOwner() {
if (msg.sender != owner) revert Unauthorised();
_;
}
// TODO implement cross-chain minting
function mint(address user, uint256 amount) external onlyOwner {
_mint(user, amount);
}
function burn(address user, uint256 amount) external onlyOwner {
_burn(user, amount);
}
// TODO implement cross-chain transfers
}
contract Router is Ownable {
struct VaultData {
address collateral;
uint256 maxDebt;
uint256 outstandingDebt;
bool active;
}
StableCoin public immutable stable;
mapping(address => VaultData) public vaults;
event VaultCreated(address indexed vault, address indexed collateral);
error Unauthorised();
error CapsExceeded();
error AlreadyExists();
constructor() Ownable(msg.sender) {
stable = new StableCoin();
}
modifier onlyVaults() {
if (!vaults[msg.sender].active) revert Unauthorised();
_;
}
function create(
uint256 maxLTV,
uint256 ir,
address collateral
) external onlyOwner returns (address) {
if (vaults[msg.sender].active) revert AlreadyExists();
// TODO use OZ CREATE2
bytes32 id = keccak256(abi.encodePacked(maxLTV, collateral));
address vault = address(new Vault{salt: id}(maxLTV, ir, collateral));
vaults[vault] = VaultData({
collateral: collateral,
maxDebt: type(uint256).max,
outstandingDebt: 0,
active: true
});
emit VaultCreated(vault, collateral);
return vault;
}
function update(address vault, uint256 maxDebt) external onlyOwner {
vaults[vault].maxDebt = maxDebt;
}
function mint(address to, uint256 amount) external onlyVaults {
VaultData storage vault = vaults[msg.sender];
if (vault.outstandingDebt + amount > vault.maxDebt)
revert CapsExceeded();
vault.outstandingDebt += amount;
stable.mint(to, amount);
}
function burn(address from, uint256 amount) external onlyVaults {
vaults[msg.sender].outstandingDebt -= amount;
stable.burn(from, amount);
}
}
contract Vault {
using SafeERC20 for IERC20;
using Math for uint256;
struct Loan {
uint256 debtShares;
uint256 collateral;
}
uint256 public constant RESOLUTION = 1e18;
Router public immutable router;
uint256 public immutable maxLTV;
IERC20 public immutable collateral;
mapping(address => Loan) public loans;
uint256 public totalDebt;
uint256 public shareSupply;
uint256 public fixedIR;
uint256 public lastUpdate;
uint256 public price; // TODO remove this variable
event Borrow(address indexed user, uint256 amount, uint256 shares);
event Repay(address indexed user, uint256 amount, uint256 shares);
event Liquidate(address indexed user, uint256 assetsRepaid, uint256 collateralPurchased, uint256 discount);
error NotEnoughCollateral();
error UnhealthyPosition();
error NotEnoughShares();
error ExceedsMaxLTV();
constructor(
uint256 _maxLTV,
uint256 _fixedIR,
address _collateral
) {
maxLTV = _maxLTV;
fixedIR = _fixedIR;
collateral = IERC20(_collateral);
router = Router(msg.sender);
price = 1;
}
function _accrueInterests() internal {
uint256 delta = block.timestamp - lastUpdate;
totalDebt += fixedIR * delta;
lastUpdate = block.timestamp;
}
// TODO remove this function
function setPrice(uint256 newPrice) external {
price = newPrice;
}
function _getCollateralPrice() internal view returns (uint256) {
return price;
}
function _calculateLTV(uint256 debt, uint256 collateralValue) internal pure returns (uint256) {
if(collateralValue == 0) return type(uint256).max;
// Calculate the LTV ratio as (Loan Amount / Collateral Value) * 100
return (debt * RESOLUTION).mulDiv(collateralValue, RESOLUTION);
}
function isHealthy(address borrower) public view returns (bool) {
Loan memory loan = loans[borrower];
if(loan.debtShares == 0) return true;
uint256 collateralAmount = loan.collateral;
uint256 collateralValueUSD = collateralAmount * _getCollateralPrice();
uint256 loanAmountUSD = convertSharesToAssets(loan.debtShares); // Assuming stablecoin price => 1 USD
uint256 ltv = _calculateLTV(loanAmountUSD, collateralValueUSD);
return ltv <= maxLTV;
}
function addCollateral(uint256 amount) external {
collateral.safeTransferFrom(msg.sender, address(this), amount);
loans[msg.sender].collateral += amount;
}
function removeCollateral(uint256 amount) external {
if (loans[msg.sender].collateral < amount) revert NotEnoughCollateral();
_accrueInterests();
Loan storage loan = loans[msg.sender];
uint256 debt = convertSharesToAssets(loan.debtShares);
// Check if removing collateral breaches the LTV limit, only if there's outstanding debt.
if(debt > 0) {
uint256 remainingCollateral = loan.collateral - amount;
uint256 collateralValueUSD = remainingCollateral * _getCollateralPrice();
if(_calculateLTV(debt, collateralValueUSD) > maxLTV) revert ExceedsMaxLTV();
}
loan.collateral -= amount;
collateral.safeTransfer(msg.sender, amount);
}
function borrow(uint256 amount) external {
_accrueInterests();
Loan storage loan = loans[msg.sender];
uint256 collateralValueUSD = loan.collateral * _getCollateralPrice(); // Assuming 1 collateral unit = 1 USD for simplicity
uint256 maxBorrowable = (collateralValueUSD * maxLTV) / 100; // Calculate max borrowable amount based on max LTV
uint256 newTotalDebt = totalDebt + amount;
if(newTotalDebt > maxBorrowable) revert ExceedsMaxLTV();
// Proceed with the borrowing if under the max LTV
uint256 shares = convertAssetsToShares(amount);
loan.debtShares += shares;
totalDebt += amount;
shareSupply += shares;
router.mint(msg.sender, amount);
emit Borrow(msg.sender, amount, shares);
}
function repay(uint256 shares) external {
if (loans[msg.sender].debtShares < shares) revert NotEnoughShares();
_accrueInterests();
uint256 amount = convertSharesToAssets(shares);
loans[msg.sender].debtShares -= shares;
totalDebt -= amount;
shareSupply -= shares;
router.burn(msg.sender, amount);
emit Repay(msg.sender, amount, shares);
}
function liquidate(address user) external {
Loan storage loan = loans[user];
require(!isHealthy(user), "Loan is healthy");
uint256 collateralValueUSD = loan.collateral * _getCollateralPrice();
uint256 debt = convertSharesToAssets(loan.debtShares);
uint256 maxBorrowableAtMaxLTV = (collateralValueUSD * maxLTV);
require(debt > maxBorrowableAtMaxLTV, "Cannot liquidate");
// Calculate the amount of debt to be repaid to bring the LTV back to the max allowed
uint256 repayAmount = debt - maxBorrowableAtMaxLTV;
// Calculate the collateral to be sold, considering a 10% fixed discount
uint256 collateralToBeSold = (repayAmount * RESOLUTION) / (_getCollateralPrice() * (RESOLUTION - (RESOLUTION / 10)));
require(loan.collateral >= collateralToBeSold, "Not enough collateral");
router.burn(msg.sender, repayAmount);
// Update user loan
uint256 sharesToBurn = convertAssetsToShares(repayAmount);
loan.debtShares -= sharesToBurn;
loan.collateral -= collateralToBeSold;
totalDebt -= repayAmount;
shareSupply -= sharesToBurn;
collateral.safeTransfer(msg.sender, collateralToBeSold);
emit Liquidate(user, repayAmount, collateralToBeSold, RESOLUTION / 10);
}
// Converts an amount of assets into shares, based on the current exchange rate
function convertAssetsToShares(uint256 assets) public view returns (uint256) {
return assets.mulDiv(shareSupply + 1, totalDebt + 1, Math.Rounding.Floor);
}
// Converts an amount of shares into assets, based on the current exchange rate
function convertSharesToAssets(uint256 shares) public view returns (uint256) {
return shares.mulDiv(totalDebt + 1, shareSupply + 1, Math.Rounding.Ceil);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment