The Standard

The Standard
DeFiHardhat
20,000 USDC
View results
Submission Details
Severity: low
Invalid

Fee & Assets are directly transferring to protocol instead of liquidator

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");
//depositing collateral to vault
await user.sendTransaction({
to: Vault.address,
value: ethCollateral,
});
// Minting EUROs
const mintedValue = ethers.utils.parseEther("900");
await Vault.connect(user).mint(user.address, mintedValue);
// stakers staking to get liquidated asset
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);
// eth / usd price drops to $1000
await ClEthUsd.setPrice(100000000000);
// running liquidation
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");
// protocol is taking complete 1 ETH that was collateral
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()
Updates

Lead Judging Commences

hrishibhat Lead Judge almost 2 years ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

informational/invalid

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.