Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/MegaSale.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;
import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {
PurchasePermitWithAllocation,
PurchasePermitWithAllocationLib
} from "sales/permits/PurchasePermitWithAllocation.sol";
/// @title MegaSale
/// @notice Public sale contract for the token offering by MegaETH
///
/// @dev
/// The sale is designed to raise funds in USDT through a competitive English auction.
/// This contract functions solely as an escrow for user commitments: it records deposits, enforces limits, and handles refunds and withdrawals at the end of the sale.
/// Auction mechanics, including clearing price determination and token allocations, take place entirely offchain, with final results provided to the contract by the settlement process.
///
/// # Sale Stages
///
/// The sale progresses through several stages:
///
/// 1. **PreOpen**: Initial state, no commitments allowed
/// 2. **Auction**: Any user can submit bids with price, amount, and lockup preferences
/// 3. **Closed**: Auction automatically closes after 30 minutes of inactivity
/// 4. **Cancellation**: Potentially winning entities can cancel their bids and definite non-winners can claim their refunds
/// 5. **Settlement**: Offchain auction clearing, allocation computation and onchain allocation setting
/// 6. **Done**: Processing refunds and withdrawing proceeds
///
/// ## Auction Phase
///
/// The auction phase allows any user with a valid purchase permit (issued by Sonar) to participate by submitting bids that include price,
/// amount, and lockup preferences.
/// Each new bid replaces the previous bid for the same entity, and bids follow a monotonic constraint where amounts and prices can only increase.
/// Lockup preferences can be enabled but cannot be disabled once set, and forced lockup can be required for specific entities as specified on the purchase permit.
///
/// Total commitment per entity cannot exceed the max amount specified on the purchase permit (expected to be 186,282 USDT).
/// Bid prices are constrained by the maximum price specified on the purchase permit, this price is determined offchain, dynamically
/// from the current clearing price to ensure that bid prices remain within bounds.
/// The auction automatically closes after 30 minutes of inactivity, though admins can manually override this mechanism if needed.
///
/// ## Cancellation Phase
///
/// Allocations are computed offchain following an English auction clearing mechanism based on the submitted bids, and the current clearing price is posted to the contract.
/// Any entity with a bid price greater than or equal to the threshold price (potential winners in the later settlement) can cancel their bid and claim their refund.
/// Any entity with a bid price less than the threshold price (definite non-winners) can claim their refund ahead of the settlement.
/// The practical difference between a cancellation and a refund is that a cancellation can only be triggered by the entity themselves, whereas a refund can also be triggered by addresses with the REFUNDER role.
///
/// ## Settlement Process
///
/// Allocations are computed offchain based on the submitted bids and offchain user data.
/// The settler role is responsible for setting allocations for each participating entity onchain.
///
/// ## Refund and Withdrawal
///
/// Refunds are calculated as the difference between total commitment and allocated amount, processed in USDT directly to the entity's address.
/// Refunds can be triggered by anyone for any entity, providing flexibility in the refund process.
/// The total allocated USDT is withdrawn to the proceeds receiver.
///
/// # Token Distribution
///
/// The distribution of tokens purchased through the allocated amounts is outside the scope of this contract and will be handled separately by the project team.
///
/// # Technical Notes
///
/// All prices throughout the contract are in units of the price tick of the English auction, i.e. 0.0001 USDT / Token.
/// The maximum price and amount a user can bid are specified offchain and are passed to the contract as part of the purchase permit.
///
/// @custom:security-contact security@echo.xyz
contract MegaSale is AccessControlEnumerable {
using SafeERC20 for IERC20;
using SafeERC20 for IERC20Metadata;
using EnumerableSet for EnumerableSet.Bytes32Set;
/// @notice The role allowed to recover tokens from the contract.
/// @dev This not intended to be granted by default, but will be granted manually by the DEFAULT_ADMIN_ROLE if needed.
bytes32 public constant TOKEN_RECOVERER_ROLE = keccak256("TOKEN_RECOVERER_ROLE");
/// @notice The role allowed to sign purchase permits.
bytes32 public constant PURCHASE_PERMIT_SIGNER_ROLE = keccak256("PURCHASE_PERMIT_SIGNER_ROLE");
/// @notice The role allowed to set the manual stage and stage related parameters.
bytes32 public constant SALE_MANAGER_ROLE = keccak256("SALE_MANAGER_ROLE");
/// @notice The role allowed to set allocations for auction clearing.
bytes32 public constant SETTLER_ROLE = keccak256("SETTLER_ROLE");
/// @notice The role allowed to pause the sale.
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
/// @notice The role allowed to refund entities.
bytes32 public constant REFUNDER_ROLE = keccak256("REFUNDER_ROLE");
error InvalidSaleUUID(bytes16 got, bytes16 want);
error PurchasePermitExpired();
error InvalidSender(address got, address want);
error UnauthorizedSigner(address signer);
error BidBelowMinAmount(uint256 newBidAmount, uint256 minAmount);
error BidExceedsMaxAmount(uint256 newBidAmount, uint256 maxAmount);
error BidLockupCannotBeUndone();
error BidMustHaveLockup();
error EntityTiedToAnotherAddress(address got, address existing, bytes16 entityID);
error AddressTiedToAnotherEntity(bytes16 got, bytes16 existing, address addr);
error ZeroAmount();
error InvalidStage(Stage);
error ZeroAddress();
error ZeroEntityID();
error BidAmountCannotBeLowered(uint256 newAmount, uint256 previousAmount);
error BidPriceCannotBeLowered(uint256 newPrice, uint256 previousPrice);
error ZeroPrice();
error AllocationAlreadySet(bytes16 entityID, uint256 acceptedAmount);
error AlreadyRefunded(bytes16 entityID);
error AlreadyWithdrawn();
error AllocationExceedsCommitment(bytes16 entityID, uint256 allocation, uint256 commitment);
error SalePaused();
error BidPriceExceedsMaxPrice(uint256 bidPrice, uint256 maxPrice);
error UnexpectedTotalAllocatedUSDT(uint256 expected, uint256 actual);
error BidAlreadyCancelled(bytes16 entityID);
error BidIsNonWinner(bytes16 entityID, uint256 bidPrice, uint256 nonWinnerPriceThreshold);
error BidIsPotentialWinner(bytes16 entityID, uint256 bidPrice, uint256 nonWinnerPriceThreshold);
error BidHasAcceptedAmount(bytes16 entityID, uint256 acceptedAmount);
error SenderWithoutRegisteredEntity(address addr);
error EntityWithoutBid(bytes16 entityID);
error SenderDoesNotOwnEntity(address sender, bytes16 entityID);
event EntityInitialized(bytes16 indexed entityID, address indexed addr);
event BidPlaced(bytes16 indexed entityID, Bid bid);
event BidCancelled(bytes16 indexed entityID, address indexed addr, uint256 amount);
event AllocationSet(bytes16 indexed entityID, uint256 acceptedAmountUSDT);
event Refunded(bytes16 indexed entityID, address indexed addr, uint256 amount);
event SkippedRefundedEntity(bytes16 indexed entityID);
event ProceedsWithdrawn(address indexed receiver, uint256 amount);
/// @notice The state of an entity in the sale.
/// @dev This tracks the entity's address, the amount of USDT they have committed, etc.
/// @param addr The address of the entity.
/// @param entityID The entity ID of the entity.
/// @param acceptedAmount The amount of USDT that has been accepted from the entity to purchase tokens after clearing the sale.
/// The accepted amount will be withdrawn as proceeds at the end of the sale.
/// The difference, i.e. `activeBid.amount - acceptedAmount`, will be refunded to the entity.
/// @param refunded Whether the entity was refunded according to the formula above.
/// @param cancelled Whether the entity cancelled their bid during the withdrawal stage. This is tracked mostly for audit purposes and is not used for any logic.
/// @param activeBid The active bid of the entity in the auction part of the sale.
/// @param bidTimestamp The timestamp of the last bid placed by the entity.
struct EntityState {
address addr;
bytes16 entityID;
uint64 acceptedAmount;
uint32 bidTimestamp;
bool refunded;
bool cancelled;
Bid activeBid;
}
/// @notice A bid in the auction.
/// @param price The willing to pay price normalized to the price tick of the English auction.
/// @param amount The amount of USDT that the entity is willing to spend.
/// @param lockup Whether the purchased tokens will be locked up.
struct Bid {
uint64 price;
uint64 amount;
bool lockup;
}
/// @notice The stages of the sale.
enum Stage {
PreOpen,
Auction,
Closed,
Cancellation,
Settlement,
Done
}
/// @notice The additional payload on the purchase permit issued by Sonar.
/// @param forcedLockup Whether the purchased tokens for a specific entity must be locked up.
/// @param maxPrice The maximum price that the entity is allowed to bid at.
struct PurchasePermitPayload {
bool forcedLockup;
uint64 maxPrice;
}
/// @notice The allocation of USDT to an entity.
/// @param entityID The entity ID of the entity.
/// @param amountUSDT The amount of USDT that has been accepted from the entity to purchase tokens after clearing the sale.
struct Allocation {
bytes16 entityID;
uint64 amountUSDT;
}
/// @notice The Sonar UUID of the sale.
bytes16 public immutable saleUUID;
/// @notice The USDT token used to fund the sale.
IERC20Metadata public immutable usdt;
/// @notice Whether the sale is paused.
/// @dev This is intended to be used in emergency situations and will disable the main external functions of the contract.
bool public paused;
/// @notice The manually set stage of the sale.
/// @dev This can differ from the actual stage of the sale (as returned by `stage()`) if this is set to `Auction`
/// and `closeAuctionAtTimestamp` or `auctionInactivityDuration` are set.
Stage public manualStage;
/// @notice The duration of the auction inactivity window after which the auction should be closed automatically.
/// @dev Automatic closing based on inactivity is disabled if set to 0
uint64 public auctionInactivityDuration;
/// @notice The timestamp at which the auction will be closed automatically.
/// @dev Automatic closing based on timestamp is disabled if set to 0
/// @dev This is intended to be used as an alternative to the inactivity closing mechanism if we detect griefing,
/// since we're intentionally not enforcing a minimum bid amount.
uint64 public closeAuctionAtTimestamp;
/// @notice The timestamp of the last bid placed.
/// @dev This will be set on each bid submission and is used to determine when the auction should be closed automatically.
uint64 public lastBidTimestamp;
/// @notice The total amount of USDT that has been committed to the auction part of the sale.
/// @dev This is the sum of all `EntityState.activeBid.amount`s across all entities, tracked when bids are placed.
/// Note: It is monotonically increasing and will not decrease on refunds/cancellations. Those are tracked separately by `totalRefundedAmount`.
uint64 public totalActiveBidAmount;
/// @notice The total amount of USDT that has been refunded to entities.
/// @dev This is the sum of all `EntityState.activeBid.amount - EntityState.acceptedAmount`s across all refunded entities.
uint64 public totalRefundedAmount;
/// @notice The total amount of USDT that has been allocated to receive tokens.
/// @dev This is the amount that will be withdrawn to the proceedsReceiver at the end of the sale.
/// @dev This is the sum of all `EntityState.acceptedAmount`s across all entities.
uint64 public totalAllocatedUSDT;
/// @notice The price threshold for non-winners.
/// @dev Users with bids less than this price are considered definite non-winners. Users with bids greater than or equal to this price are considered potential winners.
/// Non-winners and potential winners can claim their refunds or cancel their bids during the `Cancellation` stage, respectively.
/// @dev The value will be set when transitioning from the auction close stage to the `Cancellation` stage and will be determined offchain (based on the clearing price of an English auction).
/// @dev The price is normalized to the tick of the English auction.
uint64 public nonWinnerPriceThreshold;
/// @notice The address that will receive the proceeds of the sale.
address public proceedsReceiver;
/// @notice Whether the proceeds have been withdrawn.
/// @dev This is used to prevent the proceeds from being withdrawn multiple times.
bool public withdrawn;
/// @notice The set of all entities that have participated in the sale.
/// @dev The actual entity IDs are bytes16, but we're using a standard bytes32 set here for convenience.
/// Standard bytes16 <-> bytes32 casts can be used to convert between the two.
EnumerableSet.Bytes32Set internal _entities;
/// @notice The mapping of entity IDs to entity states.
/// @dev This is used to track the state of each entity.
mapping(bytes16 => EntityState) internal _entityStateByID;
/// @notice The mapping of addresses to entity IDs.
/// @dev This is used to track the entity ID for each address to prevent multiple entities from using the same address.
mapping(address => bytes16) internal _entityIDByAddress;
/// @notice The initialization parameters for the sale.
struct Init {
bytes16 saleUUID;
address admin;
IERC20Metadata usdt;
address purchasePermitSigner;
address proceedsReceiver;
address pauser;
}
constructor(Init memory init) {
saleUUID = init.saleUUID;
usdt = init.usdt;
auctionInactivityDuration = 30 minutes;
proceedsReceiver = init.proceedsReceiver;
_grantRole(DEFAULT_ADMIN_ROLE, init.admin);
_grantRole(PURCHASE_PERMIT_SIGNER_ROLE, init.purchasePermitSigner);
_grantRole(PAUSER_ROLE, init.pauser);
}
/// @notice Returns the current stage of the sale.
/// @dev The stage is either computed automatically if `manualStage` is set to `Automatic`,
/// or just returns the `manualStage` otherwise. This allows the contract to automatically
/// move between active sale stages, while still allowing the admin to manually override
/// the stage if needed.
function stage() public view returns (Stage) {
if (manualStage != Stage.Auction) {
return manualStage;
}
if (closeAuctionAtTimestamp > 0 && block.timestamp >= closeAuctionAtTimestamp) {
return Stage.Closed;
}
if (
auctionInactivityDuration > 0 && lastBidTimestamp > 0
&& block.timestamp >= lastBidTimestamp + auctionInactivityDuration
) {
return Stage.Closed;
}
return Stage.Auction;
}
/// @notice Moves the sale to the `Auction` stage, allowing any user to submit bids.
function openAuction() external onlyRole(SALE_MANAGER_ROLE) onlyStage(Stage.PreOpen) {
manualStage = Stage.Auction;
}
/// @notice Resets the last bid timestamp, allowing the auction to be reopened.
/// @dev This is not intended to be used during regular operation, but useful if the auction stalled for some reason.
function resetLastBidTimestamp() external onlyRole(SALE_MANAGER_ROLE) {
lastBidTimestamp = 0;
}
/// @notice Tracks entities that committed funds to the sale (deposits and/or bids).
/// @dev Ensures that entities can only use a single address and addresses can only be tied to a single entityID.
function _trackEntity(bytes16 entityID, address addr) internal {
if (entityID == bytes16(0)) {
revert ZeroEntityID();
}
if (addr == address(0)) {
revert ZeroAddress();
}
// Ensure that addresses can only be tied to a single entityID
bytes16 existingEntityIDForAddress = _entityIDByAddress[addr];
if (existingEntityIDForAddress != bytes16(0) && existingEntityIDForAddress != entityID) {
revert AddressTiedToAnotherEntity(entityID, existingEntityIDForAddress, addr);
}
EntityState storage state = _entityStateByID[entityID];
// Ensure that entities can only use a single address
address existingAddrForEntity = state.addr;
if (existingAddrForEntity != address(0) && existingAddrForEntity != addr) {
revert EntityTiedToAnotherAddress(addr, existingAddrForEntity, entityID);
}
if (existingAddrForEntity == address(0)) {
// new entity so we track them
state.entityID = entityID;
state.addr = addr;
_entities.add(entityID);
_entityIDByAddress[addr] = entityID;
emit EntityInitialized(entityID, addr);
}
}
/// @notice Allows any wallet to bid during the `Auction` stage using a valid purchase permit.
/// @dev When a new bid is submitted, it fully replaces any previous bid for the same entity.
/// Only the difference in bid amount (if positive) is transferred from the bidder to the sale contract.
function replaceBidWithApproval(
Bid calldata bid,
PurchasePermitWithAllocation calldata purchasePermit,
bytes calldata purchasePermitSignature
) external onlyStage(Stage.Auction) onlyUnpaused {
uint64 amountDelta = _processBid(bid, purchasePermit, purchasePermitSignature);
if (amountDelta > 0) {
usdt.safeTransferFrom(msg.sender, address(this), amountDelta);
}
}
/// @notice Processes a bid during the `Auction` stage, validating the purchase permit and updating the bid.
/// @dev The maximum amount of USDT, the maximum price and whether a bid must be locked are specified on the purchase permit (`maxAmount`, `maxPrice` and `forcedLockup`, respectively).
function _processBid(
Bid calldata newBid,
PurchasePermitWithAllocation calldata purchasePermit,
bytes calldata purchasePermitSignature
) internal returns (uint64) {
_validatePurchasePermit(purchasePermit, purchasePermitSignature);
if (newBid.price == 0) {
revert ZeroPrice();
}
if (newBid.amount == 0) {
revert ZeroAmount();
}
PurchasePermitPayload memory payload = abi.decode(purchasePermit.permit.payload, (PurchasePermitPayload));
if (payload.forcedLockup && !newBid.lockup) {
revert BidMustHaveLockup();
}
if (newBid.price > payload.maxPrice) {
revert BidPriceExceedsMaxPrice(newBid.price, payload.maxPrice);
}
EntityState storage state = _entityStateByID[purchasePermit.permit.entityID];
// additional safety check: to avoid any bookkeeping issues, we disallow new bids for entities that have already been refunded.
// this can theoretically happen if the auction was reopened after already refunding some entities.
if (state.refunded) {
revert AlreadyRefunded(purchasePermit.permit.entityID);
}
Bid memory previousBid = state.activeBid;
if (newBid.amount < previousBid.amount) {
revert BidAmountCannotBeLowered(newBid.amount, previousBid.amount);
}
if (newBid.price < previousBid.price) {
revert BidPriceCannotBeLowered(newBid.price, previousBid.price);
}
if (newBid.amount < purchasePermit.minAmount) {
revert BidBelowMinAmount(newBid.amount, purchasePermit.minAmount);
}
if (newBid.amount > purchasePermit.maxAmount) {
revert BidExceedsMaxAmount(newBid.amount, purchasePermit.maxAmount);
}
if (previousBid.lockup && !newBid.lockup) {
revert BidLockupCannotBeUndone();
}
_trackEntity(purchasePermit.permit.entityID, msg.sender);
uint64 amountDelta = newBid.amount - previousBid.amount;
state.activeBid = newBid;
state.bidTimestamp = uint32(block.timestamp);
lastBidTimestamp = uint64(block.timestamp);
totalActiveBidAmount += amountDelta;
emit BidPlaced(purchasePermit.permit.entityID, newBid);
return amountDelta;
}
/// @notice Validates a purchase permit.
/// @dev This ensures that the permit was issued for the right sale (preventing the reuse of the same permit across sales),
/// is not expired, and is signed by the purchase permit signer.
function _validatePurchasePermit(PurchasePermitWithAllocation memory permit, bytes calldata signature)
internal
view
{
if (permit.permit.saleUUID != saleUUID) {
revert InvalidSaleUUID(permit.permit.saleUUID, saleUUID);
}
if (permit.permit.expiresAt <= block.timestamp) {
revert PurchasePermitExpired();
}
if (permit.permit.wallet != msg.sender) {
revert InvalidSender(msg.sender, permit.permit.wallet);
}
address recoveredSigner = PurchasePermitWithAllocationLib.recoverSigner(permit, signature);
if (!hasRole(PURCHASE_PERMIT_SIGNER_ROLE, recoveredSigner)) {
revert UnauthorizedSigner(recoveredSigner);
}
}
/// @notice Moves the sale to the `Cancellation` stage, allowing non-winners to claim refunds and potential winners to cancel their bids.
/// @param nonWinnerPriceThresholdInTicks The price threshold for non-winner (strictly less than this price) in units of the price tick of the English auction.
function openCancellation(uint64 nonWinnerPriceThresholdInTicks)
external
onlyRole(DEFAULT_ADMIN_ROLE)
onlyStage(Stage.Closed)
{
nonWinnerPriceThreshold = nonWinnerPriceThresholdInTicks;
manualStage = Stage.Cancellation;
}
/// @notice Cancels a bid during the `Cancellation` stage, allowing potential winners to cancel their bids.
/// @dev This differs from a refund for non-winners in that it can only be triggered by the entity themselves and additionally markes the bid as cancelled.
function cancelBid() external onlyStage(Stage.Cancellation) onlyUnpaused {
bytes16 entityID = _entityIDByAddress[msg.sender];
if (entityID == bytes16(0)) {
revert SenderWithoutRegisteredEntity(msg.sender);
}
EntityState storage state = _entityStateByID[entityID];
// this is not expected since we don't allow zero bids
if (state.activeBid.amount == 0) {
revert EntityWithoutBid(entityID);
}
if (state.cancelled) {
revert BidAlreadyCancelled(entityID);
}
// we only allow potential winners to cancel their bids
if (state.activeBid.price < nonWinnerPriceThreshold) {
revert BidIsNonWinner(entityID, state.activeBid.price, nonWinnerPriceThreshold);
}
state.cancelled = true;
emit BidCancelled(entityID, msg.sender, state.activeBid.amount);
_refund(entityID);
}
/// @notice Moves the sale to the `Settlement` stage, allowing the settler to set allocations.
function openSettlement() external onlyRole(DEFAULT_ADMIN_ROLE) onlyStage(Stage.Cancellation) {
manualStage = Stage.Settlement;
}
/// @notice Interface for the settler to set allocations for the entities that participated in the sale.
/// @dev Allocations are computed offchain.
function setAllocations(Allocation[] calldata allocations, bool allowOverwrite)
external
onlyRole(SETTLER_ROLE)
onlyStage(Stage.Settlement)
{
for (uint256 i = 0; i < allocations.length; i++) {
_setAllocation(allocations[i], allowOverwrite);
}
}
/// @notice Sets an allocation for an entity, ensuring that the allocation is not greater than their commitment.
function _setAllocation(Allocation calldata allocation, bool allowOverwrite) internal {
EntityState storage state = _entityStateByID[allocation.entityID];
// we cannot grant more allocation than a user committed
// this also ensures that we can only set allocations for entities that have participated in the sale
uint64 totalCommitment = state.activeBid.amount;
if (allocation.amountUSDT > totalCommitment) {
revert AllocationExceedsCommitment(allocation.entityID, allocation.amountUSDT, totalCommitment);
}
if (state.refunded) {
revert AlreadyRefunded(allocation.entityID);
}
uint64 prevAcceptedAmount = state.acceptedAmount;
if (prevAcceptedAmount > 0) {
if (!allowOverwrite) {
revert AllocationAlreadySet(allocation.entityID, state.acceptedAmount);
}
totalAllocatedUSDT -= prevAcceptedAmount;
}
state.acceptedAmount = allocation.amountUSDT;
totalAllocatedUSDT += allocation.amountUSDT;
emit AllocationSet(allocation.entityID, allocation.amountUSDT);
}
/// @notice Moves the sale to the `Done` stage, allowing users to claim refunds and the admin to withdraw the proceeds.
/// @dev This is intended to be called after the settler has set allocations for all entities.
function finalizeSettlement(uint256 expectedTotalAllocatedUSDT)
external
onlyRole(DEFAULT_ADMIN_ROLE)
onlyStage(Stage.Settlement)
{
if (totalAllocatedUSDT != expectedTotalAllocatedUSDT) {
revert UnexpectedTotalAllocatedUSDT(expectedTotalAllocatedUSDT, totalAllocatedUSDT);
}
manualStage = Stage.Done;
}
/// @notice Refunds an entity their unallocated USDT.
/// @dev The refund amount is computed as their commitment minus their allocation.
/// @dev Refunds can be triggered by addresses with the REFUNDER role or by entities themselves.
/// @param entityIDs The IDs of the entities to refund.
/// @param skipAlreadyRefunded Whether to skip already refunded entities. If this is false and an entity is already refunded, the transaction will revert.
function processRefunds(bytes16[] calldata entityIDs, bool skipAlreadyRefunded)
external
onlyStages(Stage.Cancellation, Stage.Done)
onlyUnpaused
{
// we only allow non-winners to claim refunds during the `Cancellation` stage
if (stage() == Stage.Cancellation) {
_ensureNonWinners(entityIDs);
}
_ensureSenderIsAllowedRefunder(entityIDs);
for (uint256 i = 0; i < entityIDs.length; i++) {
EntityState storage state = _entityStateByID[entityIDs[i]];
if (skipAlreadyRefunded && state.refunded) {
emit SkippedRefundedEntity(entityIDs[i]);
continue;
}
_refund(entityIDs[i]);
}
}
/// @notice Ensures that the entities are non-winners.
/// @dev This is used to ensure that only non-winners can claim refunds during the `Cancellation` stage.
function _ensureNonWinners(bytes16[] calldata entityIDs) internal view {
uint256 nonWinnerPriceThreshold_ = nonWinnerPriceThreshold;
for (uint256 i = 0; i < entityIDs.length; i++) {
EntityState storage state = _entityStateByID[entityIDs[i]];
if (state.activeBid.price >= nonWinnerPriceThreshold_) {
revert BidIsPotentialWinner(entityIDs[i], state.activeBid.price, nonWinnerPriceThreshold_);
}
// additionally sanity check since we never expect non-winners to have accepted amounts
if (state.acceptedAmount > 0) {
revert BidHasAcceptedAmount(entityIDs[i], state.acceptedAmount);
}
}
}
/// @notice Ensures that the sender is allowed to refund the given entities.
/// @dev The sender either has to have the REFUNDER role or be the owner of the entities.
function _ensureSenderIsAllowedRefunder(bytes16[] calldata entityIDs) internal view {
if (hasRole(REFUNDER_ROLE, msg.sender)) {
return;
}
for (uint256 i = 0; i < entityIDs.length; i++) {
EntityState storage state = _entityStateByID[entityIDs[i]];
if (state.addr != msg.sender) {
revert SenderDoesNotOwnEntity(msg.sender, entityIDs[i]);
}
}
}
/// @notice Refunds an entity their unallocated USDT.
/// @dev The refund amount is computed as their commitment minus their allocation.
function _refund(bytes16 entityID) internal {
EntityState storage state = _entityStateByID[entityID];
if (state.activeBid.amount == 0) {
revert EntityWithoutBid(entityID);
}
if (state.refunded) {
revert AlreadyRefunded(entityID);
}
uint64 refundAmount = state.activeBid.amount - state.acceptedAmount;
state.refunded = true;
emit Refunded(entityID, state.addr, refundAmount);
if (refundAmount > 0) {
totalRefundedAmount += refundAmount;
usdt.safeTransfer(state.addr, refundAmount);
}
}
/// @notice Withdraws the proceeds of the sale to the proceeds receiver.
/// @dev This is intended to be called after the sale is finalized.
function withdraw() external onlyRole(DEFAULT_ADMIN_ROLE) onlyStage(Stage.Done) {
if (withdrawn) {
revert AlreadyWithdrawn();
}
withdrawn = true;
emit ProceedsWithdrawn(proceedsReceiver, totalAllocatedUSDT);
usdt.safeTransfer(proceedsReceiver, totalAllocatedUSDT);
}
/// @notice Sets the manual stage of the sale.
/// @dev This is not intended to be used during regular operation (use `openAuction` and `openSettlement` instead),
/// but only for emergency situations.
function setManualStage(Stage s) external onlyRole(DEFAULT_ADMIN_ROLE) {
manualStage = s;
}
/// @notice Sets the auction inactivity window.
/// @dev Setting this to 0 will disable the automatic closing of the auction on inactivity.
function setAuctionInactivityWindow(uint64 window) external onlyRole(SALE_MANAGER_ROLE) {
auctionInactivityDuration = window;
}
/// @notice Sets the timestamp at which the auction will be closed automatically.
/// @dev Setting this to 0 will disable the automatic closing of the auction at a specific timestamp.
function setCloseAuctionAtTimestamp(uint64 timestamp) external onlyRole(SALE_MANAGER_ROLE) {
closeAuctionAtTimestamp = timestamp;
}
/// @notice Sets the address that will receive the proceeds of the sale.
function setProceedsReceiver(address newproceedsReceiver) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (newproceedsReceiver == address(0)) {
revert ZeroAddress();
}
proceedsReceiver = newproceedsReceiver;
}
/// @notice Pauses the sale.
/// @dev This is intended to be used in emergency situations.
function pause() external onlyRole(PAUSER_ROLE) {
paused = true;
}
/// @notice Sets whether the sale is paused.
/// @dev This is intended to unpause the sale after a pause.
function setPaused(bool isPaused) external onlyRole(DEFAULT_ADMIN_ROLE) {
paused = isPaused;
}
/// @notice Returns the number of entities that have participated in the sale.
function numEntities() public view returns (uint256) {
return _entities.length();
}
/// @notice Returns the entity ID at a given index.
function entityAt(uint256 index) public view returns (bytes16) {
return bytes16(_entities.at(index));
}
/// @notice Returns the entity IDs in given index range.
function entitiesIn(uint256 from, uint256 to) external view returns (bytes16[] memory) {
bytes16[] memory ids = new bytes16[](to - from);
for (uint256 i = from; i < to; i++) {
ids[i - from] = entityAt(i);
}
return ids;
}
/// @notice Returns the entity ID for a given address.
function entityByAddress(address addr) public view returns (bytes16) {
return _entityIDByAddress[addr];
}
/// @notice Returns the entity IDs for a given addresses.
function entitiesByAddress(address[] calldata addrs) external view returns (bytes16[] memory) {
bytes16[] memory ids = new bytes16[](addrs.length);
for (uint256 i = 0; i < addrs.length; i++) {
ids[i] = entityByAddress(addrs[i]);
}
return ids;
}
/// @notice Returns the state of an entity.
function entityStateByID(bytes16 entityID) public view returns (EntityState memory) {
return _entityStateByID[entityID];
}
/// @notice Returns the states of a given entities.
function entityStatesByID(bytes16[] calldata entityIDs) external view returns (EntityState[] memory) {
EntityState[] memory states = new EntityState[](entityIDs.length);
for (uint256 i = 0; i < entityIDs.length; i++) {
states[i] = entityStateByID(entityIDs[i]);
}
return states;
}
/// @notice Returns the states of entities in a given index range.
function entityStatesIn(uint256 from, uint256 to) external view returns (EntityState[] memory) {
EntityState[] memory states = new EntityState[](to - from);
for (uint256 i = from; i < to; i++) {
states[i - from] = entityStateByID(entityAt(i));
}
return states;
}
/// @notice Recovers any ERC20 tokens that are sent to the contract.
/// @dev This can be used to recover any tokens that are sent to the contract by mistake.
function recoverTokens(IERC20 token, uint256 amount, address to) external onlyRole(TOKEN_RECOVERER_ROLE) {
token.safeTransfer(to, amount);
}
/// @notice Modifier to ensure the sale is in the desired stage.
modifier onlyStage(Stage want) {
Stage s = stage();
if (s != want) {
revert InvalidStage(s);
}
_;
}
/// @notice Modifier to ensure the sale is in the desired stage.
modifier onlyStages(Stage want1, Stage want2) {
Stage s = stage();
if (s != want1 && s != want2) {
revert InvalidStage(s);
}
_;
}
/// @notice Modifier to ensure the sale is not paused.
modifier onlyUnpaused() {
if (paused) {
revert SalePaused();
}
_;
}
}
"
},
"lib/openzeppelin-contracts/contracts/access/extensions/AccessControlEnumerable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (access/extensions/AccessControlEnumerable.sol)
pragma solidity ^0.8.20;
import {IAccessControlEnumerable} from "./IAccessControlEnumerable.sol";
import {AccessControl} from "../AccessControl.sol";
import {EnumerableSet} from "../../utils/structs/EnumerableSet.sol";
import {IERC165} from "../../utils/introspection/ERC165.sol";
/**
* @dev Extension of {AccessControl} that allows enumerating the members of each role.
*/
abstract contract AccessControlEnumerable is IAccessControlEnumerable, AccessControl {
using EnumerableSet for EnumerableSet.AddressSet;
mapping(bytes32 role => EnumerableSet.AddressSet) private _roleMembers;
/// @inheritdoc IERC165
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IAccessControlEnumerable).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev Returns one of the accounts that have `role`. `index` must be a
* value between 0 and {getRoleMemberCount}, non-inclusive.
*
* Role bearers are not sorted in any particular way, and their ordering may
* change at any point.
*
* WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
* you perform all queries on the same block. See the following
* https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]
* for more information.
*/
function getRoleMember(bytes32 role, uint256 index) public view virtual returns (address) {
return _roleMembers[role].at(index);
}
/**
* @dev Returns the number of accounts that have `role`. Can be used
* together with {getRoleMember} to enumerate all bearers of a role.
*/
function getRoleMemberCount(bytes32 role) public view virtual returns (uint256) {
return _roleMembers[role].length();
}
/**
* @dev Return all accounts that have `role`
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function getRoleMembers(bytes32 role) public view virtual returns (address[] memory) {
return _roleMembers[role].values();
}
/**
* @dev Overload {AccessControl-_grantRole} to track enumerable memberships
*/
function _grantRole(bytes32 role, address account) internal virtual override returns (bool) {
bool granted = super._grantRole(role, account);
if (granted) {
_roleMembers[role].add(account);
}
return granted;
}
/**
* @dev Overload {AccessControl-_revokeRole} to track enumerable memberships
*/
function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) {
bool revoked = super._revokeRole(role, account);
if (revoked) {
_roleMembers[role].remove(account);
}
return revoked;
}
}
"
},
"lib/openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (utils/structs/EnumerableSet.sol)
// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js.
pragma solidity ^0.8.20;
import {Arrays} from "../Arrays.sol";
import {Math} from "../math/Math.sol";
/**
* @dev Library for managing
* https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive
* types.
*
* Sets have the following properties:
*
* - Elements are added, removed, and checked for existence in constant time
* (O(1)).
* - Elements are enumerated in O(n). No guarantees are made on the ordering.
* - Set can be cleared (all elements removed) in O(n).
*
* ```solidity
* contract Example {
* // Add the library methods
* using EnumerableSet for EnumerableSet.AddressSet;
*
* // Declare a set state variable
* EnumerableSet.AddressSet private mySet;
* }
* ```
*
* The following types are supported:
*
* - `bytes32` (`Bytes32Set`) since v3.3.0
* - `address` (`AddressSet`) since v3.3.0
* - `uint256` (`UintSet`) since v3.3.0
* - `string` (`StringSet`) since v5.4.0
* - `bytes` (`BytesSet`) since v5.4.0
*
* [WARNING]
* ====
* Trying to delete such a structure from storage will likely result in data corruption, rendering the structure
* unusable.
* See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.
*
* In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an
* array of EnumerableSet.
* ====
*/
library EnumerableSet {
// To implement this library for multiple types with as little code
// repetition as possible, we write it in terms of a generic Set type with
// bytes32 values.
// The Set implementation uses private functions, and user-facing
// implementations (such as AddressSet) are just wrappers around the
// underlying Set.
// This means that we can only create new EnumerableSets for types that fit
// in bytes32.
struct Set {
// Storage of set values
bytes32[] _values;
// Position is the index of the value in the `values` array plus 1.
// Position 0 is used to mean a value is not in the set.
mapping(bytes32 value => uint256) _positions;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function _add(Set storage set, bytes32 value) private returns (bool) {
if (!_contains(set, value)) {
set._values.push(value);
// The value is stored at length-1, but we add 1 to all indexes
// and use 0 as a sentinel value
set._positions[value] = set._values.length;
return true;
} else {
return false;
}
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function _remove(Set storage set, bytes32 value) private returns (bool) {
// We cache the value's position to prevent multiple reads from the same storage slot
uint256 position = set._positions[value];
if (position != 0) {
// Equivalent to contains(set, value)
// To delete an element from the _values array in O(1), we swap the element to delete with the last one in
// the array, and then remove the last element (sometimes called as 'swap and pop').
// This modifies the order of the array, as noted in {at}.
uint256 valueIndex = position - 1;
uint256 lastIndex = set._values.length - 1;
if (valueIndex != lastIndex) {
bytes32 lastValue = set._values[lastIndex];
// Move the lastValue to the index where the value to delete is
set._values[valueIndex] = lastValue;
// Update the tracked position of the lastValue (that was just moved)
set._positions[lastValue] = position;
}
// Delete the slot where the moved value was stored
set._values.pop();
// Delete the tracked position for the deleted slot
delete set._positions[value];
return true;
} else {
return false;
}
}
/**
* @dev Removes all the values from a set. O(n).
*
* WARNING: This function has an unbounded cost that scales with set size. Developers should keep in mind that
* using it may render the function uncallable if the set grows to the point where clearing it consumes too much
* gas to fit in a block.
*/
function _clear(Set storage set) private {
uint256 len = _length(set);
for (uint256 i = 0; i < len; ++i) {
delete set._positions[set._values[i]];
}
Arrays.unsafeSetLength(set._values, 0);
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function _contains(Set storage set, bytes32 value) private view returns (bool) {
return set._positions[value] != 0;
}
/**
* @dev Returns the number of values on the set. O(1).
*/
function _length(Set storage set) private view returns (uint256) {
return set._values.length;
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function _at(Set storage set, uint256 index) private view returns (bytes32) {
return set._values[index];
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function _values(Set storage set) private view returns (bytes32[] memory) {
return set._values;
}
/**
* @dev Return a slice of the set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function _values(Set storage set, uint256 start, uint256 end) private view returns (bytes32[] memory) {
unchecked {
end = Math.min(end, _length(set));
start = Math.min(start, end);
uint256 len = end - start;
bytes32[] memory result = new bytes32[](len);
for (uint256 i = 0; i < len; ++i) {
result[i] = Arrays.unsafeAccess(set._values, start + i).value;
}
return result;
}
}
// Bytes32Set
struct Bytes32Set {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {
return _add(set._inner, value);
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {
return _remove(set._inner, value);
}
/**
* @dev Removes all the values from a set. O(n).
*
* WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
* function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.
*/
function clear(Bytes32Set storage set) internal {
_clear(set._inner);
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {
return _contains(set._inner, value);
}
/**
* @dev Returns the number of values in the set. O(1).
*/
function length(Bytes32Set storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {
return _at(set._inner, index);
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
bytes32[] memory store = _values(set._inner);
bytes32[] memory result;
assembly ("memory-safe") {
result := store
}
return result;
}
/**
* @dev Return a slice of the set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(Bytes32Set storage set, uint256 start, uint256 end) internal view returns (bytes32[] memory) {
bytes32[] memory store = _values(set._inner, start, end);
bytes32[] memory result;
assembly ("memory-safe") {
result := store
}
return result;
}
// AddressSet
struct AddressSet {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(AddressSet storage set, address value) internal returns (bool) {
return _add(set._inner, bytes32(uint256(uint160(value))));
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(AddressSet storage set, address value) internal returns (bool) {
return _remove(set._inner, bytes32(uint256(uint160(value))));
}
/**
* @dev Removes all the values from a set. O(n).
*
* WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
* function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.
*/
function clear(AddressSet storage set) internal {
_clear(set._inner);
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(AddressSet storage set, address value) internal view returns (bool) {
return _contains(set._inner, bytes32(uint256(uint160(value))));
}
/**
* @dev Returns the number of values in the set. O(1).
*/
function length(AddressSet storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(AddressSet storage set, uint256 index) internal view returns (address) {
return address(uint160(uint256(_at(set._inner, index))));
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(AddressSet storage set) internal view returns (address[] memory) {
bytes32[] memory store = _values(set._inner);
address[] memory result;
assembly ("memory-safe") {
result := store
}
return result;
}
/**
* @dev Return a slice of the set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(AddressSet storage set, uint256 start, uint256 end) internal view returns (address[] memory) {
bytes32[] memory store = _values(set._inner, start, end);
address[] memory result;
assembly ("memory-safe") {
result := store
}
return result;
}
// UintSet
struct UintSet {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(UintSet storage set, uint256 value) internal returns (bool) {
return _add(set._inner, bytes32(value));
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(UintSet storage set, uint256 value) internal returns (bool) {
return _remove(set._inner, bytes32(value));
}
/**
* @dev Removes all the values from a set. O(n).
*
* WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
* function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.
*/
function clear(UintSet storage set) internal {
_clear(set._inner);
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(UintSet storage set, uint256 value) internal view returns (bool) {
return _contains(set._inner, bytes32(value));
}
/**
* @dev Returns the number of values in the set. O(1).
*/
function length(UintSet storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(UintSet storage set, uint256 index) internal view returns (uint256) {
return uint256(_at(set._inner, index));
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(UintSet storage set) internal view returns (uint256[] memory) {
bytes32[] memory store = _values(set._inner);
uint256[] memory result;
assembly ("memory-safe") {
result := store
}
return result;
}
/**
* @dev Return a slice of the set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(UintSet storage set, uint256 start, uint256 end) internal view returns (uint256[] memory) {
bytes32[] memory store = _values(set._inner, start, end);
uint256[] memory result;
assembly ("memory-safe") {
result := store
}
return result;
}
struct StringSet {
// Storage of set values
string[] _values;
// Position is the index of the value in the `values` array plus 1.
// Position 0 is used to mean a value is not in the set.
mapping(string value => uint256) _positions;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(StringSet storage set, string memory value) internal returns (bool) {
if (!contains(set, value)) {
set._values.push(value);
// The value is stored at length-1, but we add 1 to all indexes
// and use 0 as a sentinel value
set._positions[value] = set._values.length;
return true;
} else {
return false;
}
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(StringSet storage set, string memory value) internal returns (bool) {
// We cache the value's position to prevent multiple reads from the same storage slot
uint256 position = set._positions[value];
if (position != 0) {
// Equivalent to contains(set, value)
// To delete an element from the _values array in O(1), we swap the element to delete with the last one in
// the array, and then remove the last element (sometimes called as 'swap and pop').
// This modifies the order of the array, as noted in {at}.
uint256 valueIndex = position - 1;
uint256 lastIndex = set._values.length - 1;
if (valueIndex != lastIndex) {
string memory lastValue = set._values[lastIndex];
// Move the lastValue to the index where the value to delete is
set._values[valueIndex] = lastValue;
// Update the tracked position of the lastValue (that was just moved)
set._positions[lastValue] = position;
}
// Delete the slot where the moved value was stored
set._values.pop();
// Delete the tracked position for the deleted slot
delete set._positions[value];
return true;
} else {
return false;
}
}
/**
* @dev Removes all the values from a set. O(n).
*
* WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
* function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.
*/
function clear(StringSet storage set) internal {
uint256 len = length(set);
for (uint256 i = 0; i < len; ++i) {
delete set._positions[set._values[i]];
}
Arrays.unsafeSetLength(set._values, 0);
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(StringSet storage set, string memory value) internal view returns (bool) {
return set._positions[value] != 0;
}
/**
* @dev Returns the number of values on the set. O(1).
*/
function length(StringSet storage set) internal view returns (uint256) {
return set._values.length;
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(StringSet storage set, uint256 index) internal view returns (string memory) {
return set._values[index];
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(StringSet storage set) internal view returns (string[] memory) {
return set._values;
}
/**
* @dev Return a slice of the set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(StringSet storage set, uint256 start, uint256 end) internal view returns (string[] memory) {
unchecked {
end = Math.min(end, length(set));
start = Math.min(start, end);
uint256 len = end - start;
string[] memory result = new string[](len);
for (uint256 i = 0; i < len; ++i) {
result[i] = Arrays.unsafeAccess(set._values, start + i).value;
}
return result;
}
}
struct BytesSet {
// Storage of set values
bytes[] _values;
// Position is the index of the value in the `values` array plus 1.
// Position 0 is used to mean a value is not in the set.
mapping(bytes value => uint256) _positions;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already p
Submitted on: 2025-10-25 18:56:13
Comments
Log in to comment.
No comments yet.