Platform: Code4rena
Start Date: 07/04/2023
Pot Size: $47,000 USDC
Total HM: 20
Participants: 120
Period: 6 days
Judge: GalloDaSballo
Total Solo HM: 4
Id: 230
League: ETH
Rank: 108/120
Findings: 1
Award: $8.03
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: adriro
Also found by: 0xNorman, 0xRobocop, Aymen0909, ElKu, GT_Blockchain, Josiah, KrisApostolov, RaymondFam, SpicyMeatball, ToonVH, Voyvoda, anodaram, aviggiano, bin2chen, climber2002, giovannidisiena, jpserrat, minhtrng, rbserver, sashik_eth, shaka, wintermute
8.0283 USDC - $8.03
https://github.com/code-423n4/2023-04-caviar/blob/main/src/PrivatePool.sol#L623-L654
In the PrivatePool contract, the flashFee
is set equal to the changeFee
which is set with 4 decimals of precision, but when the flashFee
is applied in the flashLoan
function it is not calculated correctly so the user who excute the flashloan will pay a very small fee not equal to what he was supposed to pay.
In the PrivatePool contract, the flashFee
is set equal to the changeFee
which is set with 4 decimals of precision, for example if changeFee == 25
and the baseToken is ETH then the actual charged fee is equal to (25/1e4) * 1e18 == 0.0025 ETH
, this is explained in the code comments :
File: PrivatePool.sol Line 91
/// @notice The change/flash fee to 4 decimals of precision. For example, 0.0025 ETH = 25. 500 USDC = 5_000_000. uint56 public changeFee;
When changeFee
is used its value should always be converted using the formula :
(changeFee * 10**decimals) / 1e4
where decimals
is the decimals value of the base token, note that this formula is used to convert the changeFee
in the changeFeeQuote
function.
Because flashFee
is the same as changeFee
, when flashFee
is applied it must also use the aforementioned formula, but when the flashFee
is used inside the flashLoan
function the formula is not used instead the flashFee
is used directly without conversion as it can been seen in the code below :
File: PrivatePool.sol Line 623-654
function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 tokenId, bytes calldata data) external payable returns (bool) { // check that the NFT is available for a flash loan if (!availableForFlashLoan(token, tokenId)) revert NotAvailableForFlashLoan(); /** @audit fee is set equal to changeFee == flashFee(token, tokenId) */ // calculate the fee uint256 fee = flashFee(token, tokenId); /** @audit fee is compared with msg.value directly which is incorrect as msg.value is in 18 decimals and fee is not */ // if base token is ETH then check that caller sent enough for the fee if (baseToken == address(0) && msg.value < fee) revert InvalidEthAmount(); // transfer the NFT to the borrower ERC721(token).safeTransferFrom(address(this), address(receiver), tokenId); // call the borrower bool success = receiver.onFlashLoan(msg.sender, token, tokenId, fee, data) == keccak256("ERC3156FlashBorrower.onFlashLoan"); // check that flashloan was successful if (!success) revert FlashLoanFailed(); // transfer the NFT from the borrower ERC721(token).safeTransferFrom(address(receiver), address(this), tokenId); /** @audit fee is transferred as if it has the same decimals as the baseToken but this is incorrect */ // transfer the fee from the borrower if (baseToken != address(0)) ERC20(baseToken).transferFrom(msg.sender, address(this), fee); return success; }
Because of this error the user who exute the flashloan call will pay a very small fee not equal to the actual one set in the contract.
For example if the baseToken
is ETH and changeFee == 1
which means that changeFee
is equal to 0.0001 * 10e18
which is in 18 decimals, but when the flashloan function is called the fee used will be equal to 1 thus equal to 10**(-18)
which is very small.
Manual review
Use the correct formula when applying the flashFee
in the flashloan function, the flashloan function should be modified as follows :
function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 tokenId, bytes calldata data) external payable returns (bool) { // check that the NFT is available for a flash loan if (!availableForFlashLoan(token, tokenId)) revert NotAvailableForFlashLoan(); /** @audit get flashFee == changeFee value with 4 decimals */ // calculate the fee uint256 _flashFee = flashFee(token, tokenId); /** @audit calculate the correct flashFee value */ uint256 exponent = baseToken == address(0) ? 18 - 4 : ERC20(baseToken).decimals() - 4; uint256 fee = _flashFee * 10 ** exponent; // if base token is ETH then check that caller sent enough for the fee if (baseToken == address(0) && msg.value < fee) revert InvalidEthAmount(); // transfer the NFT to the borrower ERC721(token).safeTransferFrom(address(this), address(receiver), tokenId); // call the borrower bool success = receiver.onFlashLoan(msg.sender, token, tokenId, fee, data) == keccak256("ERC3156FlashBorrower.onFlashLoan"); // check that flashloan was successful if (!success) revert FlashLoanFailed(); // transfer the NFT from the borrower ERC721(token).safeTransferFrom(address(receiver), address(this), tokenId); // transfer the fee from the borrower if (baseToken != address(0)) ERC20(baseToken).transferFrom(msg.sender, address(this), fee); return success; }
#0 - c4-pre-sort
2023-04-20T15:08:04Z
0xSorryNotSorry marked the issue as duplicate of #864
#1 - c4-judge
2023-05-01T07:06:51Z
GalloDaSballo changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-05-01T07:07:06Z
GalloDaSballo marked the issue as satisfactory