Summary
In SmartVaultV3.sol
contract, a user can mint and burn EUROs. Every mint and burn action accrues a fee to the protocol. For smaller amounts for mint and burn, the fee becomes 0, and the user can hence grief the protocol of any fees.
Vulnerability Details
Below are the mint()
and burn()
functions in SmartVaultV3
:
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);
}
The fee is calculated as:
(amount * feeRate) / HUNDRED_PERCENT
Now, if (amount * feeRate)
is less than HUNDRED_PERCENT
, then the above expression evaluates to 0 because of rounding in Solidity, which then makes fee as 0.
Proof of Concept
The below test can be used in the test file test/smartVault.js
. The test assumes that the mint and burn fee rates are PROTOCOL_FEE_RATE
for proof of concept.
describe('Proof of Concept', async () => {
it('Minting and Burning without fee', async () => {
const mintAndBurnValue = BigNumber.from(Math.ceil(HUNDRED_PC / PROTOCOL_FEE_RATE) - 1);
expect(mintAndBurnValue).to.equal(199);
const collateral = ethers.utils.parseEther('1');
await user.sendTransaction({ to: Vault.address, value: collateral });
await Vault.connect(user).mint(user.address, mintAndBurnValue);
expect((await Vault.status()).minted).to.equal(mintAndBurnValue);
expect((await EUROs.balanceOf(protocol.address))).to.equal(0);
await Vault.connect(user).burn(mintAndBurnValue);
expect((await Vault.status()).minted).to.equal(0);
expect((await EUROs.balanceOf(protocol.address))).to.equal(0);
});
});
Tools Used
Manual Review
Recommendations
There are different ways to solve this problem, depending on what protocol thinks is fair:
Enforce a minimum amount to be minted and burnt, so that the fee is not zero
Introduce a base fee so that the fee is never zero.