Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save toddstephens335/e9bde6e496e57c75be47bae415641ef0 to your computer and use it in GitHub Desktop.
Save toddstephens335/e9bde6e496e57c75be47bae415641ef0 to your computer and use it in GitHub Desktop.
Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=soljson-v0.8.19+commit.7dd6d404.js&optimize=true&runs=200&gist=
// 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);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment