// SPDX-License-Identifier: MIT // Copyright (c) 2021 the ethier authors (github.com/divergencetech/ethier) pragma solidity >=0.8.0 <0.9.0; import "../utils/Monotonic.sol"; import "../utils/OwnerPausable.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/utils/Context.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; /** @notice An abstract contract providing the _purchase() function to: - Enforce per-wallet / per-transaction limits - Calculate required cost, forwarding to a beneficiary, and refunding extra */ abstract contract Seller is OwnerPausable, ReentrancyGuard { using Address for address payable; using Monotonic for Monotonic.Increaser; using Strings for uint256; /** @dev Note that the address limits are vulnerable to wallet farming. @param maxPerAddress Unlimited if zero. @param maxPerTex Unlimited if zero. @param freeQuota Maximum number that can be purchased free of charge by the contract owner. @param reserveFreeQuota Whether to excplitly reserve the freeQuota amount and not let it be eroded by regular purchases. @param lockFreeQuota If true, calls to setSellerConfig() will ignore changes to freeQuota. Can be locked after initial setting, but not unlocked. This allows a contract owner to commit to a maximum number of reserved items. @param lockTotalInventory Similar to lockFreeQuota but applied to totalInventory. */ struct SellerConfig { uint256 totalInventory; uint256 maxPerAddress; uint256 maxPerTx; uint248 freeQuota; bool reserveFreeQuota; bool lockFreeQuota; bool lockTotalInventory; } constructor(SellerConfig memory config, address payable _beneficiary) { setSellerConfig(config); setBeneficiary(_beneficiary); } /// @notice Configuration of purchase limits. SellerConfig public sellerConfig; /// @notice Sets the seller config. function setSellerConfig(SellerConfig memory config) public onlyOwner { require( config.totalInventory >= config.freeQuota, "Seller: excessive free quota" ); require( config.totalInventory >= _totalSold.current(), "Seller: inventory < already sold" ); require( config.freeQuota >= purchasedFreeOfCharge.current(), "Seller: free quota < already used" ); // Overriding the in-memory fields before copying the whole struct, as // against writing individual fields, gives a greater guarantee of // correctness as the code is simpler to read. if (sellerConfig.lockTotalInventory) { config.lockTotalInventory = true; config.totalInventory = sellerConfig.totalInventory; } if (sellerConfig.lockFreeQuota) { config.lockFreeQuota = true; config.freeQuota = sellerConfig.freeQuota; } sellerConfig = config; } /// @notice Recipient of revenues. address payable public beneficiary; /// @notice Sets the recipient of revenues. function setBeneficiary(address payable _beneficiary) public onlyOwner { beneficiary = _beneficiary; } /** @dev Must return the current cost of a batch of items. This may be constant or, for example, decreasing for a Dutch auction or increasing for a bonding curve. @param n The number of items being purchased. @param metadata Arbitrary data, propagated by the call to _purchase() that can be used to charge different prices. This value is a uint256 instead of bytes as this allows simple passing of a set cost (see ArbitraryPriceSeller). */ function cost(uint256 n, uint256 metadata) public view virtual returns (uint256); /** @dev Called by both _purchase() and purchaseFreeOfCharge() after all limits have been put in place; must perform all contract-specific sale logic, e.g. ERC721 minting. When _handlePurchase() is called, the value returned by Seller.totalSold() will be the POST-purchase amount to allow for the checks-effects-interactions (ECI) pattern as _handlePurchase() may include an interaction. _handlePurchase() MUST itself implement the CEI pattern. @param to The recipient of the item(s). @param n The number of items allowed to be purchased, which MAY be less than to the number passed to _purchase() but SHALL be greater than zero. @param freeOfCharge Indicates that the call originated from purchaseFreeOfCharge() and not _purchase(). */ function _handlePurchase( address to, uint256 n, bool freeOfCharge ) internal virtual; /** @notice Tracks total number of items sold by this contract, including those purchased free of charge by the contract owner. */ Monotonic.Increaser private _totalSold; /// @notice Returns the total number of items sold by this contract. function totalSold() public view returns (uint256) { return _totalSold.current(); } /** @notice Tracks the number of items already bought by an address, regardless of transferring out (in the case of ERC721). @dev This isn't public as it may be skewed due to differences in msg.sender and tx.origin, which it treats in the same way such that sum(_bought)>=totalSold(). */ mapping(address => uint256) private _bought; /** @notice Returns min(n, max(extra items addr can purchase)) and reverts if 0. @param zeroMsg The message with which to revert on 0 extra. */ function _capExtra( uint256 n, address addr, string memory zeroMsg ) internal view returns (uint256) { uint256 extra = sellerConfig.maxPerAddress - _bought[addr]; if (extra == 0) { revert(string(abi.encodePacked("Seller: ", zeroMsg))); } return Math.min(n, extra); } /// @notice Emitted when a buyer is refunded. event Refund(address indexed buyer, uint256 amount); /// @notice Emitted on all purchases of non-zero amount. event Revenue( address indexed beneficiary, uint256 numPurchased, uint256 amount ); /// @notice Tracks number of items purchased free of charge. Monotonic.Increaser private purchasedFreeOfCharge; /** @notice Allows the contract owner to purchase without payment, within the quota enforced by the SellerConfig. */ function purchaseFreeOfCharge(address to, uint256 n) public onlyOwner whenNotPaused { /** * ##### CHECKS */ uint256 freeQuota = sellerConfig.freeQuota; n = Math.min(n, freeQuota - purchasedFreeOfCharge.current()); require(n > 0, "Seller: Free quota exceeded"); uint256 totalInventory = sellerConfig.totalInventory; n = Math.min(n, totalInventory - _totalSold.current()); require(n > 0, "Seller: Sold out"); /** * ##### EFFECTS */ _totalSold.add(n); purchasedFreeOfCharge.add(n); /** * ##### INTERACTIONS */ _handlePurchase(to, n, true); assert(_totalSold.current() <= totalInventory); assert(purchasedFreeOfCharge.current() <= freeQuota); } /** @notice Convenience function for calling _purchase() with empty costMetadata when unneeded. */ function _purchase(address to, uint256 requested) internal virtual { _purchase(to, requested, 0); } /** @notice Enforces all purchase limits (counts and costs) before calling _handlePurchase(), after which the received funds are disbursed to the beneficiary, less any required refunds. @param to The final recipient of the item(s). @param requested The number of items requested for purchase, which MAY be reduced when passed to _handlePurchase(). @param costMetadata Arbitrary data, propagated in the call to cost(), to be optionally used in determining the price. */ function _purchase( address to, uint256 requested, uint256 costMetadata ) internal nonReentrant whenNotPaused { /** * ##### CHECKS */ SellerConfig memory config = sellerConfig; uint256 n = config.maxPerTx == 0 ? requested : Math.min(requested, config.maxPerTx); uint256 maxAvailable; uint256 sold; if (config.reserveFreeQuota) { maxAvailable = config.totalInventory - config.freeQuota; sold = _totalSold.current() - purchasedFreeOfCharge.current(); } else { maxAvailable = config.totalInventory; sold = _totalSold.current(); } n = Math.min(n, maxAvailable - sold); require(n > 0, "Seller: Sold out"); if (config.maxPerAddress > 0) { bool alsoLimitSender = _msgSender() != to; // solhint-disable-next-line avoid-tx-origin bool alsoLimitOrigin = tx.origin != _msgSender() && tx.origin != to; n = _capExtra(n, to, "Buyer limit"); if (alsoLimitSender) { n = _capExtra(n, _msgSender(), "Sender limit"); } if (alsoLimitOrigin) { // solhint-disable-next-line avoid-tx-origin n = _capExtra(n, tx.origin, "Origin limit"); } _bought[to] += n; if (alsoLimitSender) { _bought[_msgSender()] += n; } if (alsoLimitOrigin) { // solhint-disable-next-line avoid-tx-origin _bought[tx.origin] += n; } } uint256 _cost = cost(n, costMetadata); if (msg.value < _cost) { revert( string( abi.encodePacked( "Seller: Costs ", (_cost / 1e9).toString(), " GWei" ) ) ); } /** * ##### EFFECTS */ _totalSold.add(n); assert(_totalSold.current() <= config.totalInventory); /** * ##### INTERACTIONS */ // As _handlePurchase() is often an ERC721 safeMint(), it constitutes an // interaction. _handlePurchase(to, n, false); // Ideally we'd be using a PullPayment here, but the user experience is // poor when there's a variable cost or the number of items purchased // has been capped. We've addressed reentrancy with both a nonReentrant // modifier and the checks, effects, interactions pattern. if (_cost > 0) { beneficiary.sendValue(_cost); emit Revenue(beneficiary, n, _cost); } if (msg.value > _cost) { address payable reimburse = payable(_msgSender()); uint256 refund = msg.value - _cost; // Using Address.sendValue() here would mask the revertMsg upon // reentrancy, but we want to expose it to allow for more precise // testing. This otherwise uses the exact same pattern as // Address.sendValue(). // solhint-disable-next-line avoid-low-level-calls (bool success, bytes memory returnData) = reimburse.call{ value: refund }(""); // Although `returnData` will have a spurious prefix, all we really // care about is that it contains the ReentrancyGuard reversion // message so we can check in the tests. require(success, string(returnData)); emit Refund(reimburse, refund); } } }