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: 88/119
Findings: 2
Award: $2.18
🌟 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#L36 https://github.com/code-423n4/2022-12-escher/blob/main/src/minters/LPDA.sol#L117-L125
Concerning the variables within the Sale
struct in LPDA
, dropPerSecond
ought to be such that:
dropPerSecond = (startPrice - finalPrice) / (endTime - startTime)
i.e. price will drop gradually over the period of the sale until it reaches finalPrice
once the sale is over, without ever dropping below finalPrice
.
However, it is left as a user input in LPDAFactory.createLPDASale()
, with the only check being that it is > 0.
Thus, the above equation may not hold, which could lead to price exhibiting unexpected behaviour:
dropPerSecond
is too high, then by the end of the sale price will drop below finalPrice
and remain there for the rest of the saledropPerSecond
is too low, then by the end of the sale price will be above finalPrice
, and remain there for the rest of the saleIn each of the above cases, finalPrice
is redundant. Such cases would likely mislead users, and does not appear to be intended.
function test_DropPerSecondTooHigh() public { // setup lpdaSale.startPrice = uint80(uint256(1 ether)); lpdaSale.finalPrice = uint80(uint256(0.1 ether)); // price should drop to ~0.05 after 1 day lpdaSale.dropPerSecond = uint80(uint256(0.95 ether) / 1 days); sale = LPDA(lpdaSales.createLPDASale(lpdaSale)); // authorize the lpda sale to mint tokens edition.grantRole(edition.MINTER_ROLE(), address(sale)); vm.warp(lpdaSale.endTime + 1); uint256 price = sale.getPrice(); // ~0.05ETH assert(price < lpdaSale.finalPrice); }
function test_DropPerSecondTooLow() public { // setup lpdaSale.startPrice = uint80(uint256(1 ether)); lpdaSale.finalPrice = uint80(uint256(0.1 ether)); // price should drop to ~0.8 after 1 day lpdaSale.dropPerSecond = uint80(uint256(0.2 ether) / 1 days); sale = LPDA(lpdaSales.createLPDASale(lpdaSale)); // authorize the lpda sale to mint tokens edition.grantRole(edition.MINTER_ROLE(), address(sale)); vm.warp(lpdaSale.endTime + 1); uint256 price = sale.getPrice(); // ~0.8ETH assert(price > lpdaSale.finalPrice); }
Foundry
dropPerSecond
input and instead compute it within LPDAFactory.createLPDASale()
using the equation in the impact sectiongetPrice()
such that if block.timestamp > end
then finalPrice
is returned instead of the existing calculation#0 - c4-judge
2022-12-11T11:38:07Z
berndartmueller marked the issue as duplicate of #392
#1 - c4-judge
2023-01-02T19:54:46Z
berndartmueller marked the issue as satisfactory
#2 - c4-judge
2023-01-02T19:54:50Z
berndartmueller changed the severity to 3 (High Risk)
🌟 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#L73 https://github.com/code-423n4/2022-12-escher/blob/main/src/minters/LPDA.sol#L87
In FixedPrice
and LPDA
, the only way to retreive ether proceeds is the internal function _end()
.
After the sale begins, this function can only be called within buy()
once the final NFT is bought.
If the collection does not ever sell out, the ether will be trapped indefinitely.
A creator uses FixedPriceFactory
to deploy a FixedPrice
contract for their collection, from which they hope to sell 100 NFTs for 1 ether each.
After selling 90 of them, interest dies out and no one else wishes to buy any more.
FixedPrice
holds 90 ether that the creator is unable to access.
If no more NFTs are sold, the creator will never be able to access the proceeds of their sales.
function testTrappedEther() public { test_Buy(); vm.warp(365 days); vm.expectRevert(); sale.cancel(); assertEq(address(sale).balance, 1 ether); }
Add new expiryDate
variable to the contract, such that after that period the owner is able to call a new function endSale()
to end the sale and collect their ether.
contract FixedPrice is ... { // ... struct Sale { // slot 1 uint48 currentId; uint48 finalId; address edition; // slot 2 uint96 price; address payable saleReceiver; // slot 3 uint96 startTime; uint96 expiryDate; // new variable } //... // new function function endSale() external onlyOwner { require(block.timestamp > sale.endTime, "TOO EARLY"); _end(sale); } }
contract LPDA is ... { //... struct Sale { // slot 1 uint48 currentId; uint48 finalId; address edition; // slot 2 uint80 startPrice; uint80 finalPrice; uint80 dropPerSecond; // slot 3 uint96 endTime; address payable saleReceiver; // slot 4 uint96 startTime; uint96 expiryDate; // new variable } //... // new function function endSale() external onlyOwner { require(block.timestamp > expiryDate, "TOO EARLY"); _end(sale); } }
#0 - c4-judge
2022-12-12T09:03:56Z
berndartmueller marked the issue as duplicate of #328
#1 - c4-judge
2023-01-02T20:21:04Z
berndartmueller changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-01-02T20:22:54Z
berndartmueller marked the issue as satisfactory