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
9 changes: 7 additions & 2 deletions script/deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity 0.8.21;

import {Vault} from "../src/vault.sol";
import {Currency} from "../src/currency.sol";
import {Liquidator} from "../src/liquidator.sol";
import {Feed} from "../src/modules/feed.sol";

import {BaseScript, stdJson, console2} from "./base.s.sol";
Expand All @@ -12,7 +13,7 @@ import {SimpleInterestRate, IRate} from "../src/modules/rate.sol";
contract DeployScript is BaseScript {
using stdJson for string;

function run() external broadcast returns (Currency xNGN, Vault vault, Feed feed, IRate rate) {
function run() external broadcast returns (Currency xNGN, Liquidator liquidator, Vault vault, Feed feed, IRate rate) {
string memory deployConfigJson = getDeployConfigJson();
uint256 baseRate = deployConfigJson.readUint(".baseRate");
uint256 debtCeiling = deployConfigJson.readUint(".debtCeiling");
Expand All @@ -21,8 +22,12 @@ contract DeployScript is BaseScript {
xNGN = new Currency("xNGN", "xNGN");
console2.log("xNGN deployed successfully at address:", address(xNGN));

console2.log("\n Deploying Liquidator contract");
liquidator = new Liquidator();
console2.log("liquidator deployed successfully at address:", address(liquidator));

console2.log("\n Deploying vault contract");
vault = new Vault(xNGN, baseRate, debtCeiling);
vault = new Vault(xNGN, baseRate, debtCeiling, liquidator);
console2.log("Vault deployed successfully at address:", address(vault));

console2.log("\n Deploying feed contract");
Expand Down
39 changes: 39 additions & 0 deletions src/interfaces/ILiquidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//SPDX-Licence-Identifier: GPL-3.0
pragma solidity 0.8.21;

import {Vault} from "../vault.sol";
import {ERC20Token} from "../mocks/ERC20Token.sol";

interface ILiquidator {
// ------------------------------------------------ CUSTOM ERRORS ------------------------------------------------
error CollateralDoesNotExist();
error PositionIsSafe();
error CollateralRatioNotImproved();

// ------------------------------------------------ EVENTS ------------------------------------------------
event Liquidated(
address indexed vault, address indexed owner, address liquidator, uint256 currencyAmountPaid, uint256 collateralAmountCovered
);

/**
* @notice liquidates a vault making sure the liquidation strictly improves the collateral ratio i.e doesn't leave it the same as before or decreases it (if that's possible)
*
* @param _vaultID contract address of vault to be liquidated
* @param _collateralToken contract address of collateral used by vault that is to be liquidate, also the token to recieved by the `_to` address after liquidation
* @param _owner owner of the vault to liquidate
* @param _to address to send the liquidated collateral (collateral covered) to
* @param _currencyAmountToPay the amount of currency tokens to pay back for `_owner`
*
* @dev updates fees accrued for `_owner`'s vault since last fee update, this is important as it ensures that the collateral-ratio check at the start and end of the function uses an updated total owed amount i.e (borrowedAmount + accruedFees) when checking `_owner`'s collateral-ratio
* @dev should revert if the collateral does not exist
* should revert if the vault is not under-water
* should revert if liqudiation did not strictly imporve the collateral ratio of the vault
*/
function liquidate(
Vault _vaultID,
ERC20Token _collateralToken,
address _owner,
address _to,
uint256 _currencyAmountToPay
) external;
}
6 changes: 1 addition & 5 deletions src/interfaces/IVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,19 @@ interface IVault {
error ZeroAddress();
error UnrecognizedParam();
error BadCollateralRatio();
error PositionIsSafe();
error ZeroCollateral();
error TotalUserCollateralBelowFloor();
error CollateralAlreadyExists();
error CollateralDoesNotExist();
error NotOwnerOrReliedUpon();
error CollateralRatioNotImproved();
error NotEnoughCollateralToPay();
error EthTransferFailed();
error GlobalDebtCeilingExceeded();
error CollateralDebtCeilingExceeded();
error InsufficientCurrencyAmountToPay();
error InvalidStabilityModule();
error NotFeedContract();
error NotLiquidatorContract();

// ------------------------------------------------ EVENTS ------------------------------------------------
event CollateralTypeAdded(address collateralAddress);
Expand All @@ -28,9 +27,6 @@ interface IVault {
event CurrencyMinted(address indexed owner, uint256 amount);
event CurrencyBurned(address indexed owner, uint256 amount);
event FeesPaid(address indexed owner, uint256 amount);
event Liquidated(
address indexed owner, address liquidator, uint256 currencyAmountPaid, uint256 collateralAmountCovered
);

// ------------------------------------------------ CUSTOM TYPES ------------------------------------------------
struct RateInfo {
Expand Down
88 changes: 88 additions & 0 deletions src/liquidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.21;

import {Ownable} from "solady/auth/Ownable.sol";
import {ERC20Token} from "./mocks/ERC20Token.sol";
import {Currency} from "./currency.sol";
import {Vault} from "./vault.sol";
import {ILiquidator} from "./interfaces/ILiquidator.sol";

contract Liquidator is ILiquidator, Ownable {

constructor() {
_initializeOwner(msg.sender);
}

/**
* @notice liquidates a vault making sure the liquidation strictly improves the collateral ratio i.e doesn't leave it the same as before or decreases it (if that's possible)
*
* @param _vault contract address of vault to be liquidated
* @param _collateralToken contract address of collateral used by vault that is to be liquidate, also the token to recieved by the `_to` address after liquidation
* @param _owner owner of the vault to liquidate
* @param _to address to send the liquidated collateral (collateral covered) to
* @param _currencyAmountToPay the amount of currency tokens to pay back for `_owner`
*
* @dev updates fees accrued for `_owner`'s vault since last fee update, this is important as it ensures that the collateral-ratio check at the start and end of the function uses an updated total owed amount i.e (borrowedAmount + accruedFees) when checking `_owner`'s collateral-ratio
* @dev should revert if the collateral does not exist
* should revert if the vault is not under-water
* should revert if liqudiation did not strictly imporve the collateral ratio of the vault
*/
function liquidate(
Vault _vault,
ERC20Token _collateralToken,
address _owner,
address _to,
uint256 _currencyAmountToPay
) external {
// get collateral ratio
// require it's below liquidation threshold
// liquidate and take discount
// burn currency from caller
if (_vault.getCollateralRate(_collateralToken) == 0) {
revert CollateralDoesNotExist();
}

// need to accrue fees first in order to use updated fees for collateral ratio calculation below
_vault.accrueLiquidationFees(_collateralToken, _owner);

(uint256 _preCollateralRatio, uint256 _liquidationThreshold) = _vault.getCollateralRatioAndLiquidationThreshold(_collateralToken, _owner);

if (_preCollateralRatio <= _liquidationThreshold) {
revert PositionIsSafe();
}

(uint256 _depositedCollateral, uint256 _borrowedAmount, uint256 _accruedFees, ) = _vault.vaultMapping(_collateralToken, _owner);

if (_currencyAmountToPay == type(uint256).max) {
// This is here to prevent frontrunning of full liquidation
// malicious owners can monitor the mempool and frontrun any attempt to liquidate their position by liquidating it
// themselves but partially, (by 1 wei of collateral is enough) which causes underflow when the liquidator's tx is to be executed'
// With this, liquidators can parse in type(uint256).max to liquidate everything regardless of the current borrowed amount.
_currencyAmountToPay = _borrowedAmount + _accruedFees;
}

uint256 _collateralAmountCovered = _vault.getCollateralAmountFromCurrencyValue(_collateralToken, _currencyAmountToPay);

(,,, uint256 _liquidationBonus,,,,,) = _vault.collateralMapping(_collateralToken);
uint256 _bonus = (_collateralAmountCovered * _liquidationBonus) / _vault.hundredPercentage();
uint256 _total = _collateralAmountCovered + _bonus;

// To make liquidations always possible, if _vault.depositedCollateral not enough to pay bonus, give out highest possible bonus
// For situations where the user's vault is insolvent, this would be called by the system stability module after a debt auction is used to raise the currency
if (_total > _depositedCollateral) {
_total = _depositedCollateral;
}

emit Liquidated(address(_vault), _owner, msg.sender, _currencyAmountToPay, _total);

_vault.withdrawCollateralL(_collateralToken, _owner, _to, _total);
_vault.burnCurrencyL(_collateralToken, _owner, msg.sender, _currencyAmountToPay);

// collateral ratio must never increase or stay the same during a liquidation.
(uint256 _postCollateralRatio, ) = _vault.getCollateralRatioAndLiquidationThreshold(_collateralToken, _owner);

if (_preCollateralRatio <= _postCollateralRatio) {
revert CollateralRatioNotImproved();
}
}
}
Loading