Platform: Code4rena
Start Date: 10/05/2022
Pot Size: $50,000 USDC
Total HM: 13
Participants: 100
Period: 5 days
Judge: HardlyDifficult
Total Solo HM: 1
Id: 122
League: ETH
Rank: 34/100
Findings: 4
Award: $110.38
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: IllIllI
Also found by: 0x52, 0xf15ers, 0xsanson, Bludya, BondiPestControl, Czar102, GimelSec, Kumpa, _Adam, berndartmueller, catchup, crispymangoes, eccentricexit, ellahi, hake, horsefacts, pedroais, peritoflores, reassor, shenwilly, shung, smiling_heretic, sseefried, throttle
8.1655 USDC - $8.17
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L117-L121 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L283-L285
Admin can change the protocol fee anytime, to any value by calling setFee() function. Even if there is no bad intention, this can potentially be used to damage the reputation of the protocol.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
Manual analysis
#0 - outdoteth
2022-05-15T19:17:36Z
owner can change fee at any time; https://github.com/code-423n4/2022-05-cally-findings/issues/47 owner can set fee greater than 100%: https://github.com/code-423n4/2022-05-cally-findings/issues/48
16.9712 USDC - $16.97
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L294-L296
exercise() function is called by a call option buyer to exercise the option. When this happens below code transfers the underlying NFT to the exerciser: // transfer the NFTs or ERC20s to the exerciser vault.tokenType == TokenType.ERC721 ? ERC721(vault.token).transferFrom(address(this), msg.sender, vault.tokenIdOrAmount) : ERC20(vault.token).safeTransfer(msg.sender, vault.tokenIdOrAmount); However, if the receiver address is a contract address which do not have ERC721 support, then the NFT will be frozen in the contract.
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L294-L296
Manual analysis
I suggest to use safeTransferFrom, so that if the receiver can not receive the NFT, exercise would revert. // transfer the NFTs or ERC20s to the exerciser vault.tokenType == TokenType.ERC721 ? ERC721(vault.token).safeTransferFrom(address(this), msg.sender, vault.tokenIdOrAmount) : ERC20(vault.token).safeTransfer(msg.sender, vault.tokenIdOrAmount);
#0 - outdoteth
2022-05-15T20:46:56Z
use safeTransferFrom to prevent stuck NFTs: https://github.com/code-423n4/2022-05-cally-findings/issues/38
🌟 Selected for report: hubble
Also found by: 0x1337, 0x1f8b, 0x4non, 0xDjango, 0xf15ers, 0xsanson, 242, Aits, AlleyCat, Bludya, BondiPestControl, BouSalman, BowTiedWardens, CertoraInc, Cityscape, Czar102, FSchmoede, Funen, Hawkeye, IllIllI, JDeryl, Kenshin, Kumpa, MaratCerby, MiloTruck, Picodes, Ruhum, TrungOre, VAD37, WatchPug, Waze, antonttc, bobirichman, catchup, cccz, cryptphi, csanuragjain, delfin454000, dipp, dirk_y, djxploit, eccentricexit, ellahi, fatherOfBlocks, hake, hansfriese, hickuphh3, horsefacts, hyh, jah, joestakey, mics, minhquanym, pedroais, pmerkleplant, radoslav11, reassor, rfa, robee, seanamani, shenwilly, shung, sikorico, sorrynotsorry, sseefried, z3s
55.1308 USDC - $55.13
feeRate is initialized as 0 which can be updated using setFee() function. It is commented in the setFee() function that the fee will be 1%. Considering 1% is the default fee, it would be better to initialise the feeRate as 1% rather than 0. In case setFee() is not called or called late by the admin for some reason, protocol fee collection would not be interrupted.
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L94
It is explained in the documentation that vault information is represented as a packed struct to save gas. I believe premiumIndex and dutchAuctionStartingStrikeIndex can both be made uint16 without requiring an extra slot: address token; // 20 bytes uint16 premiumIndex; // 2 bytes uint8 durationDays; // 1 byte uint16 dutchAuctionStartingStrikeIndex; // 2 bytes uint32 currentExpiration; // 4 bytes bool isExercised; // 1 byte bool isWithdrawing; // 1 byte total : 31 bytes Then premiumOptions and strikeOptions look-up tables can be completely deleted (saving gas). Rather than using look-up tables, premium can be calculated by scaling the premiumIndex value using a max limit. For example:
uint256 private constant premiumMax = 100 ether; function getPremium(uint256 vaultId) public view returns (uint256 premium) { Vault memory vault = _vaults[vaultId]; uint256 premium = vault.premiumIndex * premiumMax / type(uint16).max return premium; }
I haven't tested the above code. I've just scrabbled it to explain the idea. If non-zero premium is needed, we need to have some controls to prevent it. With this approach a very high resolution can be achieved for premium and strike values with probably lower gas costs.
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L90 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L92 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L394-396
Functions which are executed only by privileged users and change important parameters should emit events. I suggest to include events for the below functions.
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L119-L121 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L124-L128
It is a good practice to include non-zero address check for important addresses. I suggest to include a non-zero address check for the token address in createVault()
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L160
msg.value is checked to be exactly same as currentStrike in exercise() function: // check correct ETH amount was sent to pay the strike require(msg.value == vault.currentStrike, "Incorrect ETH sent for strike"); It might be better to check with >= , as it was done for premium within buyOption() function: // check enough eth was sent to cover premium uint256 premium = getPremium(vaultId); require(msg.value >= premium, "Incorrect ETH amount sent");
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L272
🌟 Selected for report: IllIllI
Also found by: 0v3rf10w, 0x1f8b, 0x4non, 0xDjango, 0xNazgul, 0xf15ers, 0xkatana, 0xsanson, Bludya, BowTiedWardens, CertoraInc, Cityscape, DavidGialdi, FSchmoede, Fitraldys, Funen, Hawkeye, Kenshin, MadWookie, MaratCerby, MiloTruck, Picodes, RagePit, Tadashi, TerrierLover, TomFrenchBlockchain, VAD37, WatchPug, Waze, _Adam, antonttc, bobirichman, catchup, defsec, delfin454000, djxploit, ellahi, fatherOfBlocks, gzeon, hake, hansfriese, hickuphh3, horsefacts, ignacio, joestakey, jonatascm, mics, minhquanym, oyc_109, pmerkleplant, rfa, robee, rotcivegaf, samruna, shung, sikorico, simon135, z3s
30.1058 USDC - $30.11
1- For loop index increments can be made unchecked. There is no risk of overflowing the index of the for loops. Therefore, they can be changed to unchecked to save gas. This would save more gas as iterations in the loop increases. 2- Also postfix increments can be changed as prefix as it is cheaper. 3- There is no need to set uint256 variable to 0 since default value is already 0.
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/CallyNft.sol#L244
I suggest to change the original code from this for (uint256 i = 0; i < data.length; i++) { str[2 + i * 2] = alphabet[uint256(uint8(data[i] >> 4))]; str[3 + i * 2] = alphabet[uint256(uint8(data[i] & 0x0f))]; } to this for (uint256 i; i < data.length; ) { str[2 + i * 2] = alphabet[uint256(uint8(data[i] >> 4))]; str[3 + i * 2] = alphabet[uint256(uint8(data[i] & 0x0f))]; unchecked { ++i; } }
Some variables are initialised with their default values which cause unnecessary gas consumption
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L94 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L95 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L126 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L180 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L181 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L183 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L282
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L170
Private state variables are cheaper than public state variables. Below instances can be private.
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L90 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L92 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L94 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L95 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L100 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L111
It is a good practice to apply non-zero amount checks for transfers to avoid unnecessary executions.
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L125 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L362
AUCTION_DURATION is only used within the contract it is defined. Therefore, it is unnecessary to define it as public.
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L87
feeRate within exercise() function is accessed twice. It can be cached in a local variable to save an SLOAD.
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L283-284
auctionStartTimestamp can be deleted. This original code: // check option associated with the vault has expired uint32 auctionStartTimestamp = vault.currentExpiration; require(block.timestamp >= auctionStartTimestamp, "Auction not started"); Can be changed as: // check option associated with the vault has expired require(block.timestamp >= vault.currentExpiration, "Auction not started");
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L227-228
It is more expensive to operate using booleans because before every write operation an SLOAD is executed to read the contents of the slot. Therefore, it is cheaper to use uint256 instead of bool. On the other hand, using bool is better for the code readability. Hence, it is a tradeoff to be decided by the developers.
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L80 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L81
SSTORE from 1 to 2 is cheaper than SSTORE from 0 to 1. https://github.com/code-423n4/2022-01-yield-findings/issues/102
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L80 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L81