CuttingBoardSyndicateLib
Title: CuttingBoardSyndicateLib
Library extracting compute-heavy internal functions from CuttingBoardSyndicate
to reduce the main contract's deployed bytecode below the 24 576 byte EVM limit.
Library functions with external visibility deploy in their own bytecode and are
invoked via DELEGATECALL.
Functions
computeFill
Run the priority fill algorithm and compute the buffer's required payment. Algorithm:
- Filter to eligible partners (maxPricePerBps × 10 000 ≥ price). weight and maxPricePerBps share Slot storage slot-0, so both are loaded with a single SLOAD per partner. Bid values are cached in memory so the sort loop requires no further storage reads. Partners whose vault has been de-whitelisted since registration are excluded so the fill adapts to live BeraChef policy. Individual weights are capped to the current maxWeightPerVault in case it decreased.
- Insertion-sort cached bid values (weight × maxPricePerBps) descending. Ties preserve registration order (insertion sort is stable on strict <).
- Greedily allocate to 10 000 bps; last included partner may be partial.
- bufferRequired = price − Σ floor(price × alloc_i / 10 000). This covers the buffer vault's exact bps cost plus rounding dust (≤ count − 1 wei). When bfWeight = 0 (partners fill all 10 000 bps), bufferRequired equals only the rounding dust; bufferDeposit must still cover it even though the buffer vault receives no bps allocation.
function computeFill(
CuttingBoardSyndicateStorage storage $,
uint256 auctionId,
uint256 price
) external view returns (FillResult memory fill);
weightsFromFill
Build IBeraChef.Weight[] from a fill result, appending the buffer entry.
function weightsFromFill(
CuttingBoardSyndicateStorage storage $,
uint256 auctionId,
address[] memory included,
uint96[] memory allocs,
uint256 count,
uint96 bfWeight
) external view returns (IBeraChef.Weight[] memory weights);
weightsFromStorage
Build IBeraChef.Weight[] from stored allocatedWeight values (Active round).
Partners with allocatedWeight == 0 were excluded at trigger time and are skipped. vault and allocatedWeight share Slot storage slot-1, so both are loaded with a single SLOAD per partner. The buffer vault entry is appended when roundBufferAllocated[auctionId] > 0.
function weightsFromStorage(
CuttingBoardSyndicateStorage storage $,
uint256 auctionId
) public view returns (IBeraChef.Weight[] memory weights);
validateSetBufferVault
Validate the new global buffer vault.
Checks whitelist and rejects duplicates against any current Open round.
function validateSetBufferVault(
CuttingBoardSyndicateStorage storage $,
address vault
) external view;
validateBufferVaultUpdate
Validate that newVault can replace the buffer vault for an Active round.
Reverts with VaultNotWhitelisted or DuplicateVaultEntry.
function validateBufferVaultUpdate(
CuttingBoardSyndicateStorage storage $,
uint256 auctionId,
address newVault
) external view;
validateVaultUpdate
Validate that newVault can be assigned to partnerKey's slot.
Reverts with VaultNotWhitelisted or DuplicateVaultEntry.
In Active rounds, checks against both the live $.bufferVault and
the per-round snapshot rounds[auctionId].bufferVault.
function validateVaultUpdate(
CuttingBoardSyndicateStorage storage $,
uint256 auctionId,
address partnerKey,
address newVault
) external view;
checkRoundAllocationValid
Check whether an Active round's stored allocation satisfies current BeraChef rules (whitelist, maxWeightPerVault, maxNumWeightsPerRewardAllocation).
function checkRoundAllocationValid(
CuttingBoardSyndicateStorage storage $,
uint256 auctionId
) external view returns (bool valid);
Returns
| Name | Type | Description |
|---|---|---|
valid | bool | True if the allocation would pass BeraChef validation. |
forceVaultUpdate
Governance-forced vault update: validates, updates slot storage, and syncs SlotNFT metadata. No ownership check.
function forceVaultUpdate(
CuttingBoardSyndicateStorage storage $,
uint256 auctionId,
address partner,
address newVault
) external;
proposeCurrentWeights
Build weights from storage and submit a proposal to CuttingBoardManager. Reverts if the control NFT is expired or invalid.
function proposeCurrentWeights(
CuttingBoardSyndicateStorage storage $,
uint256 auctionId
) external;
validateFill
Validate a FillResult against BeraChef constraints and buffer state. Reverts with a specific error if any check fails.
function validateFill(
CuttingBoardSyndicateStorage storage $,
FillResult memory fill
) external view;
_rejectDuplicateVault
Revert if vault matches any partner's vault in the round,
optionally skipping skipAddr (use address(0) to skip nobody).
Private — inlined by the compiler into each external caller.
function _rejectDuplicateVault(
CuttingBoardSyndicateStorage storage $,
uint256 auctionId,
address skipAddr,
address vault
) private view;
Errors
VaultNotWhitelisted
error VaultNotWhitelisted();
DuplicateVaultEntry
error DuplicateVaultEntry(address vault);
NoSlot
error NoSlot();
NFTExpiredOrInvalid
error NFTExpiredOrInvalid();
BufferWeightExceedsMax
error BufferWeightExceedsMax();
NoBidders
error NoBidders();
TooManyPartners
error TooManyPartners();
NoBufferVault
error NoBufferVault();
BufferVaultNotWhitelisted
error BufferVaultNotWhitelisted();
InsufficientBuffer
error InsufficientBuffer();
Structs
FillResult
Transient result of the fill algorithm — packed into a struct to avoid stack-too-deep in triggerClaim.
struct FillResult {
address[] included; // partners in descending bid-value order
uint96[] allocs; // allocated bps per partner
uint256 count; // number of included partners
uint96 bfWeight; // remaining bps for the buffer vault
uint256 bufferRequired; // tokens the buffer must supply (bps cost + rounding dust)
}
Round
Storage layout: 3 slots. Slot 0: validatorHash (32 bytes). Slot 1: claimPrice (16) + tokenId (12) + state (1) = 29 bytes. Slot 2: bufferVault (20 bytes).
struct Round {
bytes32 validatorHash; // keccak256(validatorPubkey) — identifies the validator
uint128 claimPrice; // actual closing price paid; set after claim
/// @dev uint96 safely holds all realistic NFT token IDs (max ~7.9e28)
uint96 tokenId; // CuttingBoardNFT token id; set after claim
RoundState state;
/// @dev Snapshot of bufferVault taken at trigger time. Using the live
/// $.bufferVault in updateSlotVault would break proposals if governance
/// changes the buffer vault while a round is Active.
address bufferVault; // buffer vault address recorded at triggerClaim()
}
Slot
Storage layout: 3 slots, optimised so the sort hot-path reads only 1 slot. Slot 0: weight (12) + maxPricePerBps (16) = 28 bytes. Both fields read together in computeFill sort comparator → 1 SLOAD. Slot 1: vault (20) + allocatedWeight (12) = 32 bytes (perfect fill). Both fields read together in weightsFromStorage → 1 SLOAD. Slot 2: deposit (16 bytes).
struct Slot {
uint96 weight; // Requested bps; positive multiple of minSlotWeight
uint128 maxPricePerBps; // Maximum price per basis point partner will accept
address vault; // BeraChef receiver vault (unique within a round)
uint96 allocatedWeight; // Actual allocated bps (set at trigger; 0 = open / excluded)
uint128 deposit; // Held tokens = maxPricePerBps × weight
}
CuttingBoardSyndicateStorage
Note: storage-location: erc7201:infrared.storage.CuttingBoardSyndicate
struct CuttingBoardSyndicateStorage {
/// @notice Dutch auction contract being targeted
CuttingBoardDutchAuction dutchAuction;
/// @notice Manager that gates cutting board proposals to Infrared
CuttingBoardManager controlManager;
/// @notice NFT representing validator control rights (syndicate holds it after claim)
CuttingBoardNFT controlNFT;
/// @notice Payment token — captured from the auction at initialisation
ERC20 paymentToken;
/// @notice BeraChef interface used for vault whitelist and weight validation
IBeraChefVaultCheck chef;
/// @notice Minimum bps per partner slot; must be a positive divisor of 10 000
uint96 minSlotWeight;
/// @notice Protocol-owned fallback vault absorbing unallocated bps
address bufferVault;
/// @notice Optional per-winner slot NFT contract
CuttingBoardSlotNFT slotNFT;
/// @notice Payment tokens held across rounds for the buffer vault's share
uint256 bufferDeposit;
/// @notice Set of addresses with a non-zero pending refund.
/// @dev Maintained as a swap-and-pop array with an index mapping for O(1) removal.
address[] refundees;
/// @notice Index of each refundee in the `refundees` array (1-based; 0 = absent)
mapping(address => uint256) refundeeIndex;
// Per-auction-round state (all mappings keyed by auctionId)
mapping(uint256 => Round) rounds;
mapping(uint256 => address[]) roundPartners;
mapping(uint256 => mapping(address => Slot)) slots;
/// @notice Buffer vault's allocated weight for a given Active round (0 otherwise)
mapping(uint256 => uint96) roundBufferAllocated;
/// @notice Refund ledger accumulated across all rounds; persists indefinitely
mapping(address => uint256) pendingRefunds;
}
Enums
RoundState
enum RoundState {
Idle, // default — no round opened for this auctionId
Open, // registration in progress
Active, // auction claimed; syndicate holds the NFT
Expired, // auction lapsed before trigger; deposits refunded
Complete // syndicate completed Active round
}