The buy
function present in the LPDA
contract has a missing validation to check that the current sale hasn't finished at the time the function is called.
The contract has a round of validations around line 62:
https://github.com/code-423n4/2022-12-escher/blob/main/src/minters/LPDA.sol#L62
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");
...
It correctly checks the sale's start time but fails to check the end time.
A buyer is allowed to still call the buy
method after the sale has ended.
The following test reproduces the issue:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "forge-std/Test.sol";
import {FixedPriceFactory} from "src/minters/FixedPriceFactory.sol";
import {FixedPrice} from "src/minters/FixedPrice.sol";
import {OpenEditionFactory} from "src/minters/OpenEditionFactory.sol";
import {OpenEdition} from "src/minters/OpenEdition.sol";
import {LPDAFactory} from "src/minters/LPDAFactory.sol";
import {LPDA} from "src/minters/LPDA.sol";
import {Escher721} from "src/Escher721.sol";
contract AuditTest is Test {
address deployer;
address creator;
address buyer;
FixedPriceFactory fixedPriceFactory;
OpenEditionFactory openEditionFactory;
LPDAFactory lpdaFactory;
function setUp() public {
deployer = makeAddr("deployer");
creator = makeAddr("creator");
buyer = makeAddr("buyer");
vm.deal(buyer, 1e18);
vm.startPrank(deployer);
fixedPriceFactory = new FixedPriceFactory();
openEditionFactory = new OpenEditionFactory();
lpdaFactory = new LPDAFactory();
vm.stopPrank();
}
function test_LPDA_buy_EndTimeNotChecked() public {
// Setup NFT and create sale
vm.startPrank(creator);
Escher721 nft = new Escher721();
nft.initialize(creator, address(0), "Test NFT", "TNFT");
uint48 startId = 0;
uint48 finalId = 1;
uint80 startPrice = 1e6;
uint80 dropPerSecond = 1;
uint96 startTime = uint96(block.timestamp);
uint96 endTime = uint96(block.timestamp + 1 hours);
LPDA.Sale memory sale = LPDA.Sale(
startId, // uint48 currentId;
finalId, // uint48 finalId;
address(nft), // address edition;
startPrice, // uint80 startPrice;
0, // uint80 finalPrice;
dropPerSecond, // uint80 dropPerSecond;
endTime, // uint96 endTime;
payable(creator), // address payable saleReceiver;
startTime // uint96 startTime;
);
LPDA lpdaSale = LPDA(lpdaFactory.createLPDASale(sale));
nft.grantRole(nft.MINTER_ROLE(), address(lpdaSale));
vm.stopPrank();
// simulate we wait until after the sale's end time
vm.warp(endTime + 1 hours);
vm.startPrank(buyer);
// The following succeeds even if we are past the end time
uint256 amount = 1;
uint256 price = lpdaSale.getPrice();
lpdaSale.buy{value: price * amount}(amount);
vm.stopPrank();
}
}
Check that the current block timestamp is before the sale's end time in the buy
function:
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");
+ require(block.timestamp < temp.endTime, "TOO LATE");
...