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: 69/119
Findings: 3
Award: $33.34
🌟 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
While i'm assuming there are checks for this on UI or off-chain, this wasn't mentioned in the docs or the comments, so its worth flagging and possibly implementating the mitigation step as a precaution since it is also cheap on gas.
The price can theoritically drop below the sale.startprice which can lead to getPrice() function call to always revert which will in turn always revert a buy function call under these conditions.
More specifically on line 125 of the code above:
priceDropPerSec * timeElapased can be larger than start price.
There is no steps taken in code to check or mitigate it and hence this can be potential source of DoS.
The below test using forge fails due to revert
function test_LPDABug() public { lpdaSale = LPDA.Sale({ currentId: uint48(0), finalId: uint48(10), edition: address(edition), startPrice: uint80(uint256(1 ether)), finalPrice: uint80(uint256(0.1 ether)), dropPerSecond: uint80(uint256(1.1 ether) / 1 days), startTime: uint96(block.timestamp), saleReceiver: payable(address(69)), endTime: uint96(block.timestamp + 1 days) }); // make the lpda sales contract sale = LPDA(lpdaSales.createLPDASale(lpdaSale)); // authorize the lpda sale to mint tokens edition.grantRole(edition.MINTER_ROLE(), address(sale)); vm.warp(block.timestamp + 1 days); sale.getPrice(); }
VSCode Forge for test
Updating the return statement of line 125 of LDPA.sol to something like the below to return final price incase the price drop ends up being bigger than start price
uint256 dropAmount = temp.dropPerSecond * timeElapsed; return dropAmount > temp.startPrice ? temp.final : Pricetemp.startPrice - dropAmount;
#0 - c4-judge
2022-12-13T11:23:56Z
berndartmueller marked the issue as duplicate of #392
#1 - c4-judge
2023-01-02T19:57:01Z
berndartmueller marked the issue as satisfactory
#2 - c4-judge
2023-01-02T19:57:04Z
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
Currently for a FixedPrice sale that started, the only way for it to end is when the entire token id range in the FixedPrice instance gets sold.
The sales receiver and fee receiver only receive their fund once the FixedPrice sale ends.
As such, in a situation where the all token ids in that FixedPrice sale proxy don't get sold then funds will be held on the smart contract without being able to retreive them.
There is a cancellation functionality that can be called by FixedPrice proxy but that can only be called before the scheduled start time for the sale and not after it started.
Below is the snippet of the cancellation functionality with a requirment for the block.timstamp being less than the start time Link to code
/// @notice Owner can cancel current sale function cancel() external onlyOwner { require(block.timestamp < sale.startTime, "TOO LATE"); _end(sale); }
While the Snippet for the ending the sale and transferring the fund to the sale receiver and fee receiver is as per below. Link to code
function _end(Sale memory _sale) internal { emit End(_sale); ISaleFactory(factory).feeReceiver().transfer(address(this).balance / 20); selfdestruct(_sale.saleReceiver); }
This seems to the only way to extract funds from the contract as there is no other withrdaw functionality visible in the code
As for ending the FixedPrice sale through once the all token ids in that FixedPrice sale instance are purchase, please ind the code below Link to code
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); }
The last line in the code (line 73) called the end functions once tokens are fully sold.
This and the cancellation function are the only two references to the end function and thus once the FixedPrice Sale starts it can only end when all tokens are sold and only then can the sales receives and fee receiver withdraw their fund from the contract
VScode
There are two way around this:
1-Allowing sales cancellation to occur after the sale started by removing the required check
2-Adding a withdaw function with the onlyOwner modifier that can transfer the current balance to the sale receiver and the fee receiver addreses
#0 - c4-judge
2022-12-12T09:58:30Z
berndartmueller marked the issue as duplicate of #328
#1 - c4-judge
2023-01-02T20:21:18Z
berndartmueller changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-01-02T20:23:06Z
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
Overall the codebase is quite readable, well organized. For the most part the codebase closley matched the documenation but there where a few mismatches that were mentioned below. Project structure is intuitive and simple to follow. Contract structure is well ogranized and makes good use of libraries and external contracts. Variable naming is mostly easily readble and intuitive. Comments were used rigourously and were beneficial and accurate in describing the object they were comenting on. Events are used in a benefical manner to track protocol outputs.
No external/public function for {_setTokenRoyalty} as mentioned in the sale flow of the docs. Currently royalty can only be set through default and all token ids.
instances:1
File: Escher721.sol Line: N/A Link
Better flow to grant the proxy minter role (escher721 minting function) during initialize instead of having to call it separately. As either way it will have to be called in order for the buy function to work
instances:3
File: FixedPrice.sol Line: 78-82 Link
function initialize(Sale memory _sale) public initializer { sale = _sale; _transferOwnership(_sale.saleReceiver); emit Start(_sale); }
File: LPDA.sol Line: 110-114 Link
function initialize(Sale memory _sale) public initializer { sale = _sale; _transferOwnership(_sale.saleReceiver); emit Start(_sale); }
File: OpenEdition.sol Line: 82-86 Link
function initialize(Sale memory _sale) public initializer { sale = _sale; _transferOwnership(_sale.saleReceiver); emit Start(_sale); }
For better readability, adding comment that curators will be added by admins manually calling AccessControl "grantRole" function.
Comment could be added above the addCreator function.
instances:1
File: Escher.sol Line: 27 Link
Typo in the comment. comment mentions "set the prices" i believe it should be "set the pieces"
instances:1
File: Generative.sol Line: 11
For better readability, fees parameter could be named feesAdress as fees implies an amount
instances:3
File: FixedPriceFactory.sol Line: 40-44 Link
/// @notice set the fee receiver for fixed price editions /// @param fees the address to receive fees function setFeeReceiver(address payable fees) public onlyOwner { feeReceiver = fees; }
File: LPDAFactory.sol Line: 44-47 Link
/// @notice set the fee receiver for fixed price editions /// @param fees the address to receive fees function setFeeReceiver(address payable fees) public onlyOwner { feeReceiver = fees; }
File: OpenEditionFactory.sol Line: 40-44 Link
/// @notice set the fee receiver for fixed price editions /// @param fees the address to receive fees function setFeeReceiver(address payable fees) public onlyOwner { feeReceiver = fees; }
Fixed price sale in the comment should be Open Edition sale
instances:2
File: FixedPriceFactory.sol Line: 27 && 40
Missing check to on Zero address for _sale.saleReceiver. Mostly for a precaution as a zero address can lead to situation where sale funds are lost.
instances:2
File: OpenEditionFactory.sol Line: 29-38 Link
function createOpenEdition(OpenEdition.Sale calldata sale) external returns (address clone) { require(IEscher721(sale.edition).hasRole(bytes32(0x00), msg.sender), "NOT AUTHORIZED"); require(sale.startTime >= block.timestamp, "START TIME IN PAST"); require(sale.endTime > sale.startTime, "END TIME BEFORE START"); clone = implementation.clone(); OpenEdition(clone).initialize(sale); emit NewOpenEditionContract(msg.sender, sale.edition, clone, sale); }
File: FixedPriceFactory.sol Line: 29-38 Link
function createOpenEdition(OpenEdition.Sale calldata sale) external returns (address clone) { require(IEscher721(sale.edition).hasRole(bytes32(0x00), msg.sender), "NOT AUTHORIZED"); require(sale.startTime >= block.timestamp, "START TIME IN PAST"); require(sale.endTime > sale.startTime, "END TIME BEFORE START"); clone = implementation.clone(); OpenEdition(clone).initialize(sale); emit NewOpenEditionContract(msg.sender, sale.edition, clone, sale); }
#0 - c4-judge
2023-01-04T10:40:04Z
berndartmueller marked the issue as grade-b