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: 13/100
Findings: 5
Award: $1,654.59
π Selected for report: 0
π Solo Findings: 0
16.9712 USDC - $16.97
A malicious admin can set feeRate to 100% and take all the money paid in exercises.
Not exactly an "exploit" to the code, but having no limitation on this parameter will raise centralization risk concern among users. Also in case of a key lost, having a hard cap like 10% can make sure even the admin key is compromised, no user funds will be at risk.
function setFee(uint128 feeRate_) external onlyOwner { require(feeRate_ < 1e17, "illegal"); feeRate = feeRate_; }
#0 - outdoteth
2022-05-15T19:10:49Z
owner can set fee greater than 100%: https://github.com/code-423n4/2022-05-cally-findings/issues/48
16.9712 USDC - $16.97
NFT can get trapped in user contract because of some mistakes.
In exercise
function, when the contract is transferring NFT to user contract, it use transferFrom
instead of safeTransferFrom
which skips the check of receiver contract is willing to receive NFT. This might lead to NFT permanently locked in a contract.
There has been debates about whether to use transferFrom or safeTransferFrom, but I personally think it's always better to use safeTransferFrom
: if the user use a contract wallet without the receiving callback, he can always transfer the optionToken to another EOA address and exercise; But, if there's a mistake and a contract accidentally receive and NFT, it will get locked forever.
consider using safeTransferFrom (and safeMint), otherwise explicitly state it somewhere in the code or document that it doesn't fully complied with ERC721.
#0 - outdoteth
2022-05-15T20:45:51Z
use safeTransferFrom to prevent stuck NFTs: https://github.com/code-423n4/2022-05-cally-findings/issues/38
1535.5172 USDC - $1,535.52
In the createVault function, the vaultType is left for the user to specify. If a user accidentally specify a wrong type, the asset will still be pull because of the interface of transferFrom
for ERC20 and ERC721 is the same, but the asset will be locked forever in the contract because no one can withdraw them.
.transfer(to, amount)
on bayc. So it reverts with "TRANSFER_FAILED".Consider adding a custom mapping of
address => {whitelisted, tokenType}
and maintain a list of tokens the protocol want to support.
Another attack factor from letting people specify token
address and tokenType is that people can create malicious options with malicious ERC20 tokens that cannot be exercise, so I believe it's better that the team controls what can be created out of the protocol.
#0 - outdoteth
2022-05-16T09:52:17Z
π 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
54.903 USDC - $54.90
tokenURI
pass in the wrong parameter for getPremium
It should accept a vaultId
, but here the code pass in premiumId
getPremium(vault.premiumIndex),
This will make the NFT showing the wrong trait.
P.S. I suggested changing the input parameter of getPremium
from vaultId
to premiumIndex
, as described below (gas optimization #2)
createVault
This revert message:
require(dutchAuctionReserveStrike < strikeOptions[dutchAuctionStartingStrikeIndex], "Reserve strike too small");
should instead be reserve strike too high
.
feeRate
and protocolUnclaimedFees
together to save gas on exercise.Because these 2 variables are used together in exercise
function, (reading feeRate and updating protocolUnclaimedFees), packing them into the same storage slot will save you one more SLOAD if feeRate > 0. on average this saves around 2000 gas. (49k -> 47K)
As it currently is, getPremium function receive vaultId read the vault from storage again and than get the premiumIndex. This will cause an extra SLOAD to load the vault from storage.
This can be avoided if we just pass in premiumIndex
, as all 2 functions that call this already have their vault instance in memory.
This makes average cost of buyOption
drops from 75.7K -> 74.8.
currentStrike
and dutchAuctionReserveStrike
into a single storage slot.Since packing variables is highly used in the vault struct, i suggest also pack dutchAuctionReserveStrike
and currentStrike
to uint128
to save 1 SLOAD and SSTORE from read and write.
The gas saving from running the benchmark is 74.8K -> 73.5K
π 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.2341 USDC - $30.23
feeRate
and protocolUnclaimedFees
together to save gas on exercise.Because these 2 variables are used together in exercise
function, (reading feeRate and updating protocolUnclaimedFees), packing them into the same storage slot will save you one more SLOAD if feeRate > 0. on average this saves around 2000 gas. (49k -> 47K)