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: 42/119
Findings: 2
Award: $80.77
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: immeas
Also found by: 0x52, 0xDecorativePineapple, AkshaySrivastav, HollaDieWaldfee, aviggiano, bin2chen, cryptonue, evan, ey88, fs0c, gzeon, hansfriese, hihen, jayphbee, minhquanym, pauliax, reassor, saian, wait
49.6134 USDC - $49.61
In LPDA.sol buy
function when all tokens are minted, the final price is set and the ether from the sale and fee is sent to the saleReceiver and feeReceiver addresses.
Since there is no check to validate if sale has ended the function can be executed with amount==0 and ether can be transferred to the feeReceiver and saleReceiver again.
Users will not be able to refund the excess ether sent during buy.
function buy(uint256 _amount) external payable { uint48 amount = uint48(_amount); Sale memory temp = sale; IEscher721 nft = IEscher721(temp.edition); require(block.timestamp >= temp.startTime, "TOO SOON"); uint256 price = getPrice(); require(msg.value >= amount * price, "WRONG PRICE"); amountSold += amount; uint48 newId = amount + temp.currentId; require(newId <= temp.finalId, "TOO MANY"); receipts[msg.sender].amount += amount; receipts[msg.sender].balance += uint80(msg.value); for (uint256 x = temp.currentId + 1; x <= newId; x++) { nft.mint(msg.sender, x); } sale.currentId = newId; emit Buy(msg.sender, amount, msg.value, temp); if (newId == temp.finalId) { sale.finalPrice = uint80(price); uint256 totalSale = price * amountSold; uint256 fee = totalSale / 20; ISaleFactory(factory).feeReceiver().transfer(fee); temp.saleReceiver.transfer(totalSale - fee); _end(); } }
Foundry test
function test_ExecuteBuyAfterEnd() 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(0.9 ether) / 1 days), startTime: uint96(block.timestamp), saleReceiver: payable(address(69)), endTime: uint96(block.timestamp + 1 days) }); sale = LPDA(lpdaSales.createLPDASale(lpdaSale)); // authorize the lpda sale to mint tokens edition.grantRole(edition.MINTER_ROLE(), address(sale)); //lets buy an NFT sale.buy{value: 1 ether}(1); assertEq(address(sale).balance, 1 ether); vm.warp(block.timestamp + 0.25 days); uint256 price = sale.getPrice(); sale.buy{value: uint256(price * 3)}(3); vm.warp(block.timestamp + 0.5 days); price = sale.getPrice(); sale.buy{value: uint256(price * 3)}(3); vm.warp(block.timestamp + 1 days); price = sale.getPrice(); // ends the sale sale.buy{value: uint256(price * 3)}(3); // execute after end sale.buy{value:0}(0); sale.buy{value:0}(0); sale.buy{value:0}(0); }
VSCode
Prevent executing buy function after sale end
require(temp.currentId < temp.finalId);
#0 - 0xSorryNotSorry
2022-12-10T08:16:59Z
Invalid. The require statement
require(newId <= temp.finalId, "TOO MANY")
will revert the transaction.
#1 - c4-judge
2022-12-11T14:48:53Z
berndartmueller marked the issue as duplicate of #16
#2 - c4-judge
2022-12-11T14:49:43Z
berndartmueller changed the severity to 3 (High Risk)
#3 - c4-judge
2023-01-04T10:13:42Z
berndartmueller marked the issue as satisfactory
#4 - C4-Staff
2023-01-10T22:21:33Z
JeeberC4 marked the issue as duplicate of #441
🌟 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
In Escher721.sol function mint is used to mint tokens to the receiver. If this address is a contract that does not handle ERC721 tokens, the minted tokens will be stuck in the contract and cannot be retrieved.
_mint(to, tokenId);
Replace mint
with safeMint
buy
and refund
will revert with high dropPerSecondIn LPDA.sol if dropPerSecond is set to a high value, functions getPrice
and refund
and will revert if startPrice
becomes less than dropPerSecond*time
. The sale cannot be ended and users will not be able to refund the excess ether sent during buy, ether will be stuck in the contract.
return temp.startPrice - (temp.dropPerSecond * timeElapsed);
Add condition to validate dropPerSecond in LPDAFactory
require(sale.startPrice > (sale.dropPerSecond * (sale.endTime - sale.startTime)));
transfer()
with call()
Since transfer forwards a fixed amount of gas 2300, the calls may fail if gas costs change in the future, or if the receiver is a smart contract that consumes more than 2300 gas.
Use .call()
to send ether instead of transfer.
refer-https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/
ISaleFactory(factory).feeReceiver().transfer(address(this).balance / 20);
ISaleFactory(factory).feeReceiver().transfer(address(this).balance / 20);
ISaleFactory(factory).feeReceiver().transfer(fee); temp.saleReceiver.transfer(totalSale - fee);
Replace transfer
with call
In FixedPrice
and OpenEdition
contracts ownership is transferred to saleReceiver during intialization. If the saleReceiver address is accidentally set to address(0), user will be unable to cancel the sale, and ether from the sale will be sent to address(0)
function cancel() external onlyOwner {
require(IEscher721(_sale.edition).hasRole(bytes32(0x00), msg.sender), "NOT AUTHORIZED"); require(_sale.startTime >= block.timestamp, "START TIME IN PAST"); require(_sale.finalId > _sale.currentId, "FINAL ID BEFORE CURRENT");
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");
require(sale.saleReceiver != address(0), "INVALID SALE RECEIVER");
Add condition to validate saleReceiver address
require(sale.saleReceiver != address(0), "INVALID SALE RECEIVER");
#0 - c4-judge
2023-01-04T10:38:41Z
berndartmueller marked the issue as grade-b