Summary
Minting fee, burning fee swap fee and liquidated asset from TheStandard::SmartVault is directly transferring to protocol address instead of first going to liquidator address ie TheStandard::LiquidationPoolManager
Vulnerability Details
According to TheStandard, fee & assets should first go to TheStandard::LiquidationPoolManager and from there fee & assets should go to protocol address and to LiquidationPool according to the poolFeePercentage, but 100 % of fee and assets are directly getting transfered to protocol address because of wrong parameter passed in liquidateNative, liquidateERC20, mint, burn functions of SmartVault
function mint(address _to, uint256 _amount) external onlyOwner ifNotLiquidated {
uint256 fee = _amount * ISmartVaultManagerV3(manager).mintFeeRate() / ISmartVaultManagerV3(manager).HUNDRED_PC();
require(fullyCollateralised(_amount + fee), UNDER_COLL);
minted = minted + _amount + fee;
EUROs.mint(_to, _amount);
@> EUROs.mint(ISmartVaultManagerV3(manager).protocol(), fee);
emit EUROsMinted(_to, _amount, fee);
}
function burn(uint256 _amount) external ifMinted(_amount) {
uint256 fee = _amount * ISmartVaultManagerV3(manager).burnFeeRate() / ISmartVaultManagerV3(manager).HUNDRED_PC();
minted = minted - _amount;
EUROs.burn(msg.sender, _amount);
@> IERC20(address(EUROs)).safeTransferFrom(msg.sender, ISmartVaultManagerV3(manager).protocol(), fee);
emit EUROsBurned(_amount, fee);
}
function liquidateNative() private {
if (address(this).balance != 0) {
@> (bool sent,) = payable(ISmartVaultManagerV3(manager).protocol()).call{value: address(this).balance}("");
require(sent, "err-native-liquidate");
}
}
function liquidateERC20(IERC20 _token) private {
@> if (_token.balanceOf(address(this)) != 0) _token.safeTransfer(ISmartVaultManagerV3(manager).protocol(), _token.balanceOf(address(this)));
}
Here is the implementation of distributeFee and runLiquidation from TheStandard::LiquidationPoolManager which clearly shows fee & assets should first come to LiquidationPoolManager and then distribute
function distributeFees() public {
IERC20 eurosToken = IERC20(EUROs);
uint256 _feesForPool = eurosToken.balanceOf(address(this)) * poolFeePercentage / HUNDRED_PC;
if (_feesForPool > 0) {
eurosToken.approve(pool, _feesForPool);
LiquidationPool(pool).distributeFees(_feesForPool);
}
eurosToken.transfer(protocol, eurosToken.balanceOf(address(this)));
}
function runLiquidation(uint256 _tokenId) external {
ISmartVaultManager manager = ISmartVaultManager(smartVaultManager);
manager.liquidateVault(_tokenId);
distributeFees();
ITokenManager.Token[] memory tokens = ITokenManager(manager.tokenManager()).getAcceptedTokens();
ILiquidationPoolManager.Asset[] memory assets = new ILiquidationPoolManager.Asset[](tokens.length);
uint256 ethBalance;
for (uint256 i = 0; i < tokens.length; i++) {
ITokenManager.Token memory token = tokens[i];
if (token.addr == address(0)) {
ethBalance = address(this).balance;
if (ethBalance > 0) assets[i] = ILiquidationPoolManager.Asset(token, ethBalance);
} else {
IERC20 ierc20 = IERC20(token.addr);
uint256 erc20balance = ierc20.balanceOf(address(this));
if (erc20balance > 0) {
assets[i] = ILiquidationPoolManager.Asset(token, erc20balance);
ierc20.approve(pool, erc20balance);
}
}
}
LiquidationPool(pool).distributeAssets{value: ethBalance}(assets, manager.collateralRate(), manager.HUNDRED_PC());
forwardRemainingRewards(tokens);
}
Here is the POC (to run this poc setup liquidationPool and liquidationPoolManager in SmartVault.js test file and setLiquidatorAddress to liquidationPoolManager in VaultManager
it("fees & assets are dirctly transfed to protocol", async () => {
const ethCollateral = ethers.utils.parseEther("1");
await user.sendTransaction({
to: Vault.address,
value: ethCollateral,
});
const mintedValue = ethers.utils.parseEther("900");
await Vault.connect(user).mint(user.address, mintedValue);
const tstStake1 = ethers.utils.parseEther("1000");
const eurosStake1 = ethers.utils.parseEther("2000");
await TST.mint(holder1.address, tstStake1);
await EUROs.mint(holder1.address, eurosStake1);
await TST.connect(holder1).approve(LiquidationPool.address, tstStake1);
await EUROs.connect(holder1).approve(
LiquidationPool.address,
eurosStake1
);
await LiquidationPool.connect(holder1).increasePosition(
tstStake1,
eurosStake1
);
const tstStake2 = ethers.utils.parseEther("4000");
const eurosStake2 = ethers.utils.parseEther("3000");
await TST.mint(holder2.address, tstStake2);
await EUROs.mint(holder2.address, eurosStake2);
await TST.connect(holder2).approve(LiquidationPool.address, tstStake2);
await EUROs.connect(holder2).approve(
LiquidationPool.address,
eurosStake2
);
await LiquidationPool.connect(holder2).increasePosition(
tstStake2,
eurosStake2
);
await fastForward(DAY);
await ClEthUsd.setPrice(100000000000);
const initialEthOfProtocol = ethers.utils.parseEther("10000");
expect(await ethers.provider.getBalance(protocol.address)).to.equal(
initialEthOfProtocol
);
await expect(LiquidationPoolManager.runLiquidation(1)).not.to.be.reverted;
const finalEthOfProtocol = ethers.utils.parseEther("10001");
expect(await ethers.provider.getBalance(protocol.address)).to.equal(
finalEthOfProtocol
);
});
Impact
Stakers will loss on fee and asset
Tools Used
Manual Review
Recommendations
Use liquidator address in all functions
- ISmartVaultManagerV3(manager).protocol()
+ ISmartVaultManagerV3(manager).liquidator()