BasketFactory

contracts/src/BasketFactory.sol · Solidity 0.8.26

Deploys Basket clones (EIP-1167 minimal proxies), funds them with seed tokens, and initializes them atomically in one transaction, then registers each. It enforces creation gating (an open-creation flag or a curator allowlist) and exposes owner controls.

  • Inherits: Ownable2Step. Uses OpenZeppelin Clones.

Why atomic: a funded-but-uninitialized clone would be hijackable by a third party, so the seed transfer and initialize() must never be split across transactions.

Storage

address public immutable basketImplementation;   // the clone target
Whitelist public immutable whitelist;            // injected into every basket
address public treasury;                         // protocol treasury injected into every basket
bool public openCreation;                        // if true, anyone may create; else curators only
mapping(address => bool) public isCurator;       // creation allowlist
mapping(address => bool) public isBasket;        // registry of created baskets
address[] internal baskets;

createBasket

function createBasket(
    address[] calldata tokens,
    uint256[] calldata seedUnits,
    uint256 seedShares,
    uint16 entryFeeBps,
    uint16 exitFeeBps,
    uint16 mgmtFeeBpsPerYear,
    string calldata name,
    string calldata symbol
) external returns (address basket)

The main entry point. msg.sender becomes the new basket's creator and its fee recipient.

Reverts NotCurator() if !openCreation && !isCurator[msg.sender]. Checks for duplicate tokens (DuplicateToken()). Then:

  1. basket = Clones.clone(basketImplementation)
  2. Seed: for each token, transfers amt = ceil(seedUnits[i] × totalSeed / 1e18) from the caller into the clone, where totalSeed = seedShares + DEAD_SHARES (rounds up so the clone always holds at least what initialize verifies).
  3. Init: calls Basket.initialize(...) with creator = msg.sender, treasury and whitelist from the factory.
  4. Registers isBasket[basket] = true, appends to baskets, emits BasketCreated.

Any revert from the seed transfers or Basket.initialize (e.g. FeeAboveCap, BadParams) bubbles up.

The caller must hold each constituent and have approved the factory for the seed amount before calling. See the Create a basket guide.

Owner functions

constructor(address gov, address impl, address wl, address treasury_) Ownable(gov)
function setCurator(address who, bool ok) external onlyOwner        // emits CuratorSet
function setOpenCreation(bool open) external onlyOwner              // emits OpenCreationSet
function setMintPaused(address basket, bool p) external onlyOwner   // pass-through to Basket.setMintPaused

setOpenCreation(true) is what makes basket creation permissionless for any wallet. setMintPaused lets the owner pause a specific basket's mint (redemption is never pausable).

View functions

function allBaskets() external view returns (address[] memory)

Plus auto-getters: basketImplementation(), whitelist(), treasury(), openCreation(), isCurator(address), isBasket(address), and the inherited Ownable2Step surface.

Events & errors

event BasketCreated(address indexed basket, address indexed creator, string symbol);
event CuratorSet(address indexed who, bool allowed);
event OpenCreationSet(bool open);

error NotCurator();      // createBasket while gated and caller isn't a curator
error DuplicateToken();  // tokens array contains a duplicate

BasketCreated is the event the backend indexer watches to discover new baskets. isBasket is what the HoodZapRouter checks before routing a zap.