The Standard

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

Double charging of minting fee to borrowers

Summary

Minting fee has been charged to borrower in two occasions, minting phase and burning phase.

Vulnerability Details

In the function mint of SmartVaultV3.sol, there is a requirement (line 172) that the borrowed amount of the user plus the minting fee, should be fully collateralised before the minting should proceed. Next step is update the variable minted by the total of borrowed amount and minting fee. Then, the borrowed amount will be minted to borrower address and the minting fee will be minted to protocol address. This transaction seems fine, it shows that the borrower already "paid" the minting fee by covering it in the fully collateralised requirement.

File: SmartVaultV3.sol
170: function mint(address _to, uint256 _amount) external onlyOwner ifNotLiquidated {
171: uint256 fee = _amount * ISmartVaultManagerV3(manager).mintFeeRate() / ISmartVaultManagerV3(manager).HUNDRED_PC();
172: require(fullyCollateralised(_amount + fee), UNDER_COLL);
173: minted = minted + _amount + fee;
174: EUROs.mint(_to, _amount); // sent to borrower or user
175: EUROs.mint(ISmartVaultManagerV3(manager).protocol(), fee); // sent to LPM contract
176: emit EUROsMinted(_to, _amount, fee);
177: }

Let's say after minting, the borrower immediately wants to remove the whole amount of collateral from the smart vault, then he should proceed first to burn the required amount of euros before recovering the assets. As you can see in the functions of removeCollateralNative, removeCollateral and removeAsset, there is a requirement that the function canRemoveCollateral should return boolean true before it can execute the function.

  • Line 154 refers to requirement canRemoveCollateral, this is the same with other removeCollateral functions

File: SmartVaultV3.sol
152: function removeCollateral(bytes32 _symbol, uint256 _amount, address _to) external onlyOwner {
153: ITokenManager.Token memory token = getTokenManager().getToken(_symbol);
154: require(canRemoveCollateral(token, _amount), UNDER_COLL);
155: IERC20(token.addr).safeTransfer(_to, _amount);
156: emit CollateralRemoved(_symbol, _amount, _to);
157: }

Next step, let's observe what does the canRemoveCollateral do, it shows here that one of the ways it can return true is that the minted variable should be equal to zero (see line 137 below).

File: SmartVaultV3.sol
136: function canRemoveCollateral(ITokenManager.Token memory _token, uint256 _amount) private view returns (bool) {
137: if (minted == 0) return true;
138: uint256 currentMintable = maxMintable();
139: uint256 eurValueToRemove = calculator.tokenToEurAvg(_token, _amount);
140: return currentMintable >= eurValueToRemove &&
141: minted <= currentMintable - eurValueToRemove;
142: }

So the next step is to make minted variable turn into zero aside from liquidation? that way is the burning of euro tokens that was minted earlier.
As you can see in this burn function, (see 181 line below) minted variable is being deducted by input amount which should be equal to the amount of minted earlier in order to turn the variable into zero.

File: SmartVaultV3.sol
179: function burn(uint256 _amount) external ifMinted(_amount) {
180: uint256 fee = _amount * ISmartVaultManagerV3(manager).burnFeeRate() / ISmartVaultManagerV3(manager).HUNDRED_PC();
181: minted = minted - _amount;
182: EUROs.burn(msg.sender, _amount);
183: IERC20(address(EUROs)).safeTransferFrom(msg.sender, ISmartVaultManagerV3(manager).protocol(), fee); // fee sent to LPM contract
184: emit EUROsBurned(_amount, fee);
185: }

However, the problem here is that the borrower only possess the borrowed euros and does not include the euros minted for minting fee (sent to protocol).
Remember in the minting process, the variable minted is equal to the total of borrowed euros and minting fee. In this case, the borrower will be forced to buy euros (equivalent to minting fee) from outside market so it can just deduct the minted and turn it into zero.

In summary, there are two occasions, that the borrower "paid" the minting fee, one is during minting phase and the other is the burning phase.

Proof of Concept

Scenario Setup

Minting Fee Rate:

  • Suppose the mint fee rate is 1% (for simplicity).

  • Let's say the borrower wants to mint 10,000 EUROs.

Minting Process:

  • The minting fee is 1% of 10,000, which is 100 EUROs.

  • The borrower must collateralize an amount covering 10,100 EUROs (10,000 borrowed + 100 fee).

  • The minted variable increases by 10,100 EUROs.

Borrower's Holdings after Minting:

  • The borrower receives 10,000 EUROs.

  • The minting fee of 100 EUROs is sent to the protocol, not the borrower.

Scenario with Real Numbers

Step 1 Initial State:

  • Borrower's collateral: Adequate for 10,100 EUROs.

  • Borrower's EUROs balance: 0.

Step 2 After Minting (10,000 EUROs):

  • Borrower's collateral: Still adequate for 10,100 EUROs.

  • Borrower's EUROs balance: 10,000.

  • minted variable: 10,100 EUROs.

Step 3 Desire to Remove Collateral:

  • To remove collateral, the borrower needs to burn EUROs to reduce minted to zero.

  • However, the borrower only has 10,000 EUROs and needs an additional 100 EUROs to fully offset the minted variable.

Step 4 Borrower's Dilemma:

  • The borrower must acquire 100 additional EUROs from an external source to burn.

  • This acquisition is essentially a second payment of the minting fee.

Step 5 Burning Process:

  • The borrower burns 10,100 EUROs.

  • minted variable becomes zero.

  • Now the borrower can remove collateral.

Vulnerability Highlight

In this scenario, the borrower is unfairly burdened with acquiring an additional 100 EUROs to complete the burning process, despite already covering this fee in their initial collateralization. This represents a business logic flaw where the borrower "pays" the minting fee twice:

  • Implicitly in Collateralization: The initial over-collateralization to cover the minting fee.

  • Explicitly in Burning: Needing to acquire and burn extra EUROs equivalent to the minting fee, despite never having received these EUROs.

Impact

Borrowers will pay twice for minting fee before they can recover the collateral assets.

Tools Used

Manual review

Recommendations

In the function mint, the minting fee should be separated or remove in coverage of fully collateralised requirement. The expectation is that the borrower wallet address should already have euros for payment of minting fee to protocol. In other words, there should be no euros minting for minting fee, only for borrowed amount.

Updates

Lead Judging Commences

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

fee-loss

oceansky Submitter
over 1 year ago
hrishibhat Lead Judge
over 1 year ago
hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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