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: 2/100
Findings: 5
Award: $3,644.36
π Selected for report: 2
π Solo Findings: 0
3071.0343 USDC - $3,071.03
The vulnerability or bug is in the implementation of the function getDutchAuctionStrike() The AUCTION_DURATION is defined as 24 hours, and consider that the dutchAuctionReserveStrike (or reserveStrike) will never be set to 0 by user.
Now if a vault is created with startingStrike value of 55 and reserveStrike of 13.5 , the auction price will drop from 55 to 13.5 midway at ~12 hours. So, after 12 hours from start of auction, the rate will be constant at reserveStrike of 13.5, and remaining time of 12 hours of auction is a waste.
Some other examples :
startStrike, reserveStrike, time-to-reach-reserveStrike 55 , 13.5 , ~12 hours 55 , 5 , ~16.7 hours 55 , 1.5 , ~20 hours 5 , 1.5 , ~11 hours
The impact is high wrt Usability, where users have reduced available time to participate in the auction (when price is expected to change). The vault-Creators or the option-Buyers may or may not be aware of this inefficiency, i.e., how much effective time is available for auction.
Contract : Cally.sol Function : getDutchAuctionStrike ()
The function getDutchAuctionStrike() can be modified such that price drops to the reserveStrike exactly at 24 hours from start of auction.
/* delta = max(auctionEnd - currentTimestamp, 0) progress = delta / auctionDuration auctionStrike = progress^2 * (startingStrike - reserveStrike) << Changes here strike = auctionStrike + reserveStrike << Changes here */ uint256 delta = auctionEndTimestamp > block.timestamp ? auctionEndTimestamp - block.timestamp : 0; uint256 progress = (1e18 * delta) / AUCTION_DURATION; uint256 auctionStrike = (progress * progress * (startingStrike-reserveStrike)) / (1e18 * 1e18); strike = auctionStrike + reserveStrike;
#0 - outdoteth
2022-05-15T17:07:21Z
We think this should be bumped to high severity. It would be easy for a user to create an auction that declines significantly faster than what they would have assumed - even over 1 or 2 blocks. It makes no sense for the auction to ever behave in this way and would result in options getting filled at very bad prices for the creator of the vault.
#1 - outdoteth
2022-05-17T11:11:41Z
The fix for this issue is here: https://github.com/outdoteth/cally/pull/2
#2 - HardlyDifficult
2022-05-20T21:40:12Z
The sponsor comment here makes sense. Agree with (1) High since this can potentially be very detrimental to the promise of this protocol.
16.9712 USDC - $16.97
https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L119-L121 https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L282-L286
When a vault is created, the creator knows the value of feeRate that will be charged. But, after the option is sold, there is a possibility of changing the feeRate to a high value by the contract Owner. In the event the exercise() is called, the beneficiary will get a lower value due to higher protocol fee.
This is a known risk, and users may not trust the protocol due to this control the contract Owner has. Vault beneficiary can also be grieved by maliciously configuring a high feeRate, before the option is exercised. The impact will be on all the protocol users who's options will be eligibile for exercise. This is also amplified as currently there is no upper bound check on the feeRate that can be set.
Any new feeRate should be applicable only to newer vaults created henceforth.
Contract : Cally.sol Function : setFee() and exercise()
In the Vault struct, add another parameter feeRate, and during createVault, set it to the current value of feeRate. In the event of exercise() function being called, the stored value of feeRate in the Vault struct to be used as opposed to the current global feeRate.
#0 - outdoteth
2022-05-15T19:09:25Z
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: 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#L296 https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L345
There is no whitelist or check on the ERC20 tokens to be used during Vault creation. So, if a feeOnTransfer ERC20 is used as token during vaultCreation, then actual amount transferred into the contract will be less than the vault.tokenIdOrAmount
Assuming only one vault is created for this specific type of token currently, and an option is sold, then if the option is exercised, the actual balance of the feeOnTransfer token will be less than the original vault.tokenIdOrAmount, hence exercise will fail on safeTransfer due to insufficient balance. Alternately if the option has expired, and the vault owner wants to withdraw the token, it will fail similarly due to insufficient balance.
Option cant be exercised by option holder or token withdrawn by vault owner, unless manually the difference amount of token is transferred to the contract by somebody. The difference amount is the feeOnTransfer value deducted during vault creation.
Either the actual amount transferred into the contract is stored in the vault.tokenIdOrAmount, or the vault creator adds the diff of the feeOnTransfer amount also, while creating the vault.
#0 - outdoteth
2022-05-15T17:16:35Z
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
537.2973 USDC - $537.30
LOW
NON-CRITICAL
Function : createVault() in Cally.sol
line 169 require(dutchAuctionReserveStrike < strikeOptions[dutchAuctionStartingStrikeIndex], "Reserve strike too small");
If the current error message is followed, user will never be able to successfully createVault()
Correct error message string
require(dutchAuctionReserveStrike < strikeOptions[dutchAuctionStartingStrikeIndex], "Reserve strike more than Starting strike");
VaultId are odd in number; if a valultId of event number is given, the function valuts() will return misleading information.
Function valuts() in Cally.sol
Check if the valutID parameter is of vault type, by adding a require statement
function vaults(uint256 vaultId) external view returns (Vault memory) { require(vaultId % 2 != 0, "Not vault type"); return _vaults[vaultId]; }
If the option is already exercised, the vault owner should not be allowed to call the initiateWithdraw() function.
Add a require statement in the function initiateWithdraw()
require(vault.isExercised == false, "Vault already exercised");
If a value of 0 is given for durationDays in the createVault() function, the transaction will revert with an ambigous message "durationDays too small" It can better stated as given below
Error message string can be changed as below.
line 170 require(durationDays > 0, "durationDays cannot be zero");
To reduce gas and storage, the protocol has currently designed to store the index of the premiumOptions[] & strikeOptions[] in the Vault structure.
This is too restrictive and may not be future proof.
One suggestion is to add another member unit8 premiumMultiplier (with default value of 1) in the Vault struct, and users can have combination of values of the premiumMultiplier and premiumIndex to define more range of premium values if required.
Same suggestion applies for adding a multiplier for the strikeOptions[]
When feeRate is changed at setFee(Cally.sol), no event is raised. It would be important to raise this event for any external integration with this system.
Contract : Cally.sol Function : setFee
event definition
event FeeRateUpdated(uint256 newFeeRate);
event emit at setFee function
require(feeRate_ != feeRate, "new feeRate should be different"); feeRate = feeRate_; emit FeeRateUpdated(feeRate_);
When beneficiary is changed at setVaultBeneficiary(Cally.sol), no event is raised. It would be important to raise this event for any external integration with this system.
Contract : Cally.sol Function : setVaultBeneficiary
event definition
event VaultBeneficiaryUpdated(uint256 indexed vaultId, address indexed beneficiary);
event emit at setVaultBeneficiary function
emit VaultBeneficiaryUpdated(vaultId, beneficiary);
In Cally.sol, function buyOption the following is the order of lines. line 208 require(vaultId % 2 != 0, "Not vault type"); line 211 Vault memory vault = _vaults[vaultId];
This can be made consistent with other functions by changing the order.
Vault memory vault = _vaults[vaultId]; require(vaultId % 2 != 0, "Not vault type");
The value of vaultIndex = 1 is never assigned to any vault.
// vault index should always be odd vaultIndex += 2; vaultId = vaultIndex; _vaults[vaultId] = vault;
This can be changed to as below, so that vaultID = 1 is also used
vaultId = vaultIndex; // vault index should always be odd vaultIndex += 2; _vaults[vaultId] = vault;
#0 - outdoteth
2022-05-16T18:59:50Z
high quality report
#1 - HardlyDifficult
2022-05-30T19:19:47Z
I agree with the low/non-critical labeling provided by the warden in this report.