Summary
There is no interest implemented into the system, but when a borrower wants to take a loan via the mint
function, pay it back via the burn
function or trade with the locked up collateral via the swap
function, a fee is charged. Borrowers can avoid paying this fee by minting, burning, or swapping a dust amount multiple times and therefore borrowing funds for free and for an infinite amount of time, as long as the given collateral is sufficient.
Vulnerability Details
Here we can see the mint
function and the fee calculation:
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);
}
As we can see, there is no minimum amount check implemented, and therefore the fee amount will round down to zero if the given amount is low enough.
The following POC can be implemented in the smartVault
test file and showcases how fees can be avoided by minting a dust amount multiple times:
describe("mint_dust_amount", async () => {
it("mint_dust_amount", async () => {
const collateral = ethers.utils.parseEther("1");
await user.sendTransaction({ to: Vault.address, value: collateral });
for (let i = 0; i < 100; i++) {
await Vault.connect(user).mint(user.address, 100);
}
const { minted } = await Vault.status();
expect(minted).to.equal(100 * 100);
});
});
The same applies to the burn
and swap
function:
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 swap(bytes32 _inToken, bytes32 _outToken, uint256 _amount) external onlyOwner {
uint256 swapFee = _amount * ISmartVaultManagerV3(manager).swapFeeRate() / ISmartVaultManagerV3(manager).HUNDRED_PC();
address inToken = getSwapAddressFor(_inToken);
uint256 minimumAmountOut = calculateMinimumAmountOut(_inToken, _outToken, _amount);
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
tokenIn: inToken,
tokenOut: getSwapAddressFor(_outToken),
fee: 3000,
recipient: address(this),
deadline: block.timestamp,
amountIn: _amount - swapFee,
amountOutMinimum: minimumAmountOut,
sqrtPriceLimitX96: 0
});
inToken == ISmartVaultManagerV3(manager).weth() ?
executeNativeSwapAndFee(params, swapFee) :
executeERC20SwapAndFee(params, swapFee);
}
Impact
Borrowers can avoid paying fees and as there is no interest implemented in the protocol borrow funds for free and for an infinite amount of time, as long as the given collateral does not go below the collateral rate.
Recommendations
Add minimum amount checks to the mint
, burn
and swap
functions.