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: 52/120
Findings: 5
Award: $81.10
🌟 Selected for report: 0
🚀 Solo Findings: 0
26.761 USDC - $26.76
Judge has assessed an item in Issue #284 as 3 risk. The relevant finding follows:
NFT tokens sent to the EthRouter contract by mistake can be drained by pool contracts. When someone calls sell, deposit or change functions on EthRouter contract, the contract gives the particular pool full approval (with setApprovalForAll) for tokens from that particular nft. This approval is not revoked later. If someone sends a token from the approved nft, the pool owner can use the execute function to withdraw this token from EthRouter to his own pool.NFT tokens sent to the EthRouter contract by mistake can be drained by pool contracts. When someone calls sell, deposit or change functions on EthRouter contract, the contract gives the particular pool full approval (with setApprovalForAll) for tokens from that particular nft. This approval is not revoked later. If someone sends a token from the approved nft, the pool owner can use the execute function to withdraw this token from EthRouter to his own pool.
#0 - c4-judge
2023-05-02T08:48:27Z
GalloDaSballo marked the issue as duplicate of #184
#1 - c4-judge
2023-05-02T08:48:33Z
GalloDaSballo marked the issue as satisfactory
🌟 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
Fee for the flashLoan operation is set as changeFee
directly which could be in most cases only few thousand's. So the pool receives only few thousand wei as fee for every flashLoan transaction. changeFee is also immutable which makes the whole situation even worse.
flashFee function is defined as follows:
function flashFee(address, uint256) public view returns (uint256) { return changeFee; }
which is called in the flashLoan function:
// calculate the fee uint256 fee = flashFee(token, tokenId);
So the fee is same as changeFee
. This is set when the pool is created by the factory. And the comment says:
The change/flash fee to 4 decimals of precision. For example, 0.0025 ETH = 25. 500 USDC = 5_000_000.
And in the changeFeeQuote function we can see that the variable changeFee
is multiplied by 10 ** exponent
.
But in the flashLoan
function, the fee is set as changeFee
directly and send to the pool. which is very very less compared to what is supposed to be actual fee. This causes loss to pool owner.
//fee here is changeFee. if (baseToken != address(0)) ERC20(baseToken).transferFrom(msg.sender, address(this), fee);
Manual Analysis
Do the necessary multiplication on changeFee before setting it to the fee.
uint256 exponent = baseToken == address(0) ? 18 - 4 : ERC20(baseToken).decimals() - 4; uint256 feePerNft = changeFee * 10 ** exponent;
#0 - c4-pre-sort
2023-04-20T15:08:20Z
0xSorryNotSorry marked the issue as duplicate of #864
#1 - c4-judge
2023-05-01T07:06:49Z
GalloDaSballo changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-05-01T07:08:58Z
GalloDaSballo marked the issue as satisfactory
5.9827 USDC - $5.98
If the base token of the pool has less than 4 decimals, then this line will revert in changeFeeQuote
function. Which means the change function which uses changeFeeQuote
will revert as well.
Looking at the changeFeeQuote
function:
function changeFeeQuote(uint256 inputAmount) public view returns (uint256 feeAmount, uint256 protocolFeeAmount) { // multiply the changeFee to get the fee per NFT (4 decimals of accuracy) uint256 exponent = baseToken == address(0) ? 18 - 4 : ERC20(baseToken).decimals() - 4; uint256 feePerNft = changeFee * 10 ** exponent; feeAmount = inputAmount * feePerNft / 1e18; protocolFeeAmount = feeAmount * Factory(factory).protocolFeeRate() / 10_000; }
The statement ERC20(baseToken).decimals() - 4;
will revert if the base token has decimals less than 4.
Tokens like Gemini USD only have 2 decimals.
Manual Analysis
Either change the precision of changeFee
or block base tokens which has less than 4 decimals.
#0 - c4-pre-sort
2023-04-20T15:22:37Z
0xSorryNotSorry marked the issue as duplicate of #858
#1 - c4-judge
2023-05-01T07:14:43Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: 0xLanterns
Also found by: AkshaySrivastav, Bason, CodingNameKiki, DadeKuma, DishWasher, Dug, ElKu, J4de, MiloTruck, Nyx, RaymondFam, Ruhum, T1MOH, Voyvoda, abiih, adriro, aviggiano, bshramin, sashik_eth, savi0ur, yixxas
9.3258 USDC - $9.33
https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/PrivatePool.sol#L236-L244 https://github.com/code-423n4/2023-04-caviar/blob/cd8a92667bcb6657f70657183769c244d04c015c/src/PrivatePool.sol#L335
Each NFT token might have a different royalty fee percentage
associated with it. Which means the royalty to be paid depends on the exact sale price. But in the buy and sell functions, the total sale price is divided by the number of bought or sold tokens and this average price is used to calculate royaltyFee
.
In buy function:
uint256 salePrice = (netInputAmount - feeAmount - protocolFeeAmount) / tokenIds.length;
In sell function:
uint256 salePrice = (netOutputAmount + feeAmount + protocolFeeAmount) / tokenIds.length;
In both functions, royalty fee is calculated as follows, with the average price:
(uint256 royaltyFee,) = _getRoyalty(tokenIds[i], salePrice);
And we send this calculated fee to recipients:
if (royaltyFee > 0 && recipient != address(0)) { if (baseToken != address(0)) { ERC20(baseToken).safeTransfer(recipient, royaltyFee); } else { recipient.safeTransferETH(royaltyFee); } }
Note that, this mistake causes unfairness, only to individual recipients and the total calculated fee doesnt get affected.
Manual analysis
Calculate the royaltyFee based on weights rather than the average price. For example the netInputAmount
could be divided into fees based on the weight of each token.
#0 - c4-pre-sort
2023-04-20T17:32:09Z
0xSorryNotSorry marked the issue as duplicate of #669
#1 - c4-judge
2023-05-01T07:27:09Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: AkshaySrivastav
Also found by: 0x5rings, 0xbepresent, ABA, Bauchibred, BenRai, DadeKuma, ElKu, RaymondFam, Rolezn, adriro, btk, chaduke, devscrooge, dingo2077, minhtrng, nemveer, p0wd3r, rbserver, ulqiorra
30.9954 USDC - $31.00
Initially the protocol fee rate will be set to be 0% however it may be increased in the future, with advanced notice.
But there is no timelock
function implemented in the smart contracts to make sure that the promise of advanced notice
will kept.
2. Though most of the pool parameters can be changed later, changeFee
remains immutable. This option could be nice for the pool owners.
virtualBaseTokenReserves
is zero then, buyQuote
and sellQuote
will return zero. If virtualNftReserves
is zero then, buyQuote
will revert.buy
and change
functions are called.flashLoan
function. This seems counterintuitive.flashLoan
usage of a token only pays pool fee
which goes to the pool owner. But royaltyFees
are not paid. This seems not right.EthRouter
contract, the contract gives the particular pool full approval (with setApprovalForAll
) for tokens from that particular nft. This approval is not revoked later. If someone sends a token from the approved nft, the pool owner can use the execute function to withdraw this token from EthRouter
to his own pool.#0 - GalloDaSballo
2023-04-30T19:48:01Z
The doc says: Initially the protocol fee rate will be set to be 0% however it may be increased in the future, with advanced notice. But there is no timelock function implemented in the smart contracts to make sure that the promise of advanced notice will kept. Invalid -3
The royalties, if enabled, requires the NFT to be listed in manifold.xyz. If its not listed there, the royalty program will fail. Ignoring Ignoring
setVirtualReserves function doesnt check if the new values are zero or not. If virtualBaseTokenReserves is zero then, buyQuote and sellQuote will return zero. If virtualNftReserves is zero then, buyQuote will revert. Ignoring, it's a way to pause the pool / make tokens free, and is intended via the invariants math
The extra msg.value sent when calling the flashLoan function doesnt get returned to the user. Though this refund is done when the buy and change functions are called. L
Pool owner can change the merkle root after increasing the weights of existing tokens in the pool, so that buyers need to pay much higher than what sellers got it when they sold it. Ignoring
The stolen NFT tokens are not not allowed to be sold into the pool. But there is no such restriction to use stolen tokens with flashLoan function. This seems counterintuitive. L
flashLoan usage of a token only pays pool fee which goes to the pool owner. But royaltyFees are not paid. This seems not right. L
NFT tokens sent to the EthRouter contract by mistake can be drained by pool contracts. When someone calls sell, deposit or change functions on EthRouter contract, the contract gives the particular pool full approval (with setApprovalForAll) for tokens from that particular nft. This approval is not revoked later. If someone sends a token from the approved nft, the pool owner can use the execute function to withdraw this token from EthRouter to his own pool. 184
#1 - GalloDaSballo
2023-05-01T07:44:00Z
3L +3 from dups
-3
3L (TEMP) 1R
#2 - GalloDaSballo
2023-05-01T07:44:07Z
6L 1R
#3 - c4-judge
2023-05-01T09:16:53Z
GalloDaSballo marked the issue as grade-b