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: 5/100
Findings: 5
Award: $3,535.51
🌟 Selected for report: 1
🚀 Solo Findings: 1
16.9712 USDC - $16.97
The setFee()
function is used to set the protocol fee that is applied when a buyer exercises a call. This setter function does not apply any validation, meaning the feeRate_
parameter may be accidently or maliciously set to a value that leads to unintended consequences. Also, changes to the feeRate
are applied to all future calls to exercise()
which can be somewhat misleading to vault owners that deposited their ERC721/ERC20 while a lower fee was active.
feeRate
is set higher than 1e18, buyer will not be able to exercise the call.If the feeRate
is set to a value higher than 1e18, a call to exercise()
will fail every time. This is because fee will be larger than msg.value
via the following formula:
fee = (msg.value * feeRate) / 1e18;
Failing on underflow here:
ethBalance[getVaultBeneficiary(vaultId)] += msg.value - fee;
Motivation: This value could be accidently set too high or a malicious owner could intentionally set it too high in order to DOS the buyers from exercising calls. The potential motivation would be to let the calls expire to hopefully buy the call at a lower price.
Motivation: A malicious owner is able to advertise a low or 0 feeRate
to attract users to deposit their ERC721s or ERC20s into Cally. At any point, the owner is able to set the feeRate
higher. Most diabolically, the owner will set the feeRate higher right after a call is bought. At this point, the vault owner does not have any control over their deposited ERC721/ERC20. The owner increases the fee potentially as high as 100%, the call is exercised, all proceeds go to Cally.
This most likely is not intended from the Cally team, but users may view this possibility as a risk and choose not to deposit into Cally.
Manual review
Firstly, I recommend adding thresholds for the min and max values that feeRate
is able to be set within. This will provide validation against accidental or intended changes to feeRate
that are not in the best interests of vault owners.
Secondly, changes to feeRate
are applied immediately despite when the vault owner deposited their ERC721/ERC20. Perhaps it would be more fair to users if the same feeRate
that was active during the deposit is stored in a mapping along with the vault id. This feeRate
would be used until the call option has expired. This will allow the vault owner to withdraw their deposit if they don't agree to the new rate.
#0 - outdoteth
2022-05-15T18:58:37Z
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
🌟 Selected for report: 0xDjango
3412.2604 USDC - $3,412.26
A griefer is able to create as many vaults as they want by simply calling createVault()
with tokenIdOrAmount = 0
. This will most likely pose problems on the front-end of the Cally protocol because there will be a ridiculously high number of malicious vaults displayed to actual users.
I define these vaults as malicious because it is possible that a user accidently buys a call on this vault which provides 0 value in return. Overall, the presence of zero-amount vaults is damaging to Cally's product image and functionality.
createVault(0,,,,);
with an ERC20 type.amount > 0
Manual review
Add the simple check require(tokenIdOrAmount > 0, "Amount must be greater than 0");
#0 - outdoteth
2022-05-15T11:12:35Z
This check should only be applied on ERC20 tokens because ERC721 tokens can still have tokenIds that have ID's with a value of 0.
#1 - outdoteth
2022-05-17T17:53:32Z
this issue is fixed here: https://github.com/outdoteth/cally/pull/12
🌟 Selected for report: 0x1337
Also found by: 0x52, 0xDjango, 0xsanson, BondiPestControl, BowTiedWardens, GimelSec, IllIllI, MaratCerby, MiloTruck, PPrieditis, TrungOre, VAD37, WatchPug, berndartmueller, cccz, dipp, hake, hickuphh3, horsefacts, hubble, minhquanym, reassor, shenwilly, smiling_heretic
10.8874 USDC - $10.89
https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L345 https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L296
If a vault owner deposits an ERC20 token that supports fee on transfer, the vault owner will not be able to withdraw their deposit. Their funds will be frozen indefinitely until another deposit is made into Cally using the same ERC20.
Similarly, a call buyer will not be able to exercise their call on the ERC20 with fee on transfer until another deposit of the same token is made. If the call expires before more tokens are deposited, the buyer will permanently lose the funds used for their premium payment when buying the call.
This issue results in frozen funds, potential loss of premium, and lack of core protocol functionality.
For this example, assume the deposited ERC20 has a 1% fee on transfer.
10 ether
of token.(10 * 99%) = 9.9 ether
10 ether
to vault owner. This fails because Cally only possesses 9.9 ether
.Exercising of calls fail because of similar logic:
10 ether
of token.(10 * 99%) = 9.9 ether
10 ether
to buyer. This fails because Cally only possesses 9.9 ether
.It should be noted that the funds would be frozen until another deposit is made that brings Cally's balance higher than the balance needed by the vault.
Manual review.
In the case of an ERC20 transfer, the vault tokenIdOrAmount
should be written with the amount that was received by Cally after the transfer.
Take the balance of the ERC20 prior to and after the transfer. The difference will be the amount that is set for the vault.
#0 - outdoteth
2022-05-15T17:13:07Z
reference issue: https://github.com/code-423n4/2022-05-cally-findings/issues/39
🌟 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
65.2889 USDC - $65.29
Revert message indicates that reserve price is too small but condition checks that reserve is lower than the starting strike.
Abstract contracts contain a floating pragma. It is recommended to deploy all contracts with a single, specific compiler version to reduce the risk of compiler-specific bugs and contracts deployed with different versions.
In buyOption()
, the amount of ETH transferred can be greater than or equal to the premium amount.
In 'exercise()`, the amount of ETH transferred must be EXACTLY the strike amount.
I recommend making the logic consistent to either be equal to or greater than or equal.
https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L224 https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L272
In the following snippet, the buyer must exercise their option BEFORE the expiration timestamp:
`require(block.timestamp < vault.currentExpiration, "Option has expired");
However, in this line, the vault owner is not able to withdraw until AFTER the expiration timestamp:
require(block.timestamp > vault.currentExpiration, "Option still active");
Neither of these checks allow for actions on the actual expiration timestamp. One of these should be inclusive of the expiration time.
https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L269 https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L330
Throughout Cally.sol
, bools are checked via == true
, == false
, bool
, and !bool
. The below snippet is a single example:
require(vault.isExercised == false, "Vault already exercised"); require(vault.isWithdrawing, "Vault not in withdrawable state");
I recommend sticking to a single form of checking bool values.
#0 - outdoteth
2022-05-16T16:30:34Z
can be bumped to medium severity:
[L-03] Inconsistency regarding ETH transfer amounts: https://github.com/code-423n4/2022-05-cally-findings/issues/84
#1 - HardlyDifficult
2022-05-22T15:06:03Z
🌟 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.0974 USDC - $30.10
Since these arrays do not have setters to change their length, define their length when defining them. In my quick testing:
Reading a value from current implemented premiumOptions: 26052 gas
Reading value from premiumOptions with defined length: 23901 gas