Copy import { expect } from "chai";
import { ethers } from "hardhat";
import { PANIC_CODES } from "@nomicfoundation/hardhat-chai-matchers/panic";
import { loadFixture, reset, time } from "@nomicfoundation/hardhat-toolbox/network-helpers";
import {
LiquidationLogic__factory,
LoanManagerLogic__factory,
LoanManager__factory,
LoanPoolLogic__factory,
MockHubPool__factory,
MockOracleManager__factory,
RewardLogic__factory,
UserLoanLogic__factory,
} from "../../typechain-types";
import { BYTES32_LENGTH, convertStringToBytes, getAccountIdBytes, getEmptyBytes, getRandomBytes } from "../utils/bytes";
import { SECONDS_IN_DAY, SECONDS_IN_HOUR, getLatestBlockTimestamp, getRandomInt } from "../utils/time";
import { UserLoanBorrow, UserLoanCollateral } from "./libraries/assets/loanData";
import { getNodeOutputData } from "./libraries/assets/oracleData";
import {
calcAverageStableRate,
calcBorrowBalance,
calcBorrowInterestIndex,
calcReserveCol,
calcStableInterestRate,
convToCollateralFAmount,
convToRepayBorrowAmount,
convToSeizedCollateralAmount,
toFAmount,
toUnderlingAmount,
} from "./utils/formulae";
describe("Liquidation", () => {
const DEFAULT_ADMIN_ROLE = getEmptyBytes(BYTES32_LENGTH);
const LISTING_ROLE = ethers.keccak256(convertStringToBytes("LISTING"));
const ORACLE_ROLE = ethers.keccak256(convertStringToBytes("ORACLE"));
const HUB_ROLE = ethers.keccak256(convertStringToBytes("HUB"));
async function deployLoanManagerFixture() {
const [admin, hub, user, ...unusedUsers] = await ethers.getSigners();
// libraries
const userLoanLogic = await new UserLoanLogic__factory(user).deploy();
const userLoanLogicAddress = await userLoanLogic.getAddress();
const loanPoolLogic = await new LoanPoolLogic__factory(user).deploy();
const loanPoolLogicAddress = await loanPoolLogic.getAddress();
const liquidationLogic = await new LiquidationLogic__factory(
{
["contracts/hub/logic/UserLoanLogic.sol:UserLoanLogic"]: userLoanLogicAddress,
},
user
).deploy();
const liquidationLogicAddress = await liquidationLogic.getAddress();
const loanManagerLogic = await new LoanManagerLogic__factory(
{
["contracts/hub/logic/UserLoanLogic.sol:UserLoanLogic"]: userLoanLogicAddress,
["contracts/hub/logic/LoanPoolLogic.sol:LoanPoolLogic"]: loanPoolLogicAddress,
["contracts/hub/logic/LiquidationLogic.sol:LiquidationLogic"]: liquidationLogicAddress,
},
user
).deploy();
const loanManagerLogicAddress = await loanManagerLogic.getAddress();
const rewardLogic = await new RewardLogic__factory(user).deploy();
const rewardLogicAddress = await rewardLogic.getAddress();
const libraries = {
userLoanLogic,
loanPoolLogic,
liquidationLogic,
loanManagerLogic,
rewardLogic,
};
// deploy contract
const oracleManager = await new MockOracleManager__factory(user).deploy();
const loanManager = await new LoanManager__factory(
{
["contracts/hub/logic/LoanManagerLogic.sol:LoanManagerLogic"]: loanManagerLogicAddress,
["contracts/hub/logic/RewardLogic.sol:RewardLogic"]: rewardLogicAddress,
},
user
).deploy(admin, oracleManager);
// set hub role
await loanManager.connect(admin).grantRole(HUB_ROLE, hub);
// common
const loanManagerAddress = await loanManager.getAddress();
return {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
};
}
async function createLoanTypeFixture() {
const { admin, hub, user, unusedUsers, loanManager, loanManagerAddress, oracleManager, libraries } =
await loadFixture(deployLoanManagerFixture);
// create loan type
const loanTypeId = 1;
const loanTargetHealth = BigInt(1.05e4); //
await loanManager.connect(admin).createLoanType(loanTypeId, loanTargetHealth);
return {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
loanTypeId,
loanTargetHealth,
};
}
async function addPoolsFixture() {
const { admin, hub, user, unusedUsers, loanManager, loanManagerAddress, oracleManager, libraries, loanTypeId } =
await loadFixture(createLoanTypeFixture);
// prepare pools
const usdcPoolId = 1;
const usdcPool = await new MockHubPool__factory(user).deploy("Folks USD Coin", "fUSDC", usdcPoolId);
const ethPoolId = 2;
const ethPool = await new MockHubPool__factory(user).deploy("Folks Ethereum", "fETH", ethPoolId);
// add pools
await loanManager.connect(admin).addPool(usdcPool);
await loanManager.connect(admin).addPool(ethPool);
return {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
loanTypeId,
usdcPoolId,
usdcPool,
ethPoolId,
ethPool,
};
}
async function addPoolToLoanTypeFixture() {
const {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
loanTypeId,
usdcPoolId,
usdcPool,
ethPoolId,
ethPool,
} = await loadFixture(addPoolsFixture);
// add pools to loan type
const rewardCollateralSpeed = BigInt(0);
const rewardBorrowSpeed = BigInt(0);
const rewardMinimumAmount = BigInt(1e18);
const collateralCap = BigInt(20e6);
const borrowCap = BigInt(10e6);
const usdcCollateralFactor = BigInt(0.8e4);
const usdcBorrowFactor = BigInt(1e4);
const usdcLiquidationBonus = BigInt(0.04e4);
const usdcLiquidationFee = BigInt(0.1e4);
const ethCollateralFactor = BigInt(0.7e4);
const ethBorrowFactor = BigInt(1e4);
const ethLiquidationBonus = BigInt(0.06e4);
const ethLiquidationFee = BigInt(0.1e4);
const pools = {
USDC: {
poolId: usdcPoolId,
pool: usdcPool,
rewardCollateralSpeed,
rewardBorrowSpeed,
rewardMinimumAmount,
collateralCap,
borrowCap,
collateralFactor: usdcCollateralFactor,
borrowFactor: usdcBorrowFactor,
liquidationBonus: usdcLiquidationBonus,
liquidationFee: usdcLiquidationFee,
tokenDecimals: BigInt(6),
},
ETH: {
poolId: ethPoolId,
pool: ethPool,
rewardCollateralSpeed,
rewardBorrowSpeed,
rewardMinimumAmount,
collateralCap,
borrowCap,
collateralFactor: ethCollateralFactor,
borrowFactor: ethBorrowFactor,
liquidationBonus: ethLiquidationBonus,
liquidationFee: ethLiquidationFee,
tokenDecimals: BigInt(18),
},
};
await loanManager
.connect(admin)
.addPoolToLoanType(
loanTypeId,
usdcPoolId,
usdcCollateralFactor,
collateralCap,
usdcBorrowFactor,
borrowCap,
usdcLiquidationBonus,
usdcLiquidationFee,
rewardCollateralSpeed,
rewardBorrowSpeed,
rewardMinimumAmount
);
await loanManager
.connect(admin)
.addPoolToLoanType(
loanTypeId,
ethPoolId,
ethCollateralFactor,
collateralCap,
ethBorrowFactor,
borrowCap,
ethLiquidationBonus,
ethLiquidationFee,
rewardCollateralSpeed,
rewardBorrowSpeed,
rewardMinimumAmount
);
return {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
loanTypeId,
pools,
};
}
/* ---------------------------- CREATE USER LOAN ---------------------------- */
async function createUserLoanFixture() {
const {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
loanTypeId,
pools,
} = await loadFixture(addPoolToLoanTypeFixture);
// create user loan
const loanId = getRandomBytes(BYTES32_LENGTH);
const accountId = getAccountIdBytes("ACCOUNT_ID");
const loanName = getRandomBytes(BYTES32_LENGTH);
const createUserLoan = await loanManager.connect(hub).createUserLoan(loanId, accountId, loanTypeId, loanName);
return {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
createUserLoan,
loanTypeId,
pools,
loanId,
accountId,
loanName,
};
}
async function depositEtherFixture() {
const {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
loanTypeId,
pools,
loanId,
accountId,
} = await loadFixture(createUserLoanFixture);
// prepare deposit
const depositAmount = BigInt(3333333000000000); // 10$ worth ether
const depositFAmount = depositAmount;
const depositInterestIndex = BigInt(1e18);
const ethPrice = BigInt(3000e18);
await pools.ETH.pool.setDepositPoolParams({
fAmount: depositFAmount,
depositInterestIndex,
priceFeed: { price: ethPrice, decimals: pools.ETH.tokenDecimals },
});
// deposit into eth pool
const deposit = await loanManager.connect(hub).deposit(loanId, accountId, pools.ETH.poolId, depositAmount);
return {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
deposit,
loanTypeId,
pools,
loanId,
accountId,
depositAmount,
depositFAmount,
};
}
after(async () => {
await reset();
});
async function depositEtherAndVariableBorrowUSDCFixture() {
const {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
loanTypeId,
pools,
loanId,
accountId,
depositAmount,
depositFAmount,
} = await loadFixture(depositEtherFixture);
// set prices
const usdcNodeOutputData = getNodeOutputData(BigInt(1e18));
await oracleManager.setNodeOutput(pools.USDC.poolId, pools.USDC.tokenDecimals, usdcNodeOutputData);
const ethNodeOutputData = getNodeOutputData(BigInt(3000e18));
await oracleManager.setNodeOutput(pools.ETH.poolId, pools.ETH.tokenDecimals, ethNodeOutputData);
// prepare borrow
const variableInterestIndex = BigInt(1.05e18);
const stableInterestRate = BigInt(0.1e18);
await pools.USDC.pool.setBorrowPoolParams({ variableInterestIndex, stableInterestRate });
await pools.USDC.pool.setUpdatedVariableBorrowInterestIndex(variableInterestIndex);
// borrow from USDC pool
const borrowAmount = BigInt(6e6); // 7 USDC
const borrow = await loanManager.connect(hub).borrow(loanId, accountId, pools.USDC.poolId, borrowAmount, BigInt(0));
return {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
borrow,
loanTypeId,
pools,
loanId,
accountId,
depositAmount,
depositFAmount,
borrowAmount,
usdcVariableInterestIndex: variableInterestIndex,
usdcStableInterestRate: stableInterestRate,
};
}
describe("Correctly Liquidate", () => {
it.only("should correctly liquidate", async () => {
const {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
loanTypeId,
pools,
loanId,
accountId,
depositAmount,
depositFAmount,
borrowAmount,
usdcVariableInterestIndex,
usdcStableInterestRate,
} = await loadFixture(depositEtherAndVariableBorrowUSDCFixture);
// With 2000$ price the borrow is 1400$, so health is 170% which is at boarder line
// ETH price drops to 1900$
const ethNodeOutputData = getNodeOutputData(BigInt(1900e18));
await oracleManager.setNodeOutput(pools.ETH.poolId, pools.ETH.tokenDecimals, ethNodeOutputData);
// Liquidate
// const liquidateAmount = BigInt(1000e6); // 1000 USDC
// create liquidator loan
const liquidatorLoanId = getRandomBytes(BYTES32_LENGTH);
const liquidatorAccountId = getAccountIdBytes("LIQUIDATOR_ACCOUNT_ID");
const liquidatorLoanName = getRandomBytes(BYTES32_LENGTH);
await loanManager
.connect(hub)
.createUserLoan(liquidatorLoanId, liquidatorAccountId, loanTypeId, liquidatorLoanName);
// deposit
const liquidatorDepositAmount = BigInt(1e18); // 1 ETH
const liquidatorDepositFAmount = liquidatorDepositAmount;
const liquidatorDepositInterestIndex = BigInt(1e18);
await pools.ETH.pool.setDepositPoolParams({
fAmount: liquidatorDepositFAmount,
depositInterestIndex: liquidatorDepositInterestIndex,
priceFeed: { price: ethNodeOutputData.price, decimals: pools.ETH.tokenDecimals },
});
await loanManager
.connect(hub)
.deposit(liquidatorLoanId, liquidatorAccountId, pools.ETH.poolId, liquidatorDepositAmount);
// calculate interest
const violatorLoanBefore = await loanManager.getUserLoan(loanId);
const violatorOldBorrow = violatorLoanBefore[5][0];
const timestamp = (await getLatestBlockTimestamp()) + getRandomInt(SECONDS_IN_HOUR);
await time.setNextBlockTimestamp(timestamp);
const newInterestIndex = calcBorrowInterestIndex(
violatorOldBorrow.stableInterestRate,
violatorOldBorrow.lastInterestIndex,
BigInt(timestamp) - violatorOldBorrow.lastStableUpdateTimestamp,
true
);
const borrowBalance = calcBorrowBalance(
violatorOldBorrow.balance,
newInterestIndex,
violatorOldBorrow.lastInterestIndex
);
// console.log("borrowBalance", borrowBalance.toString()); // 1400e6 USDC
const repayAmount = BigInt(6e6); // 100 USDC
const usdcPrice = BigInt(1e18);
const collateralFAmount = convToCollateralFAmount(
repayAmount,
ethNodeOutputData.price,
pools.ETH.tokenDecimals,
usdcPrice,
pools.USDC.tokenDecimals,
BigInt(1e18)
); // value of 100 USDC in ETH
// console.log("collateralFAmount", ethers.formatEther(collateralFAmount), " ETH"); // 0.05 ETH
const seizeCollateralAmount = convToSeizedCollateralAmount(
repayAmount,
ethNodeOutputData.price,
pools.ETH.tokenDecimals,
usdcPrice,
pools.USDC.tokenDecimals,
pools.USDC.liquidationBonus
);
// This include liquidation bonus
// console.log("seizeCollateralAmount", ethers.formatEther(seizeCollateralAmount), " ETH"); // 0.054736842105263157
// 4% of 0.052631578947368421 is 0.002105263157894737 which is bonus
const liquidationBonusCollateral = seizeCollateralAmount - collateralFAmount;
// console.log("Liquidation Bonus collatera", ethers.formatEther(liquidationBonusCollateral), " ETH");
const seizeCollateralFAmount = toFAmount(seizeCollateralAmount, BigInt(1e18));
const reserveCollateralFAmount = calcReserveCol(
seizeCollateralFAmount,
collateralFAmount,
pools.ETH.liquidationFee
);
// 10 % of 0.002105263157894737 is 0.000210526315789474 = protocol fee
// at last liquidatator will get 0.002105263157894737 - 0.000210526315789474 = 0.001894736842105263
// console.log("Protocol fee collateral", ethers.formatEther(reserveCollateralFAmount), " ETH"); // 0.002736842105263158
console.log(
"Liquidator profit collateral",
ethers.formatEther(liquidationBonusCollateral - reserveCollateralFAmount),
" ETH"
); // 0.052631578947368421
const liquidatorCollateralFAmount = seizeCollateralFAmount - reserveCollateralFAmount;
// How much collateral liquidator get. it will be worth 103.60 USDC as per my calculation, means 3.60 USDC profit - gas fee
// console.log("liquidatorCollateralFAmount", ethers.formatEther(liquidatorCollateralFAmount), " ETH"); // 0.052631578947368421
// liquidate
const minSeizedAmount = BigInt(0);
const liquidate = await loanManager
.connect(hub)
.liquidate(
loanId,
liquidatorLoanId,
liquidatorAccountId,
pools.ETH.poolId,
pools.USDC.poolId,
repayAmount,
minSeizedAmount
);
//TODO:repay, See if this leaves any bad debt to protocol
// Check violator loan and see how much is remaining
const violatorLoan = await loanManager.getUserLoan(loanId);
// console.log("Violator collateral", violatorLoan[4][0].balance);
// console.log("Violator Loan", violatorLoan[5]);
const liquidatorDebt = await loanManager.getUserLoan(liquidatorLoanId);
// console.log("Liquidator collateral", liquidatorDebt[4][0].balance);
// console.log("Liquidator Loan", liquidatorDebt[5][0]);
// Check liquidator loan and see how much is remaining
});
});
});