Created
March 4, 2024 10:38
-
-
Save OxMarco/d9c6984ec96e60073d7d76808d0ff321 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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