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: 10/100
Findings: 6
Award: $2,626.28
π Selected for report: 1
π Solo Findings: 0
π Selected for report: WatchPug
Also found by: BondiPestControl, IllIllI
2072.9482 USDC - $2,072.95
There are no checks to ensure the the vault.token
contract exists when creating a vault.
Token address are deterministic in the EVM and can be known ahead of time. As a result it is possible for a user to call createVault()
with token
set to a ERC20 contract that has not yet been deployed but will be deployed in the future.
Now if a user calls createVault()
before a new ERC20 token has been created it will credit the vault with these tokens. The cause is from ERC20(token).safeTransferFrom()
which will succeed if token
has zero bytecode at the address.
The later when the ERC20 token is deployed and another user calls createVault()
with this token, the attacker is able to call withdraw()
and there will be no tokens left in the contract for the genuine vault.
There are no checks before calling safeTransferFrom
to ensure vault.token
has bytecode.
ERC20(vault.token).safeTransferFrom(msg.sender, address(this), vault.tokenIdOrAmount);
Use the extcodesize
EVM opcode to ensure that there is bytecode at the vault.token
address during createVault()
otherwise revert the transaction.
#0 - outdoteth
2022-05-15T21:02:11Z
options can be sold on for erc20s that have not been deployed yet: https://github.com/code-423n4/2022-05-cally-findings/issues/225
π 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/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L119-L121 https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L289
It is possible for the owner of the contract to set the feeRate
equal to or above 100%.
The impact of setting the contract above 100% (1e18
) is that the fee
which is msg.value * feeRate / 1e18
may be greater than the msg.value
. There will be an overflow in line #289 causing all calls to exercise()
to fail.
ethBalance[getVaultBeneficiary(vaultId)] += msg.value - fee;
Furthermore, the owner may perform a rug pull attack by setting the feeRate = 1e18
(exactly 100%). This would allow the owner to exercise any options without paying any funds. That is due to the fact that the entire strike amount will be considered fees and attributed to protocolUnclaimedFees
which the owner may later withdraw. The owner would then also receive the token which was held in the vault.
There are no restrictions on the function setFee()
.
function setFee(uint256 feeRate_) external onlyOwner { feeRate = feeRate_; }
The owner may then claim all fees and receive tokens in exercise()
, the only cost to the owner would by the premium amount when they purchase the option from the auction.
// collect protocol fee uint256 fee = 0; if (feeRate > 0) { fee = (msg.value * feeRate) / 1e18; protocolUnclaimedFees += fee; } // increment vault beneficiary's ETH balance ethBalance[getVaultBeneficiary(vaultId)] += msg.value - fee; emit ExercisedOption(optionId, msg.sender); // 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);
Consider setting an reasonable upper limit of the feeRate
such as 5% in setFee()
.
Furthermore, consider have vaults lock in the feeRate
when they first createVault()
. This would prevent the owner manipulating the value when a user is trying to exercise the option.
#0 - outdoteth
2022-05-15T18:57:58Z
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: BondiPestControl
Also found by: 0xf15ers, GimelSec, IllIllI, MadWookie, MiloTruck, Ruhum, VAD37, berndartmueller, cccz, csanuragjain, dipp, hake, horsefacts, jayjonah8, m9800, pedroais, throttle
31.6149 USDC - $31.61
It is possible for a user purchasing an option to accidentally overpay the premium during buyOption()
.
Any excess funds paid for in excess of the premium will be transferred to the vault creator.
The premium is fixed at the time the vault is first created by vault.premiumIndex
. Hence there is no need to allow users to overpay since there will be no benefit.
buyOption()
allows msg.value > premium
uint256 premium = getPremium(vaultId); require(msg.value >= premium, "Incorrect ETH amount sent");
Consider modifying the check such that the msg.value
is exactly equal to the premuim
. e.g.
uint256 premium = getPremium(vaultId); require(msg.value == premium, "Incorrect ETH amount sent");
#0 - outdoteth
2022-05-17T17:25:57Z
this issue is fixed in: https://github.com/outdoteth/cally/pull/9
#1 - HardlyDifficult
2022-05-20T22:20:45Z
Agree with 2 (Medium) for this. The issue doesn't really open the door for an attack, except for maybe via a malicious frontend. But it could potentially leak value in terms of over compensating the vault creator.
#2 - HickupHH3
2022-06-08T06:11:30Z
QA report #182 should have its issue bumped up and marked as a duplicate IMO
π Selected for report: hickuphh3
Also found by: BondiPestControl, GimelSec, VAD37, sseefried
447.7568 USDC - $447.76
There is a potential overflow in the function buyOption()
which will revert if triggered.
The overflow occurs when durationDays >= 195
. As a result it is impossible for any user to call buyOption()
if the duration exceeds this value.
The vault creator will still be able to withdraw through withdraw()
The following line may cause an overflow due to (vault.durationDays * 1 days)
.
vault.currentExpiration = uint32(block.timestamp) + (vault.durationDays * 1 days);
The issue occurs since vault.durationDays
is of type uint8
and 1 days
is of type uint24
(solidity defaults to the lowest size that the value fits).
As a result vault.durationDays * 1 days
are treated as uint24 * uint24
.
1 days = 86,400
and
195 * 86,400 = 16,848,000
Since,
2**24 = 16,777,216
there is an overflow for the uint24
. Overflows in solidity 0.8.13
will cause the transaction to revert.
Since vault.durationDays
cannot be changes all calls to buyOption()
for this token ID will fail if durationDays >= 195
Consider updating the multiplication such that durationDays
is first cast to uint32
. This will prevent an overflow since the max value of durationDays = 255
multiplied by 1 days
is less than 2**32 and so will not overflow a uint32
.
e.g.
vault.currentExpiration = uint32(block.timestamp) + (uint32(vault.durationDays) * 1 days);
#0 - outdoteth
2022-05-15T11:04:48Z
This should be bumped to a high risk issue - a user can unintentionally create vaults that would never have been able to have been filled and result in them losing funds; in terms of gas prices at 100 gwei (which it frequently was a few months ago), 140k gas is not a tiny amount.
#1 - outdoteth
2022-05-15T17:22:32Z
reference issue: https://github.com/code-423n4/2022-05-cally-findings/issues/16
π 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/main/contracts/src/Cally.sol#L200 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L296
When a user creates a ERC20
vault by calling createVault()
, tokens are transferred from the caller to the contract equivalent tokenIdOrAmount
. If the token contract used charges a fee for the transfer, the transfer initiated by an exercised option will ultimately fail.
As a result, users could set up a honey pot where they create a vault using a fee-on-transfer token, sell options on this vault with whatever strike price and premium and with certainty keep their tokens and the premium amount as an exercise of this option will fail. The vault owner will always profit from this trade and the option buyer will always lose the premium they paid.
Because this leads to a loss of funds by an honest option buyer, I believe high
severity is deserving.
Consider snapshotting the before and after balance for all ERC20
transfers in and out of the contract. This will ensure the balance for each vault is strictly correct, regardless if a vault is created upon a fee-on-transfer token.
#0 - outdoteth
2022-05-15T17:15:29Z
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
54.8976 USDC - $54.90
https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L198-L200
While the Cally.sol
contract is compatible with standard ERC20
and ERC721
tokens, it is not able to handle popular non-standard ERC721
tokens such as cryptopunks. The typical transferFrom()
call will fail as a result, and these users will likely opt to use another protocol which does support options on their specific NFT.
NFTX protocol has implemented a way to handle the transfer of both standard and non-standard ERC721
tokens. The relevant implementation can be found here. The solution provided also utilises OpenZeppelin's safeTransferFrom()
function on most transfers as this ensures the function reverts on failure.
#0 - outdoteth
2022-05-15T13:43:59Z
Technically correct but we have no intention to support these tokens. users can use those tokens by getting a wrapped version of them that conforms to the erc721 spec
#1 - HardlyDifficult
2022-05-22T15:23:08Z
This is a common issue when working with NFTs. Wrappers, native support, or simply not supporting these non-standard tokens are reasonable courses. Lowing to 1 (Low) and converting into a QA report for the warden.