Platform: Code4rena
Start Date: 06/12/2022
Pot Size: $36,500 USDC
Total HM: 16
Participants: 119
Period: 3 days
Judge: berndartmueller
Total Solo HM: 2
Id: 189
League: ETH
Rank: 37/119
Findings: 5
Award: $91.58
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: adriro
Also found by: 0x446576, 0xA5DF, 0xDave, 0xDecorativePineapple, 0xRobocop, 0xbepresent, 8olidity, Aymen0909, Ch_301, Chom, Franfran, HollaDieWaldfee, Madalad, Parth, Ruhum, Tricko, bin2chen, carrotsmuggler, chaduke, danyams, evan, gz627, hansfriese, hihen, imare, immeas, jadezti, jayphbee, jonatascm, kaliberpoziomka8552, kiki_dev, kree-dotcom, ladboy233, lukris02, lumoswiz, mahdikarimi, minhquanym, minhtrng, nameruse, neumo, obront, pauliax, poirots, reassor, rvierdiiev, slvDev, sorrynotsorry, yixxas, zapaz
0.8413 USDC - $0.84
https://github.com/code-423n4/2022-12-escher/blob/main/src/minters/LPDAFactory.sol#L29-L42
LPDAFactory should not allow to create LPDA where startPrice < (end - start) * dropRate. Otherwise it's possible that LPDA contract will be out of service, because getPrice will always revert.
Let's look at LPDA.getPrice function. https://github.com/code-423n4/2022-12-escher/blob/main/src/minters/LPDA.sol#L117-L125
function getPrice() public view returns (uint256) { Sale memory temp = sale; (uint256 start, uint256 end) = (temp.startTime, temp.endTime); if (block.timestamp < start) return type(uint256).max; if (temp.currentId == temp.finalId) return temp.finalPrice; uint256 timeElapsed = end > block.timestamp ? block.timestamp - start : end - start; return temp.startPrice - (temp.dropPerSecond * timeElapsed); }
The price is decreasing during the time and decreased amount depends on temp.dropPerSecond and timeElapsed.
In case if temp.dropPerSecond * timeElapsed > temp.startPrice then getPrice function will be always reverting, because of underflow error. As result it will not be possible to finalize sales and seller will never receive funds for tokens that were sold.
This is because, LPDAFactory.createLPDASale doesn't check that.
VsCode
Check that startPrice >= (end - start) * dropRate in LPDAFactory.createLPDASale function. Otherwise revert.
#0 - c4-judge
2022-12-11T11:38:25Z
berndartmueller marked the issue as duplicate of #392
#1 - c4-judge
2023-01-02T19:55:24Z
berndartmueller marked the issue as satisfactory
🌟 Selected for report: RaymondFam
Also found by: 0xdeadbeef0x, 0xhacksmithh, AkshaySrivastav, Awesome, Bnke0x0, CRYP70, HollaDieWaldfee, JC, Parth, Rahoz, Tutturu, __141345__, ahmedov, ajtra, asgeir, aviggiano, bin2chen, btk, carrotsmuggler, cccz, chaduke, cryptonue, dic0de, fatherOfBlocks, fs0c, hansfriese, jonatascm, karanctf, ladboy233, lumoswiz, martin, obront, pashov, pauliax, rvierdiiev, shark, simon135, supernova, tourist, yellowBirdy, zapaz, zaskoh
0.6136 USDC - $0.61
https://github.com/code-423n4/2022-12-escher/blob/main/src/minters/FixedPrice.sol#L109
In different contracts protocol uses transfer function to send ethers to recipient. But in case if recipient is contract and his receive/fallback function needs more than 2300 gas(that is send with transfer), then the function will revert.
When FixedPrice contract has sold all tokens then _end() function is called to send payment. https://github.com/code-423n4/2022-12-escher/blob/main/src/minters/FixedPrice.sol#L107-L111
function _end(Sale memory _sale) internal { emit End(_sale); ISaleFactory(factory).feeReceiver().transfer(address(this).balance / 20); selfdestruct(_sale.saleReceiver); }
In case if feeReceiver is contract and his receive/fallback function needs more than 2300 gas to finish then the function will revert and as result it will not be possible to finalize sales and last token will not be sold.
Same approach is used in another contracts. For example in LPDA when user wants to get refund, transfer is used as well.
VsCode
Use call function instead of transfer.
#0 - c4-judge
2022-12-10T12:11:35Z
berndartmueller marked the issue as duplicate of #99
#1 - c4-judge
2023-01-03T12:49:58Z
berndartmueller marked the issue as satisfactory
🌟 Selected for report: AkshaySrivastav
Also found by: 0x52, 0xA5DF, 0xdeadbeef0x, KingNFT, Madalad, Parth, Soosh, _Adam, adriro, csanuragjain, danyams, eyexploit, gasperpre, gz627, gzeon, hansfriese, hihen, immeas, jadezti, jonatascm, kiki_dev, kree-dotcom, ladboy233, lukris02, lumoswiz, mahdikarimi, minhtrng, nalus, nameruse, obront, reassor, rvierdiiev, seyni, tnevler, wait, yixxas
1.3417 USDC - $1.34
https://github.com/code-423n4/2022-12-escher/blob/main/src/minters/FixedPrice.sol#L57-L74
In case if all tokens are not sold in FixedPrice contract then seller can't withdraw received amount from sales.
FixedPrice.buy function allows someone to buy some amount of tokens. If all tokens that seller wanted to sell are sold, then sales is over and payment is sent to fee receiver and seller. This is the only option to finish sales after it started.
So in case if not all tokens were sold and noone else wants to buy, seller can't withdraw funds that were paid for already sold tokens, this funds are stucked inside FixedPrice contract.
https://github.com/code-423n4/2022-12-escher/blob/main/src/minters/FixedPrice.sol#L57-L74
function buy(uint256 _amount) external payable { Sale memory sale_ = sale; IEscher721 nft = IEscher721(sale_.edition); require(block.timestamp >= sale_.startTime, "TOO SOON"); require(_amount * sale_.price == msg.value, "WRONG PRICE"); uint48 newId = uint48(_amount) + sale_.currentId; require(newId <= sale_.finalId, "TOO MANY"); for (uint48 x = sale_.currentId + 1; x <= newId; x++) { nft.mint(msg.sender, x); } sale.currentId = newId; emit Buy(msg.sender, _amount, msg.value, sale); if (newId == sale_.finalId) _end(sale); }
VsCode
Add ability for seller to withdraw already paid amount or add salesEnd param after which seller can end sales.
#0 - c4-judge
2022-12-12T09:05:17Z
berndartmueller marked the issue as duplicate of #328
#1 - c4-judge
2023-01-02T20:21:10Z
berndartmueller changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-01-02T20:22:59Z
berndartmueller marked the issue as satisfactory
🌟 Selected for report: ladboy233
Also found by: 0x1f8b, Matin, UniversalCrypto, gzeon, karanctf, minhquanym, obront, rvierdiiev, seyni, slvDev, yixxas
57.6274 USDC - $57.63
https://github.com/code-423n4/2022-12-escher/blob/main/src/minters/LPDA.sol#L71
In case if payment of user were more than uint80.max value, then the balance stored to receipt will be truncated to uint80.max. As result user will not be able to refund all that he overpaid.
When user wants to buy tokens he calls LPDA.buy and provides ether with function. Later the amount that he paid is stored in the receipt. It will be used when the price will decrease and user will be able to refund overpaid tokens.
The problem is that msg.value here is unsafely casted to uint80. That means that if user paid more than uint80.max value, then delta amount is lost as it's not stored into receipts[msg.sender].balance.
receipts[msg.sender].balance += uint80(msg.value);
So later he will not be able to withdraw all overpaid amount using refund as it uses his balance.
When casting check that msg.value is less than uint80.max
#0 - c4-judge
2022-12-10T17:13:12Z
berndartmueller marked the issue as duplicate of #369
#1 - c4-judge
2023-01-03T13:56:52Z
berndartmueller changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-01-03T13:56:57Z
berndartmueller marked the issue as satisfactory
🌟 Selected for report: 0xSmartContract
Also found by: 0x1f8b, 0x4non, 0xA5DF, 0xNazgul, 0xRobocop, 0xfuje, Bnke0x0, HollaDieWaldfee, Lambda, RaymondFam, TomJ, _Adam, adriro, ajtra, carrotsmuggler, cccz, danyams, gasperpre, hansfriese, helios, immeas, joestakey, ladboy233, nameruse, obront, oyc_109, rvierdiiev, saian, sakshamguruji, seyni, slvDev, yixxas, zaskoh
31.1555 USDC - $31.16
https://github.com/code-423n4/2022-12-escher/blob/main/src/minters/FixedPrice.sol#L66
In different contracts protocol uses ERC721 mint function to create new token for address instead of safeMint. Because of that it's possible to mint token to the contract that doesn't support ERC721 and token will be stucked there,
Inside FixedPrice.buy function new tokens are minted using mint function. In case if msg.sender is contract and it doesn't support ERC721, using safeMint is better. Otherwise token can be stucked inside contract.
VsCode
Use safeMint that will check that recipient supports ERC721 tokens.
#0 - berndartmueller
2022-12-10T12:25:26Z
Downgrading to QA. See https://github.com/code-423n4/2022-12-escher-findings/issues/508#issuecomment-1345252989
#1 - c4-judge
2022-12-10T12:25:34Z
berndartmueller changed the severity to QA (Quality Assurance)
#2 - c4-judge
2023-01-04T10:48:08Z
berndartmueller marked the issue as grade-b